@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
@@ -1,332 +0,0 @@
1
- #!/usr/bin/env node
2
- // corpus/wordnet/generate.mjs — converts a LOCAL Open English WordNet (OEWN)
3
- // checkout into ConceptNet-shape fact rows. Curated + committed, tier-2-shaped
4
- // — NOT "tier-3" (corpus/README.md's tiering policy already uses that name
5
- // for something else, runtime-learned facts that are never committed); this
6
- // bundle is just too large to hand-author like the other tier-2 corpuses.
7
- // NOT part of the product path — a maintainer tool, run by hand, offline, $0;
8
- // its OUTPUT (corpus/wordnet/wordnet-{xl,full}.jsonl + manifest.json) is what
9
- // gets committed, never the source YAML itself.
10
- //
11
- // node corpus/wordnet/generate.mjs [yamlDir]
12
- // TMCT_WORDNET_YAML_DIR=/path/to/yaml node corpus/wordnet/generate.mjs
13
- //
14
- // Input: `~/projects/globalwordnet/english-wordnet/src/yaml/` by default (a
15
- // LOCAL clone, never vendored/committed — CC-BY-4.0, see LICENSE-NOTICE in
16
- // this directory) — Princeton WordNet's 107,526 synsets across
17
- // `noun.*.yaml`/`verb.*.yaml`/`adj.*.yaml`/`adv.*.yaml` (45 of the checkout's
18
- // 73 yaml files; the other 28 are `entries-<letter>.yaml` word-form indexes
19
- // this converter doesn't need — synset records alone carry every relation and
20
- // member list this converter reads).
21
- //
22
- // Two passes, same discipline as corpus/conceptnet/fetch-slice.mjs and
23
- // corpus/tier2/generate.mjs (deterministic, sorted, one JSON object per
24
- // line, `{start, rel, end, weight, surfaceText}` — the exact tier-1 slice
25
- // shape):
26
- // 1. load every synset (across ALL 45 files) into one global
27
- // synsetId -> record map — synset ids ("00034778-n") are globally
28
- // unique, POS suffix included, so a single flat Map is correct.
29
- // 2. walk every synset's structural relations + its own `members` list,
30
- // emitting one row per edge (see RELATION_MAP / synonymPairs below).
31
- //
32
- // The hand-rolled `parseYaml` this file reuses (imported, not duplicated) is
33
- // src/domain/wordnet/yaml.mjs's tiny YAML-subset reader — already proven
34
- // against this exact OEWN dump shape by the persona-tier tooling. Reusing it
35
- // (rather than adding a general YAML dependency, or re-deriving a second
36
- // hand-rolled parser) keeps this converter self-consistent with the rest of
37
- // the tooling that already reads this same source.
38
- //
39
- // Licence: Open English WordNet content is CC-BY-4.0 (Princeton WordNet +
40
- // Open English Wordnet team) — see LICENSE-NOTICE in this directory. The
41
- // code in this file is tmct code under the repository's MPL-2.0; only the
42
- // generated data (corpus/wordnet/*.jsonl) carries CC-BY-4.0.
43
-
44
- import { readFile, readdir, writeFile, mkdir } from "node:fs/promises";
45
- import { homedir } from "node:os";
46
- import { createHash } from "node:crypto";
47
- import { fileURLToPath } from "node:url";
48
- import { dirname, join } from "node:path";
49
- import { parseYaml } from "../../src/domain/wordnet/yaml.mjs";
50
-
51
- const HERE = dirname(fileURLToPath(import.meta.url));
52
- export const WORDNET_OUT_DIR = HERE;
53
-
54
- export const DEFAULT_YAML_DIR = join(homedir(), "projects", "globalwordnet", "english-wordnet", "src", "yaml");
55
-
56
- /** Resolve the input yaml directory: CLI positional arg > env var > default.
57
- * Exposed as a pure function (argv/env injectable) so it's unit-testable
58
- * without touching real process.argv/env. */
59
- export function resolveYamlDir(argv = process.argv.slice(2), env = process.env) {
60
- return argv[0] || env.TMCT_WORDNET_YAML_DIR || DEFAULT_YAML_DIR;
61
- }
62
-
63
- // ---- relation mapping table (pure, unit-tested) ----------------------------
64
- // WordNet relation key -> { rel: ConceptNet relation URI, flip }. `flip`
65
- // governs which side of the WordNet edge becomes ConceptNet's `start` vs
66
- // `end` — WordNet's meronymy relations point WHOLE -> part (the synset that
67
- // OWNS a `mero_part`/`mero_member` list is the whole; its listed target is
68
- // the part/member — confirmed via scripts/wordnet.py's own
69
- // inverse_synset_rels table: mero_part's inverse is holo_part, so "A mero_part
70
- // B" reads "B is a part-meronym of A", i.e. A HAS PART B), which is the
71
- // OPPOSITE of ConceptNet's own /r/PartOf convention (start=part, end=whole —
72
- // confirmed against a real corpus/conceptnet/slice.jsonl row: "action PartOf
73
- // keyboard" — action, the PART, is start). mero_part/mero_member both need
74
- // `flip: true` for exactly this reason. mero_substance needs NO flip:
75
- // ConceptNet's /r/MadeOf is start=whole, end=substance (confirmed against
76
- // slice.jsonl's "computer MadeOf hardware" — computer, the WHOLE, is start),
77
- // which already matches WordNet's own A(whole) -> mero_substance -> B
78
- // (substance) direction untouched. hypernym/causes/attribute/similar/also
79
- // are all natural A->B mappings with no direction conflict (the specific
80
- // synset/adjective source relation IS the ConceptNet start side already).
81
- export const RELATION_MAP = Object.freeze({
82
- hypernym: { rel: "/r/IsA", flip: false },
83
- mero_part: { rel: "/r/PartOf", flip: true },
84
- mero_member: { rel: "/r/PartOf", flip: true },
85
- mero_substance: { rel: "/r/MadeOf", flip: false },
86
- causes: { rel: "/r/Causes", flip: false },
87
- attribute: { rel: "/r/HasProperty", flip: false },
88
- similar: { rel: "/r/SimilarTo", flip: false },
89
- also: { rel: "/r/RelatedTo", flip: false },
90
- });
91
-
92
- // Deliberately excluded — no mapped ConceptNet relation, and task scope is to
93
- // use ONLY the relations Phase 1 already added to conceptnet-map.toml, never
94
- // invent a new map row: `entails` (verb implication — no ConceptNet analog)
95
- // and `exemplifies` (instance-of-category — closest is /r/IsA, but that would
96
- // blur "kind of" and "example of", a real semantic difference ConceptNet
97
- // itself keeps separate via /r/InstanceOf, which conceptnet-map.toml doesn't
98
- // carry). `definition`/`example` are free prose, never structured facts.
99
- export const SKIPPED_RELATIONS = Object.freeze(["entails", "exemplifies"]);
100
-
101
- // ---- term encoding (pure, unit-tested) -------------------------------------
102
- // Exact inverse of src/adapters/corpus/conceptnet.mjs's termText() decode
103
- // (`/^\/c\/en\/([^/]+)/` then `.replace(/_/g, " ")`): lowercase, spaces ->
104
- // underscores, wrapped as `/c/en/<term>`. Nothing else is touched — any other
105
- // punctuation (apostrophes, hyphens) round-trips through termText() unchanged
106
- // because termText only ever substitutes underscores back to spaces.
107
- export function encodeTerm(raw) {
108
- const t = String(raw ?? "").trim().toLowerCase().replace(/\s+/g, "_");
109
- return t ? `/c/en/${t}` : null;
110
- }
111
-
112
- /** `/c/en/ice_cream` -> "ice cream" — the human-readable form used in
113
- * surfaceText, mirroring corpus/tier2/generate.mjs's own `humanize`. */
114
- export const humanize = (term) => String(term ?? "").replace(/_/g, " ");
115
-
116
- // ---- synonym chaining (pure, unit-tested) ----------------------------------
117
- // A synset's `members` list are synonyms of each other. Chained N-1 (member
118
- // [0] paired with each of member[1..N-1]) rather than the full N*(N-1)/2
119
- // cross product — members average 1.72/synset (185,149 member-slots across
120
- // 107,526 synsets), and the chain already connects every member into one
121
- // component (a synonym-chases-synonym graph read), so the cross product would
122
- // roughly double the fact count for no new information the chain doesn't
123
- // already encode transitively.
124
- export function synonymPairs(members) {
125
- if (!Array.isArray(members) || members.length < 2) return [];
126
- const [first, ...rest] = members;
127
- return rest.map((m) => [first, m]);
128
- }
129
-
130
- // ---- pass 1: load every synset across every yaml file ----------------------
131
- const SYNSET_FILE_RE = /^(noun|verb|adj|adv)\..+\.yaml$/;
132
-
133
- export async function loadAllSynsets(yamlDir) {
134
- const all = await readdir(yamlDir);
135
- const files = all.filter((f) => SYNSET_FILE_RE.test(f)).sort();
136
- if (!files.length) {
137
- throw new Error(`${yamlDir}: no noun./verb./adj./adv. yaml files found — wrong path?`);
138
- }
139
- const bySynset = new Map();
140
- for (const f of files) {
141
- const text = await readFile(join(yamlDir, f), "utf8");
142
- const parsed = parseYaml(text);
143
- for (const [id, rec] of Object.entries(parsed)) bySynset.set(id, rec);
144
- }
145
- return { bySynset, files };
146
- }
147
-
148
- const repTerm = (synset) => {
149
- const m = Array.isArray(synset?.members) ? synset.members : [];
150
- return m.length ? m[0] : null;
151
- };
152
-
153
- // ---- pass 2: synset map -> deduped, sorted ConceptNet-shape rows -----------
154
-
155
- function makeRowBuilder() {
156
- const rows = new Map(); // dedupe key -> row
157
- const add = (rawSubject, rel, rawObject) => {
158
- const start = encodeTerm(rawSubject);
159
- const end = encodeTerm(rawObject);
160
- if (!start || !end || start === end) return; // self-loop / empty term — noise, not a fact
161
- const key = `${rel} ${start} ${end}`;
162
- if (rows.has(key)) return;
163
- rows.set(key, {
164
- start,
165
- rel,
166
- end,
167
- weight: 1,
168
- surfaceText: `[[${humanize(rawSubject)}]] ${rel.replace("/r/", "")} [[${humanize(rawObject)}]]`,
169
- });
170
- };
171
- return { rows, add };
172
- }
173
-
174
- const sortRows = (rows) => rows.slice().sort((a, b) => (
175
- a.rel !== b.rel ? (a.rel < b.rel ? -1 : 1)
176
- : a.start !== b.start ? (a.start < b.start ? -1 : 1)
177
- : a.end < b.end ? -1 : a.end > b.end ? 1 : 0
178
- ));
179
-
180
- /** Every structural + synonym-chain fact, deterministically sorted — the
181
- * wordnet-full.jsonl content. */
182
- export function buildFullFacts(bySynset) {
183
- const { rows, add } = makeRowBuilder();
184
- for (const synset of bySynset.values()) {
185
- const A = repTerm(synset);
186
- if (!A) continue;
187
- for (const [wnRel, { rel, flip }] of Object.entries(RELATION_MAP)) {
188
- const targets = Array.isArray(synset[wnRel]) ? synset[wnRel] : [];
189
- for (const targetId of targets) {
190
- const B = repTerm(bySynset.get(targetId));
191
- if (!B) continue;
192
- if (flip) add(B, rel, A); else add(A, rel, B);
193
- }
194
- }
195
- for (const [m0, mi] of synonymPairs(synset.members)) add(m0, "/r/Synonym", mi);
196
- }
197
- return sortRows([...rows.values()]);
198
- }
199
-
200
- // ---- XL slice: hypernym backbone + synonym chains for the most commonly-
201
- // referenced synsets, budget-bounded ----------------------------------------
202
-
203
- /** How many times each synset appears as a hypernym TARGET — a synset's
204
- * "commonly referenced" proxy: a category many other synsets specialize
205
- * (e.g. "person", "act", "object") is referenced far more often than a
206
- * narrow leaf synset. */
207
- export function hypernymRefCounts(bySynset) {
208
- const refCount = new Map();
209
- for (const synset of bySynset.values()) {
210
- const targets = Array.isArray(synset.hypernym) ? synset.hypernym : [];
211
- for (const t of targets) refCount.set(t, (refCount.get(t) || 0) + 1);
212
- }
213
- return refCount;
214
- }
215
-
216
- /** Build the bounded XL slice: the `budget` fact rows are split between the
217
- * hypernym backbone and synonym chains in the SAME proportion the full
218
- * corpus's real hypernym-edge-count : synonym-chain-fact-count ratio has
219
- * (measured from `full`, not a hand-picked constant) — the backbone gets the
220
- * hypernym edges whose TARGET is most commonly referenced first; the
221
- * synonym budget goes to the full chain of the most commonly-referenced
222
- * synsets (same ranking), walked in ranked order until the budget is spent. */
223
- export function buildXlFacts(bySynset, full, budget = 24000) {
224
- const refCount = hypernymRefCounts(bySynset);
225
-
226
- const hypernymTotal = full.filter((r) => r.rel === "/r/IsA").length;
227
- const synonymTotal = full.filter((r) => r.rel === "/r/Synonym").length;
228
- const denom = hypernymTotal + synonymTotal || 1;
229
- const hypernymBudget = Math.round(budget * (hypernymTotal / denom));
230
- const synonymBudget = budget - hypernymBudget;
231
-
232
- // Backbone: every hypernym EDGE (not yet a fact row), ranked by how
233
- // commonly-referenced its TARGET synset is, ties broken deterministically.
234
- const edges = [];
235
- for (const [id, synset] of bySynset) {
236
- const targets = Array.isArray(synset.hypernym) ? synset.hypernym : [];
237
- for (const t of targets) edges.push({ source: id, target: t });
238
- }
239
- edges.sort((a, b) => {
240
- const byRef = (refCount.get(b.target) || 0) - (refCount.get(a.target) || 0);
241
- if (byRef) return byRef;
242
- if (a.target !== b.target) return a.target < b.target ? -1 : 1;
243
- return a.source < b.source ? -1 : 1;
244
- });
245
-
246
- const { rows, add } = makeRowBuilder();
247
- for (const { source, target } of edges) {
248
- if (rows.size >= hypernymBudget) break;
249
- const A = repTerm(bySynset.get(source));
250
- const B = repTerm(bySynset.get(target));
251
- if (!A || !B) continue;
252
- add(A, "/r/IsA", B);
253
- }
254
-
255
- // Synonym chains: rank EVERY synset by the same "commonly referenced"
256
- // proxy (0 for a synset that never appears as a hypernym target), then walk
257
- // down taking each synset's FULL N-1 chain until the synonym budget is met
258
- // (a synset's chain is never split — "~budget", not an exact cap).
259
- const rankedSynsets = [...bySynset.entries()].sort((a, b) => {
260
- const byRef = (refCount.get(b[0]) || 0) - (refCount.get(a[0]) || 0);
261
- if (byRef) return byRef;
262
- return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
263
- });
264
- let synonymCount = 0;
265
- for (const [, synset] of rankedSynsets) {
266
- if (synonymCount >= synonymBudget) break;
267
- const pairs = synonymPairs(synset.members);
268
- if (!pairs.length) continue;
269
- for (const [m0, mi] of pairs) add(m0, "/r/Synonym", mi);
270
- synonymCount += pairs.length;
271
- }
272
-
273
- return sortRows([...rows.values()]);
274
- }
275
-
276
- // ---- output ------------------------------------------------------------
277
-
278
- const toJsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n") + "\n";
279
- const sha256 = (text) => createHash("sha256").update(text).digest("hex");
280
-
281
- async function main() {
282
- const yamlDir = resolveYamlDir();
283
- process.stderr.write(`corpus/wordnet/generate.mjs: reading ${yamlDir}\n`);
284
- const { bySynset, files } = await loadAllSynsets(yamlDir);
285
- process.stderr.write(` loaded ${bySynset.size} synsets across ${files.length} files\n`);
286
-
287
- const full = buildFullFacts(bySynset);
288
- const xl = buildXlFacts(bySynset, full);
289
- process.stderr.write(` wordnet-full: ${full.length} facts\n`);
290
- process.stderr.write(` wordnet-xl: ${xl.length} facts\n`);
291
-
292
- await mkdir(WORDNET_OUT_DIR, { recursive: true });
293
- const fullText = toJsonl(full);
294
- const xlText = toJsonl(xl);
295
- await writeFile(join(WORDNET_OUT_DIR, "wordnet-full.jsonl"), fullText);
296
- await writeFile(join(WORDNET_OUT_DIR, "wordnet-xl.jsonl"), xlText);
297
-
298
- const manifest = {
299
- version: 1,
300
- generated: "by corpus/wordnet/generate.mjs",
301
- corpuses: [
302
- {
303
- id: "wordnet-xl",
304
- kind: "language",
305
- description: "A bounded ~24,000-fact slice of Princeton WordNet / Open English WordNet, prioritizing the hypernym (IsA) backbone and synonym chains for the most commonly-referenced synsets. Mechanically derived from Open English WordNet (CC-BY-4.0).",
306
- source: { kind: "curated", tool: "corpus/wordnet/generate.mjs" },
307
- file: "wordnet-xl.jsonl",
308
- facts: xl.length,
309
- bytes: Buffer.byteLength(xlText),
310
- sha256: sha256(xlText),
311
- license: "CC-BY-4.0",
312
- },
313
- {
314
- id: "wordnet-full",
315
- kind: "language",
316
- description: "The complete ConceptNet-shape conversion of Princeton WordNet / Open English WordNet's structural relations (hypernym, meronymy, causes, attribute, similar, also) plus member-synonym chains. Derived from Open English WordNet (CC-BY-4.0).",
317
- source: { kind: "curated", tool: "corpus/wordnet/generate.mjs" },
318
- file: "wordnet-full.jsonl",
319
- facts: full.length,
320
- bytes: Buffer.byteLength(fullText),
321
- sha256: sha256(fullText),
322
- license: "CC-BY-4.0",
323
- },
324
- ],
325
- };
326
- const manifestText = JSON.stringify(manifest, null, 2) + "\n";
327
- await writeFile(join(WORDNET_OUT_DIR, "manifest.json"), manifestText);
328
- process.stderr.write(`wrote corpus/wordnet/manifest.json (${manifest.corpuses.length} corpuses)\n`);
329
- }
330
-
331
- const isMain = process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href;
332
- if (isMain) await main();
@@ -1,98 +0,0 @@
1
- // prose-tokens.mjs — the adapters layer's own tokenizer, shared by every
2
- // adapter that writes prose tokens (the memory store, graph-build). Adapters
3
- // may not import the domain layer, while prose.mjs (the graph/ask side's
4
- // canonical tokenizer) is domain and may not import adapters — so this layer
5
- // carries its own copy of the primitives it stores tokens with. The two copies
6
- // must stay byte-identical: the parity suite in
7
- // test/adapters/prose-tokens.test.mjs pins every function here to its
8
- // prose.mjs twin, so a change to either side fails loudly until both move
9
- // together.
10
-
11
- const STOPWORDS = new Set(
12
- ("a an and or but the of to in on at for with from by as is are was were be been being " +
13
- "it its this that these those i you he she they we me my your our do does did not no " +
14
- "yes if then else than so such can will would should could may might about into over " +
15
- "under out up down off again more most some any all what which who whom whose when " +
16
- "where why how").split(/\s+/),
17
- );
18
-
19
- const MAX_TOKEN_LEN = 40; // drops hash-like/garbage tokens
20
- const MAX_TOKENS_PER_DOC = 120; // bounds cost on a pathologically long docstring/name
21
-
22
- /** Split an identifier or a path-like name into lowercase word tokens.
23
- * Handles camelCase, PascalCase, snake_case, kebab-case, dotted names, path
24
- * separators, and acronym runs ("HTTPSConnection" -> https/connection,
25
- * "parseXML" -> parse/xml). Filters single-character tokens (loop-variable noise). */
26
- export function splitIdentifierWords(raw) {
27
- if (!raw) return [];
28
- let s = String(raw).replace(/\.[A-Za-z0-9]+$/, ""); // strip a trailing file extension only
29
- s = s
30
- .replace(/[/\\]/g, " ") // path separators
31
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2") // camelCase / word|Digit boundary
32
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") // acronym run -> TitleCase (HTTPSConnection)
33
- .replace(/([A-Za-z])([0-9])/g, "$1 $2")
34
- .replace(/([0-9])([A-Za-z])/g, "$1 $2")
35
- .replace(/[_\-.]+/g, " ");
36
- return s.split(/\s+/).map((w) => w.toLowerCase()).filter((w) => w.length > 1 && w.length <= MAX_TOKEN_LEN);
37
- }
38
-
39
- /** Tokenize free prose (a docstring/doc-comment) — lowercase words, punctuation stripped,
40
- * common stopwords and single-/over-length tokens dropped, capped at MAX_TOKENS_PER_DOC. */
41
- export function tokenizeProse(text) {
42
- if (!text) return [];
43
- const out = [];
44
- const seen = new Set();
45
- for (const raw of String(text).toLowerCase().split(/[^a-z0-9]+/)) {
46
- if (raw.length < 2 || raw.length > MAX_TOKEN_LEN || STOPWORDS.has(raw)) continue;
47
- if (seen.has(raw)) continue;
48
- seen.add(raw);
49
- out.push(raw);
50
- if (out.length >= MAX_TOKENS_PER_DOC) break;
51
- }
52
- return out;
53
- }
54
-
55
- /** The combined, deduped, sorted token set for one individual: its (decomposed) name
56
- * plus any captured doc text. Returns [] if there's nothing to index (never null). */
57
- export function proseTokensFor({ name, doc } = {}) {
58
- const set = new Set([...splitIdentifierWords(name), ...tokenizeProse(doc)]);
59
- return [...set].sort();
60
- }
61
-
62
- /** Attach a `prose_tokens` attribute to every individual, from its (decomposed)
63
- * name and captured doc text — except Commit, whose `label` is a truncated
64
- * SHA, not a decomposable identifier: it tokenizes `message` instead. Mutates
65
- * and returns the same array; `enabled=false` is a no-op. */
66
- export function attachProseTokens(individuals, { enabled = true } = {}) {
67
- if (!enabled) return individuals;
68
- for (const ind of individuals) {
69
- const attrs = ind.attributes || [];
70
- const isCommit = ind.class === "Commit";
71
- const name = isCommit ? null : ind.label;
72
- const doc = isCommit
73
- ? attrs.find((a) => a.key === "message")?.value
74
- : attrs.find((a) => a.key === "doc")?.value;
75
- const tokens = proseTokensFor({ name, doc });
76
- if (tokens.length) {
77
- ind.attributes = [...(ind.attributes || []), { prop: "mgx:hasProseTokens", key: "prose_tokens", value: tokens.join(" ") }];
78
- }
79
- }
80
- return individuals;
81
- }
82
-
83
- /** Build the inverted index (word -> sorted, deduped [individual ids]) from individuals
84
- * that already carry a `prose_tokens` attribute. Plain object, JSON-serializable —
85
- * this is what lands as the payload's `proseIndex`. */
86
- export function buildProseIndex(individuals) {
87
- const index = Object.create(null);
88
- for (const ind of individuals) {
89
- const tokAttr = (ind.attributes || []).find((a) => a.key === "prose_tokens");
90
- if (!tokAttr?.value) continue;
91
- for (const word of tokAttr.value.split(" ")) {
92
- if (!index[word]) index[word] = [];
93
- index[word].push(ind.id);
94
- }
95
- }
96
- for (const word of Object.keys(index)) index[word].sort();
97
- return index;
98
- }
@@ -1,70 +0,0 @@
1
- // wordnet-source.mjs — reads a LOCAL Open English WordNet clone off disk and
2
- // indexes it. The clone is never vendored, never committed, never part of the
3
- // npm package: point TMCT_WORDNET_SRC at it, or keep it at the default path.
4
- //
5
- // This is the disk half of the WordNet reader. The parsing half is pure and
6
- // lives in src/domain/wordnet/yaml.mjs, so it is testable with no clone
7
- // present; everything here needs the real files.
8
-
9
- import { readFile, readdir } from "node:fs/promises";
10
- import { existsSync } from "node:fs";
11
- import { homedir } from "node:os";
12
- import { join } from "node:path";
13
- import { parseYaml } from "../domain/wordnet/yaml.mjs";
14
-
15
- export const WORDNET_SRC = process.env.TMCT_WORDNET_SRC || join(homedir(), "projects", "globalwordnet", "english-wordnet");
16
- export const WORDNET_YAML_DIR = join(WORDNET_SRC, "src", "yaml");
17
-
18
- /** True iff a WordNet clone is readable at `yamlDir`. Callers use this to fail
19
- * with a one-line message rather than a stack trace: these are maintainer
20
- * tools, never a build dependency. */
21
- export function hasWordnetSource(yamlDir = WORDNET_YAML_DIR) {
22
- return existsSync(yamlDir);
23
- }
24
-
25
- /** Load one or more noun.<x>/verb.<x>.yaml files into a flat synset-id -> record map. */
26
- export async function loadSynsets(files, yamlDir = WORDNET_YAML_DIR) {
27
- const map = new Map();
28
- for (const f of files) {
29
- const path = join(yamlDir, f);
30
- if (!existsSync(path)) continue;
31
- const parsed = parseYaml(await readFile(path, "utf8"));
32
- for (const [id, rec] of Object.entries(parsed)) map.set(id, rec);
33
- }
34
- return map;
35
- }
36
-
37
- /** Load the entries-<letter>.yaml files that could contain any of `words`
38
- * (only the letters actually needed — 28 files, ~1MB-3MB each, no reason to
39
- * load all 28 when a clump only needs a handful of letters). Returns
40
- * word -> { n: [{id, synset}], v: [...], a: [...] }. */
41
- export async function loadEntriesFor(words, yamlDir = WORDNET_YAML_DIR) {
42
- const letters = new Set();
43
- for (const w of words) {
44
- const c = w[0].toLowerCase();
45
- letters.add(/[a-z]/.test(c) ? c : "0");
46
- }
47
- const index = new Map();
48
- for (const letter of letters) {
49
- const path = join(yamlDir, `entries-${letter}.yaml`);
50
- if (!existsSync(path)) continue;
51
- const parsed = parseYaml(await readFile(path, "utf8"));
52
- for (const [word, byPos] of Object.entries(parsed)) {
53
- if (!words.has(word)) continue;
54
- const senses = {};
55
- for (const [pos, rec] of Object.entries(byPos || {})) {
56
- if (pos === "form") continue;
57
- const list = Array.isArray(rec?.sense) ? rec.sense : [];
58
- senses[pos] = list.map((s) => ({ id: s.id, synset: s.synset })).filter((s) => s.synset);
59
- }
60
- index.set(word, senses);
61
- }
62
- }
63
- return index;
64
- }
65
-
66
- /** Every noun.*.yaml synset in the clone. */
67
- export async function loadAllNounSynsets(yamlDir = WORDNET_YAML_DIR) {
68
- const files = (await readdir(yamlDir)).filter((f) => f.startsWith("noun."));
69
- return loadSynsets(files, yamlDir);
70
- }
@@ -1,87 +0,0 @@
1
- // corpus-matrix.mjs — the fold and the two gap heuristics behind the
2
- // capability-by-lane coverage matrix, plus the table renderer. Pure: rows in,
3
- // counts and text out, so the heuristics can be tested against a handful of
4
- // made-up rows instead of whatever test/corpus happens to hold today.
5
- //
6
- // scripts/corpus-matrix.mjs keeps the readdir, the readFile and the printing.
7
-
8
- /** A row's capability group: the first two dot-segments of its key, so
9
- * "ask.alias.two-hop" and "ask.alias.miss" are one capability. */
10
- export const groupOfKey = (key) => key.split(".").slice(0, 2).join(".");
11
-
12
- /** The key a row is counted under. A row with no key is still a row, and
13
- * hiding it would understate the lane. */
14
- export const keyOfRow = (row) => String(row.key ?? "(no key)");
15
-
16
- /** A key segment naming a miss, a guard or a negation — the row that pins what
17
- * a capability DECLINES to do, as opposed to its happy path. */
18
- const NEGATIVE_RE = /(honest-miss|miss|guard|negation|negative|never|decline|refus|unsolvable|unknown|hedge|no-antecedent|untouched|empty)/;
19
-
20
- export const isNegativeKey = (key) => NEGATIVE_RE.test(key);
21
-
22
- /** Fold `{ lane, row }` pairs into the two indexes every view needs: the count
23
- * per group per lane, and the full keys each group was built from. */
24
- export function tallyRows(entries) {
25
- const counts = new Map(); // group -> Map<lane, rowCount>
26
- const fullKeys = new Map(); // group -> Set<full key>
27
- for (const { lane, row } of entries) {
28
- const key = keyOfRow(row);
29
- const group = groupOfKey(key);
30
- if (!counts.has(group)) counts.set(group, new Map());
31
- const perLane = counts.get(group);
32
- perLane.set(lane, (perLane.get(lane) ?? 0) + 1);
33
- if (!fullKeys.has(group)) fullKeys.set(group, new Set());
34
- fullKeys.get(group).add(key);
35
- }
36
- return { counts, fullKeys };
37
- }
38
-
39
- /** The groups the gap heuristics judge. bench.* rows assert a rig runs rather
40
- * than pinning a capability, so "no negative row" says nothing there. */
41
- export const behaviourGroups = ({ counts }) =>
42
- [...counts.keys()].filter((g) => !g.startsWith("bench.")).sort();
43
-
44
- const rowTotal = (counts, group) => [...counts.get(group).values()].reduce((a, b) => a + b, 0);
45
-
46
- /** Groups a single row pins end to end. A review candidate, not a hole. */
47
- export function thinGroups(tally) {
48
- return behaviourGroups(tally).filter((g) => rowTotal(tally.counts, g) === 1);
49
- }
50
-
51
- /** Groups whose keys never name a miss, guard or negation — a happy path is
52
- * pinned and the decline is not. A review candidate, not a hole. */
53
- export function groupsWithNoNegativeRow(tally) {
54
- return behaviourGroups(tally).filter((g) => ![...tally.fullKeys.get(g)].some(isNegativeKey));
55
- }
56
-
57
- /** The lanes a group has rows in, in the order given. */
58
- export const lanesOfGroup = ({ counts }, group) => [...counts.get(group).keys()];
59
-
60
- /** One row per group, one column per lane, an empty cell where a lane has no
61
- * row for that group. The header row comes first. */
62
- export function matrixRows({ counts }, lanes) {
63
- const groups = [...counts.keys()].sort();
64
- return [
65
- ["key", ...lanes],
66
- ...groups.map((group) => [
67
- group,
68
- ...lanes.map((lane) => {
69
- const n = counts.get(group).get(lane);
70
- return n ? String(n) : "";
71
- }),
72
- ]),
73
- ];
74
- }
75
-
76
- /** `rows` (header first) as fixed-width text, with a rule under the header.
77
- * Each column is as wide as its widest cell; trailing padding is trimmed. */
78
- export function renderTable(rows) {
79
- const [header, ...body] = rows;
80
- const widths = header.map((h, col) => Math.max(h.length, ...body.map((r) => r[col].length)));
81
- const renderLine = (cells) => cells.map((c, col) => c.padEnd(widths[col])).join(" ").trimEnd();
82
- return [
83
- renderLine(header),
84
- renderLine(widths.map((w) => "-".repeat(w))),
85
- ...body.map(renderLine),
86
- ].join("\n");
87
- }
@@ -1,67 +0,0 @@
1
- // inflect.mjs — the regular English -s/-ed/-ing rules, applied to a lemma.
2
- //
3
- // WordNet carries lemmas only ("rest" is present, "rests" is absent), and it is
4
- // the inflected forms that collide with the fuzzy repair tier's targets —
5
- // "rests" is one edit from "tests". So the real-word collision table expands
6
- // every lemma through these rules before it looks for collisions.
7
- //
8
- // These are the REGULAR rules and nothing else. No irregular table, no stress
9
- // model: pastOf("run") is "runned" and pastOf("make") is "maked". That is the
10
- // intended shape. The table's job is to name words the repair tier must not
11
- // rewrite, and inflectionsOf is generous on purpose (see below) — an extra form
12
- // costs one repair we decline to make, and the sentence misses honestly, while
13
- // a missing form costs a real word rewritten into a different question,
14
- // answered with confidence. The first is the cheaper mistake.
15
-
16
- import { STOPWORDS } from "./interpret/normalize.mjs";
17
- import {
18
- FUZZY_TARGET_WORDS, FUZZY_REPAIR_MIN_LENGTH, fuzzyMatchInSet, fuzzyBound,
19
- } from "./interpret/fuzzy.mjs";
20
-
21
- const VOWELS = new Set(["a", "e", "i", "o", "u"]);
22
- const isVowel = (c) => VOWELS.has(c);
23
-
24
- /** A single final consonant after a single vowel doubles before -ed/-ing
25
- * ("run" -> "running"). w, x and y never double. Stress is not modelled, so a
26
- * second syllable doubles too ("visit" -> "visitting"). */
27
- export function doublesFinalConsonant(w) {
28
- const [c3, c2, c1] = [w.at(-3), w.at(-2), w.at(-1)];
29
- if (!c3 || isVowel(c1) || "wxy".includes(c1)) return false;
30
- return isVowel(c2) && !isVowel(c3);
31
- }
32
-
33
- export function pluralOf(w) {
34
- if (/(?:s|x|z|ch|sh)$/.test(w)) return `${w}es`;
35
- if (/[^aeiou]y$/.test(w)) return `${w.slice(0, -1)}ies`;
36
- return `${w}s`;
37
- }
38
-
39
- export function pastOf(w) {
40
- if (w.endsWith("e")) return `${w}d`;
41
- if (/[^aeiou]y$/.test(w)) return `${w.slice(0, -1)}ied`;
42
- if (doublesFinalConsonant(w)) return `${w}${w.at(-1)}ed`;
43
- return `${w}ed`;
44
- }
45
-
46
- export function gerundOf(w) {
47
- if (w.endsWith("ie")) return `${w.slice(0, -2)}ying`;
48
- if (w.endsWith("e") && !/(?:ee|oe|ye)$/.test(w)) return `${w.slice(0, -1)}ing`;
49
- if (doublesFinalConsonant(w)) return `${w}${w.at(-1)}ing`;
50
- return `${w}ing`;
51
- }
52
-
53
- /** Every surface form of `w` the collision table counts as real English. */
54
- export const inflectionsOf = (w) => [w, pluralOf(w), pastOf(w), gerundOf(w)];
55
-
56
- /** The words in `realWords` that the repair tier would rewrite onto one of its
57
- * targets: long enough to reach the tier, not a stopword, not a target itself,
58
- * and within the fuzzy bound of some target. Sorted, so the table it feeds is
59
- * reproducible. */
60
- export function collisionsFrom(realWords) {
61
- return [...realWords]
62
- .filter((w) => w.length >= FUZZY_REPAIR_MIN_LENGTH)
63
- .filter((w) => !STOPWORDS.has(w))
64
- .filter((w) => !FUZZY_TARGET_WORDS.includes(w))
65
- .filter((w) => fuzzyMatchInSet(w, FUZZY_TARGET_WORDS, fuzzyBound(w)) !== null)
66
- .sort();
67
- }