@polycode-projects/the-mechanical-code-talker 1.5.5 → 1.8.4

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 (45) 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 +1273 -137
  24. package/src/cli-args.mjs +164 -0
  25. package/src/codegraph.mjs +170 -32
  26. package/src/extensions.mjs +100 -19
  27. package/src/grammar/ace.mjs +85 -3
  28. package/src/grammar/lexicon-core.json +9531 -63
  29. package/src/grammar/lexicon.mjs +58 -8
  30. package/src/graph-merge.mjs +114 -0
  31. package/src/index.mjs +14 -0
  32. package/src/init.mjs +40 -14
  33. package/src/interpret/normalize.mjs +75 -1
  34. package/src/interpret/strategies/grammar.mjs +10 -0
  35. package/src/interpret/strategies/keywords.mjs +20 -0
  36. package/src/interpret/strategies/noise-strip.mjs +73 -4
  37. package/src/memory/core.mjs +466 -8
  38. package/src/router/goal-reasoner.mjs +41 -7
  39. package/src/router/guardrail.mjs +37 -7
  40. package/src/router/resolver.mjs +50 -4
  41. package/src/sessions.mjs +5 -1
  42. package/src/source.mjs +54 -1
  43. package/src/syllogise.mjs +398 -27
  44. package/src/toml-config.mjs +13 -4
  45. package/src/viz.mjs +541 -0
@@ -30,19 +30,40 @@
30
30
  import { capabilityByName, preconditionsOf, PRECOND } from "./registry.mjs";
31
31
  import { hallucinationsIn } from "./call-validator.mjs";
32
32
 
33
+ /** PLAN_BREADTH_FIRST_NLU.md §4 — the same read-only breadth-first enrichment as
34
+ * resolver.mjs's `dispatchEachCandidate`: every registered capability is
35
+ * `readOnly:true` with an empty delete-list, so dispatching the SAME tool once
36
+ * per tied candidate is safe. Returns `[{candidate, result}, ...]`, or
37
+ * undefined when there is no dispatcher to run it with. */
38
+ async function dispatchEachCandidate(pool, capName, arg, ctx) {
39
+ if (!ctx.dispatch) return undefined;
40
+ const results = [];
41
+ for (const c of pool) {
42
+ const res = await ctx.dispatch(capName, { [arg]: c.label });
43
+ results.push({ candidate: c.label, result: res });
44
+ }
45
+ return results;
46
+ }
47
+
33
48
  /** Validate a proposed tool_use. Returns a glass-box verdict:
34
- * { ok, tool, denied:[{reason,detail}], steps:[{pred,ok,...}], provenance }
49
+ * { ok, tool, denied:[{reason,detail}], steps:[{pred,ok,...}], provenance, candidateResults? }
35
50
  * - ok=false with a `default-deny`/`undeclared`/`unknown-arg`/`missing-arg`
36
51
  * denial is a STRUCTURAL rejection (no graph needed).
37
52
  * - ok=false with an `unresolved` step is a BINDING rejection (a `resolves`
38
- * precondition whose term matched no entity, or matched ambiguously).
53
+ * precondition whose term matched no entity, or matched ambiguously). An
54
+ * ambiguous `resolves` term stays a denial (never a guess at which candidate
55
+ * is "the" one) but, when `ctx.dispatch` is wired, ADDITIONALLY carries a
56
+ * top-level `candidateResults`: the SAME tool dispatched once per tied
57
+ * candidate (PLAN_BREADTH_FIRST_NLU.md §4 — mirrors resolver.mjs's
58
+ * resolveOne on the exact same ambiguity shape).
39
59
  * - ok=true means the call is RESOLVABLE + well-formed (NOT proven antecedent-
40
60
  * correct — see the file header).
41
61
  * `declaredNames` may be null to skip the declared-set check (validate against
42
62
  * the registry alone); pass it to also enforce the case/session toolset.
43
63
  * `ctx.resolve(term)` is the resolveObject oracle; omit it to skip binding proof
44
- * (structural-only validation). */
45
- export function guard(toolUse, declaredNames = null, ctx = {}) {
64
+ * (structural-only validation). Async only because an ambiguous `resolves` term
65
+ * may dispatch each candidate via `ctx.dispatch`. */
66
+ export async function guard(toolUse, declaredNames = null, ctx = {}) {
46
67
  const name = toolUse?.name;
47
68
  const input = toolUse && typeof toolUse.input === "object" && toolUse.input ? toolUse.input : {};
48
69
  const denied = [];
@@ -68,6 +89,7 @@ export function guard(toolUse, declaredNames = null, ctx = {}) {
68
89
  }
69
90
 
70
91
  // 2. PRECONDITION CHECK — the STRIPS safety gate, step by step (the proof).
92
+ let candidateResults;
71
93
  for (const pre of preconditionsOf(name)) {
72
94
  if (pre.pred === PRECOND.graphLoaded) {
73
95
  // graph presence is the harness's responsibility; if a resolver is wired we
@@ -106,15 +128,23 @@ export function guard(toolUse, declaredNames = null, ctx = {}) {
106
128
  ? `${name}.${pre.param}="${term}" is ambiguous (narrow it)`
107
129
  : `${name}.${pre.param}="${term}" resolves to no graph entity`,
108
130
  });
131
+ if (r && r.ambiguous) {
132
+ const pool = [r.match, ...(r.candidates || [])].slice(0, 4);
133
+ const dispatched = await dispatchEachCandidate(pool, name, pre.param, ctx);
134
+ if (dispatched) candidateResults = dispatched;
135
+ }
109
136
  }
110
137
  }
111
138
  }
112
139
 
113
140
  const ok = denied.length === 0;
114
- return { ok, tool: name, denied, steps, provenance: ok ? "resolvable (NOT proven antecedent-correct)" : "denied" };
141
+ return {
142
+ ok, tool: name, denied, steps, provenance: ok ? "resolvable (NOT proven antecedent-correct)" : "denied",
143
+ ...(candidateResults ? { candidateResults } : {}),
144
+ };
115
145
  }
116
146
 
117
147
  /** Convenience boolean: does a proposed tool_use PASS the guardrail? */
118
- export function admits(toolUse, declaredNames = null, ctx = {}) {
119
- return guard(toolUse, declaredNames, ctx).ok;
148
+ export async function admits(toolUse, declaredNames = null, ctx = {}) {
149
+ return (await guard(toolUse, declaredNames, ctx)).ok;
120
150
  }
@@ -41,11 +41,23 @@
41
41
 
42
42
  import { parseQuery } from "../ask.mjs";
43
43
  import { selectTool } from "../server-http.mjs";
44
+ import { SUPERLATIVE_EXTREMES } from "../ask-vocab.mjs";
44
45
  import {
45
46
  capabilities, capabilityByName, preconditionsOf, effectsOf, PRECOND,
46
47
  } from "./registry.mjs";
47
48
  import { hallucinationsIn } from "./call-validator.mjs";
48
49
 
50
+ // A ranking/superlative cue ("most", "biggest", …) in the request — the SAME
51
+ // declared vocabulary ask.mjs's own superlative grammar reads (SUPERLATIVE_EXTREMES),
52
+ // never a new keyword table. Used below to keep a flat imperative frame (a
53
+ // single unranked capability call) from claiming a request that is actually
54
+ // asking to be RANKED — that's the goal-reasoner's job (src/router/goal-reasoner.mjs's
55
+ // keystone argmax over a declared priorityTopic), not a flat listing's.
56
+ const SUPERLATIVE_RE = new RegExp(
57
+ `\\b(?:${Object.keys(SUPERLATIVE_EXTREMES).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+")).join("|")})\\b`,
58
+ "i",
59
+ );
60
+
49
61
  // ---- the ask-kind -> epistemic-topic MAPPING (the Stage-1 core) --------------
50
62
  // Keyed `${shape}:${kind}` off parseQuery's simple-clause output. The VALUE is
51
63
  // the epistemic TOPIC a capability's add-effect must achieve; backwardChain then
@@ -101,7 +113,13 @@ export const NOT_NL_REACHABLE = Object.freeze({});
101
113
  // backward-chained to a capability just like an NL intent, so the frame table
102
114
  // adds PHRASINGS, never a new routing path. ----
103
115
  export const FRAMES = Object.freeze([
104
- { re: /\buntested\b|\bwithout\s+(?:a\s+)?tests?\b|\bhas\s+no\s+tests?\b|\bneeds?\s+(?:a\s+)?tests?\b/i, topic: "untested", noArg: true },
116
+ // skipIfSuperlative: "untested" is a flat listing (which achieves it says
117
+ // nothing about RANK). A request carrying a superlative cue ("what MOST
118
+ // needs a test") is asking to be ranked — that's the goal-reasoner's
119
+ // keystone-argmax job (declared priorityTopic:"impact"), not this frame's.
120
+ // Skipping here lets the request fall through to an honest C1 refuse, which
121
+ // driver-goal.mjs escalates to the C2 meta-loop that already ranks.
122
+ { re: /\buntested\b|\bwithout\s+(?:a\s+)?tests?\b|\bhas\s+no\s+tests?\b|\bneeds?\s+(?:a\s+)?tests?\b/i, topic: "untested", noArg: true, skipIfSuperlative: true },
105
123
  { re: /\bblast\s*radius\b|\bimpacts?\b|\bimpacted\b|what\s+(?:a\s+)?change.*(?:reach|affect|touch)|what\s+(?:depends?\s+on|dependents?)\b/i, topic: "impact", arg: "module" },
106
124
  // tmct_calls (Stage 2 — the reachability win): the RAW call-edge dump, a grain
107
125
  // the relational "call" verb collides with (which routes callers/callees). This
@@ -203,6 +221,7 @@ export function mapParse(parse) {
203
221
  export function mapFrame(request) {
204
222
  for (const f of FRAMES) {
205
223
  if (!f.re.test(request)) continue;
224
+ if (f.skipIfSuperlative && SUPERLATIVE_RE.test(request)) continue;
206
225
  const cap = backwardChain(f.topic);
207
226
  if (!cap) continue;
208
227
  if (f.noArg) return { name: cap.name, noArg: true, topic: f.topic, source: "frame", why: [`imperative frame => goal (knows ${f.topic})`, `backward-chain => ${cap.name}`] };
@@ -240,14 +259,37 @@ export function proofFor(name, input) {
240
259
  return steps;
241
260
  }
242
261
 
243
- const REFUSE = (why) => ({ selected: null, refused: true, reason: why });
262
+ const REFUSE = (why, extra) => ({ selected: null, refused: true, reason: why, ...(extra || {}) });
263
+
264
+ /** PLAN_BREADTH_FIRST_NLU.md §4 — breadth-first ambiguity, read-only capabilities
265
+ * ONLY (every registered capability is `readOnly:true` with an empty delete-list,
266
+ * so dispatching the SAME tool once per tied candidate carries no double-write
267
+ * risk). Runs the bound call for each candidate in `pool` (capped, matching the
268
+ * reason string's own display cap) and returns `[{candidate, result}, ...]`, or
269
+ * undefined when there is no dispatcher to run it with (`execute:false` or no
270
+ * `ctx.dispatch` — e.g. a structural-only / planning caller). Never throws: a
271
+ * per-candidate dispatch failure is just an honest miss for that one candidate. */
272
+ async function dispatchEachCandidate(pool, capName, arg, ctx, execute) {
273
+ if (!execute || !ctx.dispatch) return undefined;
274
+ const results = [];
275
+ for (const c of pool) {
276
+ const res = await ctx.dispatch(capName, { [arg]: c.label });
277
+ results.push({ candidate: c.label, result: res });
278
+ }
279
+ return results;
280
+ }
244
281
 
245
282
  /** Select a capability for a request and BIND its arguments — the full resolver.
246
283
  * Order: command register -> NL parse -> imperative frame. On a bound selection
247
284
  * it delegates entity binding to ctx.resolve (resolveObject) and, unless
248
285
  * `execute:false`, grounds it via ctx.dispatch. Returns
249
286
  * { selected:{name,input}, proof, why, resolved, observed? } — a grounded call
250
- * { selected:null, refused:true, reason } — an honest refusal
287
+ * { selected:null, refused:true, reason, candidateResults? } — an honest refusal
288
+ * An ambiguous-term refusal stays `refused:true` (never a guess at which
289
+ * candidate is "the" one) but, when a dispatcher is available, ADDITIONALLY
290
+ * carries `candidateResults`: the SAME capability dispatched once per tied
291
+ * candidate, so a machine caller gets both the honest "still ambiguous" signal
292
+ * and every candidate's real answer (PLAN_BREADTH_FIRST_NLU.md §4).
251
293
  * NEVER emits an ungrounded / ambiguous / undeclared call. */
252
294
  export async function resolveOne(request, declaredNames, ctx, { execute = true } = {}) {
253
295
  const declared = new Set(declaredNames);
@@ -293,7 +335,11 @@ export async function resolveOne(request, declaredNames, ctx, { execute = true }
293
335
  // DELEGATE binding to resolveObject — the resolves(param,as) precondition.
294
336
  const r = ctx.resolve ? ctx.resolve(term) : { match: { label: term }, ambiguous: false };
295
337
  if (!r || !r.match) return REFUSE(`"${term}" does not resolve to any graph entity (honest miss)`);
296
- if (r.ambiguous) return REFUSE(`"${term}" is ambiguous (${[r.match, ...(r.candidates || [])].slice(0, 4).map((m) => m.label).join(", ")}) — narrow it`);
338
+ if (r.ambiguous) {
339
+ const pool = [r.match, ...(r.candidates || [])].slice(0, 4);
340
+ const candidateResults = await dispatchEachCandidate(pool, pick.name, pick.arg, ctx, execute);
341
+ return REFUSE(`"${term}" is ambiguous (${pool.map((m) => m.label).join(", ")}) — narrow it`, candidateResults ? { candidateResults } : undefined);
342
+ }
297
343
  resolved = r.match;
298
344
  input = { [pick.arg]: r.match.label };
299
345
  why = [...why, `resolveObject: "${term}" => ${r.match.label} (${r.match.class || "?"}, tier ${r.tier})`];
package/src/sessions.mjs CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
27
27
  import { basename, dirname, join } from "node:path";
28
- import { appendUtterances, CREATED_AT_PROP } from "./memory/core.mjs";
28
+ import { appendUtterances, CREATED_AT_PROP, UPDATED_AT_PROP } from "./memory/core.mjs";
29
29
 
30
30
  export const SESSIONS_DIR_REL = join(".tmct", "sessions");
31
31
 
@@ -117,6 +117,10 @@ export function upsertSession(entities, record) {
117
117
  attributes: [
118
118
  // referenced via the imported constant (single-sourced from memory/core.mjs)
119
119
  { prop: CREATED_AT_PROP, key: "createdAt", value: priorCreatedAt || started || new Date().toISOString() },
120
+ // this IS a genuinely-mutated individual (re-written every chat turn, PLAN_VIZ.md §2's
121
+ // concrete "Session" case) — stamp updatedAt explicitly since the derived "max over edges"
122
+ // rule can't see an own-attribute rewrite that touches no edge.
123
+ { prop: UPDATED_AT_PROP, key: "updatedAt", value: new Date().toISOString() },
120
124
  { prop: "mgx:sessionStarted", key: "started", value: started },
121
125
  { prop: "mgx:sessionEnded", key: "ended", value: ended },
122
126
  { prop: "mgx:sessionTurns", key: "turns", value: String(turns.length) },
package/src/source.mjs CHANGED
@@ -12,12 +12,15 @@
12
12
 
13
13
  import { readFile } from "node:fs/promises";
14
14
  import { ToolError } from "./config.mjs";
15
+ import { mergeEntityPayloads } from "./graph-merge.mjs";
15
16
 
16
17
  let cache = null; // { file, payload } — one artifact per process; cheap re-reads.
18
+ let mergedCache = null; // { key, payload } — the multi-graph path's own cache (config.graphFiles.length > 1)
17
19
  let provider = null; // registered custom loader (config) => entities payload | Promise
18
20
 
19
21
  export function clearCache() {
20
22
  cache = null;
23
+ mergedCache = null;
21
24
  }
22
25
 
23
26
  /** Register a custom graph provider: an async (or sync) `(config) => payload`
@@ -50,6 +53,46 @@ export function emptyEntities() {
50
53
  };
51
54
  }
52
55
 
56
+ /** Read + parse ONE graph artifact file — the same per-file tolerance the
57
+ * single-graph path below has always had: a MISSING file (ENOENT) is not an
58
+ * error, it's the bootstrap payload; any other read/parse failure is a clean
59
+ * ToolError naming the file. Shared by the single- and multi-graph paths. */
60
+ async function readOneGraphFile(file) {
61
+ let text;
62
+ try {
63
+ text = await readFile(file, "utf8");
64
+ } catch (e) {
65
+ if (e?.code === "ENOENT") return emptyEntities();
66
+ throw new ToolError(`cannot read graph artifact at ${file} (${e?.code || e?.message || e})`);
67
+ }
68
+ try {
69
+ return JSON.parse(text);
70
+ } catch {
71
+ throw new ToolError(`graph artifact ${file} is not valid JSON`);
72
+ }
73
+ }
74
+
75
+ /** The multi-graph path (src/graph-merge.mjs): config.graphFiles names MORE
76
+ * THAN ONE graph file. Reads each (same per-file ENOENT→bootstrap tolerance
77
+ * as the single-graph path), merges them via mergeEntityPayloads, and caches
78
+ * the merge under a composite key (the sorted, joined file list) — so
79
+ * fetching the identical set of graphs twice in a row, in any order, is a
80
+ * cheap cache hit. `config.graphNames[i]`, when present, names graph i for
81
+ * mergeEntityPayloads's collision-prefixing (falls back to the array index). */
82
+ async function fetchMergedEntities(config) {
83
+ const files = config.graphFiles;
84
+ const key = [...files].map(String).sort().join("|");
85
+ if (mergedCache && mergedCache.key === key) return mergedCache.payload;
86
+ const entries = [];
87
+ for (let i = 0; i < files.length; i++) {
88
+ const payload = await readOneGraphFile(files[i]);
89
+ entries.push({ file: files[i], payload, name: config.graphNames?.[i] });
90
+ }
91
+ const merged = mergeEntityPayloads(entries);
92
+ mergedCache = { key, payload: merged };
93
+ return merged;
94
+ }
95
+
53
96
  /** Fetch the entities payload through the provider seam. With a registered
54
97
  * provider, its result is returned as-is (uncached — a live provider owns its
55
98
  * own caching/refresh policy); a non-object result is a clean ToolError.
@@ -57,7 +100,14 @@ export function emptyEntities() {
57
100
  * process. A MISSING artifact (ENOENT) is not an error: the chat surface
58
101
  * starts from an empty graph and the first session fold-in creates the file —
59
102
  * so we return the bootstrap payload (uncached, so the freshly written file is
60
- * picked up next fetch). Every other failure still throws a clean ToolError. */
103
+ * picked up next fetch). Every other failure still throws a clean ToolError.
104
+ *
105
+ * MULTI-GRAPH: when `config.graphFiles` names more than one file, this
106
+ * delegates to fetchMergedEntities (src/graph-merge.mjs) instead — a
107
+ * SEPARATE code path from the block below. The single-graph case (one
108
+ * `config.graphFile`, or a one-element `config.graphFiles`) always falls
109
+ * through to the unchanged block below — byte-identical to before multi-graph
110
+ * support existed. */
61
111
  export async function fetchEntities(config) {
62
112
  if (provider) {
63
113
  let payload;
@@ -72,6 +122,9 @@ export async function fetchEntities(config) {
72
122
  }
73
123
  return payload;
74
124
  }
125
+ if (Array.isArray(config.graphFiles) && config.graphFiles.length > 1) {
126
+ return fetchMergedEntities(config);
127
+ }
75
128
  if (cache && cache.file === config.graphFile) return cache.payload;
76
129
  let text;
77
130
  try {