@polycode-projects/the-mechanical-code-talker 1.9.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +441 -217
  2. package/bin/tmct.mjs +126 -1
  3. package/corpus/seon/README.md +1 -2
  4. package/package.json +4 -2
  5. package/src/answer-variants.mjs +8 -36
  6. package/src/ask-browser-entry.mjs +5 -23
  7. package/src/ask-browser.bundle.js +1 -2
  8. package/src/ask-nlp.mjs +9 -23
  9. package/src/ask-vocab.mjs +139 -589
  10. package/src/ask.mjs +627 -1729
  11. package/src/chat.mjs +1684 -2874
  12. package/src/cli-args.mjs +14 -28
  13. package/src/codegraph.mjs +236 -644
  14. package/src/completions/complete.mjs +18 -62
  15. package/src/completions/graph-adapter.mjs +14 -60
  16. package/src/completions/group.mjs +12 -68
  17. package/src/completions/infer.mjs +38 -126
  18. package/src/completions/prune.mjs +17 -70
  19. package/src/completions/rank.mjs +16 -69
  20. package/src/completions/search.mjs +8 -31
  21. package/src/concept.mjs +32 -88
  22. package/src/conformance.mjs +11 -15
  23. package/src/corpus/conceptnet.mjs +31 -89
  24. package/src/corpus/templates.mjs +19 -45
  25. package/src/corpus/unknown-ingest.mjs +31 -92
  26. package/src/embed.mjs +10 -22
  27. package/src/extensions.mjs +50 -154
  28. package/src/finish.mjs +35 -91
  29. package/src/grammar/ace.mjs +16 -40
  30. package/src/grammar/assert.mjs +1 -1
  31. package/src/grammar/lexicon-core.json +1 -1
  32. package/src/grammar/lexicon.mjs +9 -27
  33. package/src/graph-merge.mjs +2 -3
  34. package/src/hash.mjs +6 -14
  35. package/src/index.mjs +6 -10
  36. package/src/init.mjs +38 -125
  37. package/src/interpret/fuzzy.mjs +10 -29
  38. package/src/interpret/merge.mjs +9 -27
  39. package/src/interpret/normalize.mjs +137 -585
  40. package/src/interpret/pipeline.mjs +23 -71
  41. package/src/interpret/strategies/ace.mjs +7 -31
  42. package/src/interpret/strategies/constructions.mjs +14 -41
  43. package/src/interpret/strategies/grammar.mjs +21 -60
  44. package/src/interpret/strategies/keywords.mjs +42 -131
  45. package/src/interpret/strategies/noise-strip.mjs +18 -89
  46. package/src/memory/bias.mjs +11 -54
  47. package/src/memory/blocks.mjs +18 -69
  48. package/src/memory/core.mjs +171 -591
  49. package/src/memory/fold.mjs +0 -0
  50. package/src/memory/inspect.mjs +7 -25
  51. package/src/memory/shacl.mjs +10 -39
  52. package/src/memory/trust.mjs +26 -127
  53. package/src/memory-ask-browser-entry.mjs +7 -30
  54. package/src/memory-ask-browser.bundle.js +1 -1
  55. package/src/paraphrase.mjs +20 -53
  56. package/src/planning.mjs +15 -157
  57. package/src/prose-nlp.mjs +4 -17
  58. package/src/prose.mjs +19 -67
  59. package/src/providers/bootstrap.mjs +1 -2
  60. package/src/providers/fixture.mjs +1 -2
  61. package/src/providers/graph-service.mjs +28 -59
  62. package/src/repository-interface.mjs +6 -8
  63. package/src/router/drive.mjs +183 -0
  64. package/src/router/goal-reasoner.mjs +66 -231
  65. package/src/router/guardrail.mjs +20 -58
  66. package/src/router/planner.mjs +15 -46
  67. package/src/router/registry.mjs +13 -43
  68. package/src/router/resolver.mjs +46 -131
  69. package/src/router/results.mjs +231 -0
  70. package/src/schema-docs.mjs +10 -27
  71. package/src/server-http.mjs +10 -19
  72. package/src/server.mjs +22 -28
  73. package/src/sessions.mjs +15 -30
  74. package/src/source-slice.mjs +5 -7
  75. package/src/source.mjs +10 -20
  76. package/src/syllogise.mjs +187 -575
  77. package/src/telemetry.mjs +3 -3
  78. package/src/toml-config.mjs +4 -4
  79. package/src/tui/app.mjs +9 -19
  80. package/src/viz.mjs +66 -123
  81. package/src/wink-model.mjs +10 -24
@@ -1,65 +1,21 @@
1
- // src/router/goal-reasoner.mjs — Stage 5 of the capability router
2
- // (PLAN_CAPABILITY_ROUTER.md / STAGE_5_GOAL_REASONER.md): THE CLOSED-WORLD C2
3
- // GOAL-REASONER. "Self-directed" is not magic — it is a canned, HARD-BOUNDED
4
- // meta-loop (Rao & Georgeff BDI × Aha/Molineaux/Cox GDA × continual planning):
1
+ // src/router/goal-reasoner.mjs — the closed-world goal-reasoner: a canned,
2
+ // hard-bounded meta-loop (BDI x goal-driven autonomy):
5
3
  //
6
- // deduce current goals (step 1 the only genuinely new part)
7
- // plan for each goal (step 2 C1: the Stage-3 planner/resolver)
8
- // arbitrate the first steps (step 3a — keystone, threat-aware)
9
- // → PERSIST the committed intention (step 3b — BDI drop conditions)
10
- // → execute ONE, observe, repeat (step 5 — Steel & Ho monitor / GDA replan)
4
+ // deduce current goals -> plan each goal (resolver.mjs's backward chain)
5
+ // -> arbitrate the first steps, threat-aware -> persist the committed intention
6
+ // -> execute ONE, observe, repeat
11
7
  //
12
- // The elegance (RFC): C2 collapses into C1 + a goal-deduction step + an
13
- // action-selection rule. Everything except goal-deduction is solved machinery.
14
- // This module supplies the goal-deduction as a DEDUCTION over a DECLARED goal
15
- // model (never a judgement over the request string) and REFUSES at the
16
- // open-world goal-generation seam rather than inventing a goal — the C2 analogue
17
- // of the resolver's "never emit a call it cannot prove".
8
+ // Goal-deduction is a DEDUCTION over a DECLARED goal model (GOAL_RULES), never a judgement
9
+ // over the request string: the only thing read off the request is a FOCUS entity (via the
10
+ // resolver's extractEntity + binding oracle). REFUSES at the open-world goal-generation seam
11
+ // rather than inventing a goal.
18
12
  //
19
- // DEDUCTION, NOT KEYWORD-MATCH. The current goals fall out of the KB via a
20
- // declared goal model (GOAL_RULES), exactly as syllogise chains a declared rule
21
- // over the graph under mechanical guards. The ONLY thing this reads off the
22
- // request is a FOCUS entity (delegated to the resolver's extractEntity + the
23
- // binding oracle entity resolution, never intent keywords). Whether a goal is
24
- // active is then deduced from the graph (is the focus module untested? what does
25
- // its change reach?), so no request-string literal steers the routing.
26
- //
27
- // MECHANICAL TERMINATION (not a convergence argument). Two independent bounds:
28
- // (1) a hard OUTER-tick budget MAX_TICKS (mirrors the planner's MAX_STEPS), and
29
- // (2) a MONOTONE-PROGRESS invariant — every tick ACHIEVES exactly one intention
30
- // (removes it from the pending set); the only growth is a SINGLE, bounded
31
- // GDA expansion (impact-of-each over the finite untested set), gated by a
32
- // one-shot flag. So the pending set strictly shrinks to the empty set in
33
- // <= (initial + |untested|) ticks, and MAX_TICKS caps it absolutely. A tick
34
- // that makes no progress HALTS (honest refuse). Termination is proven
35
- // mechanically, not argued from BDI convergence.
36
- //
37
- // THREAT-AWARENESS (POP threats lifted to the meta-level). A first step that
38
- // clobbers another live goal's precondition is a threat. Here it is PROVABLY
39
- // absent: every registry capability is read-only with an EMPTY delete-list
40
- // (queries mutate nothing — the STRIPS closed world), so no step can delete a
41
- // condition another goal depends on. We compute this from the registry rather
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.
13
+ // Termination: MAX_TICKS caps the loop; each tick achieves exactly one intention (monotone
14
+ // progress), with one bounded GDA expansion allowed. Threat safety: every registry capability
15
+ // is read-only (empty delete-list), so no step can clobber another goal's precondition
16
+ // derived from the registry, not assumed (threatsAmong). In GLOBAL mode (no bound focus),
17
+ // relevance is additionally screened by parsing the request through ask.mjs's own NL grammar,
18
+ // since the declared toolset alone doesn't prove the request is about this graph.
63
19
 
64
20
  import { backwardChain, extractEntity } from "./resolver.mjs";
65
21
  import { capabilityByName, effectsOf } from "./registry.mjs";
@@ -67,50 +23,27 @@ import { hallucinationsIn } from "./call-validator.mjs";
67
23
  import { intersect } from "./set-algebra.mjs";
68
24
  import { parseQuery } from "../ask.mjs";
69
25
 
70
- // Hard OUTER-tick budget — the meta-loop runs at most this many ticks, then
71
- // REFUSES (escalate). Independent of BDI convergence and of the monotone
72
- // invariant: a belt-and-braces mechanical stop, the meta-level twin of the
73
- // planner's MAX_STEPS. A deduce->plan->observe cycle can never wedge the caller.
26
+ // Hard outer-tick budget — the meta-loop runs at most this many ticks, then refuses.
74
27
  export const MAX_TICKS = 16;
75
28
 
76
- // ---- the DECLARED goal model (data, mirroring registry.mjs's STRIPS operators)
77
- // A goal-rule is a maintenance INVARIANT over the graph, plus the epistemic
78
- // sub-goals whose facts decide whether it is violated and the DECLARED priority
79
- // that settles ties in first-step arbitration. Growing this set is the "long-chain
80
- // deduction library" the RFC flags — same discipline as syllogise's rule set.
81
- //
82
- // Each rule declares (pure frozen data, no code):
83
- // focusClass — the entity class the scoped reading binds its focus to.
84
- // modes — which deduced scopes the rule covers ("scoped" = a bound focus;
85
- // "global" = whole-graph keystone arbitration). A rule whose
86
- // sub-goals are all entity-scoped has no global reading.
87
- // subGoals — the epistemic facts to gather, IN DECLARED ORDER (each
88
- // backward-chains to a capability, exactly like an NL intent).
89
- // compose — the declarative fold of the gathered facts into the scoped
90
- // answer: intersect(a, b), each side naming a gathered topic,
91
- // optionally bound to the focus (`of:"focus"`), optionally with
92
- // the focus itself unioned in (`withFocus` — the change footprint).
93
- // priorityTopic / coverageTopic — the global keystone arbitration keys
94
- // (argmax |priority(m)| over the coverage-violating set).
95
- // achieves — the meta-goal topic the composed answer achieves.
29
+ // ---- the DECLARED goal model (data, mirroring registry.mjs's STRIPS operators). Each rule:
30
+ // focusClass — the entity class a scoped reading binds its focus to.
31
+ // modes — "scoped" (bound focus) and/or "global" (whole-graph keystone arbitration).
32
+ // subGoals — epistemic facts to gather, in declared order (each backward-chains to a
33
+ // capability).
34
+ // compose — intersect(a, b) over two gathered topics, optionally focus-bound/unioned.
35
+ // priorityTopic/coverageTopic the global keystone arbitration keys.
36
+ // achieves — the meta-goal topic the composed answer achieves.
96
37
  export const GOAL_RULES = Object.freeze([
97
38
  Object.freeze({
98
39
  id: "coverage-invariant",
99
40
  kind: "maintenance",
100
- // INVARIANT: a Module whose change reaches other modules (non-empty impact
101
- // closure) MUST have direct test coverage. A Module that is untested AND
102
- // impactful VIOLATES it — an active goal to close the coverage gap.
103
41
  invariant: "an impactful module must be tested",
104
42
  focusClass: "Module",
105
43
  modes: Object.freeze(["scoped", "global"]),
106
44
  subGoals: Object.freeze(["impact", "untested"]),
107
- // the DECLARED priority key for first-step arbitration: a violation's
108
- // priority is its blast radius |impact(module)| — the wider the reach, the
109
- // higher the goal (keystone = the widest-reach untested module).
110
45
  priorityTopic: "impact",
111
- // the coverage predicate the invariant screens on.
112
46
  coverageTopic: "untested",
113
- // scoped fold: untested ∩ ({focus} ∪ impact(focus)) — the change footprint.
114
47
  compose: Object.freeze({
115
48
  op: "intersect",
116
49
  a: Object.freeze({ topic: "untested" }),
@@ -118,24 +51,17 @@ export const GOAL_RULES = Object.freeze([
118
51
  names: "the change's untested footprint",
119
52
  empty: "no coverage gap",
120
53
  }),
121
- // the meta-goal topic the composed answer achieves (backward-chained below).
122
54
  achieves: "coverage-gap",
123
55
  }),
124
56
  Object.freeze({
125
57
  id: "cochange-risk-invariant",
126
58
  kind: "maintenance",
127
- // INVARIANT: a module CHANGE-COUPLED with the focus (they historically land
128
- // in the same commits) MUST have direct test coverage. A coupled module that
129
- // is untested VIOLATES it — an active goal over the focus's coupling set.
130
59
  invariant: "a module change-coupled with the focus must be tested",
131
60
  focusClass: "Module",
132
- // scoped ONLY: both sub-goals are read relative to a bound focus; there is
133
- // no whole-graph keystone reading declared for change-coupling.
134
61
  modes: Object.freeze(["scoped"]),
135
62
  subGoals: Object.freeze(["cochanges", "untested"]),
136
63
  priorityTopic: "cochanges",
137
64
  coverageTopic: "untested",
138
- // scoped fold: cochanges(focus) ∩ untested — the coupled-but-untested set.
139
65
  compose: Object.freeze({
140
66
  op: "intersect",
141
67
  a: Object.freeze({ topic: "cochanges", of: "focus" }),
@@ -147,30 +73,16 @@ export const GOAL_RULES = Object.freeze([
147
73
  }),
148
74
  ]);
149
75
 
150
- /** Backward-chain a meta-goal topic to the declared goal-rule that achieves it
151
- * the goal-level twin of resolver.backwardChain (capability selection). Pure. */
76
+ /** Backward-chain a meta-goal topic to the declared goal-rule that achieves it. Pure. */
152
77
  export function backwardChainGoal(topic) {
153
78
  return GOAL_RULES.find((r) => r.achieves === topic) || null;
154
79
  }
155
80
 
156
- /** THE RULE-SELECTION DEDUCTION (replaces the old single-rule hard-wiring): a
157
- * declared goal-rule APPLIES to a request iff
158
- * (1) every one of its epistemic sub-goal topics backward-chains to a
159
- * capability IN the declared toolset (the closed-world groundability
160
- * screen the meta-level twin of "never select an out-of-set call"),
161
- * (2) the deduced mode is one of the rule's declared modes, and
162
- * (3) a scoped reading's bound focus is of the rule's declared focusClass.
163
- * Pure over the goal model + registry: nothing here reads the request string.
164
- * The caller REFUSES on zero matches (the open-world goal-generation seam) and
165
- * on more than one (an ambiguous meta-goal — arbitration between meta-goals is
166
- * undeclared, so guessing one would be an invented goal). */
167
- // `ruleSet` defaults to the real, shipped GOAL_RULES — every existing caller
168
- // (driver-goal.mjs, this module's own goalReason, every test) is unaffected.
169
- // The override exists ONLY for PLAN_CODE.md Track 1 (§1.4): the synthesis
170
- // oracle clones a CANDIDATE rule into its own array and runs it through this
171
- // SAME trusted function — never a re-derivation — to check it decides
172
- // applicability exactly like a hand-written rule would. synthbench/ is the
173
- // only caller that ever passes a non-default ruleSet.
81
+ /** A declared goal-rule APPLIES to a request iff (1) every sub-goal topic backward-chains
82
+ * to a capability in the declared toolset, (2) the deduced mode is one of the rule's
83
+ * declared modes, and (3) a scoped focus is of the rule's declared focusClass. The caller
84
+ * refuses on zero matches or more than one (ambiguous meta-goal). `ruleSet` defaults to
85
+ * GOAL_RULES; the synthesis-oracle test harness is the only caller that overrides it. */
174
86
  export function applicableRules(declaredTools, focus, mode, ruleSet = GOAL_RULES) {
175
87
  const declared = Array.isArray(declaredTools) ? declaredTools : [];
176
88
  return ruleSet.filter((rule) =>
@@ -184,26 +96,19 @@ export function applicableRules(declaredTools, focus, mode, ruleSet = GOAL_RULES
184
96
 
185
97
  const refuse = (why, driver) => ({ calls: [], refused: true, terminated: true, proof: [], composed: null, driver, why });
186
98
 
187
- /** THREATS lifted to the meta-level: any pending intention whose needed condition
188
- * a candidate step's DELETE-effects would clobber (POP threats over the
189
- * conjunction of active goals). Computed from the registry's delete-lists. In
190
- * this read-only registry every capability's delete-list is empty, so this is
191
- * provably [] — but we DERIVE it rather than assume it, so the guarantee holds
192
- * the day a mutating capability is ever registered. Pure over the registry. */
99
+ /** Threats: whether a candidate step's DELETE-effects would clobber another pending
100
+ * intention. Computed from the registry's delete-lists (always [] today, since every
101
+ * capability is read-only) rather than assumed, so the guarantee holds if that changes. */
193
102
  export function threatsAmong(candidateName, _pending) {
194
103
  const cap = capabilityByName(candidateName);
195
104
  const del = cap ? effectsOf(cap.name).del : [];
196
105
  return del.length ? [{ name: candidateName, deletes: del }] : [];
197
106
  }
198
107
 
199
- /** Resolve the FOCUS the request scopes the goal model to an entity binding
200
- * (extractEntity + the graph oracle), NOT an intent keyword. Returns
201
- * `{match, ambiguous, candidates}`: `match` is the bound individual or null (no
202
- * bindable focus => a whole-graph / global goal); `ambiguous`+`candidates` let
203
- * the caller distinguish "genuinely no focus" from "a focus term that TIED"
204
- * (PLAN_BREADTH_FIRST_NLU.md §4 — the latter must never silently fall back to
205
- * a global answer, since that would silently answer a DIFFERENT goal than the
206
- * one the user actually named). */
108
+ /** Resolve the FOCUS the request scopes the goal model to (entity binding, not a keyword).
109
+ * `match` is the bound individual or null (no focus => a global goal); `ambiguous` lets the
110
+ * caller distinguish "no focus" from "a focus term that tied" (never silently falls back
111
+ * to global on a tie). */
207
112
  function focusOf(request, ctx) {
208
113
  const term = extractEntity(String(request || ""));
209
114
  if (!term || !ctx || !ctx.resolve) return { match: null, ambiguous: false, candidates: [] };
@@ -213,16 +118,9 @@ function focusOf(request, ctx) {
213
118
  return { match: null, ambiguous: false, candidates: [] };
214
119
  }
215
120
 
216
- /** The GLOBAL-MODE DOMAIN GATE's primitive: what entity CLASS (if any) did
217
- * ask.mjs's own compositional NL grammar recognize in the request? Walks
218
- * parseQuery's AST (the same shapes resolver.mjs's mapParse/mapFrame already
219
- * consume) for its declared `entityType` field, unwrapping the wrapper nodes
220
- * (`clause`, `inner`, `base`) that carry no entityType of their own. Returns the
221
- * class name, or null when the grammar placed nothing (an outright non-parse) or
222
- * placed a MISS with no recognized entity kind at all ("how many pizzas are
223
- * there" -> {node:"miss", reason:"count needs a known entity kind..."} carries no
224
- * entityType, same as a flat null). Pure; no request-string keyword table — it
225
- * reads a field ask.mjs's grammar already computes for every request. */
121
+ /** What entity CLASS (if any) did ask.mjs's NL grammar recognize in the request? Walks
122
+ * parseQuery's AST for its `entityType` field, unwrapping wrapper nodes (`clause`, `inner`,
123
+ * `base`). Returns null on a non-parse or an entity-less miss. */
226
124
  function parsedEntityType(node) {
227
125
  if (!node || typeof node !== "object") return null;
228
126
  if (typeof node.entityType === "string") return node.entityType;
@@ -232,16 +130,12 @@ function parsedEntityType(node) {
232
130
  return null;
233
131
  }
234
132
 
235
- /** Ground ONE epistemic sub-goal (a topic + optional bound entity) into a
236
- * grounded, EXECUTED call, or null when it is not groundable in the declared
237
- * toolset (=> the meta-loop escalates). Backward-chains topic->capability, binds
238
- * the entity, self-checks the same zero-hallucination gate the grader enforces,
239
- * then dispatches. Mirrors the resolver/planner's honest-miss discipline. */
133
+ /** Ground ONE epistemic sub-goal into an executed call, or null when not groundable in the
134
+ * declared toolset (=> the meta-loop escalates). Backward-chains topic->capability, binds
135
+ * the entity, self-checks the zero-hallucination gate, then dispatches. */
240
136
  async function groundSubGoal(topic, entityLabel, tools, ctx) {
241
137
  const cap = backwardChain(topic);
242
138
  if (!cap || !tools.includes(cap.name)) return null; // no declared capability => escalate
243
- // the arg grain: a no-arg coverage scan (untested) binds nothing; an entity
244
- // topic binds the focus label to the capability's single slot.
245
139
  const param = cap.parameters.find((p) => p.required);
246
140
  if (param && !entityLabel) return null; // an entity topic with nothing to bind
247
141
  const input = param && entityLabel ? { [param.arg]: entityLabel } : {};
@@ -252,54 +146,30 @@ async function groundSubGoal(topic, entityLabel, tools, ctx) {
252
146
  return { call, result: Array.isArray(res.result) ? res.result : [] };
253
147
  }
254
148
 
255
- /** BDI DROP CONDITIONS (Rao & Georgeff): an intention persists until it is
256
- * achieved / impossible / its goal lapses. `focusClass` is the SELECTED rule's
257
- * declared focus class (never a literal here — the rule is the authority).
258
- * Returns the reason string, or null to KEEP committing to it. Pure. */
149
+ /** An intention persists until achieved or its goal lapses. Returns the reason string, or
150
+ * null to keep committing to it. Pure. */
259
151
  export function dropCondition(intention, observed, mode, focus, focusClass) {
260
152
  if (observed.has(intention.key)) return "achieved"; // fact now gathered
261
153
  if (mode === "scoped" && (!focus || focus.class !== focusClass)) return "lapsed"; // focus moved
262
154
  return null; // else keep the commitment
263
155
  }
264
156
 
265
- /** THE META-LOOP. deduce current goals -> plan-each (C1) -> threat-aware
266
- * persistent first-step arbitration -> execute one -> observe -> repeat,
267
- * HARD-BOUNDED. Returns a loopResult { calls, refused, terminated, proof, why,
268
- * composed, driver } — a composed answer (the coverage-gap set for a focus, or
269
- * the keystone module globally) or an HONEST REFUSE at the open-world
270
- * goal-generation seam.
157
+ /** THE META-LOOP: deduce -> plan (C1) -> threat-aware arbitration -> execute -> observe ->
158
+ * repeat, hard-bounded. Returns a loopResult { calls, refused, terminated, proof, why,
159
+ * composed, driver } or an honest refuse.
271
160
  *
272
161
  * ctx: { dispatch(name,input)->{ok,result}, resolve(term)->{match,ambiguous} }.
273
- *
274
- * `ruleSet` defaults to the real, shipped GOAL_RULES (every product call site
275
- * omits it). PLAN_CODE.md Track 1's synthesis oracle (synthbench/rules/
276
- * oracle.mjs) is the one caller that overrides it, with a CLONED array
277
- * holding a candidate rule — the candidate then runs through this exact same
278
- * meta-loop a hand-written rule does, never a parallel re-implementation.
279
- *
280
- * `pinnedFocus` (internal — PLAN_BREADTH_FIRST_NLU.md §4's breadth-first
281
- * ambiguity fix) skips `focusOf` entirely and scopes straight to the given
282
- * individual: the mechanism the ambiguous-focus branch below uses to run this
283
- * SAME meta-loop once per tied candidate, "pin, don't re-resolve" (the same
284
- * idiom PLAN_BREADTH_FIRST_NLU.md §1 uses for ask.mjs's entity ties). No
285
- * product call site ever passes it. */
162
+ * `ruleSet` defaults to GOAL_RULES; only the synthesis-oracle test harness overrides it.
163
+ * `pinnedFocus` (internal) skips `focusOf` and scopes straight to the given individual —
164
+ * used to run this loop once per tied candidate on an ambiguous focus. */
286
165
  export async function goalReason(request, tools, ctx, { driver = "goal-0.8.1", ruleSet = GOAL_RULES, pinnedFocus = null } = {}) {
287
166
  const declared = Array.isArray(tools) ? tools : [];
288
167
 
289
- // STEP 1 — deduce the goal scope from the DECLARED model + a bound focus,
290
- // then SELECT the goal-rule by pure applicability (no request keyword ever):
291
- // a bound focus reads scoped, no focus reads global (keystone arbitration).
292
168
  const focusRes = pinnedFocus ? { match: pinnedFocus, ambiguous: false, candidates: [] } : focusOf(request, ctx);
293
169
  const focus = focusRes.match;
294
170
 
295
- // An AMBIGUOUS focus term must never silently collapse to "global" that
296
- // would silently answer a DIFFERENT goal (whole-graph keystone arbitration)
297
- // than the one the user actually named. Refuse honestly, the same "never a
298
- // guess" discipline resolver.mjs/guardrail.mjs apply to an ambiguous resolved
299
- // term — and, when the tied candidates share ONE class a declared goal-rule
300
- // scopes and a dispatcher is wired, ADDITIONALLY run this SAME meta-loop once
301
- // per (pinned) candidate, so a machine caller gets both the honest "still
302
- // ambiguous" signal and every candidate's real composed answer.
171
+ // An ambiguous focus must never silently collapse to "global". When the tied candidates
172
+ // share one class a goal-rule scopes, additionally run this loop once per candidate.
303
173
  if (!pinnedFocus && focusRes.ambiguous) {
304
174
  const term = extractEntity(String(request || ""));
305
175
  const pool = focusRes.candidates.slice(0, 4);
@@ -312,26 +182,16 @@ export async function goalReason(request, tools, ctx, { driver = "goal-0.8.1", r
312
182
  }
313
183
  const mode = focus ? "scoped" : "global";
314
184
 
315
- // The open-world goal-generation seam, named honestly: a resolved focus whose
316
- // class NO declared goal-rule scopes is REFUSED, never given an invented goal.
185
+ // A resolved focus whose class no declared goal-rule scopes is refused, never invented.
317
186
  if (focus && !ruleSet.some((r) => r.focusClass === focus.class)) {
318
187
  const covered = [...new Set(ruleSet.map((r) => r.focusClass))].join("/");
319
188
  return refuse(`open-world: no declared goal-rule covers a ${focus.class} focus (the declared goal-rules are ${covered}-scoped) — escalate`, driver);
320
189
  }
321
190
 
322
- // Rule selection is a DEDUCTION over the goal model + the declared toolset:
323
- // 0 applicable rules => the same open-world seam (nothing declared grounds the
324
- // request's scope in this toolset); >1 => an AMBIGUOUS meta-goal (arbitration
325
- // between meta-goals is undeclared) — both are honest refusals, never a guess.
326
191
  const applicable = applicableRules(declared, focus, mode, ruleSet);
327
192
 
328
- // THE GLOBAL-MODE DOMAIN GATE (Bug 8 fix, see the module header). SCOPED mode
329
- // already proved relevance via a bound graph entity; GLOBAL mode has not, so
330
- // `applicable` alone (a pure function of the caller's DECLARED TOOLSET) is not
331
- // enough — it says nothing about whether THIS request is even about the graph.
332
- // Screen it against ask.mjs's own NL grammar: the request must parse to a shape
333
- // naming the candidate rule's declared focusClass, or it is refused as honestly
334
- // off-domain rather than answered with someone else's goal.
193
+ // GLOBAL mode has no bound focus to prove relevance, so also screen the request against
194
+ // ask.mjs's NL grammar: it must parse to a shape naming the candidate rule's focusClass.
335
195
  let domainRelevant = applicable;
336
196
  if (mode === "global" && applicable.length) {
337
197
  const requestClass = parsedEntityType(parseQuery(request));
@@ -350,8 +210,6 @@ export async function goalReason(request, tools, ctx, { driver = "goal-0.8.1", r
350
210
  }
351
211
  const rule = domainRelevant[0];
352
212
 
353
- // the glass-box WHY, citing the declared goal-rule by backward-chain (the C2
354
- // twin of resolver.mjs's "backward-chain => <capability>" provenance).
355
213
  const why = [
356
214
  `goal-deduction: backward-chain (achieves ${rule.achieves}) => goal-rule "${rule.id}" (${rule.invariant})`,
357
215
  `mode: ${mode}${focus ? ` (focus ${focus.label} [${focus.class}])` : " (whole-graph / keystone arbitration)"}`,
@@ -361,13 +219,8 @@ export async function goalReason(request, tools, ctx, { driver = "goal-0.8.1", r
361
219
  const calls = [];
362
220
  const observed = new Map(); // intention.key -> gathered result set
363
221
 
364
- // STEP 2/3 — the pending INTENTIONS: the rule's epistemic sub-goals IN
365
- // DECLARED ORDER (arbitration is least-commitment: min order first, the
366
- // keystone selection over the gathered facts happens at compose). A topic
367
- // binds the focus iff its capability declares a REQUIRED parameter (read from
368
- // the registry, never special-cased by topic name); in global mode there is
369
- // no focus to bind, so entity-scoped topics are deferred to the GDA expansion
370
- // (gather the coverage scan first, then EXPAND to priority-of-each violator).
222
+ // The pending intentions: the rule's sub-goals in declared order. In global mode,
223
+ // entity-scoped topics are deferred to the GDA expansion (no focus to bind yet).
371
224
  const bindsEntity = (topic) => {
372
225
  const cap = backwardChain(topic);
373
226
  return Boolean(cap && cap.parameters.some((p) => p.required));
@@ -383,41 +236,32 @@ export async function goalReason(request, tools, ctx, { driver = "goal-0.8.1", r
383
236
  let ticks = 0;
384
237
 
385
238
  while (pending.length) {
386
- // (1) HARD OUTER BOUND — mechanical, independent of the monotone invariant.
387
239
  if (ticks >= MAX_TICKS) return refuse(`meta-loop tick budget exhausted (${MAX_TICKS}) — escalate`, driver);
388
240
  ticks += 1;
389
241
 
390
- // (3b) PERSISTENCE — keep the committed intention unless a BDI drop condition
391
- // fires; only THEN re-arbitrate. This is the "commitment, not recomputed
392
- // preference" that stops the loop thrashing.
242
+ // Keep the committed intention unless a drop condition fires; only then re-arbitrate.
393
243
  if (committed && dropCondition(committed, observed, mode, focus, rule.focusClass)) committed = null;
394
244
  if (!committed || !pending.includes(committed)) {
395
- // (3a) FIRST-STEP ARBITRATION — least-commitment: the lowest declared order
396
- // among pending. Threat-aware: skip a step that would clobber another
397
- // live goal (provably never, read-only) before committing.
245
+ // Least-commitment: lowest declared order among pending, skipping threatened steps.
398
246
  const admissible = pending.filter((i) => threatsAmong(backwardChain(i.topic)?.name, pending).length === 0);
399
247
  if (!admissible.length) return refuse("all first steps are threatened (would clobber a live goal) — escalate", driver);
400
248
  committed = admissible.slice().sort((a, b) => a.order - b.order)[0];
401
249
  }
402
250
 
403
- // (5) EXECUTE ONE, then OBSERVE (Steel & Ho monitor).
404
251
  const grounded = await groundSubGoal(committed.topic, committed.of, declared, ctx);
405
252
  if (!grounded) return refuse(`sub-goal (knows ${committed.topic}${committed.of ? ` ${committed.of}` : ""}) not groundable in the declared toolset — escalate`, driver);
406
253
  calls.push(grounded.call);
407
254
  observed.set(committed.key, grounded.result);
408
255
  proof.push({ step: "causal-link", producer: "graph", condition: committed.of ?? committed.topic, consumer: `${committed.topic}:${grounded.call.name}`, ok: true });
409
256
 
410
- // MONOTONE PROGRESS — this tick ACHIEVED exactly one intention: drop it.
257
+ // This tick achieved exactly one intention: drop it (monotone progress).
411
258
  const before = pending.length;
412
259
  const achievedTopic = committed.topic;
413
260
  pending.splice(pending.indexOf(committed), 1);
414
261
  committed = null;
415
262
 
416
- // GDA EXPANSION (monitor -> replan), ONCE: on observing the coverage set in
417
- // global mode (guarded on the rule DECLARING a global reading), expand to
418
- // the priority sub-goal for each violating module, so arbitration can rank
419
- // them. Bounded by the finite coverage set and fired at most once (the
420
- // `expanded` guard) => the pending set still converges.
263
+ // GDA expansion, once: on observing the coverage set in global mode, expand to the
264
+ // priority sub-goal for each violating module so arbitration can rank them.
421
265
  let expandedThisTick = false;
422
266
  if (mode === "global" && rule.modes.includes("global") && achievedTopic === rule.coverageTopic && !expanded) {
423
267
  expanded = true;
@@ -426,21 +270,15 @@ export async function goalReason(request, tools, ctx, { driver = "goal-0.8.1", r
426
270
  violating.forEach((m, i) => pending.push({ topic: rule.priorityTopic, of: m, key: `${rule.priorityTopic}:${m}`, order: 100 + i }));
427
271
  }
428
272
 
429
- // the invariant, enforced mechanically: the pending set shrank by one this
430
- // tick (progress) OR grew ONLY by the one-shot bounded expansion. Anything
431
- // else is non-progress => HALT honestly rather than risk a livelock.
273
+ // Anything other than shrink-by-one or the one-shot expansion is non-progress: halt.
432
274
  if (pending.length > before - 1 && !expandedThisTick) {
433
275
  return refuse("meta-loop made no monotone progress — halting", driver);
434
276
  }
435
277
  }
436
278
 
437
- // STEP 3a (the answer) COMPOSE + arbitrate the keystone from the gathered
438
- // facts (all INSIDE the driver's timeout guard; no unbounded post-work).
279
+ // Compose the answer: intersect two gathered sides (scoped), or keystone-arbitrate (global).
439
280
  let composed;
440
281
  if (mode === "scoped") {
441
- // interpret the rule's DECLARATIVE compose spec: intersect two gathered
442
- // sides, each a topic (optionally focus-bound, optionally with the focus
443
- // itself unioned in — the change-footprint shape). ∅ is a real answer.
444
282
  const sideSet = (side) => {
445
283
  const key = side.of === "focus" ? `${side.topic}:${focus.label}` : side.topic;
446
284
  const set = observed.get(key) || [];
@@ -453,10 +291,7 @@ export async function goalReason(request, tools, ctx, { driver = "goal-0.8.1", r
453
291
  composed = intersect(sideSet(spec.a), sideSet(spec.b));
454
292
  why.push(`compose: ${sideDesc(spec.a)} ∩ ${sideDesc(spec.b)} = ${spec.names} (${composed.length ? composed.join(", ") : `∅ — ${spec.empty}`})`);
455
293
  } else {
456
- // KEYSTONE arbitration: among the coverage violations, pick the highest
457
- // declared priority — the widest |priority(m)| set — tie broken by label
458
- // order. The single most-worth-covering module. Only a rule declaring a
459
- // global mode ever reaches here (applicability screened on rule.modes).
294
+ // Keystone: among the coverage violations, pick the highest priority, tie by label.
460
295
  const violating = observed.get(rule.coverageTopic) || [];
461
296
  const ranked = violating
462
297
  .map((m) => ({ m, weight: (observed.get(`${rule.priorityTopic}:${m}`) || []).length }))
@@ -1,39 +1,21 @@
1
- // src/router/guardrail.mjs — Stage 4 of the capability router
2
- // (PLAN_CAPABILITY_ROUTER.md): THE GUARDRAIL. Validate an EXTERNALLY-proposed
3
- // `tool_use` (e.g. an LLM's chosen call in the hybrid fast-path) against the
4
- // registry's declared preconditions, and DEFAULT-DENY anything outside the
5
- // declared, registered envelope. This is the precondition-checking half of the
6
- // STRIPS model: a call fires only when its preconditions are provably satisfied.
1
+ // src/router/guardrail.mjs — the guardrail. Validate an
2
+ // EXTERNALLY-proposed `tool_use` against the registry's declared preconditions, and
3
+ // DEFAULT-DENY anything outside the declared, registered envelope.
7
4
  //
8
- // WHAT IT PROVES and what it deliberately does NOT.
9
- // The guardrail proves RESOLVABILITY: the tool exists in the registry, it was
10
- // declared, every arg-key is one the operator accepts, every required arg is
11
- // present, and every `resolves(param, as)` precondition binds to a real graph
12
- // entity (delegated to resolveObject the same oracle Stage 1 uses).
13
- //
14
- // It does NOT prove ANTECEDENT-CORRECTNESS. A cross-turn mis-binding — "it" ->
15
- // the wrong Commit, say — produces a call whose symbol STILL resolves to a real
16
- // entity, so it PASSES the guardrail. That is by design: binding-confidence
17
- // across turns is the CHAT LEVER's job (pronoun/focus binding), not the
18
- // guardrail's. A gate that leaned on Stage 4 for antecedent correctness would be
19
- // trusting the wrong layer. The guardrail's contract is narrow and honest:
20
- // "this symbol denotes SOMETHING real and the call is well-formed", never "this
21
- // is the RIGHT something".
22
- //
23
- // DEFAULT-DENY: a tool name that is not a registered capability is denied outright
24
- // (identical to a hallucinated/invented tool). The intentionally-unregistered
25
- // unbounded tools (tmct_snippet/tmct_context*) are therefore denied here too — the
26
- // registry is the whole trust boundary.
5
+ // Proves RESOLVABILITY (the tool is registered/declared, args are well-formed, every
6
+ // `resolves(param, as)` precondition binds to a real graph entity) NOT antecedent
7
+ // correctness: a cross-turn mis-binding ("it" -> the wrong Commit) still resolves to a
8
+ // real entity and passes. "This symbol denotes something real and the call is
9
+ // well-formed", never "this is the right something".
27
10
  //
28
11
  // Pure over its inputs + ctx.resolve (the binding oracle). No network, no Date.now.
29
12
 
30
13
  import { capabilityByName, preconditionsOf, PRECOND } from "./registry.mjs";
31
14
  import { hallucinationsIn } from "./call-validator.mjs";
32
15
 
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
16
+ /** The same read-only breadth-first enrichment as resolver.mjs's `dispatchEachCandidate`:
17
+ * every registered capability is `readOnly:true` with an empty delete-list, so dispatching
18
+ * the SAME tool once per tied candidate is safe. Returns `[{candidate, result}, ...]`, or
37
19
  * undefined when there is no dispatcher to run it with. */
38
20
  async function dispatchEachCandidate(pool, capName, arg, ctx) {
39
21
  if (!ctx.dispatch) return undefined;
@@ -47,53 +29,35 @@ async function dispatchEachCandidate(pool, capName, arg, ctx) {
47
29
 
48
30
  /** Validate a proposed tool_use. Returns a glass-box verdict:
49
31
  * { ok, tool, denied:[{reason,detail}], steps:[{pred,ok,...}], provenance, candidateResults? }
50
- * - ok=false with a `default-deny`/`undeclared`/`unknown-arg`/`missing-arg`
51
- * denial is a STRUCTURAL rejection (no graph needed).
52
- * - ok=false with an `unresolved` step is a BINDING rejection (a `resolves`
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).
59
- * - ok=true means the call is RESOLVABLE + well-formed (NOT proven antecedent-
60
- * correct — see the file header).
61
- * `declaredNames` may be null to skip the declared-set check (validate against
62
- * the registry alone); pass it to also enforce the case/session toolset.
63
- * `ctx.resolve(term)` is the resolveObject oracle; omit it to skip binding proof
64
- * (structural-only validation). Async only because an ambiguous `resolves` term
65
- * may dispatch each candidate via `ctx.dispatch`. */
32
+ * ok=false with a default-deny/undeclared/unknown-arg/missing-arg denial is structural (no
33
+ * graph needed); an `unresolved` step is a binding rejection, and an ambiguous `resolves`
34
+ * term additionally carries `candidateResults` (the tool dispatched once per tied candidate)
35
+ * when `ctx.dispatch` is wired. `declaredNames=null` skips the declared-set check.
36
+ * `ctx.resolve(term)` is the resolveObject oracle; omit it for structural-only validation. */
66
37
  export async function guard(toolUse, declaredNames = null, ctx = {}) {
67
38
  const name = toolUse?.name;
68
39
  const input = toolUse && typeof toolUse.input === "object" && toolUse.input ? toolUse.input : {};
69
40
  const denied = [];
70
41
  const steps = [];
71
42
 
72
- // 1. DEFAULT-DENY unknown/unregistered tool is an automatic reject. Reuse the
73
- // grader's hallucination check as the single source of truth for structural
74
- // well-formedness (unknown-tool / undeclared / unknown-arg / missing-arg).
43
+ // Default-deny: unknown/unregistered tool is an automatic reject.
75
44
  const declaredList = declaredNames ? [...declaredNames] : null;
76
45
  const cap = capabilityByName(name);
77
46
  if (!cap) {
78
47
  denied.push({ reason: "default-deny", detail: `"${name ?? "(none)"}" is not a registered capability` });
79
48
  return { ok: false, tool: name ?? null, denied, steps, provenance: "registry default-deny" };
80
49
  }
81
- // structural well-formedness against the registry (and the declared set when
82
- // given — an undeclared-but-registered tool is still a policy denial).
83
50
  const structural = hallucinationsIn({ name, input }, declaredList ?? [name]);
84
51
  for (const p of structural) {
85
- // when no declared set is supplied, an "undeclared" finding is not a real
86
- // denial (we synthesised [name] as the set) — filter it out.
52
+ // no declared set supplied => "undeclared" isn't a real denial (we synthesised [name]).
87
53
  if (!declaredList && p.reason === "undeclared") continue;
88
54
  denied.push(p);
89
55
  }
90
56
 
91
- // 2. PRECONDITION CHECK — the STRIPS safety gate, step by step (the proof).
57
+ // Precondition check — the STRIPS safety gate, step by step.
92
58
  let candidateResults;
93
59
  for (const pre of preconditionsOf(name)) {
94
60
  if (pre.pred === PRECOND.graphLoaded) {
95
- // graph presence is the harness's responsibility; if a resolver is wired we
96
- // treat graph-loaded as satisfied (resolveObject would throw without one).
97
61
  steps.push({ step: "precondition", pred: pre.pred, ok: true });
98
62
  } else if (pre.pred === PRECOND.anyPresent) {
99
63
  const ok = pre.params.some((k) => input[k] !== undefined && input[k] !== null && String(input[k]).trim() !== "");
@@ -103,12 +67,10 @@ export async function guard(toolUse, declaredNames = null, ctx = {}) {
103
67
  const term = input[pre.param];
104
68
  const present = term !== undefined && term !== null && String(term).trim() !== "";
105
69
  if (!present) {
106
- // a missing required arg is already flagged structurally; record the step.
107
70
  steps.push({ step: "precondition", pred: pre.pred, param: pre.param, value: null, ok: false });
108
71
  continue;
109
72
  }
110
- // DELEGATE to resolveObject (the binding oracle). No oracle wired we can
111
- // only assert the arg is PRESENT, not that it binds (structural mode).
73
+ // No oracle wired: assert the arg is PRESENT only, not that it binds.
112
74
  if (!ctx.resolve) {
113
75
  steps.push({ step: "precondition", pred: pre.pred, param: pre.param, value: term, ok: true, note: "structural-only (no resolver wired)" });
114
76
  continue;