@polycode-projects/the-mechanical-code-talker 0.9.6 → 0.9.8
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/ROADMAP.md +33 -9
- package/package.json +1 -1
- package/src/ask.mjs +60 -2
- package/src/chat.mjs +20 -3
- package/src/codegraph.mjs +22 -2
- package/src/interpret/normalize.mjs +7 -0
- package/src/router/goal-reasoner.mjs +62 -4
package/ROADMAP.md
CHANGED
|
@@ -12,11 +12,28 @@ mapped into them (item numbers retained for traceability); the seven sketches
|
|
|
12
12
|
formerly held in `code-talker-ideas.txt` are folded into items 8–11 below and
|
|
13
13
|
the file has been deleted.
|
|
14
14
|
|
|
15
|
-
## Where we are now (2026-07-
|
|
15
|
+
## Where we are now (2026-07-08)
|
|
16
16
|
|
|
17
|
-
`npm test` green (**
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
`npm test` green (**1055**). **v0.9.6, pushed** (0.8.2 → 0.9.6 across this session — see
|
|
18
|
+
`HANDOVER.md` for the exact release chain).
|
|
19
|
+
|
|
20
|
+
### Doing right now (5 background clusters, all worktree-isolated, merged sequentially as each lands)
|
|
21
|
+
|
|
22
|
+
- **Cluster A** — Bug 6 (scoped-listing false-empty) + Bug 7 (modal-in-fuzzy-cascade) + the
|
|
23
|
+
chat-feel residual pair (items 1-2 of the trio's siblings) + `edgesOfKind` memoization +
|
|
24
|
+
ontology numeric-vocabulary declaration.
|
|
25
|
+
- **Cluster B** — Bug 8 (goal-reasoner confident-wrong on off-domain requests) + refreshing
|
|
26
|
+
`PLAN_CAPABILITY_ROUTER.md`'s stale stage-5 status text.
|
|
27
|
+
- **Cluster C** — `PLAN_CHAT_FEEL.md` item 6, the pronoun/temporal/discourse-count trio
|
|
28
|
+
(measured red sets from an earlier advisor tick; re-measured against the current graded pool
|
|
29
|
+
before any fix).
|
|
30
|
+
- **Cluster D** — `PLAN_CODE.md` Track 1 (GOAL_RULE/PHRASING_FRAMES synthesis) — operator
|
|
31
|
+
sign-off given this session; Tracks 2-4 remain unsigned-off and untouched.
|
|
32
|
+
- **Cluster E** — `PLAN_INFERENCE_TESTING.md` Stage 1 (`cax-sco` rule) + Stage 2 (proof-chain
|
|
33
|
+
receipts) in `src/syllogise.mjs`, targeting the measured chat-A2 50% ceiling.
|
|
34
|
+
|
|
35
|
+
Also in flight: playtest sprint rounds 4-8 (3/3 rounds so far shipped a real fix), the strategy
|
|
36
|
+
advisor (restarted, 5-min re-arm).
|
|
20
37
|
|
|
21
38
|
### Now: shipped this session
|
|
22
39
|
|
|
@@ -44,16 +61,23 @@ one patch per shipped playtest-sprint fix — see `HANDOVER.md` for the exact re
|
|
|
44
61
|
- **4 new plan docs.** `PLAN_ontology-hierarchies.md`, `PLAN_INFERENCE_TESTING.md` (revised so
|
|
45
62
|
infbench generation is mechanical, not hand-authored), `archive/PLAN_PREDICATE_QUERIES.md`, and
|
|
46
63
|
`PLAN_CODE.md` (new, program synthesis over tmct's closed DSLs, gated on explicit operator
|
|
47
|
-
sign-off per track,
|
|
64
|
+
sign-off per track — Track 1 signed off, see "Doing" above).
|
|
65
|
+
- **`PLAN_TMCT_ECOSYSTEM_INTEGRATION.md`**, landed — a code-grounded tmct/bedrock-meter/marginalia
|
|
66
|
+
integration plan. Corrects a standing wrong assumption (the `/v1/messages` shim was believed
|
|
67
|
+
unmerged; it's been on `main` since 0.8.0) and confirms seonix has already fully migrated onto
|
|
68
|
+
tmct as a real dependency.
|
|
69
|
+
- **A `/narrate` debug mode.** Opt-in (`/narrate on`, `--narrate`, `TMCT_NARRATE=1`) per-turn
|
|
70
|
+
decision narrative — which lane/pattern matched, results + sources, a deterministic goal
|
|
71
|
+
deduction — built for playtest-sprint debugging; zero cost when off (byte-identical regression
|
|
72
|
+
guard).
|
|
48
73
|
|
|
49
74
|
### Next: the open follow-ups
|
|
50
75
|
|
|
51
76
|
In priority order (full detail and measured targets in `HANDOVER.md`):
|
|
52
77
|
|
|
53
|
-
1. **Bug 8.**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
zero-hallucination capability. Needs a real semantic gate.
|
|
78
|
+
1. **Bug 8 — FIXED.** A global-mode domain gate now requires the request itself (not just the
|
|
79
|
+
caller's declared toolset) to parse as a query about the deduced goal's domain, reusing
|
|
80
|
+
`ask.mjs`'s own grammar. Zero-hallucination baseline unchanged; independently re-verified.
|
|
57
81
|
2. **Bug 6.** Scoped listing false-empty: "list modules in `<pkg>`" returns empty though the
|
|
58
82
|
unscoped lister shows matches. Found dogfooding a 191k-entity monorepo graph.
|
|
59
83
|
3. **Bug 7.** A modal auxiliary ("should") survives the fuzzy-correction cascade and misreads as
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
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
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// edges (mgx:touchedByCommit / mgx:changeCoupledWith), which is a different (and
|
|
39
39
|
// simpler) question than the browser's time-scrubbing view.
|
|
40
40
|
|
|
41
|
-
import { relationKind, impactClosure } from "./codegraph.mjs";
|
|
41
|
+
import { relationKind, impactClosure, normPath } from "./codegraph.mjs";
|
|
42
42
|
import {
|
|
43
43
|
VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND,
|
|
44
44
|
CONTEXT_PRONOUNS, META_MEANING_VERBS,
|
|
@@ -70,12 +70,36 @@ export { normalizeQuery, applyNegationFrames };
|
|
|
70
70
|
// shipping a ~1MB language model inside the page.
|
|
71
71
|
import { nlpAdapter } from "./ask-nlp.mjs";
|
|
72
72
|
|
|
73
|
+
/** Per-graph, per-kind memo for THIS file's own edgesOfKind copy — same WeakMap<graph,
|
|
74
|
+
* Map<kind, edge[]>> shape as codegraph.mjs's twin (and this file's own qualCache,
|
|
75
|
+
* below), kept as an independent cache rather than sharing codegraph.mjs's (same
|
|
76
|
+
* commit-boundary reasoning as the function copy itself: both derive the identical
|
|
77
|
+
* result from the identical relationKind classification, so two caches can never
|
|
78
|
+
* disagree, only duplicate a little memory). Correctness rests on the same
|
|
79
|
+
* invariant qualCache already relies on: a loaded graph's `relations` are never
|
|
80
|
+
* mutated in place (a refresh always builds a NEW graph object via parseEntities).
|
|
81
|
+
* Deliberately NAMED DIFFERENTLY from codegraph.mjs's `edgesOfKindCache` — the
|
|
82
|
+
* inlined viewer bundle (viz.mjs's askSource) literally CONCATENATES a stripped
|
|
83
|
+
* codegraph.mjs + this file into one classic script (test/ask-nlp.test.mjs pins
|
|
84
|
+
* this), so two `const`s with the same name would be a real SyntaxError there. */
|
|
85
|
+
const askEdgesOfKindCache = new WeakMap();
|
|
86
|
+
|
|
73
87
|
/** All edges of a classified relation kind, flattened across relation groups —
|
|
74
88
|
* a local copy of codegraph.mjs's private edgesOfKind (kept local rather than
|
|
75
89
|
* exported+imported to avoid coupling this file's commit boundary to concurrent
|
|
76
90
|
* in-flight edits elsewhere in codegraph.mjs; both read the same relationKind
|
|
77
|
-
* classification, so they cannot drift in meaning).
|
|
91
|
+
* classification, so they cannot drift in meaning). Memoized per (graph, kind) —
|
|
92
|
+
* perf lever, HANDOVER follow-up #8: this is the query engine's hottest path,
|
|
93
|
+
* called repeatedly on the same (graph, kind) pair across a single query's
|
|
94
|
+
* compositional evaluation, and at monorepo scale (tens of thousands of modules)
|
|
95
|
+
* the repeated O(relations) scan is a real latency/GC cost — not a correctness
|
|
96
|
+
* fix (the stack-overflow bug this file's twin comment references is already
|
|
97
|
+
* fixed and unrelated). */
|
|
78
98
|
function edgesOfKind(graph, kind) {
|
|
99
|
+
let byKind = askEdgesOfKindCache.get(graph);
|
|
100
|
+
if (!byKind) { byKind = new Map(); askEdgesOfKindCache.set(graph, byKind); }
|
|
101
|
+
const cached = byKind.get(kind);
|
|
102
|
+
if (cached) return cached;
|
|
79
103
|
const out = [];
|
|
80
104
|
// Plain-loop append, NOT out.push(...g.edges): argument spread overflows the call
|
|
81
105
|
// stack past ~100k edges on graph-scale relation groups (see codegraph.mjs twin).
|
|
@@ -83,6 +107,7 @@ function edgesOfKind(graph, kind) {
|
|
|
83
107
|
if (relationKind(g) !== kind) continue;
|
|
84
108
|
for (const e of g.edges) out.push(e);
|
|
85
109
|
}
|
|
110
|
+
byKind.set(kind, out);
|
|
86
111
|
return out;
|
|
87
112
|
}
|
|
88
113
|
|
|
@@ -980,6 +1005,19 @@ function uniqueById(inds) {
|
|
|
980
1005
|
return out;
|
|
981
1006
|
}
|
|
982
1007
|
|
|
1008
|
+
/** The Module individuals whose path lives strictly UNDER the directory named by
|
|
1009
|
+
* `term` — a proper path-segment prefix match (normPath(label).startsWith(dir +
|
|
1010
|
+
* "/")), never a bare substring, so "src/lib" cannot spuriously catch
|
|
1011
|
+
* "src/libfoo/x.mjs". Mirrors renderArchitecture's own pkg-prefix scoping
|
|
1012
|
+
* (codegraph.mjs) but returns individuals rather than a summary string — this is
|
|
1013
|
+
* the "membership" AST node's directory-scope branch (see its call site above). */
|
|
1014
|
+
function directoryScopeModules(graph, term) {
|
|
1015
|
+
const norm = normPath(term);
|
|
1016
|
+
if (!norm) return [];
|
|
1017
|
+
const prefix = `${norm}/`;
|
|
1018
|
+
return graph.individuals.filter((i) => i.class === "Module" && normPath(i.label).startsWith(prefix));
|
|
1019
|
+
}
|
|
1020
|
+
|
|
983
1021
|
// Per-graph memo for the qualifier attribute/edge sets (exported symbols, tested
|
|
984
1022
|
// modules, symbol→module map) — computed once, so a qualifier filter over a large
|
|
985
1023
|
// result set stays cheap and deterministic.
|
|
@@ -1242,7 +1280,27 @@ function evalSet(graph, ast, opts) {
|
|
|
1242
1280
|
return forwardOverSet(graph, ast.kind, ids);
|
|
1243
1281
|
}
|
|
1244
1282
|
case "membership": {
|
|
1283
|
+
// DIRECTORY SCOPE ("modules in src/lib", "files in src/handlers"): a bare
|
|
1284
|
+
// path term with no exact node of its own is a DIRECTORY, not a single
|
|
1285
|
+
// container individual — resolveObject's fuzzy tiers used to land it on ONE
|
|
1286
|
+
// arbitrarily-chosen module whose label merely CONTAINS the path substring
|
|
1287
|
+
// (e.g. "src/lib" fuzzy-matching "src/lib/logger.mjs"), then traversed that
|
|
1288
|
+
// one module's own membership edges for entityType "Module" — which a module
|
|
1289
|
+
// never has, so the answer was a false-empty ("no modules in this index.")
|
|
1290
|
+
// even though several modules genuinely live under the directory. An EXACT
|
|
1291
|
+
// node match (tier 1 — a real file/symbol named that) still wins outright
|
|
1292
|
+
// (unchanged single-container-node behavior, e.g. "methods in widget.mjs");
|
|
1293
|
+
// only when there is no exact match do we try directory-prefix scope first.
|
|
1245
1294
|
const r = resolveObject(graph, ast.term);
|
|
1295
|
+
if (!(r.match && r.tier === 1)) {
|
|
1296
|
+
const dirMods = directoryScopeModules(graph, ast.term);
|
|
1297
|
+
if (dirMods.length) {
|
|
1298
|
+
if (!ast.entityType || ast.entityType === "Module") return dirMods;
|
|
1299
|
+
const ids = new Set(dirMods.map((m) => m.id));
|
|
1300
|
+
const objs = uniqueById(MEMBERSHIP_KINDS.flatMap((k) => forwardOverSet(graph, k, ids)));
|
|
1301
|
+
return objs.filter((o) => o.class === ast.entityType);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1246
1304
|
if (!r.match) return [];
|
|
1247
1305
|
const ids = new Set([r.match.id]);
|
|
1248
1306
|
const objs = uniqueById(MEMBERSHIP_KINDS.flatMap((k) => forwardOverSet(graph, k, ids)));
|
package/src/chat.mjs
CHANGED
|
@@ -1052,12 +1052,22 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
1052
1052
|
return { text: moduleOverviewText(graph, ind), via: "meta" };
|
|
1053
1053
|
}
|
|
1054
1054
|
|
|
1055
|
-
async function metaLane(query, { graph, memoryDir }) {
|
|
1055
|
+
async function metaLane(query, { graph, memoryDir, last = null }) {
|
|
1056
1056
|
const q = String(query).trim().toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
1057
1057
|
if (WHAT_KNOW_RE.test(q) || q === "what have you learned" || q === "what have you learnt") {
|
|
1058
1058
|
return { text: await memorySummary(memoryDir, graph), via: "meta" };
|
|
1059
1059
|
}
|
|
1060
|
-
if (META_ORIENT_RE.test(q))
|
|
1060
|
+
if (META_ORIENT_RE.test(q)) {
|
|
1061
|
+
// Chat-feel residual (0.8.2 confirmation playtest, follow-up #3): Bug B1 only
|
|
1062
|
+
// taught the isConversational-triggered orientation branch (below, via:"template")
|
|
1063
|
+
// to shorten on an identical repeat — this META_ORIENT_RE branch is a SEPARATE
|
|
1064
|
+
// route to the same class of full-blurb text ("what does this app do" reprinted
|
|
1065
|
+
// orientationText(graph) verbatim on every repeat, never collapsing). Mirrors
|
|
1066
|
+
// ORIENTATION_REPEAT_ONELINER's identity-check pattern exactly, with its own
|
|
1067
|
+
// distinct oneliner text (self-limiting for the same reason).
|
|
1068
|
+
const text = orientationText(graph);
|
|
1069
|
+
return { text: last?.answer === text ? META_ORIENT_REPEAT_ONELINER : text, via: "meta" };
|
|
1070
|
+
}
|
|
1061
1071
|
// Bug E: an arbitrary "what does <term> do" that META_ORIENT_RE's closed noun
|
|
1062
1072
|
// list didn't claim — try the module-grain overview before falling through to
|
|
1063
1073
|
// the author-sha check below (disjoint triggers; order doesn't matter, but
|
|
@@ -1272,6 +1282,13 @@ const WALL_REPEAT_ONELINER = "still couldn't parse that — /help lists every qu
|
|
|
1272
1282
|
* re-offers the full orientation instead of droning the one-liner forever. */
|
|
1273
1283
|
const ORIENTATION_REPEAT_ONELINER = "still the same overview — /help lists every command and query shape.";
|
|
1274
1284
|
|
|
1285
|
+
/** metaLane's own repeat-suppression twin for META_ORIENT_RE ("what does this app
|
|
1286
|
+
* do", etc. — see metaLane's doc above) — a genuinely separate route to the same
|
|
1287
|
+
* orientation-class text that Bug B1 didn't cover. MUST differ from
|
|
1288
|
+
* ORIENTATION_REPEAT_ONELINER (a distinct string, checked by identity) so the two
|
|
1289
|
+
* independent repeat-suppression sites can never be confused with one another. */
|
|
1290
|
+
const META_ORIENT_REPEAT_ONELINER = "still the same overview — /stats for the full one, /help for commands.";
|
|
1291
|
+
|
|
1275
1292
|
// ---- repo-root resolution: default the target to the GIT ROOT, not raw cwd ----
|
|
1276
1293
|
|
|
1277
1294
|
/** The git top-level for `cwd`, or null if not in a repo (or git is unavailable).
|
|
@@ -2455,7 +2472,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2455
2472
|
// codebase", "how do i start") → a summary / orientation, answered before the
|
|
2456
2473
|
// fact-dump readers so "what do you know" gets a summary, not raw facts.
|
|
2457
2474
|
if (miss) {
|
|
2458
|
-
const meta = await metaLane(query, { graph, memoryDir });
|
|
2475
|
+
const meta = await metaLane(query, { graph, memoryDir, last });
|
|
2459
2476
|
if (meta) {
|
|
2460
2477
|
answer = meta.text; via = meta.via; recordMiss = false; handled = true;
|
|
2461
2478
|
note(trace, `lane: (1) META/SELF — bare self/session question recognized, answered via="${meta.via}"`);
|
package/src/codegraph.mjs
CHANGED
|
@@ -115,7 +115,7 @@ export function relationKind(group) {
|
|
|
115
115
|
|
|
116
116
|
// ---- symbol resolution (exact → normalised path → substring) ------------------
|
|
117
117
|
|
|
118
|
-
function normPath(s) {
|
|
118
|
+
export function normPath(s) {
|
|
119
119
|
return String(s || "")
|
|
120
120
|
.trim()
|
|
121
121
|
.toLowerCase()
|
|
@@ -1166,12 +1166,31 @@ export function renderSearch(graph, query, { limit = SEARCH_LIMIT, kind = "", de
|
|
|
1166
1166
|
// history / call neighbours). Each answers ONE question in one compact call so
|
|
1167
1167
|
// the agent need not Read/Grep. All keep the bounded-output discipline. -------
|
|
1168
1168
|
|
|
1169
|
+
/** Per-graph, per-kind memo for edgesOfKind's own flattened scan — WeakMap<graph,
|
|
1170
|
+
* Map<kind, edge[]>>, mirroring qualCache's (ask.mjs) established per-graph-object
|
|
1171
|
+
* caching convention: a loaded graph's `relations` are never mutated in place after
|
|
1172
|
+
* parseEntities builds it (every refresh constructs a NEW graph object), so caching
|
|
1173
|
+
* keyed on graph object identity is correctness-safe for a graph's whole lifetime —
|
|
1174
|
+
* same invariant qualCache already relies on in production. edgesOfKind is called
|
|
1175
|
+
* repeatedly on the SAME (graph, kind) pair across a single query's traversal
|
|
1176
|
+
* (evalSet/traverse/adjacencyForKinds/renderArchitecture/… all re-derive it), and at
|
|
1177
|
+
* monorepo scale (tens of thousands of modules) that repeated O(relations) scan is a
|
|
1178
|
+
* real latency/GC cost — this collapses every call after the first to an O(1) lookup. */
|
|
1179
|
+
const edgesOfKindCache = new WeakMap();
|
|
1180
|
+
|
|
1169
1181
|
/** All edges whose relation classifies to `kind`, flattened across relation groups. */
|
|
1170
1182
|
/** All edges of a classified relation kind (imports/calls/defines/tests/touches/inherits/
|
|
1171
1183
|
* cochange/reexports/callsSymbol/touchesSymbol/contains — see relationKind/PROP_KIND above),
|
|
1172
1184
|
* flattened across every raw relation group that classifies to it. Exported for ask.mjs's
|
|
1173
|
-
* mechanical NL-query engine (PLAN_MECHANICAL_CHAT.md) to orchestrate rather than duplicate.
|
|
1185
|
+
* mechanical NL-query engine (PLAN_MECHANICAL_CHAT.md) to orchestrate rather than duplicate.
|
|
1186
|
+
* Memoized per (graph, kind) — see edgesOfKindCache's own doc above (perf lever, HANDOVER
|
|
1187
|
+
* follow-up #8: latency/GC on monorepo-scale graphs, not a correctness fix — the earlier
|
|
1188
|
+
* stack-overflow bug below is already fixed and unrelated). */
|
|
1174
1189
|
export function edgesOfKind(graph, kind) {
|
|
1190
|
+
let byKind = edgesOfKindCache.get(graph);
|
|
1191
|
+
if (!byKind) { byKind = new Map(); edgesOfKindCache.set(graph, byKind); }
|
|
1192
|
+
const cached = byKind.get(kind);
|
|
1193
|
+
if (cached) return cached;
|
|
1175
1194
|
const out = [];
|
|
1176
1195
|
// Plain-loop append, NOT out.push(...g.edges): argument spread materialises every
|
|
1177
1196
|
// element as a call argument and overflows the stack past ~100k edges (live report:
|
|
@@ -1180,6 +1199,7 @@ export function edgesOfKind(graph, kind) {
|
|
|
1180
1199
|
if (relationKind(g) !== kind) continue;
|
|
1181
1200
|
for (const e of g.edges) out.push(e);
|
|
1182
1201
|
}
|
|
1202
|
+
byKind.set(kind, out);
|
|
1183
1203
|
return out;
|
|
1184
1204
|
}
|
|
1185
1205
|
|
|
@@ -491,6 +491,13 @@ export const STOPWORDS = new Set([
|
|
|
491
491
|
// the module alone). Same trade as every other stopword: a symbol literally named
|
|
492
492
|
// "usually" would be the accepted residual cost.
|
|
493
493
|
"usually", "typically", "generally", "normally", "often", "commonly", "mostly",
|
|
494
|
+
// modal auxiliaries ("what SHOULD i look at first") — found live: with no modal in
|
|
495
|
+
// this set, "should" reached the cascade's bounded fuzzy-correction step and landed
|
|
496
|
+
// within edit distance of the unrelated closed-vocab word "hold" ("defines" synonym,
|
|
497
|
+
// ask-vocab.mjs), corrupting the whole query into "what hold i at". Same trade as
|
|
498
|
+
// every other stopword: a symbol literally named "should" would be the accepted
|
|
499
|
+
// residual cost.
|
|
500
|
+
"should", "would", "could", "can", "will", "shall", "might", "must",
|
|
494
501
|
]);
|
|
495
502
|
|
|
496
503
|
/** Split free text into words: trailing "?" run stripped, commas treated as
|
|
@@ -40,11 +40,32 @@
|
|
|
40
40
|
// (queries mutate nothing — the STRIPS closed world), so no step can delete a
|
|
41
41
|
// condition another goal depends on. We compute this from the registry rather
|
|
42
42
|
// than assume it (threatsAmong), so the guarantee is grounded, not asserted.
|
|
43
|
+
//
|
|
44
|
+
// THE GLOBAL-MODE DOMAIN GATE (Bug 8 fix). In SCOPED mode, relevance is already
|
|
45
|
+
// proven structurally: the focus is a REAL bound graph entity (resolveObject
|
|
46
|
+
// found it), so the request is provably about something in the graph. In GLOBAL
|
|
47
|
+
// mode there is no focus to bind, and `applicableRules` alone only screens the
|
|
48
|
+
// CALLER'S DECLARED TOOLSET — a caller-constant fact that says nothing about
|
|
49
|
+
// whether THIS request has any connection to the deduced goal (a caller who
|
|
50
|
+
// declares tmct_untested/tmct_impact once per session would ground
|
|
51
|
+
// coverage-invariant for every off-topic turn). The fix reuses ask.mjs's OWN
|
|
52
|
+
// compositional NL grammar (parseQuery — the SAME primitive the C1 resolver
|
|
53
|
+
// already parses every request with, see resolver.mjs mapParse) as a structural
|
|
54
|
+
// relevance check, never a new keyword table: does the request even COMPILE to a
|
|
55
|
+
// recognized graph-query shape naming a known entity kind, and does that kind
|
|
56
|
+
// match the rule's declared focusClass? A request parseQuery cannot place at all
|
|
57
|
+
// (null — "write a haiku about pizza") or places without landing on any
|
|
58
|
+
// recognized entity kind (a miss with no entity kind — "how many pizzas are
|
|
59
|
+
// there") is an honest "not about this graph" signal; a request parseQuery
|
|
60
|
+
// resolves to a real AST naming the rule's focus class ("which module is the
|
|
61
|
+
// biggest testing risk" -> {node:"superlative", entityType:"Module", ...})
|
|
62
|
+
// stays exactly as reachable as before. Zero request keywords added.
|
|
43
63
|
|
|
44
64
|
import { backwardChain, extractEntity } from "./resolver.mjs";
|
|
45
65
|
import { capabilityByName, effectsOf } from "./registry.mjs";
|
|
46
66
|
import { hallucinationsIn } from "./call-validator.mjs";
|
|
47
67
|
import { intersect } from "./set-algebra.mjs";
|
|
68
|
+
import { parseQuery } from "../ask.mjs";
|
|
48
69
|
|
|
49
70
|
// Hard OUTER-tick budget — the meta-loop runs at most this many ticks, then
|
|
50
71
|
// REFUSES (escalate). Independent of BDI convergence and of the monotone
|
|
@@ -178,6 +199,25 @@ function focusOf(request, ctx) {
|
|
|
178
199
|
return r && r.match && !r.ambiguous ? r.match : null;
|
|
179
200
|
}
|
|
180
201
|
|
|
202
|
+
/** The GLOBAL-MODE DOMAIN GATE's primitive: what entity CLASS (if any) did
|
|
203
|
+
* ask.mjs's own compositional NL grammar recognize in the request? Walks
|
|
204
|
+
* parseQuery's AST (the same shapes resolver.mjs's mapParse/mapFrame already
|
|
205
|
+
* consume) for its declared `entityType` field, unwrapping the wrapper nodes
|
|
206
|
+
* (`clause`, `inner`, `base`) that carry no entityType of their own. Returns the
|
|
207
|
+
* class name, or null when the grammar placed nothing (an outright non-parse) or
|
|
208
|
+
* placed a MISS with no recognized entity kind at all ("how many pizzas are
|
|
209
|
+
* there" -> {node:"miss", reason:"count needs a known entity kind..."} carries no
|
|
210
|
+
* entityType, same as a flat null). Pure; no request-string keyword table — it
|
|
211
|
+
* reads a field ask.mjs's grammar already computes for every request. */
|
|
212
|
+
function parsedEntityType(node) {
|
|
213
|
+
if (!node || typeof node !== "object") return null;
|
|
214
|
+
if (typeof node.entityType === "string") return node.entityType;
|
|
215
|
+
if (node.clause) return parsedEntityType(node.clause);
|
|
216
|
+
if (node.inner) return parsedEntityType(node.inner);
|
|
217
|
+
if (node.base) return parsedEntityType(node.base);
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
181
221
|
/** Ground ONE epistemic sub-goal (a topic + optional bound entity) into a
|
|
182
222
|
* grounded, EXECUTED call, or null when it is not groundable in the declared
|
|
183
223
|
* toolset (=> the meta-loop escalates). Backward-chains topic->capability, binds
|
|
@@ -237,13 +277,31 @@ export async function goalReason(request, tools, ctx, { driver = "goal-0.8.1" }
|
|
|
237
277
|
// request's scope in this toolset); >1 => an AMBIGUOUS meta-goal (arbitration
|
|
238
278
|
// between meta-goals is undeclared) — both are honest refusals, never a guess.
|
|
239
279
|
const applicable = applicableRules(declared, focus, mode);
|
|
240
|
-
|
|
280
|
+
|
|
281
|
+
// THE GLOBAL-MODE DOMAIN GATE (Bug 8 fix, see the module header). SCOPED mode
|
|
282
|
+
// already proved relevance via a bound graph entity; GLOBAL mode has not, so
|
|
283
|
+
// `applicable` alone (a pure function of the caller's DECLARED TOOLSET) is not
|
|
284
|
+
// enough — it says nothing about whether THIS request is even about the graph.
|
|
285
|
+
// Screen it against ask.mjs's own NL grammar: the request must parse to a shape
|
|
286
|
+
// naming the candidate rule's declared focusClass, or it is refused as honestly
|
|
287
|
+
// off-domain rather than answered with someone else's goal.
|
|
288
|
+
let domainRelevant = applicable;
|
|
289
|
+
if (mode === "global" && applicable.length) {
|
|
290
|
+
const requestClass = parsedEntityType(parseQuery(request));
|
|
291
|
+
domainRelevant = applicable.filter((r) => requestClass === r.focusClass);
|
|
292
|
+
if (!domainRelevant.length) {
|
|
293
|
+
const classes = [...new Set(applicable.map((r) => r.focusClass))].join("/");
|
|
294
|
+
return refuse(`open-world: the request does not parse as a query about ${classes} (ask.mjs's own NL grammar names ${requestClass || "no recognized entity kind"} in it) — global goal deduction needs the REQUEST ITSELF to be about the deduced goal's domain, not just a declared toolset that happens to ground it — escalate`, driver);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!domainRelevant.length) {
|
|
241
299
|
return refuse(`open-world: no declared goal-rule is applicable in ${mode} mode (each needs a sub-goal capability outside the declared toolset, or a scope it does not declare) — escalate`, driver);
|
|
242
300
|
}
|
|
243
|
-
if (
|
|
244
|
-
return refuse(`ambiguous meta-goal: ${
|
|
301
|
+
if (domainRelevant.length > 1) {
|
|
302
|
+
return refuse(`ambiguous meta-goal: ${domainRelevant.length} declared goal-rules apply (${domainRelevant.map((r) => r.id).join(", ")}) — meta-goal arbitration is undeclared, refuse rather than guess — escalate`, driver);
|
|
245
303
|
}
|
|
246
|
-
const rule =
|
|
304
|
+
const rule = domainRelevant[0];
|
|
247
305
|
|
|
248
306
|
// the glass-box WHY, citing the declared goal-rule by backward-chain (the C2
|
|
249
307
|
// twin of resolver.mjs's "backward-chain => <capability>" provenance).
|