@polycode-projects/the-mechanical-code-talker 1.9.1 → 1.10.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 (81) hide show
  1. package/README.md +441 -217
  2. package/bin/tmct.mjs +126 -1
  3. package/corpus/seon/README.md +1 -2
  4. package/package.json +4 -2
  5. package/src/answer-variants.mjs +8 -36
  6. package/src/ask-browser-entry.mjs +5 -23
  7. package/src/ask-browser.bundle.js +1 -2
  8. package/src/ask-nlp.mjs +9 -23
  9. package/src/ask-vocab.mjs +139 -589
  10. package/src/ask.mjs +627 -1729
  11. package/src/chat.mjs +1684 -2874
  12. package/src/cli-args.mjs +14 -28
  13. package/src/codegraph.mjs +236 -644
  14. package/src/completions/complete.mjs +18 -62
  15. package/src/completions/graph-adapter.mjs +14 -60
  16. package/src/completions/group.mjs +12 -68
  17. package/src/completions/infer.mjs +38 -126
  18. package/src/completions/prune.mjs +17 -70
  19. package/src/completions/rank.mjs +16 -69
  20. package/src/completions/search.mjs +8 -31
  21. package/src/concept.mjs +32 -88
  22. package/src/conformance.mjs +11 -15
  23. package/src/corpus/conceptnet.mjs +31 -89
  24. package/src/corpus/templates.mjs +19 -45
  25. package/src/corpus/unknown-ingest.mjs +31 -92
  26. package/src/embed.mjs +10 -22
  27. package/src/extensions.mjs +50 -154
  28. package/src/finish.mjs +35 -91
  29. package/src/grammar/ace.mjs +16 -40
  30. package/src/grammar/assert.mjs +1 -1
  31. package/src/grammar/lexicon-core.json +1 -1
  32. package/src/grammar/lexicon.mjs +9 -27
  33. package/src/graph-merge.mjs +2 -3
  34. package/src/hash.mjs +6 -14
  35. package/src/index.mjs +6 -10
  36. package/src/init.mjs +38 -125
  37. package/src/interpret/fuzzy.mjs +10 -29
  38. package/src/interpret/merge.mjs +9 -27
  39. package/src/interpret/normalize.mjs +137 -585
  40. package/src/interpret/pipeline.mjs +23 -71
  41. package/src/interpret/strategies/ace.mjs +7 -31
  42. package/src/interpret/strategies/constructions.mjs +14 -41
  43. package/src/interpret/strategies/grammar.mjs +21 -60
  44. package/src/interpret/strategies/keywords.mjs +42 -131
  45. package/src/interpret/strategies/noise-strip.mjs +18 -89
  46. package/src/memory/bias.mjs +11 -54
  47. package/src/memory/blocks.mjs +18 -69
  48. package/src/memory/core.mjs +171 -591
  49. package/src/memory/fold.mjs +0 -0
  50. package/src/memory/inspect.mjs +7 -25
  51. package/src/memory/shacl.mjs +10 -39
  52. package/src/memory/trust.mjs +26 -127
  53. package/src/memory-ask-browser-entry.mjs +7 -30
  54. package/src/memory-ask-browser.bundle.js +1 -1
  55. package/src/paraphrase.mjs +20 -53
  56. package/src/planning.mjs +15 -157
  57. package/src/prose-nlp.mjs +4 -17
  58. package/src/prose.mjs +19 -67
  59. package/src/providers/bootstrap.mjs +1 -2
  60. package/src/providers/fixture.mjs +1 -2
  61. package/src/providers/graph-service.mjs +28 -59
  62. package/src/repository-interface.mjs +6 -8
  63. package/src/router/drive.mjs +183 -0
  64. package/src/router/goal-reasoner.mjs +66 -231
  65. package/src/router/guardrail.mjs +20 -58
  66. package/src/router/planner.mjs +15 -46
  67. package/src/router/registry.mjs +13 -43
  68. package/src/router/resolver.mjs +46 -131
  69. package/src/router/results.mjs +231 -0
  70. package/src/schema-docs.mjs +10 -27
  71. package/src/server-http.mjs +10 -19
  72. package/src/server.mjs +22 -28
  73. package/src/sessions.mjs +15 -30
  74. package/src/source-slice.mjs +5 -7
  75. package/src/source.mjs +10 -20
  76. package/src/syllogise.mjs +187 -575
  77. package/src/telemetry.mjs +3 -3
  78. package/src/toml-config.mjs +4 -4
  79. package/src/tui/app.mjs +9 -19
  80. package/src/viz.mjs +66 -123
  81. package/src/wink-model.mjs +10 -24
@@ -1,5 +1,5 @@
1
1
  // grammar/lexicon.mjs — the declared lexicon behind tmct's ACE-OWL
2
- // sub-fragment parser (ROADMAP Phase 2, item 2; see ace.mjs). The lexicon is
2
+ // sub-fragment parser (see ace.mjs). The lexicon is
3
3
  // LOAD-BEARING: the grammar is only deterministic because every noun, verb
4
4
  // (with any preposition), adjective (with its declared type) and proper name
5
5
  // is DECLARED — the parser never guesses a word's category. Undeclared words
@@ -13,9 +13,6 @@
13
13
  //
14
14
  // Namespace: every lexicon carries a `.ns` field (the CURIE prefix ace.mjs
15
15
  // stamps onto every term it mints) — always "tmct:" here (DEFAULT_NS).
16
- // ace.mjs and predicateOf() read it off the lexicon rather than hardcoding
17
- // the prefix inline, purely so a caller can supply its own already-namespaced
18
- // lexicon; tmct itself only ever runs the one namespace.
19
16
  //
20
17
  // Morphology is deliberately tiny and deterministic (no NLP dependency): a
21
18
  // suffix-fold for plurals/3rd-person-singular ("repositories"→repository,
@@ -154,26 +151,17 @@ export function loadLexicon(extra, ns = DEFAULT_NS) {
154
151
  return lex;
155
152
  }
156
153
 
157
- /** Noun lookup with plural folding; returns the entry ({lemma, property?}) or null.
158
- * `opts.singularOnly` (set by ace.mjs's resolveNP for an "a"/"an" determiner
159
- * the only ACE determiners that are grammatically singular-ONLY) prunes the
160
- * irregular-plural fold when the SAME surface word is ALSO declared as its own
161
- * standalone noun (die/dice, person/people, tooth/teeth): a singular-only
162
- * determiner is incompatible with the plural-fold reading but fully compatible
163
- * with the standalone-singular reading, so that's the one grammar agreement
164
- * allows. A general rule keyed on determiner agreement, not a per-word carve-out
165
- * — see lookupNounCandidates below for the multi-candidate form this wraps. */
154
+ /** Noun lookup with plural folding; returns the entry ({lemma, property?}) or
155
+ * null. `opts.singularOnly` (an "a"/"an" determiner) prunes the irregular-
156
+ * plural fold in favor of a standalone-singular entry when both exist for
157
+ * the same surface word (die/dice, person/people). */
166
158
  export function lookupNoun(lexicon, word, opts = {}) {
167
159
  return lookupNounCandidates(lexicon, word, opts)[0] ?? null;
168
160
  }
169
161
 
170
- /** Every lexicon entry `word` could plausibly resolve to, ranked with the SAME
171
- * top choice lookupNoun would return (opts.singularOnly applies identically)
172
- * but without discarding a genuine alternate — e.g. the die/dice collision
173
- * returns BOTH the `dice` and `die` entries (order depends on
174
- * opts.singularOnly), a regular -s fold with both forms independently
175
- * declared returns both. Additive: existing callers that only want the single
176
- * best answer keep using lookupNoun untouched. */
162
+ /** Every lexicon entry `word` could plausibly resolve to, ranked the same as
163
+ * lookupNoun's top choice but without discarding a genuine alternate (e.g.
164
+ * die/dice returns both entries). */
177
165
  export function lookupNounCandidates(lexicon, word, opts = {}) {
178
166
  const w = String(word ?? "").toLowerCase();
179
167
  const standalone = lexicon.nouns.get(w);
@@ -208,13 +196,7 @@ export function lookupVerb(lexicon, word) {
208
196
  }
209
197
 
210
198
  /** Every verb entry `word` could plausibly resolve to via foldCandidates,
211
- * most-specific-fold-first (same order lookupVerb's single answer already
212
- * used) — additive sibling of lookupNounCandidates, for a caller that wants
213
- * to know about a genuine fold collision instead of only the first hit. In
214
- * practice a verb fold rarely collides (unlike nouns' irregular-plural
215
- * table), but the shape is symmetric with lookupNounCandidates on purpose —
216
- * ace.mjs's multi-candidate relation search (parseRelationHits) reads
217
- * whichever of the two a token's part of speech calls for. */
199
+ * most-specific-fold-first the verb sibling of lookupNounCandidates. */
218
200
  export function lookupVerbCandidates(lexicon, word) {
219
201
  const w = String(word ?? "").toLowerCase();
220
202
  const out = [];
@@ -1,5 +1,4 @@
1
- // graph-merge.mjs — multi-graph payload merging (the CLI/config unification
2
- // batch's multi-graph support). Used ONLY by source.mjs's fetchEntities when a
1
+ // graph-merge.mjs — multi-graph payload merging. Used ONLY by source.mjs's fetchEntities when a
3
2
  // config carries more than one graph file (`config.graphFiles.length > 1`);
4
3
  // the single-graph path never calls this — that byte-identical guarantee
5
4
  // lives in source.mjs, not here.
@@ -8,7 +7,7 @@
8
7
  // ids as `mod:${relativePath}` (repo-relative), so two graphs describing
9
8
  // similarly-structured repos can collide. mergeEntityPayloads concatenates the
10
9
  // straightforward arrays (classes/vocabulary/objectProperties/individuals),
11
- // unions proseIndex (merging the id-array per word key), and — Option A, only
10
+ // unions proseIndex (merging the id-array per word key), and — only
12
11
  // on an ACTUAL collision — prefixes the specific colliding ids (and every
13
12
  // in-payload reference to them: derived_from entries, mentions, edge subject/
14
13
  // object, proseIndex entries) with `<graphName>/`. Ids that never collide pass
package/src/hash.mjs CHANGED
@@ -1,19 +1,11 @@
1
1
  // hash.mjs — the single home for tmct's content-address hash.
2
2
  //
3
- // FNV-1a 32-bit is deliberately home-grown (see archive/PLAN_DEPENDENCY_STRATEGY.md): it
4
- // must be synchronous, browser-safe, dependency-free, and — critically —
5
- // CROSS-VERSION STABLE, because fact ids are content-addressed by it and a fact's
6
- // id is its identity across the whole memory graph. Every library candidate fails
7
- // at least one of those; this eight-line function fails none. It lives here, once,
8
- // so the fact-id contract has exactly one definition.
9
- //
10
- // Two historical copies are reconciled here without changing a single output byte:
11
- // - src/memory/core.mjs used the hex form for fact ids;
12
- // - chatbench/graded.mjs used the integer form as a PRNG seed, with a redundant
13
- // mid-loop `>>> 0`. That `>>> 0` was always a no-op: `^` and Math.imul both
14
- // apply ToInt32 to their operands, so the 32-bit pattern is invariant between
15
- // iterations whether the accumulator is stored signed or unsigned. The final
16
- // `h >>> 0` therefore yields the same value either way — proven, not assumed.
3
+ // FNV-1a 32-bit is deliberately home-grown: it must be synchronous, browser-safe,
4
+ // dependency-free, and — critically — CROSS-VERSION STABLE, because fact ids are
5
+ // content-addressed by it and a fact's id is its identity across the whole memory
6
+ // graph. Every library candidate fails at least one of those; this eight-line
7
+ // function fails none. It lives here, once, so the fact-id contract has exactly
8
+ // one definition.
17
9
 
18
10
  /** FNV-1a 32-bit. Returns the unsigned 32-bit integer (0 … 2^32−1). */
19
11
  export function fnv1a32(str) {
package/src/index.mjs CHANGED
@@ -1,14 +1,10 @@
1
1
  // @polycode-projects/the-mechanical-code-talker (tmct) — library entry point.
2
2
  //
3
- // tmct began as a whole-package lift of an earlier chat surface (see README
4
- // provenance). Internal module filenames and symbols were kept to preserve the
5
- // shape and its green test suite; the branding throughout is now `tmct`.
6
- //
7
3
  // This entry re-exports the adapter primitives a library consumer needs. The
8
- // clean chat/primitives split (ROADMAP item 13) is done: the movable
9
- // conversational grammar lives in src/interpret/ (normalization pre-pass, the
10
- // registered parsing strategies, the merge rule), while ask.mjs keeps the core
11
- // primitives (resolveObject, traverse, render) and the ask() orchestration.
4
+ // movable conversational grammar lives in src/interpret/ (normalization
5
+ // pre-pass, the registered parsing strategies, the merge rule), while
6
+ // ask.mjs keeps the core primitives (resolveObject, traverse, render) and
7
+ // the ask() orchestration.
12
8
 
13
9
  // Chat surface (also reachable as the `./chat` subpath export).
14
10
  export { runChat, COMMANDS, answerCount, renderStats } from "./chat.mjs";
@@ -16,7 +12,7 @@ export { runChat, COMMANDS, answerCount, renderStats } from "./chat.mjs";
16
12
  // Grammar / NL-over-graph primitives.
17
13
  export { ask, resolveObject } from "./ask.mjs";
18
14
 
19
- // The interpretation pipeline (ROADMAP item 8): normalize once, run every
15
+ // The interpretation pipeline: normalize once, run every
20
16
  // registered strategy (grammar, keyword-spot, …) over the text, merge same-class
21
17
  // results, surround distinct-class results — no graph access; pair it with ask()
22
18
  // or the primitives to answer. `interpret(text, ctx)` returns the full record
@@ -29,7 +25,7 @@ export { relationKind, impactClosure } from "./codegraph.mjs";
29
25
  // Tool dispatch (slash-commands and CLI tool calls route through here).
30
26
  export { dispatchTool } from "./server.mjs";
31
27
 
32
- // Conversational memory (ROADMAP item 9) — tmct's OWN OWL-labelled graph under
28
+ // Conversational memory — tmct's OWN OWL-labelled graph under
33
29
  // .tmct/memory/, distinct from any provider-supplied code graph.
34
30
  export { loadMemory, appendUtterance, appendFact } from "./memory/core.mjs";
35
31
  export { retrieveBlocks, saveBlock, rankBlocks } from "./memory/blocks.mjs";
package/src/init.mjs CHANGED
@@ -1,29 +1,17 @@
1
1
  // init.mjs — `tmct init`: the interface's onboarding surface.
2
2
  //
3
- // One command takes a bare directory (a user's repo, or a host package such as
4
- // seonix) to a WORKING tmct install: it creates the `.tmct/` artifact tree,
5
- // writes an externalised `tmct.toml` (the seonix.toml documented-config pattern),
6
- // seeds the committed tier-1 corpus into memory, and records provenance of what
7
- // it did. See ROADMAP Phase 8 ("Distribution: tmct init") and the Phase-4
8
- // corpus-tiering policy.
3
+ // One command takes a bare directory to a WORKING tmct install: creates the `.tmct/`
4
+ // artifact tree, writes an externalised `tmct.toml`, seeds the committed tier-1 corpus
5
+ // into memory, and records provenance. Offline/deterministic/$0 (tiers 2-3 are additive
6
+ // config, never run here); idempotent and non-destructive (a benign re-init never
7
+ // throws); failure-tolerant seed (a missing/broken corpus degrades to unseeded, not a
8
+ // crash).
9
9
  //
10
10
  // initRepo(dir, { force?, seed?, env? }) → { created, config, seeded, ... }
11
11
  //
12
- // DESIGN RULES (load-bearing):
13
- // - OFFLINE, DETERMINISTIC, $0. The seed is the tier-1 committed ConceptNet
14
- // slice already in the tarball no network, ever. The $0-offline default is
15
- // inviolable (ROADMAP Phase 4); tiers 2-3 are additive config, never run here.
16
- // - IDEMPOTENT and NON-DESTRUCTIVE. Safe to re-run. A benign re-init NEVER
17
- // throws — it returns an honest result whose `message` says nothing changed.
18
- // Existing `tmct.toml` and an existing seed are preserved unless `force`.
19
- // - FAILURE-TOLERANT SEED. A missing/broken corpus degrades to an unseeded (but
20
- // still initialised) repo — the directory scaffold and config always land.
21
- //
22
- // The seed marker + limit + prefer mirror src/chat.mjs's W3 bootstrap
23
- // (SEED_MARKER_REL / SEED_LIMIT / SEED_PREFER) ON PURPOSE: both write the same
24
- // `.tmct/memory/corpus-seed.json`, so whichever of `tmct init` and first-run
25
- // bootstrap happens first wins and the other short-circuits. They are re-declared
26
- // here (not imported) to keep init off chat.mjs's heavy module graph.
12
+ // The seed marker/limit/prefer mirror chat.mjs's W3 bootstrap constants on purpose: both
13
+ // write the same `.tmct/memory/corpus-seed.json`, so whichever runs first wins. Re-declared
14
+ // here rather than imported, to keep init off chat.mjs's heavy module graph.
27
15
 
28
16
  import { mkdir, readFile, writeFile, stat } from "node:fs/promises";
29
17
  import { dirname, join, resolve } from "node:path";
@@ -56,31 +44,11 @@ export function defaultConfig() {
56
44
  };
57
45
  }
58
46
 
59
- /** `tmct init --with-persona <name>` presets (Part 7 of the extension-pack
60
- * batch; PLAN_SEED.md §2 flips which preset is the SHIPPED implicit
61
- * default): a named bundle of `extensions`/`bias` overrides, written into
62
- * tmct.toml alongside the plain defaults.
63
- *
64
- * - `human` — the NEW implicit default (src/extensions.mjs's
65
- * BUILTIN_EXTENSIONS already ships `human` active and `seon`/`conceptnet`
66
- * inactive) made EXPLICIT: an empty `extensions` override (nothing to
67
- * add — the builtin defaults already are this persona) plus an explicit
68
- * `[bias] human = 1.0`, so a repo that asks for `--with-persona human`
69
- * has a self-documenting tmct.toml rather than relying on an unstated
70
- * implicit default — the exact same discipline `code` below already used
71
- * for the OLD default.
72
- * - `code` — TODAY'S OLD IMPLICIT DEFAULT, now something a repo must
73
- * request explicitly: re-activates `seon`+`conceptnet` (both now
74
- * shipped inactive) and sets their bias, for a caller (e.g. seonix's own
75
- * code-domain chat surface) that still wants the software-domain seed.
76
- * - `empty` — the advanced escape hatch (PLAN_SEED.md §7): deactivates the
77
- * one bundle now active by default (`human`), leaving a repo genuinely
78
- * empty of corpus facts for a consumer bringing its own ontology/lexicon/
79
- * corpus. Does NOT reduce npm package size (§7's own documented caveat —
80
- * `corpus/` ships unconditionally either way).
81
- *
82
- * Kept minimal on purpose — this batch's job is the persona SEAM, not a
83
- * curated library of presets. */
47
+ /** `tmct init --with-persona <name>` presets: a named bundle of `extensions`/`bias`
48
+ * overrides written into tmct.toml. `human` makes the implicit default explicit; `code`
49
+ * re-activates the software-domain `seon`+`conceptnet` bundles; `empty` deactivates
50
+ * `human`, leaving a repo genuinely empty of corpus facts. Kept minimal on purpose —
51
+ * the persona seam, not a curated library of presets. */
84
52
  export const PERSONA_PRESETS = Object.freeze({
85
53
  human: { extensions: {}, bias: { human: 1.0 } },
86
54
  code: { extensions: { seon: { active: true }, conceptnet: { active: true } }, bias: { seon: 1.0, conceptnet: 1.0 } },
@@ -148,18 +116,12 @@ enabled = ${seed.enabled ? "true" : "false"}
148
116
  # To cap it, uncomment and set a number (definitional band first):
149
117
  ${seed.limit != null ? `limit = ${Number(seed.limit)}` : "# limit = 500"}
150
118
  `;
151
- // [memory] backend — ONLY emitted when a caller actually supplies it (an
152
- // explicit `tmct init --memory-backend <...>`, or a manual override); the
153
- // plain zero-flag `tmct init` output stays BYTE-IDENTICAL to before this
154
- // knob existed, same discipline as the extras block below. "default" is
155
- // written out explicitly rather than omitted, so `--memory-backend default`
156
- // leaves a self-documenting trace of the choice (mirrors --with-persona's
157
- // own "make the default explicit" behaviour).
119
+ // [memory] backend — only emitted when a caller actually supplies it.
158
120
  let out = base;
159
121
  if (config.memory && config.memory.backend !== undefined) {
160
122
  out += `
161
123
  [memory]
162
- # Storage backend for taught facts + the memory graph (PLAN_SEED.md §6).
124
+ # Storage backend for taught facts + the memory graph.
163
125
  # Precedence: --memory-backend flag > TMCT_MEMORY_BACKEND env > this file >
164
126
  # "default" (the built-in fallback).
165
127
  # "default" — the flat OWL-labelled JSON file under .tmct/memory/. The default.
@@ -169,12 +131,7 @@ backend = ${JSON.stringify(config.memory.backend)}
169
131
  `;
170
132
  }
171
133
 
172
- // Extension-pack / bias sections (src/extensions.mjs) ONLY emitted when a
173
- // caller actually supplies them (an explicit `--with-persona`, or a manual
174
- // override); the plain zero-flag `tmct init` output stays BYTE-IDENTICAL to
175
- // before this feature existed. Rendered via smol-toml's own stringify (not
176
- // hand-written prose like the base file above) — a plain, uncommented
177
- // config fragment is honest about being machine-written/round-tripped.
134
+ // Extension-pack / bias sections — only emitted when a caller supplies them.
178
135
  const extras = {};
179
136
  if (config.extensions !== undefined) extras.extensions = config.extensions;
180
137
  if (config.bias !== undefined) extras.bias = config.bias;
@@ -204,32 +161,20 @@ function seedRequested({ optSeed, configEnabled, env }) {
204
161
  *
205
162
  * @param {string} dir target directory (a repo root, or a host package root).
206
163
  * @param {object} [opts]
207
- * @param {boolean} [opts.force] re-write tmct.toml + re-record provenance even
208
- * when the repo is already initialised (never deletes memory/seed data).
209
- * @param {boolean} [opts.seed] force seeding on/off, overriding tmct.toml's
210
- * `seed.enabled` (TMCT_NO_SEED still vetoes).
211
- * @param {object} [opts.env] environment (for TMCT_NO_SEED); defaults to
212
- * process.env.
213
- * @param {object} [opts.persona] a resolved PERSONA_PRESETS entry
214
- * ({extensions?, bias?}) to merge into the FRESH config before it's written
215
- * (Part 7, `tmct init --with-persona <name>`). Name -> preset resolution
216
- * and unknown-name validation are the CALLER'S job (bin/tmct.mjs) — this
217
- * only ever sees an already-resolved preset object (or nothing). Has no
218
- * effect when tmct.toml already exists and `force` isn't set (the existing
219
- * "preserve a user's tmct.toml" rule wins, same as `seed`/`corpus.tier`).
220
- * @param {string} [opts.memoryBackend] "default" | "memory" | "sqlite" — merged
221
- * into the FRESH config's `[memory] backend` before it's written (same
222
- * "fresh write only" rule as `persona`, above; `tmct init --memory-backend
223
- * <...>` on an ALREADY-initialized repo is bin/tmct.mjs's own job, mirroring
224
- * how `--graph` amends an existing tmct.toml post-hoc). Also selects which
225
- * backend the corpus SEED below (step 3) writes into — src/memory/core.mjs's
226
- * `openMemoryBackend`, the same resolver chat.mjs's createSession uses, so a
227
- * seeded fact and a later chat-taught fact always land in the same store.
164
+ * @param {boolean} [opts.force] re-write tmct.toml + re-record provenance even when
165
+ * already initialised (never deletes memory/seed data).
166
+ * @param {boolean} [opts.seed] force seeding on/off (TMCT_NO_SEED still vetoes).
167
+ * @param {object} [opts.env] environment (for TMCT_NO_SEED); defaults to process.env.
168
+ * @param {object} [opts.persona] a resolved PERSONA_PRESETS entry ({extensions?, bias?})
169
+ * merged into a FRESH config only; name resolution is the caller's job (bin/tmct.mjs).
170
+ * @param {string} [opts.memoryBackend] "default" | "memory" | "sqlite" — merged into a
171
+ * FRESH config's `[memory] backend`, and selects which backend the corpus seed writes
172
+ * into (via src/memory/core.mjs's openMemoryBackend).
228
173
  * @returns {Promise<{
229
174
  * created: string[], config: object, seeded: boolean,
230
175
  * alreadyInitialized: boolean, seedResult: (object|null), message: string
231
- * }>} `created` lists the ABSOLUTE paths this call brought into being (empty on a
232
- * benign no-op re-init). Never throws on a benign re-init or a corpus failure.
176
+ * }>} `created` lists the absolute paths this call brought into being. Never throws on
177
+ * a benign re-init or a corpus failure.
233
178
  */
234
179
  export async function initRepo(dir, { force = false, seed, env = process.env, persona = null, memoryBackend = null } = {}) {
235
180
  const root = resolve(dir);
@@ -255,11 +200,7 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
255
200
 
256
201
  // ---- 2. The externalised config (preserve an existing file unless force) ----
257
202
  let config = defaultConfig();
258
- // Persona overrides (Part 7) apply ONLY to a FRESH write — an empty preset
259
- // field (e.g. `code`'s `extensions: {}`) is a genuine no-op, never an
260
- // explicit-empty-section write (renderTomlConfig only emits [extensions]/
261
- // [bias] when the merged config actually carries a non-empty one — the
262
- // plain zero-flag `tmct init` output stays byte-identical either way).
203
+ // Persona overrides apply only to a fresh write.
263
204
  if (persona) {
264
205
  if (persona.extensions && Object.keys(persona.extensions).length) config.extensions = persona.extensions;
265
206
  if (persona.bias && Object.keys(persona.bias).length) config.bias = persona.bias;
@@ -270,21 +211,11 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
270
211
  await writeFile(paths.toml, renderTomlConfig(config));
271
212
  if (!tomlPresent) created.push(paths.toml);
272
213
  } else {
273
- // Honour the user's committed tmct.toml — read its knobs back so the returned
274
- // config (and the seed decision) reflect what's actually on disk.
214
+ // Honour the user's committed tmct.toml — read its knobs back into the returned config.
275
215
  config = await readWrittenConfig(paths.toml, config);
276
216
  }
277
217
 
278
218
  // ---- 3. Seed the committed corpus (offline, failure-tolerant) ----
279
- // DELIBERATE BUG FIX (this batch): `tmct init`'s zero-flag seed step used to
280
- // seed ONLY the ConceptNet band, never the curated SEON ontology — unlike
281
- // chat.mjs's own first-run bootstrap (seedBootstrapMemory), which has always
282
- // seeded BOTH. Both now go through the SAME unified loop
283
- // (src/extensions.mjs's resolveExtensions + seedActiveCorpusEntries), so
284
- // `tmct init`'s seed matches chat's bootstrap exactly — this changes
285
- // `initRepo`'s seeded fact COUNT (test/init.test.mjs's seed-count assertions
286
- // were updated for the larger post-fix totals, deliberately, in the same
287
- // commit as this fix).
288
219
  let seeded = false;
289
220
  let seedResult = null;
290
221
  let seedNote = "";
@@ -296,17 +227,8 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
296
227
  } else if ((await exists(paths.marker)) && !force) {
297
228
  seedNote = "seed skipped (already seeded — marker present)";
298
229
  } else {
299
- // BUG FIX (found in review): this step used to call seedActiveCorpusEntries
300
- // with the plain `root` string ALWAYS Backend A only — regardless of
301
- // `config.memory.backend`. A `tmct init --memory-backend sqlite` repo ended
302
- // up with its corpus facts trapped in an inert .tmct/memory/graph.json that
303
- // a sqlite-backend chat session (createSession, which IS backend-aware)
304
- // createSession resolves it — src/memory/core.mjs's openMemoryBackend — so
305
- // the seed lands in whichever backend `config.memory.backend` actually
306
- // names. "memory" is skipped outright: it's an in-process-only store that
307
- // vanishes the moment this one-shot init process exits, so seeding it is
308
- // pure wasted work — a later `tmct chat --memory-backend memory` opens a
309
- // brand new, unrelated in-memory store anyway.
230
+ // Seeds into whichever backend config.memory.backend names. "memory" is skipped
231
+ // outright: it's an in-process-only store that vanishes when this process exits.
310
232
  const backendChoice = String(config.memory?.backend || "").trim().toLowerCase();
311
233
  if (backendChoice === "memory") {
312
234
  seedNote = "seed skipped (memory backend is in-process only — nothing would persist past this command)";
@@ -316,22 +238,14 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
316
238
  try {
317
239
  const { resolveExtensions, seedActiveCorpusEntries } = await import("./extensions.mjs");
318
240
  const { entries } = await resolveExtensions(root);
319
- // `tmct.toml`'s `[seed] limit` knob is documented as capping the tier-1
320
- // ConceptNet band specifically (the curated SEON ontology is small and
321
- // always seeds whole) — so it overrides ONLY the resolved "conceptnet"
322
- // entry's limit, exactly like the pre-fix single-corpus seed did.
241
+ // `[seed] limit` caps the tier-1 ConceptNet band specifically (SEON is small
242
+ // and always seeds whole).
323
243
  if (config.seed?.limit != null && entries.has("conceptnet")) {
324
244
  entries.set("conceptnet", { ...entries.get("conceptnet"), limit: Number(config.seed.limit) });
325
245
  }
326
246
  const { appended, skipped, total, perBundle } = await seedActiveCorpusEntries(memoryDir, entries);
327
- // seedActiveCorpusEntries is failure-tolerant PER BUNDLE (a bad third-party
328
- // pack never aborts the others) but initRepo's own "FAILURE-TOLERANT
329
- // SEED" contract is about the SEED AS A WHOLE degrading honestly. If every
330
- // active bundle failed (e.g. the memory graph file itself is unwritable —
331
- // see test/init.test.mjs "seed failure degrades"), re-throw the first
332
- // bundle's error so the SAME outer catch below reports the familiar "seed
333
- // skipped (corpus unavailable: …)" note, rather than claiming success with
334
- // zero facts actually written.
247
+ // If every active bundle failed, re-throw the first error so the outer catch
248
+ // reports it, rather than claiming success with zero facts written.
335
249
  const bundleNames = Object.keys(perBundle);
336
250
  const allFailed = bundleNames.length > 0 && bundleNames.every((n) => perBundle[n].error);
337
251
  if (allFailed) throw new Error(perBundle[bundleNames[0]].error);
@@ -401,9 +315,8 @@ async function readWrittenConfig(tomlPath, base) {
401
315
  if (raw.seed.enabled !== undefined) cfg.seed.enabled = Boolean(raw.seed.enabled);
402
316
  if (raw.seed.limit !== undefined) cfg.seed.limit = Number(raw.seed.limit);
403
317
  }
404
- // Sparse pass-through (src/extensions.mjs validates; this layer just carries
405
- // the raw tables through unmodified, same discipline as toml-config.mjs's
406
- // normalizeConfig).
318
+ // Sparse pass-through src/extensions.mjs validates; this layer just carries the
319
+ // raw tables through unmodified.
407
320
  if (raw.extensions !== undefined) cfg.extensions = raw.extensions;
408
321
  if (raw.bias !== undefined) cfg.bias = raw.bias;
409
322
  if (raw.memory && raw.memory.backend !== undefined) {
@@ -1,22 +1,12 @@
1
- // interpret/fuzzy.mjs — the bounded-edit-distance fuzzy tier (two-level fuzzy,
2
- // 2026-07-02), extracted MOVE-only from ask.mjs (item 13). A reusable SERVICE the
3
- // strategies call, not a strategy itself: the keyword-spotting strategy's tier-3
4
- // vocabulary rewrite and resolveObject's tier-5 label pass both read editDistance/
5
- // fuzzyBound from here, and the "assuming you meant …" announcement discipline
6
- // (a unique within-bound hit is announced, a tie is refused or surfaced as
7
- // ambiguity, never a silently-broken guess) is enforced by the callers off these
8
- // primitives. Deliberately coupled to the curated vocab tables via explicit
9
- // imports — the fuzzy TARGETS are a closed, curated set, same ethos as the
10
- // tables themselves. Pure JS, no deps.
1
+ // interpret/fuzzy.mjs — the bounded-edit-distance fuzzy tier, shared by the
2
+ // keyword-spotting and object-resolution strategies. A distance tie is always
3
+ // refused or surfaced as ambiguity, never broken by a guess.
11
4
 
12
5
  import { VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND } from "../ask-vocab.mjs";
13
6
  import { STOPWORDS } from "./normalize.mjs";
14
7
 
15
- // ---- bounded edit distance — hand-rolled Damerau-Levenshtein (optimal string
16
- // alignment: substitution/insertion/deletion + adjacent transposition), bounded
17
- // with an early row-minimum exit. Fires only after every exact/curated tier
18
- // missed, and a distance TIE is refused (keyword) or surfaced as ambiguity
19
- // (object), never broken by a guess. ----
8
+ // ---- bounded edit distance — hand-rolled Damerau-Levenshtein, bounded with an
9
+ // early row-minimum exit ----
20
10
 
21
11
  /** Distance between a and b, or max+1 as soon as it provably exceeds `max`. */
22
12
  export function editDistance(a, b, max) {
@@ -52,13 +42,9 @@ export const VOCAB_WORDS = new Set(
52
42
  .flatMap((p) => p.split(" ")),
53
43
  );
54
44
 
55
- /** Fuzzy-correction TARGETS: verb-phrase and modifier constituents only, length ≥4.
56
- * Entity nouns are deliberately excluded — real identifiers collide with them at
57
- * distance ≤2 far too easily ("myfile" is 2 edits from "file", "caller" 2 from
58
- * "calls"-family words), and entity-noun typos are already owned by the curated
59
- * MISSPELLINGS table where such calls are made deliberately. Short constituents
60
- * ("of", "to", "in", "on") are excluded for the same reason: at bound 1 half of
61
- * English is adjacent to them. */
45
+ /** Fuzzy-correction TARGETS: verb-phrase and modifier constituents only,
46
+ * length ≥4. Entity nouns and short words are excluded — real identifiers
47
+ * collide with them too easily at this distance bound. */
62
48
  const FUZZY_TARGET_WORDS = [...new Set(
63
49
  [...Object.keys(VERB_TO_KIND), ...Object.keys(MODIFIER_TO_KIND)]
64
50
  .flatMap((p) => p.split(" "))
@@ -79,13 +65,8 @@ export function fuzzyVocabWord(w) {
79
65
  return fuzzyMatchInSet(w, FUZZY_TARGET_WORDS, fuzzyBound(w));
80
66
  }
81
67
 
82
- /** GENERIC unique-within-bound fuzzy match of `w` against an arbitrary candidate
83
- * list, or null same discipline as fuzzyVocabWord/resolveObject's tier-5 pass
84
- * (a distance tie between two distinct candidates is refused, never guessed),
85
- * factored out so callers outside the ask/keyword pipeline (e.g. chat.mjs's
86
- * conversational recognizers) can reuse the primitive without a bespoke
87
- * target-list wrapper. `bound` defaults to fuzzyBound(w) but callers may pass a
88
- * tighter budget (e.g. to keep short conversational tokens conservative). */
68
+ /** Generic unique-within-bound fuzzy match of `w` against an arbitrary
69
+ * candidate list, or null on a tie never guessed. */
89
70
  export function fuzzyMatchInSet(w, candidates, bound = fuzzyBound(w)) {
90
71
  let best = bound + 1;
91
72
  let hit = null;
@@ -1,33 +1,15 @@
1
- // interpret/merge.mjs — class-grouped merging of strategy results (ROADMAP item 8).
2
- // Grown from ask.mjs's original two-way merge ("one strategy hit -> use it; both
3
- // hit and agree -> use it; both hit and DISAGREE -> a genuine parse-level
4
- // ambiguity; neither hits -> the honest grammar miss") into a general rule over N
5
- // strategies and N result CLASSES:
6
- //
7
- // · SAME-class candidates merge and rank — identical parses (sameParse) dedupe
8
- // onto the highest-precedence strategy's candidate, exactly as the legacy
9
- // merge returned "either" result on agreement; multiple DISTINCT parses
10
- // surviving in the winning class are the legacy {ambiguousParse, candidates}
11
- // surface, rendered by ask.mjs as "this could mean more than one thing: …".
12
- // · DISTINCT-class groups produce the ambiguity SURROUND: the winning class
13
- // answers, and each other class's best candidate is listed as an
14
- // "if you mean X then …" line (alternateLines below) — the generalization of
15
- // the engine's existing announced-correction shapes ("assuming you meant …").
16
- //
17
- // Winner selection is deterministic: the class holding the highest-confidence
18
- // candidate wins; a confidence tie goes to the earlier-registered strategy. The
19
- // two legacy strategies share one class ("graph-query"), so their merge reduces
20
- // exactly to the original behavior by construction.
1
+ // interpret/merge.mjs — class-grouped merging of strategy results: same-class
2
+ // candidates merge/rank (identical parses dedupe onto the highest-precedence
3
+ // one; distinct ones surface as an honest {ambiguousParse} tie); distinct-class
4
+ // groups surface each other's best candidate as an "if you mean X then …"
5
+ // alternate. Winner = highest-confidence class, ties go to the
6
+ // earlier-registered strategy.
21
7
 
22
8
  const DEFAULT_CONFIDENCE = 0.5;
23
9
 
24
- // "commit abc1234" and bare "abc1234" are the SAME term once resolveObject's
25
- // commit-sha tier strips the nounthe anchored strategy captures the noun inside
26
- // its object span while keyword-spot consumes it as the entity keyword, so without
27
- // this the two strategies would "disagree" over a word that names no different thing.
28
- // A leading DETERMINER is the same kind of non-difference (0.8.2 feel wave): the
29
- // anchored grammar captures "the logger" while keyword-spot captures "logger", and
30
- // the resulting "ambiguity" asked the user to choose between identical readings.
10
+ // Term equality ignores a leading "commit " on a bare SHA and a leading
11
+ // determiner ("the logger" == "logger")surface differences the two
12
+ // strategies otherwise disagree over despite meaning the same thing.
31
13
  const cmpTerm = (s) => String(s || "").trim().toLowerCase().replace(/\s+/g, " ")
32
14
  .replace(/^(?:the|a|an)\s+/, "")
33
15
  .replace(/^commit\s+(?=[0-9a-f]{7,40}$)/, "");