diff --git a/docs/architecture/cognition-runtime.md b/docs/architecture/cognition-runtime.md index d0b8a67f..2d82a107 100644 --- a/docs/architecture/cognition-runtime.md +++ b/docs/architecture/cognition-runtime.md @@ -30,13 +30,16 @@ interface is the source of truth for concrete API names. ## Mock recomputation rules The first mock graph has three layers: file text, file-level summaries, and -repo/query context. File-level summaries read one file input. Repo context reads -all known file summaries and seeds missing summaries from known file inputs on -its first build. Query context reads repo context and materializes repo context -when needed. When a new file input appears after repo context already exists, or +repo/query context. File-level summaries read one file input. A workspace file +registry tracks active file inputs. Repo context reads summaries for active +files and seeds missing summaries from registered file inputs on its first +build. Query context reads repo context and materializes repo context when +needed. When a new file input appears after repo context already exists, or after query context has observed repo context, the runtime dirties the corresponding file-level summary and repo context so repo context can adopt the -new dependency on the next recomputation. +new dependency on the next recomputation. Removing a file unregisters it, +removes its file text and summary artifacts, and dirties repo/query context so +the next recomputation excludes the deleted file. When an input changes, the store marks transitive dependents dirty. Recomputing dirty artifacts proceeds only when their dependencies are clean, so unrelated diff --git a/lib/cognition/cognition_test.mbt b/lib/cognition/cognition_test.mbt index b5588008..3f491e6b 100644 --- a/lib/cognition/cognition_test.mbt +++ b/lib/cognition/cognition_test.mbt @@ -35,6 +35,45 @@ test "set_input only accepts FileText Text pairs" { inspect(store.get_value(FileText("a.mbt")) is None, content="true") } +///| +test "known_files tracks FileText inputs in sorted order" { + let store = CognitionStore::new() + let _ = store.set_input(FileText("b.mbt"), Text("two")) + let _ = store.set_input(FileText("a.mbt"), Text("one")) + + inspect(store.known_files().join(","), content="a.mbt,b.mbt") +} + +///| +test "remove_file unregisters file and recomputes repo without deleted summary" { + let store = CognitionStore::new() + let _ = store.set_input(FileText("a.mbt"), Text("one")) + let _ = store.set_input(FileText("b.mbt"), Text("two")) + let _ = store.compute(QueryContext("what changed?")) + + inspect(store.remove_file("missing.mbt"), content="false") + inspect(store.remove_file("a.mbt"), content="true") + inspect(store.known_files().join(","), content="b.mbt") + inspect(store.get_value(FileText("a.mbt")) is None, content="true") + inspect(store.get_value(FileSummary("a.mbt")) is None, content="true") + inspect(store.is_dirty(RepoSummary), content="true") + inspect(store.is_dirty(QueryContext("what changed?")), content="true") + + let recomputed = store.recompute_dirty() + inspect(recomputed.contains(RepoSummary), content="true") + inspect(recomputed.contains(QueryContext("what changed?")), content="true") + let deps = store.dependencies_of(RepoSummary) + inspect(deps.contains(FileSummary("a.mbt")), content="false") + inspect(deps.contains(FileSummary("b.mbt")), content="true") + match store.get_value(RepoSummary) { + Some(Summary(summary)) => { + inspect(summary.contains("a.mbt"), content="false") + inspect(summary.contains("b.mbt"), content="true") + } + _ => fail("expected repo summary") + } +} + ///| test "changing one FileText marks its FileSummary dirty" { let store = CognitionStore::new() diff --git a/lib/cognition/pkg.generated.mbti b/lib/cognition/pkg.generated.mbti index 755da35b..bcaa6793 100644 --- a/lib/cognition/pkg.generated.mbti +++ b/lib/cognition/pkg.generated.mbti @@ -34,10 +34,12 @@ pub fn CognitionStore::dirty_keys(Self) -> Array[CognitionKey] pub fn CognitionStore::get_revision(Self, CognitionKey) -> Revision? pub fn CognitionStore::get_value(Self, CognitionKey) -> CognitionValue? pub fn CognitionStore::is_dirty(Self, CognitionKey) -> Bool +pub fn CognitionStore::known_files(Self) -> Array[String] pub fn CognitionStore::mark_dirty(Self, CognitionKey) -> Unit pub fn CognitionStore::new() -> Self pub fn CognitionStore::recompute_count(Self, CognitionKey) -> Int pub fn CognitionStore::recompute_dirty(Self) -> Array[CognitionKey] +pub fn CognitionStore::remove_file(Self, String) -> Bool pub fn CognitionStore::set_input(Self, CognitionKey, CognitionValue) -> Bool pub(all) enum CognitionValue { diff --git a/lib/cognition/store.mbt b/lib/cognition/store.mbt index 543afcbe..6bbe04de 100644 --- a/lib/cognition/store.mbt +++ b/lib/cognition/store.mbt @@ -2,6 +2,7 @@ /// Mutable store for cognition values, revisions, dependencies, and dirtiness. pub(all) struct CognitionStore { priv values : Map[CognitionKey, CognitionValue] + priv workspace_files : Map[String, Bool] priv revisions : Map[CognitionKey, Revision] priv dependencies : Map[CognitionKey, Array[CognitionKey]] priv reverse_dependencies : Map[CognitionKey, Array[CognitionKey]] @@ -15,6 +16,7 @@ pub fn CognitionStore::new() -> CognitionStore { let dependencies : Map[CognitionKey, Array[CognitionKey]] = Map::default() { values: Map::default(), + workspace_files: Map::default(), revisions: Map::default(), dependencies, reverse_dependencies: Map::default(), @@ -40,12 +42,48 @@ pub fn CognitionStore::set_input( } } +///| +/// Return the active workspace file paths known to the cognition store. +pub fn CognitionStore::known_files(self : CognitionStore) -> Array[String] { + let paths = self.workspace_files.keys().to_array() + paths.sort_by(fn(a, b) { a.compare(b) }) + paths +} + +///| +/// Remove one workspace file input and dirty derived artifacts that referenced it. +pub fn CognitionStore::remove_file( + self : CognitionStore, + path : String, +) -> Bool { + if !self.workspace_files.contains(path) { + false + } else { + let text_key = FileText(path) + let summary_key = FileSummary(path) + self.workspace_files.remove(path) + if self.repo_context_is_observed() { + self.mark_dirty(RepoSummary) + } + let _ = self.dirty_dependents(summary_key) + let _ = self.dirty_dependents(text_key) + self.remove_artifact_state(text_key) + self.remove_artifact_state(summary_key) + let _ = self.reactive.bump_revision() + true + } +} + ///| fn CognitionStore::store_input( self : CognitionStore, key : CognitionKey, value : CognitionValue, ) -> Unit { + match key { + FileText(path) => self.workspace_files[path] = true + _ => () + } self.values[key] = value.copy_value() self.bump_revision(key) self.dirty.remove(key) @@ -206,6 +244,45 @@ fn CognitionStore::bump_revision( self.revisions[key] = revision } +///| +fn CognitionStore::remove_artifact_state( + self : CognitionStore, + key : CognitionKey, +) -> Unit { + self.values.remove(key) + self.revisions.remove(key) + self.dirty.remove(key) + self.recompute_counts.remove(key) + self.remove_dependency_record(key) +} + +///| +fn CognitionStore::remove_dependency_record( + self : CognitionStore, + key : CognitionKey, +) -> Unit { + match self.dependencies.get(key) { + Some(old_inputs) => { + for input in old_inputs { + match self.reverse_dependencies.get(input) { + Some(dependents) => { + let remaining = dependents.filter(fn(k) { k != key }) + if remaining.length() == 0 { + self.reverse_dependencies.remove(input) + } else { + self.reverse_dependencies[input] = remaining + } + } + None => () + } + } + self.dependencies.remove(key) + self.reactive.bump_dependency_revision() + } + None => () + } +} + ///| fn CognitionStore::collect_dirty_dependents( self : CognitionStore, @@ -389,17 +466,11 @@ fn CognitionStore::recompute_query_context( fn CognitionStore::ensure_file_summaries_for_known_texts( self : CognitionStore, ) -> Unit { - for key, _ in self.values { - match key { - FileText(path) => { - let summary_key = FileSummary(path) - if !self.values.contains(summary_key) || - self.dirty.contains(summary_key) { - self.recompute_file_summary(path) - self.dirty.remove(summary_key) - } - } - _ => () + for path in self.known_files() { + let summary_key = FileSummary(path) + if !self.values.contains(summary_key) || self.dirty.contains(summary_key) { + self.recompute_file_summary(path) + self.dirty.remove(summary_key) } } } @@ -409,13 +480,12 @@ fn CognitionStore::known_file_summary_keys( self : CognitionStore, ) -> Array[CognitionKey] { let keys : Array[CognitionKey] = [] - for key, _ in self.values { - match key { - FileSummary(_) => keys.push(key) - _ => () + for path in self.known_files() { + let summary_key = FileSummary(path) + if self.values.contains(summary_key) { + keys.push(summary_key) } } - keys.sort_by(fn(a, b) { a.compare(b) }) keys }