@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
@@ -61,7 +61,7 @@
61
61
  {
62
62
  "id": "human",
63
63
  "kind": "domain",
64
- "description": "The default human-world persona (PLAN_SEED.md): everyday people, places, objects, nature, time/events, body/food and mind vocabulary, hand-curated from Open English WordNet (CC-BY-4.0) and bridged to Schema.org's (Apache-2.0) top-level classes — replaces the code-domain SEON+ConceptNet default.",
64
+ "description": "The default human-world persona: everyday people, places, objects, nature, time/events, body/food and mind vocabulary, hand-curated from Open English WordNet (CC-BY-4.0) and bridged to Schema.org's (Apache-2.0) top-level classes — replaces the code-domain SEON+ConceptNet default.",
65
65
  "source": {
66
66
  "kind": "curated",
67
67
  "tool": "corpus/tier2/generate.mjs"
@@ -75,7 +75,7 @@
75
75
  {
76
76
  "id": "human-medium",
77
77
  "kind": "domain",
78
- "description": "The Medium tier of the default human-world persona (PLAN_SEED.md): incremental facts beyond Small only — activated alongside \"human\" via --persona-size medium, never active by default.",
78
+ "description": "The Medium tier of the default human-world persona: incremental facts beyond Small only — activated alongside \"human\" via --persona-size medium, never active by default.",
79
79
  "source": {
80
80
  "kind": "curated",
81
81
  "tool": "corpus/tier2/generate.mjs"
@@ -89,7 +89,7 @@
89
89
  {
90
90
  "id": "human-large",
91
91
  "kind": "domain",
92
- "description": "The Large tier of the default human-world persona (PLAN_SEED.md): incremental facts beyond Medium only — activated alongside \"human\" via --persona-size large, never active by default.",
92
+ "description": "The Large tier of the default human-world persona: incremental facts beyond Medium only — activated alongside \"human\" via --persona-size large, never active by default.",
93
93
  "source": {
94
94
  "kind": "curated",
95
95
  "tool": "corpus/tier2/generate.mjs"
@@ -5,8 +5,14 @@
5
5
  # the goal is that every disk rests on peg-c. solve it.
6
6
  #
7
7
  # Variations, each stretching a different direction:
8
- # scale — first teach: "disk-4 is a disk. disk-3 is smaller than disk-4."
9
- # start all four on peg-a, same goal. 15 moves (2^4 - 1).
8
+ # scale — first teach: "disk-4 is a disk."
9
+ # then: "disk-1 is smaller than disk-4. disk-2 is smaller than disk-4.
10
+ # disk-3 is smaller than disk-4."
11
+ # then: "disk-1 rests on disk-2. disk-2 rests on disk-3. disk-3 rests on
12
+ # disk-4. disk-4 rests on peg-a." — same goal. 15 moves (2^4 - 1).
13
+ # Every smaller-than pair is taught: the relation is stored as given and
14
+ # is not chased transitively, so disk-3 < disk-4 alone leaves disk-1 and
15
+ # disk-2 with no legal move onto disk-4 and no plan exists.
10
16
  # any start — disk-1 rests on peg-b. disk-2 rests on peg-c. disk-3 rests on peg-a.
11
17
  # the goal is that every disk rests on peg-c. solve it.
12
18
  # other goal — same start, "the goal is that every disk rests on peg-b. solve it."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
@@ -51,10 +51,23 @@
51
51
  "files": [
52
52
  "bin/",
53
53
  "src/",
54
+ "!src/adapters/pii-scan.mjs",
55
+ "!src/domain/corpus-matrix.mjs",
56
+ "!src/domain/inflect.mjs",
57
+ "!src/domain/licences.mjs",
58
+ "!src/domain/markdown-links.mjs",
59
+ "!src/domain/pack-manifest.mjs",
60
+ "!src/domain/persona/codegen.mjs",
61
+ "!src/domain/pii-rules.mjs",
62
+ "!src/domain/publish-gate.mjs",
63
+ "!src/domain/schemaorg/turtle.mjs",
64
+ "!src/domain/version-stamp.mjs",
54
65
  "README.md",
55
66
  "ROADMAP.md",
56
67
  "LICENSE",
57
68
  "corpus/",
69
+ "!corpus/wordnet/generate.mjs",
70
+ "!corpus/namenet/generate.mjs",
58
71
  "data/"
59
72
  ],
60
73
  "publishConfig": {
@@ -69,7 +82,9 @@
69
82
  },
70
83
  "scripts": {
71
84
  "help": "node bin/tmct.mjs --help",
72
- "test": "node --test \"test/**/*.test.mjs\"",
85
+ "test": "node --test --test-concurrency=8 \"test/**/*.test.mjs\"",
86
+ "test:smoke": "node --test test/smoke/*.test.mjs",
87
+ "test:fast": "node --test --test-concurrency=8 test/smoke/*.test.mjs test/fast/*.test.mjs test/tools/ask.test.mjs test/tools/server.test.mjs test/estate/import-layers.test.mjs",
73
88
  "test:e2e": "node --test \"e2e/**/*.test.mjs\"",
74
89
  "e2e:browsers": "playwright install chromium",
75
90
  "check:links": "node scripts/check-links.mjs",
@@ -78,8 +93,9 @@
78
93
  "check:licences": "node scripts/check-licences.mjs",
79
94
  "check:publint": "npx --no-install publint",
80
95
  "check:tool-docs": "node scripts/generate-tool-docs.mjs --check",
96
+ "check:budgets": "node scripts/check-tier-budgets.mjs",
81
97
  "check:publish": "node scripts/check-publish.mjs",
82
- "check:all": "npm run check:links && npm run check:pii && npm run check:licences && npm run check:publint && npm run check:tool-docs",
98
+ "check:all": "npm run check:links && npm run check:pii && npm run check:licences && npm run check:publint && npm run check:tool-docs && npm run check:budgets",
83
99
  "smoke:deploy": "node scripts/post-deploy-smoke.mjs",
84
100
  "chat": "node bin/tmct.mjs",
85
101
  "chat:repo": "node bin/tmct.mjs chat --repo",
@@ -122,13 +138,16 @@
122
138
  "build:demo-memory": "node scripts/build-demo-memory.mjs",
123
139
  "gen:tool-docs": "node scripts/generate-tool-docs.mjs",
124
140
  "gen:collisions": "node scripts/generate-real-word-collisions.mjs",
141
+ "gen:prose-corpus": "node scripts/fetch-prose-corpus.mjs",
125
142
  "gen:variants": "node scripts/generate-template-variants.mjs",
126
- "extract:facts": "node scripts/extract-facts-from-text.mjs"
143
+ "extract": "node bin/tmct.mjs extract",
144
+ "extract:facts": "node bin/tmct.mjs extract"
127
145
  },
128
146
  "devDependencies": {
129
147
  "esbuild": "^0.28.1",
130
148
  "ink-testing-library": "^4.0.0",
131
149
  "playwright": "1.61.1",
132
- "publint": "0.3.21"
150
+ "publint": "0.3.21",
151
+ "yaml": "2.9.0"
133
152
  }
134
153
  }
@@ -0,0 +1,13 @@
1
+ // Discover corpus lanes on disk: every .jsonl under `dir` (recursively),
2
+ // returned as lane names — the path relative to dir, without the .jsonl suffix
3
+ // — sorted. Sharded lane families live one directory down, e.g.
4
+ // games/openers.jsonl -> "games/openers".
5
+ import { readdirSync } from "node:fs";
6
+ import { join, relative } from "node:path";
7
+
8
+ export function corpusLanes(dir) {
9
+ return readdirSync(dir, { withFileTypes: true, recursive: true })
10
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
11
+ .map((entry) => relative(dir, join(entry.parentPath, entry.name)).slice(0, -".jsonl".length))
12
+ .sort();
13
+ }
@@ -15,12 +15,10 @@
15
15
  // mgx:touchesSymbol Commit -> CodeEntity (commit changed-line-range x symbol span)
16
16
  // mgx:callsSymbol Function/Method -> Function/Class (symbol-granular, unambiguous)
17
17
  // seon:containsCodeEntity Class -> Method/Attribute (class membership)
18
- // mgx:subclassOf Class -> Class (inheritance)
18
+ // seon:hasSuperType Class -> Class (inheritance)
19
19
 
20
- import { attachProseTokens, buildProseIndex } from "./prose-tokens.mjs";
21
-
22
- const isTestPath = (p) =>
23
- p.startsWith("tests/") || /(^|\/)tests?\//.test(p) || /(^|\/)test_[^/]*\.py$/.test(p) || /\.tests(\.|$)/.test(p);
20
+ import { attachProseTokens, buildProseIndex } from "../domain/prose.mjs";
21
+ import { isTestPath } from "../domain/module-paths.mjs";
24
22
 
25
23
  const lastIdent = (name) => {
26
24
  const m = String(name).match(/([A-Za-z_][A-Za-z0-9_]*)\s*$/);
@@ -145,7 +143,7 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
145
143
  if (d.raises?.length) attrs.push({ prop: "seon:throwsException", key: "raises", value: list(d.raises) });
146
144
  if (d.catches?.length) attrs.push({ prop: "seon:catchesException", key: "catches", value: list(d.catches) });
147
145
  if (d.self_fields?.length) attrs.push({ prop: "seon:accessesField", key: "self_fields", value: list(d.self_fields) });
148
- if (d.subkind) attrs.push({ prop: "seon:subKind", key: "subkind", value: String(d.subkind) });
146
+ if (d.subkind) attrs.push({ prop: "mgx:subKind", key: "subkind", value: String(d.subkind) });
149
147
  if (d.is_static) attrs.push({ prop: "seon:isStatic", key: "isStatic", value: "true" });
150
148
  if (d.is_abstract) attrs.push({ prop: "seon:isAbstract", key: "isAbstract", value: "true" });
151
149
  if (d.is_constant) attrs.push({ prop: "seon:isConstant", key: "isConstant", value: "true" });
@@ -392,7 +390,7 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
392
390
  { prop: "seon:isStatic", note: "@staticmethod/@classmethod" },
393
391
  { prop: "seon:isAbstract", note: "@abstractmethod/@abstractproperty" },
394
392
  { prop: "seon:isConstant", note: "ALL_CAPS module global" },
395
- { prop: "seon:subKind", note: "type flavour on a Class define when not a plain class (interface/enum/struct/record); kind stays class" },
393
+ { prop: "mgx:subKind", note: "type flavour on a Class define when not a plain class (interface/enum/struct/record); kind stays class; owned — SEON has no subKind property" },
396
394
  { prop: "seon:hasAccessModifier", note: "visibility from leading underscore (private/protected); public omitted" },
397
395
  { prop: "seon:hasDoc", note: "first docstring line (capped) — one-line purpose without the body" },
398
396
  ],
@@ -0,0 +1,28 @@
1
+ // Every file under `root` that `entry`'s relative imports reach, walked from
2
+ // source at read time and returned as paths relative to root, sorted.
3
+ //
4
+ // `seeds` names extra entry points to fold into the same closure — modules that
5
+ // arrive through a computed specifier a static read cannot follow (a lemma/POS
6
+ // tier loaded by a variable), so they and everything they reach still get
7
+ // walked.
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { dirname, join, relative, resolve } from "node:path";
10
+
11
+ import { relativeSpecifiers } from "../domain/relative-specifiers.mjs";
12
+
13
+ export function importClosure(entry, { root, seeds = [] } = {}) {
14
+ const reached = new Set();
15
+ const visit = (file) => {
16
+ const rel = relative(root, file);
17
+ if (reached.has(rel) || !existsSync(file)) return;
18
+ reached.add(rel);
19
+ if (file.endsWith(".json")) return;
20
+ const source = readFileSync(file, "utf8");
21
+ for (const specifier of relativeSpecifiers(source)) {
22
+ visit(resolve(dirname(file), specifier));
23
+ }
24
+ };
25
+ visit(join(root, entry));
26
+ for (const file of seeds) visit(join(root, file));
27
+ return [...reached].sort();
28
+ }
@@ -6,7 +6,8 @@
6
6
 
7
7
  import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
8
8
  import { join } from "node:path";
9
- import { splitIdentifierWords, tokenizeProse } from "../prose-tokens.mjs";
9
+ import { splitIdentifierWords, tokenizeProse } from "../../domain/prose.mjs";
10
+ import { idfWeight } from "../../domain/text-stats.mjs";
10
11
  import { SOURCE_PRIOR } from "../../domain/memory/trust.mjs";
11
12
 
12
13
  // A block inherits its Source's trust (operator 1.0, corpus 0.7); retrieval
@@ -182,12 +183,12 @@ export async function retrieveBlocks(dir, query, k = 3) {
182
183
  if (!qTokens.length) return [];
183
184
 
184
185
  const sets = entries.map(([, b]) => new Set(b.tokens || []));
185
- // idf per query token, computed once: log(1 + N/(1+df)) the codegraph.mjs
186
- // locate weighting (~0 for ubiquitous tokens, large for rare ones).
186
+ // idf per query token, computed once over the block set (~0 for ubiquitous
187
+ // tokens, large for rare ones) — the same weighting codegraph.mjs locate uses.
187
188
  const idf = new Map(qTokens.map((t) => {
188
189
  let df = 0;
189
190
  for (const s of sets) if (s.has(t)) df += 1;
190
- return [t, Math.log(1 + N / (1 + df))];
191
+ return [t, idfWeight(N, df)];
191
192
  }));
192
193
  const scored = [];
193
194
  for (let i = 0; i < N; i += 1) {
@@ -4,10 +4,17 @@
4
4
  // appendFact), and Sessions are all typed twice — payload `class` and an
5
5
  // `rdf:type` attribute. Every append is crash-safe and idempotent (utterance
6
6
  // ids are deterministic, fact ids hash the triple).
7
+ //
8
+ // A Fact is textbook RDF reification: `rdf:type rdf:Statement` plus
9
+ // rdf:subject/predicate/object, which carries the per-triple provenance and
10
+ // polarity stamps a bare triple has no room for. This is the RDF 1.1
11
+ // reification vocabulary, which RDF 1.2 reclassifies as legacy (steering new
12
+ // systems toward triple terms) but does not deprecate. See
13
+ // docs/references/schemas/rdf-reification-and-rdf-star.md.
7
14
 
8
15
  import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
9
16
  import { dirname, join } from "node:path";
10
- import { proseTokensFor, buildProseIndex } from "../prose-tokens.mjs";
17
+ import { proseTokensFor, buildProseIndex } from "../../domain/prose.mjs";
11
18
  import { fnv1aHex, normText, normFactTerm, normFactPredicate, factIdFor, factIdForTriple } from "../../domain/hash.mjs";
12
19
 
13
20
  // Fact identity (normalization + id derivation) lives in hash.mjs — the one
@@ -80,7 +87,7 @@ const MEMORY_VOCABULARY = [
80
87
  { prop: "mgx:ruleBaseCase", note: "recursive only: the base-case relation name (hop zero)" },
81
88
  { prop: "mgx:ruleRecStep", note: "recursive only: the self-referential recursive-step relation name" },
82
89
  { prop: CREATED_AT_PROP, note: "when an individual was FIRST written, ISO-8601 (first-write-wins on upsert); the audit 'when', the recency input to trust, the novelty signal" },
83
- { prop: UPDATED_AT_PROP, note: "when an individual's OWN attributes were last mutated in place (upsertSession, recomputeFactTrust, recomputeSourceReliability) — most individuals never carry this and instead derive 'updated' from codegraph.mjs's derivedUpdatedAt (max createdAt over their edges)" },
90
+ { prop: UPDATED_AT_PROP, note: "an audit / last-modified stamp: when an individual's OWN attributes were last mutated in place (upsertSession, recomputeFactTrust, recomputeSourceReliability) — most individuals never carry it at all. NOT transaction time: a mutable stamp cannot record when the previous version stopped being current, so tmct is not bitemporal" },
84
91
  { prop: DERIVED_FROM_PROP, predicate: "derivedFrom", note: "umbrella: a Fact derived from a Source (or another Fact). ext ref prov:wasDerivedFrom (UNVERIFIED-pending-web-check)" },
85
92
  { prop: STATED_BY_PROP, predicate: "statedBy", note: "subPropertyOf derivedFrom: a Source directly asserts this Fact (one edge per independent source — replaces the factProvenance union)" },
86
93
  { prop: CANONICALISED_FROM_PROP, predicate: "canonicalisedFrom", note: "subPropertyOf derivedFrom: a canonical Fact cleaned from a raw Block/Source, never replacing it" },
@@ -519,8 +526,8 @@ export async function snapshotMemory(dir, { retentionVersions } = {}) {
519
526
  * append creates the file). The result is a raw entities payload;
520
527
  * parseEntities() loads it. */
521
528
  export async function loadMemory(dir) {
522
- if (isMemoryHandle(dir)) return dir.payload;
523
- if (isSqliteHandle(dir)) return readSqlitePayload(dir);
529
+ if (isMemoryHandle(dir)) return migrateLegacyFactIds(dir.payload);
530
+ if (isSqliteHandle(dir)) return migrateLegacyFactIds(readSqlitePayload(dir));
524
531
  let text;
525
532
  try {
526
533
  text = await readFile(memoryGraphFile(dir), "utf8");
@@ -528,7 +535,50 @@ export async function loadMemory(dir) {
528
535
  if (e?.code === "ENOENT") return emptyMemory();
529
536
  throw e;
530
537
  }
531
- return JSON.parse(text);
538
+ return migrateLegacyFactIds(JSON.parse(text));
539
+ }
540
+
541
+ // A Fact id written before factIdFor widened to 64 bits — `fact:` + exactly 8
542
+ // hex. A current id is 16 hex, so this anchored test never matches one, and a
543
+ // migrated store pays only string checks with no rehash on load.
544
+ const LEGACY_FACT_ID_RE = /^fact:[0-9a-f]{8}$/;
545
+
546
+ /** Bring a store written under the old 32-bit fact id up to the current
547
+ * factIdFor id, in place, on load. Every Fact carries its own (s, p, o) in
548
+ * rdf:subject/predicate/object, so the current id recomputes from the payload
549
+ * with no external key. Rewrites the Fact ids and everything that points at
550
+ * them by id: statedBy/derivedFrom (and every other) edge endpoint, a rule's
551
+ * mgx:factJustification premise-id list, and derived_from links. Idempotent —
552
+ * a Fact already on a wide id is skipped by LEGACY_FACT_ID_RE, so a
553
+ * fully-migrated store makes no change and does no work beyond the shape test.
554
+ * This is what lets a pre-widening store keep resolving its facts: a lookup by
555
+ * the current id finds the Fact because load moved it onto that id. */
556
+ function migrateLegacyFactIds(payload) {
557
+ if (!Array.isArray(payload?.individuals)) return payload;
558
+ const remap = new Map(); // old fact id -> current fact id
559
+ for (const ind of payload.individuals) {
560
+ if (ind?.class !== FACT_CLASS || !LEGACY_FACT_ID_RE.test(ind.id || "")) continue;
561
+ const get = (k) => (ind.attributes || []).find((a) => a?.key === k)?.value || "";
562
+ const currentId = factIdFor(get("subject"), get("predicate"), get("object"));
563
+ if (currentId === ind.id) continue;
564
+ remap.set(ind.id, currentId);
565
+ ind.id = currentId;
566
+ }
567
+ if (!remap.size) return payload;
568
+ const remapId = (id) => remap.get(id) || id;
569
+ for (const group of payload.objectProperties || []) {
570
+ for (const e of group.examples || []) {
571
+ if (!e) continue;
572
+ if (remap.has(e.subject)) e.subject = remap.get(e.subject);
573
+ if (remap.has(e.object)) e.object = remap.get(e.object);
574
+ }
575
+ }
576
+ for (const ind of payload.individuals) {
577
+ if (Array.isArray(ind?.derived_from) && ind.derived_from.length) ind.derived_from = ind.derived_from.map(remapId);
578
+ const just = (ind?.attributes || []).find((a) => a?.prop === "mgx:factJustification");
579
+ if (just?.value) just.value = just.value.split(" ").filter(Boolean).map(remapId).join(" ");
580
+ }
581
+ return payload;
532
582
  }
533
583
 
534
584
  /** Persist a mutated payload back to `dir`: an atomic file write (Backend A),
@@ -627,6 +677,29 @@ function sourceIdFor(desc) {
627
677
 
628
678
  const sourceLabel = (id) => String(id).replace(/^src:/, "");
629
679
 
680
+ // The read-side of the Source split ontology/tmct-core.ttl declares. tmct:Source
681
+ // is one flat class over a sourceType key whose values fall into all three of
682
+ // PROV's disjoint top classes; this maps each stored sourceType to its read-side
683
+ // subclass and PROV top class. Nothing on disk changes — sourceType is already
684
+ // stored on every Source — so this is derivation, not migration.
685
+ const PROV_CLASS_BY_SOURCE_TYPE = Object.freeze({
686
+ operator: { subClass: "tmct:AgentSource", prov: "prov:Agent" },
687
+ teach: { subClass: "tmct:AgentSource", prov: "prov:Agent" },
688
+ provider: { subClass: "tmct:AgentSource", prov: "prov:Agent" },
689
+ corpus: { subClass: "tmct:DocumentSource", prov: "prov:Entity" },
690
+ corpusWeak: { subClass: "tmct:DocumentSource", prov: "prov:Entity" },
691
+ web: { subClass: "tmct:DocumentSource", prov: "prov:Entity" },
692
+ extracted: { subClass: "tmct:DocumentSource", prov: "prov:Entity" },
693
+ entailed: { subClass: "tmct:ActivitySource", prov: "prov:Activity" },
694
+ });
695
+
696
+ /** The read-side PROV subclass and top class a Source's mgx:sourceType maps to,
697
+ * or null for an unrecognised type (never force-fit). Mirrors the Source-split
698
+ * subclasses of tmct:Source in ontology/tmct-core.ttl. */
699
+ export function provSourceClassFor(sourceType) {
700
+ return PROV_CLASS_BY_SOURCE_TYPE[sourceType] || null;
701
+ }
702
+
630
703
  /** Upsert a Source individual (deterministic id → idempotent, edges never
631
704
  * dangle). createdAt is first-write-wins; a recovered @<ts> (desc.createdAt)
632
705
  * seeds it when present. Returns the Source id, or null for an unknown kind. */
@@ -31,6 +31,12 @@ const RULE_SLOT_PROPS = {
31
31
  ],
32
32
  };
33
33
 
34
+ // A term that runs past a sentence terminator into the next sentence
35
+ // ("disk-2. disk-2 rests on disk-3") is a whole un-split line captured as one
36
+ // term, not a term. Requires the word character after the terminator, so an
37
+ // abbreviation or a version ("v1.2", "core.mjs") stays a legal term.
38
+ const SPANS_A_SENTENCE_BOUNDARY_RE = /[.!?]\s+\w/;
39
+
34
40
  function attrValue(ind, prop) {
35
41
  const a = (ind?.attributes || []).find((x) => x?.prop === prop);
36
42
  return a ? String(a.value ?? "") : undefined;
@@ -54,6 +60,12 @@ function checkFact(ind, violations) {
54
60
  for (const prop of ["rdf:subject", "rdf:predicate", "rdf:object"]) {
55
61
  if (!nonEmpty(attrValue(ind, prop))) violations.push(`a Fact needs a non-empty ${prop}`);
56
62
  }
63
+ for (const prop of ["rdf:subject", "rdf:object"]) {
64
+ const term = attrValue(ind, prop);
65
+ if (term !== undefined && SPANS_A_SENTENCE_BOUNDARY_RE.test(term)) {
66
+ violations.push(`a Fact's ${prop} must be a single term, not text spanning a sentence boundary (got ${JSON.stringify(term)})`);
67
+ }
68
+ }
57
69
  const prov = attrValue(ind, "mgx:factProvenance");
58
70
  if (prov !== undefined && !nonEmpty(prov)) violations.push("mgx:factProvenance, when present, must be non-empty");
59
71
  }
@@ -20,6 +20,7 @@ import {
20
20
  impactClosure,
21
21
  scoreSymbolsRanked,
22
22
  searchModulesRanked,
23
+ compileNameFilter,
23
24
  SEARCH_LIMIT,
24
25
  contextPlan,
25
26
  sizeBundle,
@@ -383,14 +384,20 @@ export function createGraphService(graph, { sourceAccess = false, repoRoot = nul
383
384
  const k = String(kind || "").trim().toLowerCase();
384
385
  const nm = String(name || "").trim();
385
386
  const dec = String(decorator || "").trim().toLowerCase();
386
- let nameRe = null;
387
- if (nm) {
388
- try { nameRe = new RegExp(nm, "i"); } catch { nameRe = null; }
389
- }
390
387
  let rankedInds; // Individual[], highest-ranked first
391
388
  if (k && k !== "module") {
389
+ let nameRe = null;
390
+ if (nm) {
391
+ const compiled = compileNameFilter(nm);
392
+ if (compiled.error) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: nm, detail: compiled.error });
393
+ nameRe = compiled;
394
+ }
392
395
  const tokens = rawQuery.toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
393
- rankedInds = scoreSymbolsRanked(graph, tokens, { kind: k, decFilter: dec, nameRe }).map((s) => s.ind);
396
+ try {
397
+ rankedInds = scoreSymbolsRanked(graph, tokens, { kind: k, decFilter: dec, nameRe }).map((s) => s.ind);
398
+ } catch (e) {
399
+ return miss(MISS_REASONS.UNRESOLVED_TERM, { term: nm, detail: String(e?.message || e) });
400
+ }
394
401
  } else {
395
402
  // label -> individual, scoped to this call: maps searchModulesRanked's path labels
396
403
  // back to real Individuals.
@@ -0,0 +1,17 @@
1
+ // Enumerate the repo's git-tracked files, optionally filtered by a git
2
+ // pathspec, via `git ls-files -z`. Node builtins + git only, no bare specifier:
3
+ // the pii and links CI jobs import this and run without npm ci.
4
+
5
+ import { execFileSync } from "node:child_process";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
10
+
11
+ export function trackedFiles(pattern) {
12
+ const args = ["ls-files", "-z"];
13
+ if (pattern) args.push(pattern);
14
+ return execFileSync("git", args, { cwd: REPO_ROOT, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 })
15
+ .split("\0")
16
+ .filter(Boolean);
17
+ }
@@ -593,6 +593,8 @@ export const CASCADE_NOISE = Object.freeze([
593
593
  "the", "a", "an", "some", "other", "about",
594
594
  "please", "pls", "plz", "kindly", "just", "simply", "maybe", "perhaps",
595
595
  "thanks", "thank", "ta", "cheers",
596
+ "too", "also", "well", "either",
597
+ "sorry", "oops", "actually", "mean", "meant", "rather",
596
598
  "hi", "hello", "hey", "yo", "hiya", "howdy", "ok", "okay",
597
599
  "matey", "mate", "buddy", "pal", "dude", "man", "bro", "bru", "fam",
598
600
  "friend", "sir", "maam", "folks", "guys", "everyone", "dear",