@polycode-projects/the-mechanical-code-talker 1.11.0 → 1.11.6

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.
package/README.md CHANGED
@@ -121,7 +121,7 @@ focus the ledger.
121
121
  Two more surfaces, both generated by tmct itself:
122
122
 
123
123
  ```bash
124
- npx tmct viz --ledger # ledger.html — your own memory as the same
124
+ npx tmct viz # ledger.html — your own memory as the same
125
125
  # readable, self-contained explorer
126
126
  npx tmct init
127
127
  npx tmct import --file .tmct/imports/games/hanoi-3.txt
@@ -427,7 +427,7 @@ tmct # bare = chat (the headline)
427
427
  tmct chat --repo /abs/path/to/repo # chat over a specific repo's graph
428
428
  tmct init # scaffold .tmct/, tmct.toml, seed + provenance
429
429
  tmct syllogise # offline: pre-derive entailed facts (maintenance)
430
- npm run viz -- --output graph.html && open graph.html # self-contained HTML graph view
430
+ npm run viz && open ledger.html # self-contained HTML memory-ledger explorer
431
431
  ```
432
432
 
433
433
  Inside the chat: `/help` lists commands, `/memory` inspects what tmct remembers
@@ -552,25 +552,18 @@ inference" above:
552
552
  [--config <path>] bounded, low-trust, retractable entailed facts (never on the chat path)
553
553
  ```
554
554
 
555
- `tmct viz` renders the memory graph to a single HTML file you can open in a browser:
555
+ `tmct viz` renders the memory graph as the ledger explorer a single,
556
+ self-contained HTML file you can open in a browser:
556
557
 
557
558
  ```
558
- tmct viz [--repo <abs>] write one self-contained, navigable HTML file rendering the
559
- [--ledger] memory graph: pan/zoom, click a node for its label/class/
560
- [--focus <id>] timestamps. Seeds from the most recently created individual
561
- [--term <word>] by default (--focus <id> or --term <word> override it);
562
- [--depth <n>] --output defaults to graph.html in the cwd. With --ledger,
563
- [--limit <n>] a readable fact ledger around one focus term instead
564
- (writes ledger.html; --limit caps the embedded fact rows).
565
- [--hub-degree <n>] --depth = max arcs (hops) from the focus node (default 3);
566
- [--edge-kind <mode>] --limit = spiral length, total nodes walked (default 300);
567
- [--output <path>] --hub-degree = stop expanding THROUGH a node above N
568
- [--config <path>] connections, still shows it (default 40); --edge-kind =
569
- meta|relation|both (default both) — which edge kinds the
570
- walk follows (provenance-only, concept-relations-only, or
571
- both — see the page's own edge-kind toggle to change this
572
- live); --term <word> resolves to the Fact(s) whose subject/
573
- object normalizes to that word and seeds from there.
559
+ tmct viz [--repo <abs>] write one self-contained HTML page: the memory graph as a
560
+ [--focus <term>] readable ledger of fact-sentences around one focus term,
561
+ [--term <word>] with segments, a two-hop minimap, and an in-page chat dock
562
+ [--limit <n>] that answers from the embedded graph. Focuses on the newest
563
+ [--output <path>] taught fact's subject by default (--focus <term> or
564
+ [--config <path>] --term <word> override it); --output defaults to
565
+ ledger.html in the cwd; --limit caps the embedded fact
566
+ rows; --term resolves via the same normalization chat uses.
574
567
  ```
575
568
 
576
569
  `tmct serve` runs an Anthropic Messages API-compatible HTTP endpoint over the graph,
@@ -620,7 +613,7 @@ recognized key set, so you can see the full surface in one place
620
613
  # Newline-delimited-file form is also accepted: repositories = "repos.txt"
621
614
  repositories = ["../other-service", "../another-service"]
622
615
 
623
- # Where generated output (e.g. tmct viz's default graph.html) resolves to.
616
+ # Where generated output (e.g. tmct viz's default ledger.html) resolves to.
624
617
  out_root = "./out"
625
618
 
626
619
  # The code-graph JSON artifact. TMCT_GRAPH_FILE overrides this at runtime.
package/ROADMAP.md CHANGED
@@ -11,7 +11,7 @@ A tolerant, ELIZA/PARRY-style chat surface over a codebase, obsessed with softwa
11
11
  obsessed with the mafia — deterministic, zero-cost, **no LLM anywhere in the product path**. Guides a
12
12
  user toward precision queries rather than guessing; every answer is grounded, restates every genuine
13
13
  reading it finds in full, or is an honest miss when nothing grounds it at all. Its visual surfaces —
14
- the ledger explorer with its in-browser chat (`tmct viz --ledger`), the animated plan page
14
+ the ledger explorer with its in-browser chat (`tmct viz`), the animated plan page
15
15
  (`chat --prompt … --render blocks`), and the Pages homepage hero — are the same graph read out loud:
16
16
  same engine, same provenance, no LLM.
17
17
 
@@ -51,19 +51,16 @@ getting silently traded away by inherited caution:
51
51
  and an NPC turn scheduler. Design-only.
52
52
  - **`PLAN_SYLLOGIST.md`** — retraction-aware consistency checking under a hard budget and trust
53
53
  tiers, the one open piece of the reasoning engine's research horizon. Design-only.
54
- - **`PLAN_HANOI.md` follow-ups** the plan itself shipped (taught game domains, the chat plan
55
- lane, `tmct import --file` + the `.tmct/imports/` scaffold, the animated plan page; see its
56
- implementation addendum). Remaining: river-crossing's two missing frames plus the multi-effect
57
- interpreter extension, and planner-side consumption of the `taught:` capability records the
58
- registry bridge now registers.
54
+ - **`PLAN_HANOI.md`** — shipped in full, including the follow-ups: river-crossing (co-travel
55
+ effects, the forbidden-together constraint frame, 7-crossing oracle) and planner-side
56
+ consumption of `taught:` capability records (`/plan` in chat and bin). See its dated addenda.
59
57
  - **`PLAN_GUESS_NUMBER.md`** — the closed-loop (observe-and-replan) planning domain for the same
60
58
  kernels. Design-only.
61
- - **`PLAN_VIZ_LEDGER.md` follow-ups** phases 1-4 shipped (the ledger explorer, its chat dock,
62
- the Pages hero, this README pass). Remaining: whether the ledger becomes the default viz
63
- surface; a `goal` field on `factAnswer`'s returns so the dock can carry the chat's goal line
64
- (needs operator sign-off it touches the ask engine); a predicate-cardinality question for
65
- `findContradictions` (multi-valued `has`/`can` facts group under the same contract as genuine
66
- disagreements); bundle weight if the ledger page outgrows its measured ~533 KB.
59
+ - **`PLAN_VIZ_LEDGER.md`**shipped in full, including the follow-ups, all resolved by
60
+ operator decision 2026-07-15: the ledger IS the `tmct viz` surface (node-link page removed),
61
+ `factAnswer`/`factReadBack` carry the additive `goal` field the dock renders, multi-valued
62
+ has/can facts are exempt from `findContradictions`, and page weight is budgeted (~561 KB
63
+ after this batch; revisit only if outgrown). See its dated addendum.
67
64
  - **`PLAN_CODE.md`** — small JS-function and HTML/CSS-fragment synthesis via a sandboxed headless
68
65
  browser (Track 1, program synthesis, already shipped). Blocked on a sandbox dependency decision.
69
66
  - **`PLAN_AGENTS.md`** — the governing plan for tmct's broader multi-repo arc (marginalia, seonix,
package/bin/tmct.mjs CHANGED
@@ -93,22 +93,14 @@ Usage:
93
93
  tmct syllogise [--repo <abs>] speculative inference (offline maintenance job): forward-
94
94
  [--depth <n>] [--budget <n>] chain the memory's rdfs:subClassOf closure, materialising
95
95
  [--config <path>] bounded, low-trust, retractable entailed facts (never on the chat path)
96
- tmct viz [--repo <abs>] write one self-contained, navigable HTML file rendering the
97
- [--ledger] memory graph: pan/zoom, click a node for its label/class/
98
- [--focus <id>] timestamps. Seeds from the most recently created individual
99
- [--term <word>] by default (--focus <id> or --term <word> override it);
100
- [--depth <n>] --output defaults to graph.html in the cwd. With --ledger,
101
- [--limit <n>] a readable fact ledger around one focus term instead
102
- (writes ledger.html; --limit caps the embedded fact rows).
103
- [--hub-degree <n>] --depth = max arcs (hops) from the focus node (default 3);
104
- [--edge-kind <mode>] --limit = spiral length, total nodes walked (default 300);
105
- [--output <path>] --hub-degree = stop expanding THROUGH a node above N
106
- [--config <path>] connections, still shows it (default 40); --edge-kind =
107
- meta|relation|both (default both) — which edge kinds the
108
- walk follows (provenance-only, concept-relations-only, or
109
- both — see the page's own edge-kind toggle to change this
110
- live); --term <word> resolves to the Fact(s) whose subject/
111
- object normalizes to that word and seeds from there.
96
+ tmct viz [--repo <abs>] write one self-contained HTML page: the memory graph as a
97
+ [--focus <term>] readable ledger of fact-sentences around one focus term,
98
+ [--term <word>] with segments, a two-hop minimap, and an in-page chat dock
99
+ [--limit <n>] that answers from the embedded graph. Focuses on the newest
100
+ [--output <path>] taught fact's subject by default (--focus <term> or
101
+ [--config <path>] --term <word> override it); --output defaults to
102
+ ledger.html in the cwd; --limit caps the embedded fact
103
+ rows; --term resolves via the same normalization chat uses.
112
104
  tmct serve [--repo <abs>] run the Anthropic Messages API-compatible endpoint
113
105
  [--host <h>] [--port <n>] (POST /v1/messages) over the graph — a deterministic,
114
106
  [--graph <path>] no-LLM "model" a tool-loop client can call; $0 usage.
@@ -1092,73 +1084,50 @@ async function main() {
1092
1084
  }
1093
1085
 
1094
1086
  if (mode === "viz") {
1095
- // `tmct viz` — one self-contained, navigable HTML file rendering the
1096
- // memory graph (PLAN_BREADTH_FIRST_NLU.md §5, PLAN_VIZ.md's design):
1097
- // pan/zoom, click-a-node, a concentric ring layout keyed on hop with a
1098
- // depth/age falloff. Same repo resolution as `memory`/`syllogise`
1099
- // resolveRuntimeConfig: --repo > git root > cwd.
1087
+ // `tmct viz` — the ledger explorer: one self-contained HTML page rendering
1088
+ // the memory graph as readable fact-sentences around a focus term, with
1089
+ // the in-browser chat dock (PLAN_VIZ_LEDGER.md). Same repo resolution as
1090
+ // `memory`/`syllogise` resolveRuntimeConfig: --repo > git root > cwd.
1091
+ // `--ledger` is accepted as a no-op: the ledger IS the viz surface now.
1100
1092
  const rest = process.argv.slice(3);
1101
- const { strFlag, boolFlag, resolveRuntimeConfig } = await import("../src/cli-args.mjs");
1102
- const { computeVizGraph, renderVizHtml, readAskBundle, readMemoryAskBundle } = await import("../src/viz.mjs");
1093
+ const retiredFlags = ["--depth", "--hub-degree", "--edge-kind"].filter((f) => rest.includes(f));
1094
+ if (retiredFlags.length) {
1095
+ process.stderr.write(
1096
+ `tmct viz: ${retiredFlags.join(", ")} belonged to the retired node-link graph page and no longer exist${retiredFlags.length === 1 ? "s" : ""}.\n`
1097
+ + "The ledger view takes: --repo <abs>, --focus <term>, --term <word>, --limit <n>, --output <path>.\n",
1098
+ );
1099
+ process.exitCode = 1;
1100
+ return;
1101
+ }
1102
+ const { strFlag, resolveRuntimeConfig } = await import("../src/cli-args.mjs");
1103
+ const { computeLedgerData, renderLedgerHtml, readMemoryAskBundle } = await import("../src/ledger-viz.mjs");
1103
1104
  const { writeFile } = await import("node:fs/promises");
1104
1105
  const { resolve } = await import("node:path");
1105
- const numFlag = (name) => {
1106
- const j = rest.indexOf(name);
1107
- const v = j !== -1 ? Number(rest[j + 1]) : NaN;
1108
- return Number.isFinite(v) ? v : undefined;
1109
- };
1110
- const ledger = boolFlag(rest, ["--ledger"]);
1106
+ const limitIdx = rest.indexOf("--limit");
1107
+ const limitRaw = limitIdx !== -1 ? Number(rest[limitIdx + 1]) : NaN;
1108
+ const rowLimit = Number.isFinite(limitRaw) ? limitRaw : undefined;
1111
1109
  const focus = strFlag(rest, ["--focus"]);
1112
- const term = strFlag(rest, ["--term"]); // PLAN_VIZ_MEMORY.md: seed via normFactTerm-matched Fact(s), alongside --focus
1113
- const depth = numFlag("--depth"); // max arcs (hops) from the focus node
1114
- const nodeLimit = numFlag("--limit"); // spiral length: total nodes walked (ledger mode: fact-row cap)
1115
- const hubDegree = numFlag("--hub-degree"); // stop expanding THROUGH a node above N connections
1116
- const edgeKindModeRaw = strFlag(rest, ["--edge-kind"]);
1117
- const edgeKindMode = ["meta", "relation", "both"].includes(edgeKindModeRaw) ? edgeKindModeRaw : undefined;
1118
- const outPath = resolve(process.cwd(), strFlag(rest, ["--output", "--out"], ledger ? "ledger.html" : "graph.html"));
1110
+ const term = strFlag(rest, ["--term"]); // seeds via normFactTerm; --focus wins when both are given
1111
+ const outPath = resolve(process.cwd(), strFlag(rest, ["--output", "--out"], "ledger.html"));
1119
1112
  const { repo } = await resolveRuntimeConfig({ argv: rest });
1120
- if (ledger) {
1121
- const { computeLedgerData, renderLedgerHtml } = await import("../src/ledger-viz.mjs");
1122
- const data = await computeLedgerData(repo, {
1123
- ...(focus ? { focus } : {}),
1124
- ...(!focus && term ? { term } : {}),
1125
- ...(nodeLimit != null ? { rowLimit: nodeLimit } : {}),
1126
- });
1127
- const memoryAskBundle = await readMemoryAskBundle();
1128
- await writeFile(outPath, renderLedgerHtml({ ...data, memoryAskBundle }), "utf8");
1129
- process.stdout.write(
1130
- `tmct viz --ledger — wrote ${data.meta.shown} fact row(s) around ${data.focus ? `'${data.focus}'` : "no focus"} to ${outPath}\n`,
1131
- );
1132
- if (data.meta.truncated) {
1133
- process.stdout.write(
1134
- `showing ${data.meta.shown} of ${data.meta.total} rows — narrow with --focus <term> or raise --limit\n`,
1135
- );
1136
- }
1137
- return;
1138
- }
1139
- const vizGraph = await computeVizGraph(repo, {
1113
+ const data = await computeLedgerData(repo, {
1140
1114
  ...(focus ? { focus } : {}),
1141
- ...(!focus && term ? { term } : {}), // --focus takes precedence when both are given
1142
- ...(depth != null ? { depth } : {}),
1143
- ...(nodeLimit != null ? { nodeLimit } : {}),
1144
- ...(hubDegree != null ? { hubDegree } : {}),
1145
- ...(edgeKindMode ? { edgeKindMode } : {}),
1115
+ ...(!focus && term ? { term } : {}),
1116
+ ...(rowLimit != null ? { rowLimit } : {}),
1146
1117
  });
1147
- // The embedded "Ask the graph" chat panels TWO real engines, bundled for
1148
- // the browser (scripts/build-ask-bundle.mjs's checked-in output): the
1149
- // code-graph ask.mjs engine, and (PLAN_VIZ_MEMORY.md Bug 1 fix) the
1150
- // memory-graph factAnswer engine. Neither read*AskBundle() ever throws; an
1151
- // empty string degrades that ONE engine gracefully rather than breaking
1152
- // the page (e.g. a fresh checkout before the bundles' first build).
1153
- const [askBundle, memoryAskBundle] = await Promise.all([readAskBundle(), readMemoryAskBundle()]);
1154
- const html = renderVizHtml({ ...vizGraph, askBundle, memoryAskBundle });
1155
- await writeFile(outPath, html, "utf8");
1156
- const chatNote = askBundle || memoryAskBundle
1157
- ? ` (with the embedded ask-the-graph chat panel${askBundle && memoryAskBundle ? "s" : ""})`
1158
- : " (no chat panel — run `npm run build:ask-bundle` first)";
1118
+ // readMemoryAskBundle never throws; an empty string renders the page with
1119
+ // an honest "chat unavailable" note instead of the dock (e.g. a fresh
1120
+ // checkout before the bundle's first build).
1121
+ const memoryAskBundle = await readMemoryAskBundle();
1122
+ await writeFile(outPath, renderLedgerHtml({ ...data, memoryAskBundle }), "utf8");
1159
1123
  process.stdout.write(
1160
- `tmct viz — wrote ${vizGraph.nodes.length} node(s), ${vizGraph.edges.length} edge(s) to ${outPath}${chatNote}\n`,
1124
+ `tmct viz — wrote ${data.meta.shown} fact row(s) around ${data.focus ? `'${data.focus}'` : "no focus"} to ${outPath}\n`,
1161
1125
  );
1126
+ if (data.meta.truncated) {
1127
+ process.stdout.write(
1128
+ `showing ${data.meta.shown} of ${data.meta.total} rows — narrow with --focus <term> or raise --limit\n`,
1129
+ );
1130
+ }
1162
1131
  return;
1163
1132
  }
1164
1133
 
@@ -1264,7 +1233,7 @@ async function main() {
1264
1233
  process.stderr.write("tmct plan: needs a request, e.g. `tmct plan \"of the modules impacted by X, which are untested\"`\n");
1265
1234
  process.exit(2);
1266
1235
  }
1267
- const { config } = await resolveRuntimeConfig({ argv: rest });
1236
+ const { repo, config } = await resolveRuntimeConfig({ argv: rest });
1268
1237
  const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } = await import("../src/router/drive.mjs");
1269
1238
  const declared = declaredCapabilityNames();
1270
1239
  let tools = declared;
@@ -1278,34 +1247,49 @@ async function main() {
1278
1247
  }
1279
1248
  let ctx;
1280
1249
  try {
1281
- ctx = await buildCapabilityPlanCtx({ config });
1250
+ ctx = await buildCapabilityPlanCtx({ config, memoryDir: repo });
1282
1251
  } catch (e) {
1283
1252
  process.stderr.write(`tmct plan: could not load the graph — ${e?.message || e}\n`);
1284
1253
  process.exit(1);
1285
1254
  }
1286
- const result = await runCapabilityPlan(request, tools, ctx);
1287
- if (jsonFlag) {
1288
- process.stdout.write(JSON.stringify({ request, ...result }, null, 2) + "\n");
1289
- process.exit(result.refused ? 1 : 0);
1290
- }
1291
- process.stdout.write(`tmct plan: "${request}"\n`);
1292
- if (result.refused) {
1293
- process.stdout.write(`no plan found — ${Array.isArray(result.why) ? result.why.join("; ") : result.why}\n`);
1294
- if (result.c1Why) {
1295
- process.stdout.write(`(the direct router also declined: ${Array.isArray(result.c1Why) ? result.c1Why.join("; ") : result.c1Why})\n`);
1255
+ // The default (no --tools) toolset is re-read AFTER the ctx build: the
1256
+ // memory store's taught: capability records only register there, and a
1257
+ // world goal refuses when its taught record is outside the toolset.
1258
+ if (!toolsFlag) tools = declaredCapabilityNames();
1259
+ try {
1260
+ const result = await runCapabilityPlan(request, tools, ctx);
1261
+ if (jsonFlag) {
1262
+ process.stdout.write(JSON.stringify({ request, ...result }, null, 2) + "\n");
1263
+ process.exit(result.refused ? 1 : 0);
1296
1264
  }
1297
- process.exit(1);
1298
- }
1299
- process.stdout.write(`driver: ${result.driver}\n\nsteps:\n`);
1300
- for (let i = 0; i < result.calls.length; i += 1) {
1301
- const c = result.calls[i];
1302
- let text = "";
1303
- try { text = await ctx.dispatch(c.name, c.input || {}).then((r) => (r.ok ? r.text : `(unresolved: ${r.error})`)); }
1304
- catch (e) { text = `(error: ${e?.message || e})`; }
1305
- process.stdout.write(` ${i + 1}. ${c.name} ${JSON.stringify(c.input || {})}\n ${String(text).split("\n").join("\n ")}\n`);
1306
- }
1307
- if (result.composed !== undefined && result.composed !== null) {
1308
- process.stdout.write(`\ncomposed answer (${result.composed.length}): ${result.composed.length ? result.composed.join(", ") : "(empty set)"}\n`);
1265
+ process.stdout.write(`tmct plan: "${request}"\n`);
1266
+ if (result.refused) {
1267
+ process.stdout.write(`no plan found — ${Array.isArray(result.why) ? result.why.join("; ") : result.why}\n`);
1268
+ if (result.c1Why) {
1269
+ process.stdout.write(`(the direct router also declined: ${Array.isArray(result.c1Why) ? result.c1Why.join("; ") : result.c1Why})\n`);
1270
+ }
1271
+ process.exit(1);
1272
+ }
1273
+ process.stdout.write(`driver: ${result.driver}\n\nsteps:\n`);
1274
+ for (let i = 0; i < result.calls.length; i += 1) {
1275
+ const c = result.calls[i];
1276
+ let text = "";
1277
+ // taught: records are simulated, never dispatchable — dispatching one
1278
+ // would print a misleading "unknown tool" under an honest plan step.
1279
+ if (c.name.startsWith("taught:")) text = "(simulated over the taught rules — execute it in chat with \"next\")";
1280
+ else {
1281
+ try { text = await ctx.dispatch(c.name, c.input || {}).then((r) => (r.ok ? r.text : `(unresolved: ${r.error})`)); }
1282
+ catch (e) { text = `(error: ${e?.message || e})`; }
1283
+ }
1284
+ process.stdout.write(` ${i + 1}. ${c.name} ${JSON.stringify(c.input || {})}\n ${String(text).split("\n").join("\n ")}\n`);
1285
+ }
1286
+ if (result.composed !== undefined && result.composed !== null) {
1287
+ process.stdout.write(`\ncomposed answer (${result.composed.length}): ${result.composed.length ? result.composed.join(", ") : "(empty set)"}\n`);
1288
+ } else if (result.observed) {
1289
+ process.stdout.write(`\n${result.observed}\n`);
1290
+ }
1291
+ } finally {
1292
+ for (const dispose of ctx.disposers || []) dispose();
1309
1293
  }
1310
1294
  return;
1311
1295
  }
@@ -0,0 +1,33 @@
1
+ # river — the wolf/goat/cabbage crossing, taught with the same closed frames
2
+ # as hanoi-3 plus two new families: a co-travel effect (the farmer rides every
3
+ # crossing) and the "may not be with ... without ..." constraint.
4
+ # Lines starting with # are skipped by import.
5
+ #
6
+ # After `tmct import --file river.txt`, try this in `tmct chat` (or via --prompt):
7
+ #
8
+ # wolf-1 stands on bank-east. goat-1 stands on bank-east.
9
+ # cabbage-1 stands on bank-east. farmer-1 stands on bank-east.
10
+ # the goal is that every passenger stands on bank-west. solve it.
11
+ #
12
+ # The classic optimum is 7 crossings: goat over, farmer back, wolf over, goat
13
+ # back, cabbage over, farmer back, goat over. From the opening position exactly
14
+ # 1 move is legal (ferry the goat) — every other crossing leaves the wolf with
15
+ # the goat, or the goat with the cabbage, without the farmer.
16
+ #
17
+ # Other things to try:
18
+ # legality — "what moves are legal now?" (one ply, no plan; shows the
19
+ # constraint pruning: 1 legal move at the start)
20
+ # read-back — "what stands on bank-east?" (plain fact question, no planning)
21
+ a passenger is a kind of game piece.
22
+ a bank is a kind of place.
23
+ wolf-1 is a wolf. wolf-1 is a passenger.
24
+ goat-1 is a goat. goat-1 is a passenger.
25
+ cabbage-1 is a cabbage. cabbage-1 is a passenger.
26
+ farmer-1 is a farmer.
27
+ bank-east is a bank. bank-west is a bank.
28
+ you can ferry a passenger onto a bank.
29
+ you can ferry a farmer onto a bank.
30
+ ferrying a passenger onto a bank makes the passenger stand on the target.
31
+ ferrying a passenger onto a bank makes the farmer stand on the target.
32
+ to ferry a passenger onto a bank, the wolf may not be with the goat without the farmer.
33
+ to ferry a passenger onto a bank, the goat may not be with the cabbage without the farmer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "1.11.0",
3
+ "version": "1.11.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
package/src/ask.mjs CHANGED
@@ -31,7 +31,7 @@ import { parseAnchored } from "./interpret/strategies/grammar.mjs";
31
31
  import { parseKeywordSpot, findPhrase } from "./interpret/strategies/keywords.mjs";
32
32
  import { runStrategiesSync } from "./interpret/pipeline.mjs";
33
33
  import { mergeStrategyResults, alternateLines } from "./interpret/merge.mjs";
34
- import { lookupByProseTokens } from "./prose.mjs";
34
+ import { lookupByProseTokens, splitIdentifierWords } from "./prose.mjs";
35
35
  import { pickPhrase } from "./answer-variants.mjs";
36
36
 
37
37
  // Normalization stays importable from its original site (tests + chat surface).
@@ -88,6 +88,20 @@ function nounFor(entityType, n) {
88
88
  return n === 1 ? s : p;
89
89
  }
90
90
 
91
+ /** A class enum rendered as prose words ("GlobalVariable" -> "global
92
+ * variable"), lowercase to match nounFor's own convention. For the render
93
+ * sites that must name the enum itself rather than a curated PLURAL_FORMS
94
+ * noun. The typeof guard is the same viewer-bundle boundary the tier-4 prose
95
+ * fallback documents: viz.mjs's askSource strips the prose.mjs import. */
96
+ export function classDisplayName(cls) {
97
+ const s = String(cls || "");
98
+ if (typeof splitIdentifierWords === "function") {
99
+ const words = splitIdentifierWords(s).join(" ");
100
+ if (words) return words;
101
+ }
102
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
103
+ }
104
+
91
105
  // Every relation kind is already the correct 3rd-person-singular verb form
92
106
  // except "cochange" ("X cochanges Y") and "reexports" (human word "export").
93
107
  const REVERSE_MISS_VERB = { cochange: "cochanges", reexports: "export" };
@@ -2342,6 +2356,43 @@ function commitTouches(graph, commit, entityType, extra = {}) {
2342
2356
  };
2343
2357
  }
2344
2358
 
2359
+ // ---- entry-point survey for the where-defined shape: "where is the (main)
2360
+ // entry point defined" names a ROLE, not a label, so resolveObject can only
2361
+ // miss it (or accidentally substring-match something unrelated). A closed
2362
+ // basename vocabulary stands in for the role; every candidate is ranked
2363
+ // deterministically and the ranking is disclosed in the answer, never a
2364
+ // silently picked single winner. ----
2365
+
2366
+ /** The where-defined object phrasings that ask for the entry-point role. */
2367
+ const ENTRY_POINT_QUERY_RE = /^(?:the\s+)?(?:main\s+|primary\s+)?entry[\s-]?points?(?:\s+(?:of|to|for)\s+(?:this|the)\s+(?:codebase|code|repo|repository|project|app))?$/i;
2368
+ /** Module basenames conventionally used as a program's entry point. */
2369
+ const ENTRY_POINT_BASENAMES = new Set(["index", "main", "app", "server", "cli", "__main__"]);
2370
+ /** Directory segments marking test/fixture territory — ranked below real code. */
2371
+ const TEST_FIXTURE_PATH_SEGMENTS = new Set(["test", "tests", "__tests__", "fixture", "fixtures", "spec", "specs", "testdata"]);
2372
+
2373
+ const moduleStemOf = (label) => String(label).toLowerCase().split("/").pop().replace(/\.[a-z0-9]+$/, "");
2374
+ const isTestFixturePath = (label) => String(label).toLowerCase().split("/").slice(0, -1)
2375
+ .some((seg) => TEST_FIXTURE_PATH_SEGMENTS.has(seg));
2376
+
2377
+ /** All entry-point-basename Modules, best first: a basename the query itself
2378
+ * names ("MAIN entry point" -> main.mjs) beats root proximity (fewer path
2379
+ * segments), which beats a non-test path over a test/fixture one; a full tie
2380
+ * falls back to label order so the ranking is stable. */
2381
+ function rankEntryPointModules(graph, term) {
2382
+ const queryWords = new Set(String(term || "").toLowerCase().split(/[\s-]+/).filter(Boolean));
2383
+ return (graph.individuals || [])
2384
+ .filter((i) => i.class === "Module" && ENTRY_POINT_BASENAMES.has(moduleStemOf(i.label)))
2385
+ .map((ind) => ({
2386
+ ind,
2387
+ named: queryWords.has(moduleStemOf(ind.label)) ? 1 : 0,
2388
+ depth: String(ind.label).split("/").length,
2389
+ fixture: isTestFixturePath(ind.label) ? 1 : 0,
2390
+ }))
2391
+ .sort((a, b) => (b.named - a.named) || (a.depth - b.depth) || (a.fixture - b.fixture)
2392
+ || String(a.ind.label).localeCompare(String(b.ind.label)))
2393
+ .map((x) => x.ind);
2394
+ }
2395
+
2345
2396
  /** Safety net: a {shape, kind, entityType} combination must be explicitly
2346
2397
  * listed here to receive real non-"direct" modifier behavior; anything else
2347
2398
  * gets an honest "not supported yet" response, never a silent fallback to
@@ -2414,6 +2465,18 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2414
2465
  };
2415
2466
  }
2416
2467
 
2468
+ // where + an entry-point ROLE phrasing: checked before object resolution,
2469
+ // which could only miss the role term or accidentally land on an unrelated
2470
+ // partial match.
2471
+ if (shape === "where" && ENTRY_POINT_QUERY_RE.test(String(parsed.object || "").trim())) {
2472
+ const ranked = rankEntryPointModules(graph, parsed.object);
2473
+ return {
2474
+ matches: ranked, objMatch: ranked[0] || null, candidates: ranked.slice(1, 5),
2475
+ ambiguous: false, entryPointShape: true,
2476
+ traversal: `Module individuals with an entry-point basename (${[...ENTRY_POINT_BASENAMES].join("/")}), ranked query-named basename first, then shallower path, then non-test path`,
2477
+ };
2478
+ }
2479
+
2417
2480
  // Checked before object resolution, so an unsupported modifier+kind
2418
2481
  // combination gets its own honest capability-gap message.
2419
2482
  if (parsed.modifier && parsed.modifier !== "direct" && !modifierIsWired(shape, kind, entityType)) {
@@ -2598,7 +2661,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2598
2661
  return {
2599
2662
  matches: [], objMatch, candidates, ambiguous, matchedVia,
2600
2663
  forwardGrainMiss: true, wantClasses: [...wantClasses],
2601
- traversal: `${fwdKinds.join("+")} edges where subject = ${objMatch.label} (grain mismatch: this "${kind}" relation never targets a ${entityType})`,
2664
+ traversal: `${fwdKinds.join("+")} edges where subject = ${objMatch.label} (grain mismatch: this "${kind}" relation never targets a ${classDisplayName(entityType)})`,
2602
2665
  };
2603
2666
  }
2604
2667
  }
@@ -2705,7 +2768,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2705
2768
  return {
2706
2769
  matches: [], objMatch: gObjMatch, candidates: gCandidates, ambiguous: gAmbiguous, matchedVia: gMatchedVia,
2707
2770
  wrongGrainMiss: true, wantClass,
2708
- traversal: `"${parsed.object}" resolved to ${gObjMatch.class} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${wantClass}, and no containing module could be found to refine to)`,
2771
+ traversal: `"${parsed.object}" resolved to ${classDisplayName(gObjMatch.class)} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${classDisplayName(wantClass)}, and no containing module could be found to refine to)`,
2709
2772
  };
2710
2773
  }
2711
2774
  } else {
@@ -2714,7 +2777,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2714
2777
  return {
2715
2778
  matches: [], objMatch: gObjMatch, candidates: gCandidates, ambiguous: gAmbiguous, matchedVia: gMatchedVia,
2716
2779
  wrongGrainMiss: true, wantClass,
2717
- traversal: `"${parsed.object}" resolved to ${gObjMatch.class} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${wantClass})`,
2780
+ traversal: `"${parsed.object}" resolved to ${classDisplayName(gObjMatch.class)} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${classDisplayName(wantClass)})`,
2718
2781
  };
2719
2782
  }
2720
2783
  }
@@ -2764,7 +2827,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2764
2827
  } else if (entityType !== "Module" && subjects.some((s) => s.class === "Module")) {
2765
2828
  const moduleIds = new Set(subjects.filter((s) => s.class === "Module").map((s) => s.id));
2766
2829
  matches = refineToEntities(graph, moduleIds, entityType);
2767
- grainNote = `, then ${entityType} defined in the matched module(s)`;
2830
+ grainNote = `, then ${classDisplayName(entityType)} defined in the matched module(s)`;
2768
2831
  } else {
2769
2832
  matches = [];
2770
2833
  }
@@ -2883,6 +2946,7 @@ function renderCore(parsed, result) {
2883
2946
  return {
2884
2947
  content: `this could mean more than one thing:\n${options}\n(ask one of these directly, or try rephrasing more specifically, to get just that reading)`,
2885
2948
  miss: false, ambiguous: true, candidates: parsed.candidates.map(describeParse),
2949
+ candidateParses: parsed.candidates,
2886
2950
  };
2887
2951
  }
2888
2952
  // Fallback (no `result.branches` — e.g. a caller invoking render() directly
@@ -2891,6 +2955,7 @@ function renderCore(parsed, result) {
2891
2955
  return {
2892
2956
  content: `this could mean more than one thing: ${options} — try rephrasing more specifically.`,
2893
2957
  miss: false, ambiguous: true, candidates: parsed.candidates.map(describeParse),
2958
+ candidateParses: parsed.candidates,
2894
2959
  };
2895
2960
  }
2896
2961
  if (result.unresolvedPronoun) {
@@ -2967,6 +3032,25 @@ function renderCore(parsed, result) {
2967
3032
  miss: false, ambiguous: false, matches: result.matches,
2968
3033
  };
2969
3034
  }
3035
+ // entry-point survey: every candidate is disclosed with the rank order,
3036
+ // never a silently picked single winner; zero candidates is an honest miss
3037
+ // naming the closed basename vocabulary that was searched.
3038
+ if (result.entryPointShape) {
3039
+ if (!result.matches.length) {
3040
+ return {
3041
+ content: `no entry-point module found in the index — no module basename matches ${listJoin([...ENTRY_POINT_BASENAMES])}.`,
3042
+ miss: true, ambiguous: false, candidates: [],
3043
+ };
3044
+ }
3045
+ const [top, ...rest] = result.matches;
3046
+ const shownRest = rest.slice(0, OVERFLOW_CAP).map((i) => i.label);
3047
+ const extra = rest.length > OVERFLOW_CAP ? `, …and ${rest.length - OVERFLOW_CAP} more` : "";
3048
+ const also = rest.length ? ` — also matched: ${listJoin(shownRest)}${extra}` : "";
3049
+ return {
3050
+ content: `ranked ${result.matches.length} entry-point match${result.matches.length === 1 ? "" : "es"}; top: ${top.label}${also}.`,
3051
+ miss: false, ambiguous: false, matches: result.matches,
3052
+ };
3053
+ }
2970
3054
  if (!result.objMatch && (!result.candidates || result.candidates.length === 0) && parsed.shape !== "ask") {
2971
3055
  // Name what kind of thing was looked for: a sha-shaped term reads as
2972
3056
  // "commit", a dotted slash-free term as "symbol" (both keep priority over
@@ -3491,6 +3575,12 @@ function dynamicClassQuery(graph, query) {
3491
3575
  // extract the term for the article-insertion fallback rather than duplicating it.
3492
3576
  const BARE_META_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
3493
3577
 
3578
+ // "what is X for" / "what is X used for" with a bare entity term — the lead
3579
+ // refuses a/an articles and pronouns so the vocabulary phrasings ("what is a
3580
+ // horse for", "what is it for") never read as an entity term; a leading "the"
3581
+ // is entity-term noise (resolveObject's own article strip) and is dropped.
3582
+ const WHATIS_FOR_FALLBACK_RE = /^what\s+is\s+(?:the\s+)?(?!(?:an?|it|this|that|these|those)\s)(.+?)\s+(?:used\s+)?for[?.!\s]*$/i;
3583
+
3494
3584
  export function ask(graph, query, { contextId = null, nlp = undefined, prev = null } = {}) {
3495
3585
  if (isHelpRequest(query)) {
3496
3586
  return {
@@ -3540,6 +3630,29 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
3540
3630
  }
3541
3631
  }
3542
3632
  }
3633
+ // "what is X for" — the purpose paraphrase of the same bare-whatis intent,
3634
+ // excluded from the block above (and from the phrasing frames) because
3635
+ // chat.mjs's module-overview lane owns this phrasing and gates on an ask()
3636
+ // miss. The meta reading is adopted ONLY when it actually answers (a unique
3637
+ // metaFallback entity); on a miss, every byte — parsed, canonical, the miss
3638
+ // text — stays exactly as the lane cascade expects. Article/pronoun-led
3639
+ // terms ("what is a horse for") belong to the memory-facts readers and are
3640
+ // never claimed.
3641
+ if (parsed === null && rendered.miss && !rendered.ambiguous) {
3642
+ const forM = normalizeQuery(String(query || "")).match(WHATIS_FOR_FALLBACK_RE);
3643
+ const forTerm = forM?.[1]?.trim();
3644
+ if (forTerm) {
3645
+ const forParsed = parseQuery(`what is a ${forTerm}`, { nlp });
3646
+ if (forParsed?.shape === "meta") {
3647
+ const forResult = traverse(graph, forParsed, { contextId, prev });
3648
+ const forRendered = render(forParsed, forResult);
3649
+ if (!forRendered.miss && !forRendered.ambiguous) {
3650
+ result = forResult;
3651
+ rendered = forRendered;
3652
+ }
3653
+ }
3654
+ }
3655
+ }
3543
3656
  // If relaxation materially rewrote the query and produced a real answer,
3544
3657
  // note it lightly so the reader knows how the question was read.
3545
3658
  let content = (relaxed && !rendered.miss && relaxed.to !== relaxed.from)
@@ -3586,7 +3699,7 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
3586
3699
  // edit-distance, announced in the content as "assuming you meant …");
3587
3700
  // null for every literal-identifier tier.
3588
3701
  matchedVia: result.matchedVia || null,
3589
- ...(rendered.ambiguous ? { candidates: rendered.candidates } : {}),
3702
+ ...(rendered.ambiguous ? { candidates: rendered.candidates, candidateParses: rendered.candidateParses } : {}),
3590
3703
  },
3591
3704
  };
3592
3705
  }