@polycode-projects/the-mechanical-code-talker 1.11.0 → 1.11.5
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 +13 -20
- package/ROADMAP.md +9 -12
- package/bin/tmct.mjs +82 -98
- package/data/games/river.txt +33 -0
- package/package.json +1 -1
- package/src/ask.mjs +119 -6
- package/src/chat.mjs +962 -51
- package/src/codegraph.mjs +24 -11
- package/src/domain.mjs +85 -6
- package/src/interpret/normalize.mjs +46 -0
- package/src/interpret/strategies/keywords.mjs +13 -9
- package/src/ledger-viz.mjs +31 -9
- package/src/memory/core.mjs +20 -1
- package/src/memory/shacl.mjs +4 -1
- package/src/memory-ask-browser-entry.mjs +5 -5
- package/src/memory-ask-browser.bundle.js +526 -47
- package/src/plan-viz.mjs +5 -6
- package/src/router/drive.mjs +122 -13
- package/src/router/resolver.mjs +13 -0
- package/src/router/taught.mjs +19 -8
- package/src/syllogise.mjs +154 -38
- package/src/viz-theme.mjs +19 -3
- package/src/ask-browser-entry.mjs +0 -19
- package/src/ask-browser.bundle.js +0 -5142
- package/src/viz.mjs +0 -959
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
|
|
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
|
|
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
|
|
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
|
|
559
|
-
[--
|
|
560
|
-
[--
|
|
561
|
-
[--
|
|
562
|
-
[--
|
|
563
|
-
[--
|
|
564
|
-
|
|
565
|
-
|
|
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
|
|
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
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
97
|
-
[--
|
|
98
|
-
[--
|
|
99
|
-
[--
|
|
100
|
-
[--
|
|
101
|
-
[--
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
1096
|
-
// memory graph
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
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
|
|
1102
|
-
|
|
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
|
|
1106
|
-
|
|
1107
|
-
|
|
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"]); //
|
|
1113
|
-
const
|
|
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
|
-
|
|
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 } : {}),
|
|
1142
|
-
...(
|
|
1143
|
-
...(nodeLimit != null ? { nodeLimit } : {}),
|
|
1144
|
-
...(hubDegree != null ? { hubDegree } : {}),
|
|
1145
|
-
...(edgeKindMode ? { edgeKindMode } : {}),
|
|
1115
|
+
...(!focus && term ? { term } : {}),
|
|
1116
|
+
...(rowLimit != null ? { rowLimit } : {}),
|
|
1146
1117
|
});
|
|
1147
|
-
//
|
|
1148
|
-
// the
|
|
1149
|
-
//
|
|
1150
|
-
|
|
1151
|
-
|
|
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 ${
|
|
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
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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.
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
process.stdout.write(`
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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.
|
|
3
|
+
"version": "1.11.5",
|
|
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
|
}
|