@polycode-projects/the-mechanical-code-talker 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +127 -28
  2. package/ROADMAP.md +3 -2
  3. package/bin/tmct.mjs +17 -90
  4. package/corpus/LICENSES.json +19 -4
  5. package/corpus/README.md +48 -0
  6. package/corpus/generated/README.md +24 -9
  7. package/corpus/generated/ace-surface-variants.jsonl +4 -1
  8. package/corpus/generated/manifest.json +4 -4
  9. package/corpus/prose/manifest.json +512 -0
  10. package/corpus/prose/sqlite/LICENSE-NOTICE +53 -0
  11. package/corpus/prose/sqlite/arch.txt +213 -0
  12. package/corpus/prose/sqlite/atomiccommit.txt +1117 -0
  13. package/corpus/prose/sqlite/faq.txt +473 -0
  14. package/corpus/prose/sqlite/fileformat.txt +1589 -0
  15. package/corpus/prose/sqlite/lang_createtable.txt +1339 -0
  16. package/corpus/prose/sqlite/lang_insert.txt +580 -0
  17. package/corpus/prose/sqlite/lang_select.txt +3293 -0
  18. package/corpus/prose/sqlite/optoverview.txt +908 -0
  19. package/corpus/prose/sqlite/queryplanner.txt +447 -0
  20. package/corpus/prose/sqlite/transactional.txt +41 -0
  21. package/corpus/prose/sqlite/wal.txt +567 -0
  22. package/corpus/prose/sqlite/whentouse.txt +300 -0
  23. package/corpus/prose/wikipedia/Apple.txt +4 -0
  24. package/corpus/prose/wikipedia/Attempto_Controlled_English.txt +169 -0
  25. package/corpus/prose/wikipedia/Automated_planning_and_scheduling.txt +67 -0
  26. package/corpus/prose/wikipedia/Bee.txt +7 -0
  27. package/corpus/prose/wikipedia/Bird.txt +8 -0
  28. package/corpus/prose/wikipedia/Bone.txt +4 -0
  29. package/corpus/prose/wikipedia/Book.txt +7 -0
  30. package/corpus/prose/wikipedia/Bread.txt +6 -0
  31. package/corpus/prose/wikipedia/Butterfly.txt +6 -0
  32. package/corpus/prose/wikipedia/Car.txt +1 -0
  33. package/corpus/prose/wikipedia/Cat.txt +1 -0
  34. package/corpus/prose/wikipedia/Child.txt +3 -0
  35. package/corpus/prose/wikipedia/City.txt +2 -0
  36. package/corpus/prose/wikipedia/Clock.txt +2 -0
  37. package/corpus/prose/wikipedia/Cooking.txt +1 -0
  38. package/corpus/prose/wikipedia/Description_logic.txt +660 -0
  39. package/corpus/prose/wikipedia/Doctor.txt +6 -0
  40. package/corpus/prose/wikipedia/Dog.txt +4 -0
  41. package/corpus/prose/wikipedia/Eagle.txt +4 -0
  42. package/corpus/prose/wikipedia/Emotion.txt +9 -0
  43. package/corpus/prose/wikipedia/Eye.txt +5 -0
  44. package/corpus/prose/wikipedia/Family.txt +3 -0
  45. package/corpus/prose/wikipedia/Farm.txt +4 -0
  46. package/corpus/prose/wikipedia/Fear.txt +4 -0
  47. package/corpus/prose/wikipedia/First-order_logic.txt +1518 -0
  48. package/corpus/prose/wikipedia/Fish.txt +10 -0
  49. package/corpus/prose/wikipedia/Flower.txt +3 -0
  50. package/corpus/prose/wikipedia/Food.txt +10 -0
  51. package/corpus/prose/wikipedia/Grass.txt +9 -0
  52. package/corpus/prose/wikipedia/Hand.txt +2 -0
  53. package/corpus/prose/wikipedia/Happiness.txt +3 -0
  54. package/corpus/prose/wikipedia/Heart.txt +4 -0
  55. package/corpus/prose/wikipedia/Horse.txt +4 -0
  56. package/corpus/prose/wikipedia/House.txt +6 -0
  57. package/corpus/prose/wikipedia/Human.txt +4 -0
  58. package/corpus/prose/wikipedia/Insect.txt +6 -0
  59. package/corpus/prose/wikipedia/Interactive_fiction.txt +112 -0
  60. package/corpus/prose/wikipedia/Knowledge.txt +5 -0
  61. package/corpus/prose/wikipedia/Knowledge_representation_and_reasoning.txt +87 -0
  62. package/corpus/prose/wikipedia/LICENSE-NOTICE +94 -0
  63. package/corpus/prose/wikipedia/Language.txt +10 -0
  64. package/corpus/prose/wikipedia/Learning.txt +4 -0
  65. package/corpus/prose/wikipedia/Mammal.txt +3 -0
  66. package/corpus/prose/wikipedia/Memory.txt +5 -0
  67. package/corpus/prose/wikipedia/Milk.txt +1 -0
  68. package/corpus/prose/wikipedia/Mountain.txt +1 -0
  69. package/corpus/prose/wikipedia/Natural_language_processing.txt +211 -0
  70. package/corpus/prose/wikipedia/Ostrich.txt +2 -0
  71. package/corpus/prose/wikipedia/Owl.txt +2 -0
  72. package/corpus/prose/wikipedia/Penguin.txt +2 -0
  73. package/corpus/prose/wikipedia/Plant.txt +5 -0
  74. package/corpus/prose/wikipedia/Rain.txt +1 -0
  75. package/corpus/prose/wikipedia/Resource_Description_Framework.txt +184 -0
  76. package/corpus/prose/wikipedia/River.txt +1 -0
  77. package/corpus/prose/wikipedia/School.txt +8 -0
  78. package/corpus/prose/wikipedia/Sea.txt +1 -0
  79. package/corpus/prose/wikipedia/Semantic_Web.txt +114 -0
  80. package/corpus/prose/wikipedia/Semantic_reasoner.txt +29 -0
  81. package/corpus/prose/wikipedia/Snow.txt +5 -0
  82. package/corpus/prose/wikipedia/Sun.txt +5 -0
  83. package/corpus/prose/wikipedia/Teacher.txt +4 -0
  84. package/corpus/prose/wikipedia/Team.txt +3 -0
  85. package/corpus/prose/wikipedia/Text-based_game.txt +17 -0
  86. package/corpus/prose/wikipedia/Tool.txt +4 -0
  87. package/corpus/prose/wikipedia/Tree.txt +7 -0
  88. package/corpus/prose/wikipedia/Weather.txt +4 -0
  89. package/corpus/prose/wikipedia/Web_Ontology_Language.txt +133 -0
  90. package/corpus/prose/wikipedia/Wind.txt +8 -0
  91. package/corpus/prose/wikipedia/Writing.txt +5 -0
  92. package/corpus/seon/README.md +1 -0
  93. package/corpus/tier2/generate.mjs +18 -18
  94. package/corpus/tier2/manifest.json +3 -3
  95. package/data/games/hanoi-3.txt +8 -2
  96. package/package.json +24 -5
  97. package/src/adapters/corpus-lanes.mjs +13 -0
  98. package/src/adapters/graph-build.mjs +5 -7
  99. package/src/adapters/import-closure.mjs +28 -0
  100. package/src/adapters/memory/blocks.mjs +5 -4
  101. package/src/adapters/memory/core.mjs +78 -5
  102. package/src/adapters/memory/shacl.mjs +12 -0
  103. package/src/adapters/providers/graph-service.mjs +12 -5
  104. package/src/adapters/tracked-files.mjs +17 -0
  105. package/src/domain/ask-vocab.mjs +2 -0
  106. package/src/domain/ask.mjs +225 -13
  107. package/src/domain/cli-verbs.mjs +201 -0
  108. package/src/domain/codegraph.mjs +142 -56
  109. package/src/domain/completions/graph-adapter.mjs +1 -1
  110. package/src/domain/completions/group.mjs +3 -17
  111. package/src/domain/completions/infer.mjs +4 -13
  112. package/src/domain/completions/rank.mjs +6 -19
  113. package/src/domain/grammar/lexicon-core.json +1 -1
  114. package/src/domain/hash.mjs +36 -13
  115. package/src/domain/interpret/fuzzy.mjs +7 -2
  116. package/src/domain/interpret/normalize.mjs +9 -0
  117. package/src/domain/interpret/strategies/keywords.mjs +19 -9
  118. package/src/domain/memory/capability.mjs +22 -3
  119. package/src/domain/memory/touched-facts.mjs +17 -0
  120. package/src/domain/module-paths.mjs +9 -0
  121. package/src/domain/persona/tiers.mjs +1 -1
  122. package/src/domain/planning.mjs +37 -0
  123. package/src/domain/prose.mjs +10 -2
  124. package/src/domain/relative-specifiers.mjs +12 -0
  125. package/src/domain/router/registry.mjs +3 -2
  126. package/src/domain/router/results.mjs +5 -18
  127. package/src/domain/seeded-random.mjs +33 -0
  128. package/src/domain/syllogise.mjs +10 -7
  129. package/src/domain/text-stats.mjs +31 -0
  130. package/src/services/chat.mjs +720 -182
  131. package/src/services/extract-facts.mjs +155 -0
  132. package/src/services/import-file.mjs +2 -2
  133. package/src/services/ledger-viz.mjs +6 -1
  134. package/src/services/sentences.mjs +26 -0
  135. package/src/surfaces/web/memory-ask-browser.bundle.js +11390 -360
  136. package/src/tools/graph-load.mjs +7 -1
  137. package/src/tools/readme-docs.mjs +113 -0
  138. package/src/tools/schema-docs.mjs +2 -2
  139. package/corpus/namenet/generate.mjs +0 -309
  140. package/corpus/wordnet/generate.mjs +0 -332
  141. package/src/adapters/prose-tokens.mjs +0 -98
  142. package/src/adapters/wordnet-source.mjs +0 -70
  143. package/src/domain/corpus-matrix.mjs +0 -87
  144. package/src/domain/inflect.mjs +0 -67
  145. package/src/domain/licences.mjs +0 -68
  146. package/src/domain/markdown-links.mjs +0 -55
  147. package/src/domain/persona/codegen.mjs +0 -123
  148. package/src/domain/publish-gate.mjs +0 -41
  149. package/src/domain/schemaorg/turtle.mjs +0 -25
  150. package/src/domain/semcor/parse.mjs +0 -87
  151. package/src/domain/version-stamp.mjs +0 -36
  152. package/src/domain/wordnet/yaml.mjs +0 -133
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The Fact rows one write touched — created, or re-asserted with a new
3
+ * provenance entry — diffed by (id → provenance) across a before/after pair of
4
+ * readFactRows() snapshots.
5
+ *
6
+ * Provenance is the key because a re-assertion of a fact that is already stored
7
+ * changes nothing else about the row: appendFact unions the new provenance in
8
+ * by id, so a row whose provenance string moved is a row this write reached.
9
+ *
10
+ * Not every write produces a touched row, and that is the honest answer rather
11
+ * than a gap: the rule-teach shapes (compose2/filter/recursive) store a Rule,
12
+ * not a Fact, so they touch no Fact row and are correctly reported as none.
13
+ */
14
+ export function touchedFactRows(before, after) {
15
+ const provenanceBefore = new Map(before.map((r) => [r.id, r.provenance]));
16
+ return after.filter((r) => provenanceBefore.get(r.id) !== r.provenance);
17
+ }
@@ -0,0 +1,9 @@
1
+ // module-paths.mjs — classifying a module by its path, for the graph builders and
2
+ // the rankers that read what they build.
3
+
4
+ /** Does this module path (or a lowercased path-shaped label) belong to test code?
5
+ * Covers `test/` and `tests/` segments, Python's `test_*.py` convention, and
6
+ * .NET's `*.Tests` assembly suffix. Case-sensitive: callers holding mixed-case
7
+ * paths lowercase first. */
8
+ export const isTestPath = (p) =>
9
+ /(^|\/)tests?\//.test(p) || /(^|\/)test_[^/]*\.py$/.test(p) || /\.tests(\.|$)/.test(p);
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Pure throughout — these read in-memory maps a caller loaded from disk, so
6
6
  // they are testable with no WordNet clone present. The loading lives in
7
- // src/adapters/wordnet-source.mjs, the fact targets and the run itself in
7
+ // scripts/lib/wordnet-source.mjs, the fact targets and the run itself in
8
8
  // scripts/build-persona-tiers.mjs.
9
9
 
10
10
  // human-base's own category roots, plus every hypernym TARGET term Small's
@@ -83,3 +83,40 @@ export function findReachableSet(startState, applyActions, { maxDepth = 50, stat
83
83
  }
84
84
  return results;
85
85
  }
86
+
87
+ /**
88
+ * Bounded, cycle-safe breadth-first walk that yields one array of newly-visited
89
+ * successor items per depth level (depths 1..maxDepth), starting from `start`
90
+ * which is pre-marked visited and never yielded. `successorsOf(id)` returns the
91
+ * successor items of a node id; `keyOf(item)` is the identity used to dedup and
92
+ * to seed the next frontier (default: the item itself is its own key).
93
+ *
94
+ * Each yielded level is in discovery order; a caller that needs a stable order
95
+ * sorts it. Empty levels are yielded too (the walk stops once a frontier is
96
+ * empty), so a caller collecting per-depth batches should skip empty ones.
97
+ *
98
+ * @template T
99
+ * @param {*} start
100
+ * @param {(id: *) => Iterable<T>} successorsOf
101
+ * @param {{ maxDepth?: number, keyOf?: (item: T) => * }} [opts]
102
+ * @returns {Generator<T[]>}
103
+ */
104
+ export function* bfsLevels(start, successorsOf, { maxDepth = 8, keyOf = (x) => x } = {}) {
105
+ const visited = new Set([start]);
106
+ let frontier = [start];
107
+ for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
108
+ const level = [];
109
+ const nextFrontier = [];
110
+ for (const id of frontier) {
111
+ for (const item of successorsOf(id) || []) {
112
+ const key = keyOf(item);
113
+ if (visited.has(key)) continue;
114
+ visited.add(key);
115
+ level.push(item);
116
+ nextFrontier.push(key);
117
+ }
118
+ }
119
+ frontier = nextFrontier;
120
+ yield level;
121
+ }
122
+ }
@@ -4,8 +4,6 @@
4
4
  // `prose_tokens` attribute (self-describing) and inverted into
5
5
  // `entities.proseIndex` (word -> ids) for O(1) lookup.
6
6
 
7
- // Exported so src/domain/completions/group.mjs can filter splitIdentifierWords'
8
- // output (which doesn't apply this list itself) down to real content words.
9
7
  export const STOPWORDS = new Set(
10
8
  ("a an and or but the of to in on at for with from by as is are was were be been being " +
11
9
  "it its this that these those i you he she they we me my your our do does did not no " +
@@ -57,6 +55,16 @@ export function proseTokensFor({ name, doc } = {}) {
57
55
  return [...set].sort();
58
56
  }
59
57
 
58
+ /** A real content word: plain alphanumerics, no stopword. Tokenizers that re-admit
59
+ * stopwords (splitIdentifierWords, tokenizeBlock) narrow their output through this —
60
+ * unfiltered, shared stopwords alone would relate or cluster almost any two texts. */
61
+ export const isContentToken = (t) => /^[a-z0-9]+$/.test(t) && !STOPWORDS.has(t);
62
+
63
+ /** Wrap a block tokenizer so it yields only content tokens. */
64
+ export function makeContentTokens(tokenizeBlock) {
65
+ return (text) => tokenizeBlock(text).filter(isContentToken);
66
+ }
67
+
60
68
  /** Attach a `prose_tokens` attribute to every individual, from its (decomposed)
61
69
  * name and captured doc text — except Commit, whose `label` is a truncated
62
70
  * SHA, not a decomposable identifier: it tokenizes `message` instead. Mutates
@@ -0,0 +1,12 @@
1
+ // The relative import/export specifiers in a module's source text, unique and
2
+ // in first-seen order. Relative only: bare specifiers (wink-nlp) and node
3
+ // builtins are somebody else's concern — an import map, a shim. Covers the
4
+ // three forms a specifier arrives in: after a from keyword (static import and
5
+ // re-export), a dynamic import call, and a bare side-effect import.
6
+ const RELATIVE_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)["'](\.[^"']*)["']/g;
7
+
8
+ export function relativeSpecifiers(text) {
9
+ const specs = new Set();
10
+ for (const [, specifier] of text.matchAll(RELATIVE_SPECIFIER)) specs.add(specifier);
11
+ return [...specs];
12
+ }
@@ -11,6 +11,7 @@
11
11
 
12
12
  const PREFIXES = Object.freeze({
13
13
  cap: "urn:tmct:cap#", // the capability/operator vocabulary (this module)
14
+ taught: "urn:tmct:taught#", // taught-action operators + their world-state predicates (taught.mjs)
14
15
  mgx: "urn:tmct:mgx#", // tmct's code-graph predicates (imports/calls/tests/…)
15
16
  seon: "http://se-on.org/ontologies/seon.owl#", // software-evolution ontology classes
16
17
  });
@@ -28,8 +29,8 @@ export const VOCAB = Object.freeze({
28
29
  // name a graph entity the resolver must prove RESOLVES before the call fires.
29
30
  export const KINDS = Object.freeze({
30
31
  Symbol: "seon:CodeEntity", // any code symbol: function/method/class/module/attribute
31
- Module: "seon:Module",
32
- Class: "seon:ClassDefinition",
32
+ Module: "mgx:Module", // SEON has no JS-module class (its nearest are Namespace/main:File); owned
33
+ Class: "seon:ClassType", // SEON's real class for a class definition
33
34
  Query: "cap:FreeText", // lexical search string — no resolution precondition
34
35
  Kind: "cap:KindFilter", // enum: function|class|method|… (search filter)
35
36
  Package: "cap:PackageName", // optional architecture-scope filter
@@ -27,11 +27,9 @@
27
27
  // dispatchTool render* functions edge-for-edge.
28
28
 
29
29
  import { impactClosure, edgesOfKind, siteOf } from "../codegraph.mjs";
30
+ import { bfsLevels } from "../planning.mjs";
30
31
  import { uniqSort } from "./set-algebra.mjs";
31
-
32
- // A test-path label, mirroring codegraph.mjs's private isTestLabel (untested view).
33
- const isTestLabel = (s) =>
34
- /(^|\/)tests?\//.test(s) || /(^|\/)test_[^/]*\.py$/.test(s) || /\.tests(\.|$)/.test(s);
32
+ import { isTestPath } from "../module-paths.mjs";
35
33
 
36
34
  /** The module id an individual belongs to — a Module is itself; a fine symbol maps
37
35
  * through its site span (`mod:<path>`), else an `fn:<path>#name` id. Mirrors the
@@ -56,7 +54,7 @@ export function untestedModules(graph) {
56
54
  graph.individuals
57
55
  .filter((i) => (i.class || "") === "Module"
58
56
  && !testModules.has(i.id)
59
- && !isTestLabel(String(i.label).toLowerCase())
57
+ && !isTestPath(String(i.label).toLowerCase())
60
58
  && !covered.has(i.id))
61
59
  .map((i) => i.label),
62
60
  );
@@ -165,19 +163,8 @@ function subclassesLabels(graph, ind) {
165
163
  childrenOf.get(e.object).push({ id: e.subject, label: e.subjectLabel || e.subject });
166
164
  }
167
165
  const labels = [];
168
- const visited = new Set([ind.id]);
169
- let frontier = [ind.id];
170
- for (let depth = 1; depth <= 8 && frontier.length; depth += 1) {
171
- const next = [];
172
- for (const id of frontier) {
173
- for (const c of childrenOf.get(id) || []) {
174
- if (visited.has(c.id)) continue;
175
- visited.add(c.id);
176
- labels.push(c.label);
177
- next.push(c.id);
178
- }
179
- }
180
- frontier = next;
166
+ for (const level of bfsLevels(ind.id, (id) => childrenOf.get(id) || [], { maxDepth: 8, keyOf: (c) => c.id })) {
167
+ for (const c of level) labels.push(c.label);
181
168
  }
182
169
  return uniqSort(labels);
183
170
  }
@@ -0,0 +1,33 @@
1
+ // seeded-random.mjs — the single home for tmct's seedable, platform-stable
2
+ // pseudo-randomness: a mulberry32 generator and a Fisher-Yates shuffle driven by
3
+ // it. Both are pure arithmetic (no I/O, no imports), so a seed reproduces the
4
+ // same sequence on every machine and every run.
5
+ //
6
+ // The bench case generators (infbench, chatbench's graded pool) draw their
7
+ // committed fixtures through these, so the byte sequence they produce is part of
8
+ // those artifacts' identity: two callers on one seed must shuffle identically or
9
+ // the committed cases.jsonl / graded pool drift. That is the same reason the
10
+ // content-address hashes live once in hash.mjs beside this file — a deterministic
11
+ // primitive shared across writers has exactly one definition.
12
+
13
+ /** mulberry32 PRNG — small, seedable, deterministic across platforms. */
14
+ export function mulberry32(seed) {
15
+ let a = seed >>> 0;
16
+ return () => {
17
+ a = (a + 0x6d2b79f5) >>> 0;
18
+ let t = a;
19
+ t = Math.imul(t ^ (t >>> 15), t | 1);
20
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
21
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
22
+ };
23
+ }
24
+
25
+ /** Fisher-Yates over a copy, driven by the supplied rng. */
26
+ export function seededShuffle(arr, rng) {
27
+ const out = [...arr];
28
+ for (let i = out.length - 1; i > 0; i -= 1) {
29
+ const j = Math.floor(rng() * (i + 1));
30
+ [out[i], out[j]] = [out[j], out[i]];
31
+ }
32
+ return out;
33
+ }
@@ -1,7 +1,7 @@
1
1
  // syllogise.mjs — tmct's speculative-inference engine, growing toward
2
2
  // tier-5 "the Syllogist". Offline, deterministic: forward-chains entailments
3
3
  // over the OWL-labelled memory graph so a future query-time MISS becomes a
4
- // lookup. `syllogise()` (the materializing pass) only runs as the explicit
4
+ // lookup. `syllogise()` (the materialising pass) only runs as the explicit
5
5
  // `npx tmct syllogise` batch job; the pure kernels below may also be reused
6
6
  // for a small, bounded, read-only live check without writing anything.
7
7
  //
@@ -61,7 +61,7 @@ export const ENTAILED_DISJOINT_PROVENANCE = `entailed:${CAX_DW_RULE}`;
61
61
  export const CAX_DW_RULE_CONFIDENCE = 0.95;
62
62
 
63
63
  /** cls-svf1: x P y, y rdf:type C2, R owl:onProperty P, R owl:someValuesFrom
64
- * C2 |= x rdf:type R (OWL 2 RL Table 8). `owl:onProperty`'s stored value and
64
+ * C2 |= x rdf:type R (OWL 2 RL Table 6). `owl:onProperty`'s stored value and
65
65
  * a taught property edge's predicate differ in casing (normFactTerm'd vs.
66
66
  * raw vocabulary spelling), so the kernel below normalizes both before
67
67
  * comparing. */
@@ -311,7 +311,7 @@ export function deriveDisjointViolations(typeEdges, subClassEdges, disjointEdges
311
311
 
312
312
  /**
313
313
  * PURE cls-svf1: x P y, y rdf:type C2, R owl:onProperty P, R
314
- * owl:someValuesFrom C2 |= x rdf:type R (OWL 2 RL Table 8). `propertyEdges`
314
+ * owl:someValuesFrom C2 |= x rdf:type R (OWL 2 RL Table 6). `propertyEdges`
315
315
  * is every taught/prior-entailed object-property assertion (raw predicate
316
316
  * spelling); `restrictionEdges` is each restriction node's (property, target)
317
317
  * declaration. `y`'s type is lifted through its full ⊑-ancestor closure, so
@@ -659,7 +659,7 @@ export function findConsistencyViolations(typeEdges, subClassEdges, disjointEdge
659
659
  * Run one bounded speculative pass over the memory graph under `repoDir`.
660
660
  * Forward-chains the five rules in order — scm-sco, cax-sco, cax-dw,
661
661
  * cls-svf1, scm-svf1 — each seeing the prior rules' conclusions from this
662
- * same pass, and materializes each new conclusion via `appendFacts` with its
662
+ * same pass, and materialises each new conclusion via `appendFacts` with its
663
663
  * `entailed:*` provenance. Trust rides the entailed hook
664
664
  * (`min(premiseTrusts) x ruleConfidence`) when premises are resolvable in the
665
665
  * pre-pass snapshot, else falls back to the bare entailed prior.
@@ -764,7 +764,7 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
764
764
  subject: d.subject, predicate: TYPE_PREDICATE, object: d.object,
765
765
  provenance: ENTAILED_TYPE_PROVENANCE,
766
766
  // The ⊑ premise is cited as the DIRECT via⊑object edge even when the
767
- // taught chain is multi-hop: scm-sco materializes that edge (this same
767
+ // taught chain is multi-hop: scm-sco materialises that edge (this same
768
768
  // pass or an earlier one), and retraction re-VERIFIES every candidate
769
769
  // anyway, so a citation left dangling by budget truncation is inert.
770
770
  justification: [
@@ -969,14 +969,17 @@ function buildSurvivorDerivabilityCheck(rows) {
969
969
  }
970
970
 
971
971
  /**
972
- * A scoped retraction slice: JTMS-style dependency-directed removal.
972
+ * A scoped retraction slice: DRed (delete-and-rederive; Gupta, Mumick &
973
+ * Subrahmanian, SIGMOD 1993), NOT JTMS. It recomputes the MATERIALISATION —
974
+ * where a JTMS would recompute belief labels — so a store that moves rows is
975
+ * DRed by construction.
973
976
  * Retracting `subject ⊑ object` removes the fact, then cascades to any
974
977
  * purely-entailed fact — across all five rules' conclusions — whose persisted
975
978
  * justification cites a removed id. Each candidate is VERIFIED (re-derivable
976
979
  * from the surviving facts, not just "cited a removed id") before it is
977
980
  * actually removed, since a fact can have a second, independent derivation
978
981
  * path (a⊑b⊑d AND a⊑c⊑d both license a⊑d) that a bare delete-by-justification
979
- * walk would wrongly discard. Repeats in rounds — a removed mid-chain link
982
+ * walk would wrongly discard. This over-delete-then-re-verify IS DRed's shape. Repeats in rounds — a removed mid-chain link
980
983
  * can ripple — bounded by `budget` (max facts examined+removed) and `depth`
981
984
  * (max cascade rounds).
982
985
  *
@@ -0,0 +1,31 @@
1
+ // text-stats.mjs — pure token-frequency statistics shared by the ranking tiers.
2
+ // Deterministic, no model calls. The IDF weight log(1 + N/(1+df)) is near-zero
3
+ // for tokens present in almost every document and grows for rare ones.
4
+
5
+ /**
6
+ * Inverse-document-frequency weight for a token seen in `df` of `N` documents.
7
+ * @param {number} N document count
8
+ * @param {number} df document frequency of the token
9
+ * @returns {number}
10
+ */
11
+ export function idfWeight(N, df) {
12
+ return Math.log(1 + N / (1 + df));
13
+ }
14
+
15
+ /**
16
+ * IDF over a document set given as an `id -> tokens` map. Document frequency
17
+ * counts each document once per distinct token; N is the document count.
18
+ *
19
+ * @param {Record<string, string[]>} idsToTokens
20
+ * @returns {{ N: number, df: Map<string, number>, idf: (t: string) => number }}
21
+ */
22
+ export function idfOver(idsToTokens) {
23
+ const ids = Object.keys(idsToTokens || {});
24
+ const N = ids.length;
25
+ const df = new Map();
26
+ for (const id of ids) {
27
+ for (const t of new Set(idsToTokens[id])) df.set(t, (df.get(t) || 0) + 1);
28
+ }
29
+ const idf = (t) => idfWeight(N, df.get(t) || 0);
30
+ return { N, df, idf };
31
+ }