@polycode-projects/the-mechanical-code-talker 1.5.4 → 1.8.3

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 (46) hide show
  1. package/README.md +123 -14
  2. package/ROADMAP.md +233 -1392
  3. package/bin/tmct.mjs +479 -98
  4. package/corpus/README.md +3 -0
  5. package/corpus/generated/README.md +43 -0
  6. package/corpus/generated/ace-surface-variants.jsonl +17 -0
  7. package/corpus/generated/manifest.json +9 -0
  8. package/corpus/tier2/generate.mjs +14668 -0
  9. package/corpus/tier2/human-examples-large.jsonl +1928 -0
  10. package/corpus/tier2/human-examples-medium.jsonl +356 -0
  11. package/corpus/tier2/human-examples.jsonl +120 -0
  12. package/corpus/tier2/human-large.jsonl +12001 -0
  13. package/corpus/tier2/human-medium.jsonl +944 -0
  14. package/corpus/tier2/human.jsonl +664 -0
  15. package/corpus/tier2/manifest.json +42 -0
  16. package/package.json +14 -8
  17. package/src/answer-variants.json +47 -0
  18. package/src/answer-variants.mjs +67 -0
  19. package/src/ask-browser-entry.mjs +34 -0
  20. package/src/ask-browser.bundle.js +5095 -0
  21. package/src/ask-vocab.mjs +93 -8
  22. package/src/ask.mjs +451 -49
  23. package/src/chat.mjs +1391 -141
  24. package/src/cli-args.mjs +164 -0
  25. package/src/codegraph.mjs +170 -32
  26. package/src/completions/graph-adapter.mjs +118 -0
  27. package/src/extensions.mjs +100 -19
  28. package/src/grammar/ace.mjs +85 -3
  29. package/src/grammar/lexicon-core.json +9531 -63
  30. package/src/grammar/lexicon.mjs +58 -8
  31. package/src/graph-merge.mjs +114 -0
  32. package/src/index.mjs +14 -0
  33. package/src/init.mjs +40 -14
  34. package/src/interpret/normalize.mjs +88 -3
  35. package/src/interpret/strategies/grammar.mjs +10 -0
  36. package/src/interpret/strategies/keywords.mjs +20 -0
  37. package/src/interpret/strategies/noise-strip.mjs +73 -4
  38. package/src/memory/core.mjs +466 -8
  39. package/src/router/goal-reasoner.mjs +41 -7
  40. package/src/router/guardrail.mjs +37 -7
  41. package/src/router/resolver.mjs +50 -4
  42. package/src/sessions.mjs +5 -1
  43. package/src/source.mjs +54 -1
  44. package/src/syllogise.mjs +398 -27
  45. package/src/toml-config.mjs +13 -4
  46. package/src/viz.mjs +541 -0
@@ -5,23 +5,41 @@
5
5
  //
6
6
  // resolveExtensions(repoRoot) → { entries: Map<name, ResolvedEntry>, biasByBundle }
7
7
  //
8
- // BUILTIN_EXTENSIONS ships the exact two bundles chat.mjs's bootstrap has
9
- // always seeded `seon` and `conceptnet`, both active plus four shipped-
10
- // but-INACTIVE tier-2 bundles (`tier2-aws` / `tier2-python` / `tier2-java` /
11
- // `tier2-general`). Activating one is a config-only edit (`tmct init --corpus
8
+ // BUILTIN_EXTENSIONS' DEFAULT ACTIVE BUNDLE IS `human` (PLAN_SEED.md, the
9
+ // persona flip): a fresh repo now seeds an everyday-world vocabulary (people,
10
+ // places, objects, nature, time/events, body/food, mind hand-curated from
11
+ // Open English WordNet + Schema.org) rather than the old implicit code-domain
12
+ // default. `seon` and `conceptnet` are SHIPPED but now INACTIVE — both are
13
+ // equally code/tech-domain-biased (conceptnet's committed slice was filtered
14
+ // via a tech-domain seed-term match, PLAN_SEED.md §2), so BOTH flip together,
15
+ // not just seon — a repo that wants the old behavior asks for it explicitly
16
+ // (`tmct init --with-persona code`, or `[extensions.seon]`/`[extensions.
17
+ // conceptnet]` `active = true`). Four more shipped-but-INACTIVE tier-2 bundles
18
+ // (`tier2-aws` / `tier2-python` / `tier2-java` / `tier2-general`) round out the
19
+ // catalog. Activating any of these is a config-only edit (`tmct init --corpus
12
20
  // aws`, or a `[extensions.tier2-aws] active = true` in tmct.toml) — zero code
13
- // change. `tier2-general` (PLAN_AGENTS.md Phase 1) is deliberately NOT a
14
- // language/domain bundle like the other three everyday-knowledge concepts
15
- // with zero code-domain framing, the "wider general-knowledge seed set"
16
- // bullet made real instead of just mechanically activatable.
21
+ // change. `tier2-general`'s own 49-fact animal/weather set (PLAN_AGENTS.md
22
+ // Phase 1) is superseded IN DEFAULT ROLE by `human`'s much larger
23
+ // `human-nature` clump, but stays shipped/selectable on its own for a caller
24
+ // that wants that narrow slice without the rest of the human persona.
25
+ //
26
+ // `human-medium`/`human-large` (also shipped-but-INACTIVE) are SIZE TIERS of
27
+ // the SAME `human` bundle, not separate personas (PLAN_SEED.md §3) — each
28
+ // holds only the facts that size adds beyond the previous one. Activated
29
+ // together via `tmct init --persona-size medium|large` (bin/tmct.mjs), which
30
+ // resolves them through this same registry.
17
31
  //
18
32
  // A `tmct.toml` may carry a top-level `[extensions]` table-of-tables
19
33
  // (`[extensions.tier2-aws]`, …): a RECOGNIZED name (one of the builtins above)
20
34
  // may override `active`/paths/etc; an UNRECOGNIZED name declares a brand new
21
- // host entry and MUST carry a `kind` (corpus | lexicon | templates | pack) — a
22
- // `pack` entry may combine any of corpus_path/lexicon_path/templates_path/
23
- // phrasebook_path under one `active` flag and one provenance name, the shape a
24
- // third-party vocabulary package hands tmct.
35
+ // host entry and MUST carry a `kind` (corpus | lexicon | templates | pack |
36
+ // ontology) — a `pack` entry may combine any of corpus_path/lexicon_path/
37
+ // templates_path/phrasebook_path under one `active` flag and one provenance
38
+ // name, the shape a third-party vocabulary package hands tmct. `ontology` is a
39
+ // DISTINCT, nameable kind for an ontology bundle (as opposed to a plain
40
+ // `corpus` bundle) — its `ontology_path` key is just an alias populating the
41
+ // SAME internal `corpusPath` field a corpus entry uses, so every downstream
42
+ // seeder/loader needs zero branching by kind name.
25
43
  //
26
44
  // A SEPARATE top-level `[bias]` table (flat: bundle-name → number) feeds
27
45
  // src/memory/bias.mjs's ranking — never nested under `[extensions.*]`.
@@ -45,7 +63,7 @@ import {
45
63
  toFacts,
46
64
  } from "./corpus/conceptnet.mjs";
47
65
 
48
- export const EXTENSION_KINDS = Object.freeze(["corpus", "lexicon", "templates", "pack"]);
66
+ export const EXTENSION_KINDS = Object.freeze(["corpus", "lexicon", "templates", "pack", "ontology"]);
49
67
 
50
68
  // The definitional-band-first predicate order chat.mjs's bootstrap has always
51
69
  // passed for the ConceptNet seed (SEED_PREFER) — re-declared here (not
@@ -57,15 +75,20 @@ const CONCEPTNET_PREFER = ["rdfs:subClassOf", "rdf:type", "mgx:usedFor", "mgx:pa
57
75
  * accidentally mutate a module-level singleton. */
58
76
  function builtinExtensions() {
59
77
  return {
78
+ // WAS active:true (the implicit code-domain default) — now opt-in.
79
+ // PLAN_SEED.md §2: re-activate explicitly (`tmct init --with-persona
80
+ // code`, or `[extensions.seon] active = true`) for the old behavior.
60
81
  seon: {
61
82
  kind: "corpus",
62
- active: true,
83
+ active: false,
63
84
  corpusPath: SEON_CONCEPTS_FILE,
64
85
  provenancePrefix: "corpus:seon",
65
86
  },
87
+ // WAS active:true — now opt-in too, not just seon (PLAN_SEED.md §2: the
88
+ // committed slice is itself tech-domain-filtered, equally biased).
66
89
  conceptnet: {
67
90
  kind: "corpus",
68
- active: true,
91
+ active: false,
69
92
  corpusPath: CONCEPTNET_SLICE_FILE,
70
93
  provenancePrefix: "corpus:conceptnet",
71
94
  // matches chat.mjs's seedBootstrapMemory exactly: uncapped, definitional
@@ -73,6 +96,40 @@ function builtinExtensions() {
73
96
  limit: undefined,
74
97
  prefer: CONCEPTNET_PREFER,
75
98
  },
99
+ // NEW — the default active bundle (PLAN_SEED.md). Everyday-world
100
+ // vocabulary: people, places, objects, nature, time/events, body/food,
101
+ // mind, plus the human-base/human-bridge scaffolding connecting WordNet's
102
+ // and Schema.org's independently-built taxonomies (PLAN_SEED.md §3, §8).
103
+ human: {
104
+ kind: "corpus",
105
+ active: true,
106
+ corpusPath: join(TIER2_DIR, "human.jsonl"),
107
+ provenancePrefix: "corpus:human",
108
+ },
109
+ // NEW — Medium/Large SIZE tiers of the SAME `human` bundle (PLAN_SEED.md
110
+ // §3), not separate personas: each file holds ONLY the facts that size
111
+ // adds beyond the previous one (Medium beyond Small, Large beyond
112
+ // Medium), so activating them is purely ADDITIVE alongside `human`
113
+ // (never a replacement for it). Both ship INACTIVE — Small stays the
114
+ // unconditional default — and are activated together via `tmct init
115
+ // --persona-size medium|large` (bin/tmct.mjs), which resolves them
116
+ // through this SAME BUILTIN_EXTENSIONS lookup and the ordinary
117
+ // `--corpus <id>` activation seam (activatePluggableInput). "large"
118
+ // activates BOTH human-medium and human-large (Large's facts are
119
+ // Medium's plus its own — both bundles must be active to reach the
120
+ // full ~13,600-fact total).
121
+ "human-medium": {
122
+ kind: "corpus",
123
+ active: false,
124
+ corpusPath: join(TIER2_DIR, "human-medium.jsonl"),
125
+ provenancePrefix: "corpus:human-medium",
126
+ },
127
+ "human-large": {
128
+ kind: "corpus",
129
+ active: false,
130
+ corpusPath: join(TIER2_DIR, "human-large.jsonl"),
131
+ provenancePrefix: "corpus:human-large",
132
+ },
76
133
  "tier2-aws": {
77
134
  kind: "corpus",
78
135
  active: false,
@@ -118,6 +175,9 @@ export function validateExtensionEntry(name, entry) {
118
175
  if (entry.kind === "corpus" && !entry.corpusPath) {
119
176
  throw new Error(`extension "${name}": a "corpus" entry needs corpus_path`);
120
177
  }
178
+ if (entry.kind === "ontology" && !entry.corpusPath) {
179
+ throw new Error(`extension "${name}": an "ontology" entry needs ontology_path`);
180
+ }
121
181
  if (entry.kind === "lexicon" && !entry.lexiconPath) {
122
182
  throw new Error(`extension "${name}": a "lexicon" entry needs lexicon_path`);
123
183
  }
@@ -154,6 +214,7 @@ function mergeExtensionEntry(name, builtin, override, repoRoot) {
154
214
  };
155
215
  const paths = [
156
216
  ["corpus_path", "corpusPath"],
217
+ ["ontology_path", "corpusPath"], // alias: an "ontology" entry's own path key, same internal field as "corpus"
157
218
  ["lexicon_path", "lexiconPath"],
158
219
  ["templates_path", "templatesPath"],
159
220
  ["phrasebook_path", "phrasebookPath"],
@@ -184,9 +245,13 @@ function mergeExtensionEntry(name, builtin, override, repoRoot) {
184
245
  * table (default {} — every bundle then ranks at bias 1, see bias.mjs).
185
246
  * No `tmct.toml` (or one with no `[extensions]`/`[bias]` tables) resolves to
186
247
  * exactly today's implicit seon+conceptnet default, byte-identical.
248
+ *
249
+ * `configFile` (optional): an explicit tmct.toml path override — `tmct extend
250
+ * --validate <dir> --config <path>` — read INSTEAD of `<repoRoot>/tmct.toml`;
251
+ * `repoRoot` still anchors every resource path (unchanged).
187
252
  */
188
- export async function resolveExtensions(repoRoot) {
189
- const raw = repoRoot ? await loadTomlConfig(repoRoot) : null;
253
+ export async function resolveExtensions(repoRoot, { configFile } = {}) {
254
+ const raw = repoRoot ? await loadTomlConfig(repoRoot, configFile ? { file: configFile } : {}) : null;
190
255
  const defs = builtinExtensions();
191
256
  const rawExtensions = (raw && raw.extensions && typeof raw.extensions === "object") ? raw.extensions : {};
192
257
  const rawBias = (raw && raw.bias && typeof raw.bias === "object") ? raw.bias : {};
@@ -216,12 +281,19 @@ export async function resolveExtensions(repoRoot) {
216
281
 
217
282
  // ---- Part 2: the unified corpus loader loop ---------------------------------
218
283
 
219
- /** Seed every ACTIVE `corpus`-kind entry (in the Map's own fixed order — seon,
284
+ /** Seed every ACTIVE `corpus`/`ontology`-kind entry, plus any ACTIVE `pack`-kind
285
+ * entry that declares a `corpusPath` (in the Map's own fixed order — seon,
220
286
  * conceptnet, then the rest sorted by name) into `repo`'s memory, ONE
221
287
  * seedMemory() call per bundle. Shared by chat.mjs's first-run bootstrap,
222
288
  * `tmct init`'s seed step and `tmct init --corpus <id>` — so all three read
223
289
  * the SAME loop instead of three independent hardcoded call sites.
224
290
  *
291
+ * BUGFIX (this batch): a `pack`-kind entry's `corpusPath` used to be silently
292
+ * skipped here despite this module's own docblock claiming pack entries
293
+ * combine corpus_path/lexicon_path/etc — a pack's corpus facts never made it
294
+ * into memory. Fixed by seeding any active pack entry that declares a
295
+ * corpusPath, alongside corpus/ontology entries.
296
+ *
225
297
  * FAILURE-TOLERANT per bundle (init.mjs's own doctrine: a missing/broken
226
298
  * corpus degrades to "not seeded", never a crash): one bad third-party pack's
227
299
  * seedMemory throw is CAUGHT and recorded as `perBundle[name].error` — logged
@@ -235,7 +307,16 @@ export async function seedActiveCorpusEntries(repo, entries) {
235
307
  let skipped = 0;
236
308
  let total = 0;
237
309
  for (const [name, entry] of entries instanceof Map ? entries : new Map()) {
238
- if (entry.kind !== "corpus" || !entry.active) continue;
310
+ // PLAN_SEED.md §2 bug fix: a "pack"-kind entry with its own corpusPath
311
+ // combines corpus/lexicon/templates under one active flag (this module's
312
+ // own docblock says so) but was previously never actually seeded here —
313
+ // only bare `kind: "corpus"` entries were. Broadened, not narrowed: every
314
+ // existing `kind: "corpus"`/`kind: "ontology"` entry (seon/conceptnet/
315
+ // human/tier2-*) behaves identically to before; only a pack entry that
316
+ // DOES carry a corpusPath newly qualifies.
317
+ if (!entry.active) continue;
318
+ const seedable = entry.kind === "corpus" || entry.kind === "ontology" || (entry.kind === "pack" && entry.corpusPath);
319
+ if (!seedable) continue;
239
320
  try {
240
321
  const res = await seedMemory(repo, {
241
322
  slicePath: entry.corpusPath,
@@ -39,6 +39,11 @@ import {
39
39
  predicateOf, numberOf, classify,
40
40
  } from "./lexicon.mjs";
41
41
 
42
+ // "a"/"an" are the only ACE determiners that are grammatically SINGULAR-ONLY —
43
+ // "the" and a bare/no determiner are number-neutral (see resolveNP's
44
+ // singularOnly below, and lexicon.mjs's lookupNoun doc for what this prunes).
45
+ const SINGULAR_ONLY_DET = new Set(["a", "an"]);
46
+
42
47
  export const PATTERN_SUB_CLASS_OF = "subClassOf";
43
48
  export const PATTERN_TYPE_ASSERTION = "typeAssertion";
44
49
  export const PATTERN_RELATION = "relation";
@@ -89,22 +94,29 @@ const stripDet = (tokens) =>
89
94
  * Returns { term, individual, extras, unknown } — `term` null on a miss with
90
95
  * the undeclared tokens in `unknown` (empty `unknown` = structurally
91
96
  * unparseable phrase → the caller returns a hard null). `extras` carries the
92
- * pattern-8 adjective triples (subclass axioms / hasValue restriction). */
97
+ * pattern-8 adjective triples (subclass axioms / hasValue restriction).
98
+ *
99
+ * `singularOnly` is grammatical-agreement pruning (see lexicon.mjs's
100
+ * lookupNoun doc): true only when the ORIGINAL (pre-strip) phrase opened
101
+ * with "a"/"an" — the one signal that a singular-plural-fold collision
102
+ * (die/dice, person/people, tooth/teeth) can be resolved by, rather than
103
+ * silently committing to whichever the lexicon happens to fold to first. */
93
104
  function resolveNP(lexicon, tokensIn) {
94
105
  const ns = lexicon.ns;
106
+ const singularOnly = tokensIn.length > 1 && SINGULAR_ONLY_DET.has(tokensIn[0].toLowerCase());
95
107
  const tokens = stripDet(tokensIn);
96
108
  if (tokens.length === 1) {
97
109
  const t = tokens[0];
98
110
  const proper = lookupProperName(lexicon, t);
99
111
  if (proper) return { term: `${ns}${proper}`, individual: true, extras: [], unknown: [] };
100
112
  if (CODE_REF.test(t)) return { term: `${ns}${t}`, individual: true, extras: [], unknown: [] };
101
- const noun = lookupNoun(lexicon, t);
113
+ const noun = lookupNoun(lexicon, t, { singularOnly });
102
114
  if (noun) return { term: `${ns}${noun.lemma}`, individual: false, noun, extras: [], unknown: [] };
103
115
  return { term: null, individual: false, extras: [], unknown: [t] };
104
116
  }
105
117
  if (tokens.length === 2) {
106
118
  const adj = lookupAdjective(lexicon, tokens[0]);
107
- const noun = lookupNoun(lexicon, tokens[1]);
119
+ const noun = lookupNoun(lexicon, tokens[1], { singularOnly });
108
120
  if (adj && noun) {
109
121
  const term = `${ns}${adj.lemma}-${noun.lemma}`;
110
122
  const extras = [
@@ -178,6 +190,76 @@ function parseRelation(lexicon, toks, lower) {
178
190
  return null;
179
191
  }
180
192
 
193
+ // ---- ambiguity: breadth-first candidate parses, dead ends pruned, survivors
194
+ // surfaced rather than guessed (the operator's own framing — see
195
+ // PLAN_DID_YOU_SEE_HER_DUCK.md's Origin section). parseRelation just above is
196
+ // UNCHANGED — it is still the greedy, first-verb-position-wins fast path
197
+ // every existing caller keeps using, so every single-reading sentence (the
198
+ // overwhelming majority) is completely unaffected. parseRelationHits and
199
+ // parseAceAmbiguous below are a separate, ADDITIVE scan that a caller opts
200
+ // into only when it wants to know whether more than one reading survives. ----
201
+
202
+ /** Pattern 3 — EVERY verb-position split, not just the first: for each token
203
+ * index that lookupVerb recognizes, resolve both sides and keep it ONLY if
204
+ * it is a complete, valid parse (a genuine hit — a missOrNull/null split is
205
+ * a dead end, pruned here rather than surfaced as "ambiguity"). Duplicate
206
+ * logic with parseRelation is deliberate: parseRelation must stay byte-for-
207
+ * byte unchanged for every existing caller, so this is a standalone reader,
208
+ * not a refactor of shared internals. */
209
+ function parseRelationHits(lexicon, toks, lower) {
210
+ const hits = [];
211
+ for (let i = 1; i < toks.length - 1; i += 1) {
212
+ const verb = lookupVerb(lexicon, lower[i]);
213
+ if (!verb) continue;
214
+ let objStart = i + 1;
215
+ if (verb.prep) {
216
+ if (lower[objStart] !== verb.prep) continue;
217
+ objStart += 1;
218
+ if (objStart >= toks.length) continue;
219
+ }
220
+ const np1 = resolveNP(lexicon, toks.slice(0, i));
221
+ const np2 = resolveNP(lexicon, toks.slice(objStart));
222
+ if (np1.term == null || np2.term == null) continue; // dead end
223
+ hits.push({
224
+ i,
225
+ verbLemma: verb.lemma,
226
+ subject: np1.term,
227
+ object: np2.term,
228
+ result: hit(PATTERN_RELATION, [np1, np2], [
229
+ { subject: np1.term, predicate: predicateOf(verb, lexicon.ns), object: np2.term, kind: "owl:ObjectProperty" },
230
+ ]),
231
+ });
232
+ }
233
+ return hits;
234
+ }
235
+
236
+ /** Public ambiguity surface: parse `sentence` and, ONLY when more than one
237
+ * independent, COMPLETE relation-pattern reading survives (parseRelationHits
238
+ * above), return them all, each labeled by the token it read as the verb.
239
+ * Returns null for the overwhelming majority of sentences: anything not
240
+ * relation-shaped (mirrors parseAce's own dispatch gate exactly, so this
241
+ * only ever fires on a sentence parseAce would ALSO route to parseRelation),
242
+ * and any relation-shaped sentence with 0 or 1 surviving readings — the
243
+ * ordinary parseAce path is authoritative and untouched either way. */
244
+ export function parseAceAmbiguous(sentence, lexicon = loadLexicon()) {
245
+ const toks = tokenize(sentence);
246
+ if (toks.length < 4) return null; // 3 tokens: exactly one verb position is even possible
247
+ const lower = toks.map((t) => t.toLowerCase());
248
+ if (lower[0] === "every" || lower[0] === "no") return null;
249
+ if (/'s$/.test(lower[0]) && lower[0].length > 2) return null;
250
+ if (lower[0] === "the" && lower.includes("of") && lower.includes("is")) return null;
251
+ if (lower.indexOf("is") > 0) return null;
252
+ const hits = parseRelationHits(lexicon, toks, lower);
253
+ if (hits.length < 2) return null;
254
+ return {
255
+ pattern: PATTERN_RELATION,
256
+ sentence,
257
+ readings: hits.map(({ i, verbLemma, subject, object, result }) => ({
258
+ i, verbLemma, subject, object, ...result,
259
+ })),
260
+ };
261
+ }
262
+
181
263
  /** Pattern 8 (copula arm) — "X is ADJ": data adjective → datatype-property
182
264
  * assertion; subclass adjective → rdf:type (individual) / rdfs:subClassOf. */
183
265
  function adjectiveCopula(lexicon, pattern, np1, adj) {