@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.
- package/README.md +123 -14
- package/ROADMAP.md +233 -1392
- package/bin/tmct.mjs +479 -98
- package/corpus/README.md +3 -0
- package/corpus/generated/README.md +43 -0
- package/corpus/generated/ace-surface-variants.jsonl +17 -0
- package/corpus/generated/manifest.json +9 -0
- package/corpus/tier2/generate.mjs +14668 -0
- package/corpus/tier2/human-examples-large.jsonl +1928 -0
- package/corpus/tier2/human-examples-medium.jsonl +356 -0
- package/corpus/tier2/human-examples.jsonl +120 -0
- package/corpus/tier2/human-large.jsonl +12001 -0
- package/corpus/tier2/human-medium.jsonl +944 -0
- package/corpus/tier2/human.jsonl +664 -0
- package/corpus/tier2/manifest.json +42 -0
- package/package.json +14 -8
- package/src/answer-variants.json +47 -0
- package/src/answer-variants.mjs +67 -0
- package/src/ask-browser-entry.mjs +34 -0
- package/src/ask-browser.bundle.js +5095 -0
- package/src/ask-vocab.mjs +93 -8
- package/src/ask.mjs +451 -49
- package/src/chat.mjs +1273 -137
- package/src/cli-args.mjs +164 -0
- package/src/codegraph.mjs +170 -32
- package/src/extensions.mjs +100 -19
- package/src/grammar/ace.mjs +85 -3
- package/src/grammar/lexicon-core.json +9531 -63
- package/src/grammar/lexicon.mjs +58 -8
- package/src/graph-merge.mjs +114 -0
- package/src/index.mjs +14 -0
- package/src/init.mjs +40 -14
- package/src/interpret/normalize.mjs +75 -1
- package/src/interpret/strategies/grammar.mjs +10 -0
- package/src/interpret/strategies/keywords.mjs +20 -0
- package/src/interpret/strategies/noise-strip.mjs +73 -4
- package/src/memory/core.mjs +466 -8
- package/src/router/goal-reasoner.mjs +41 -7
- package/src/router/guardrail.mjs +37 -7
- package/src/router/resolver.mjs +50 -4
- package/src/sessions.mjs +5 -1
- package/src/source.mjs +54 -1
- package/src/syllogise.mjs +398 -27
- package/src/toml-config.mjs +13 -4
- package/src/viz.mjs +541 -0
package/src/router/guardrail.mjs
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
}
|
package/src/router/resolver.mjs
CHANGED
|
@@ -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
|
-
|
|
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 }
|
|
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)
|
|
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 {
|