@polycode-projects/the-mechanical-code-talker 0.7.1 → 0.8.1
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 +120 -11
- package/bin/tmct.mjs +45 -0
- package/package.json +3 -1
- package/src/ask-vocab.mjs +1 -1
- package/src/ask.mjs +123 -3
- package/src/chat.mjs +79 -7
- package/src/concept.mjs +5 -0
- package/src/conformance.mjs +1 -1
- package/src/corpus/templates.mjs +1 -1
- package/src/finish.mjs +1 -1
- package/src/hash.mjs +1 -1
- package/src/interpret/normalize.mjs +65 -1
- package/src/interpret/pipeline.mjs +18 -3
- package/src/interpret/strategies/ace.mjs +49 -0
- package/src/interpret/strategies/keywords.mjs +2 -2
- package/src/providers/bootstrap.mjs +1 -1
- package/src/providers/fixture.mjs +1 -1
- package/src/providers/graph-service.mjs +1 -1
- package/src/repository-interface.mjs +1 -1
- package/src/router/goal-reasoner.mjs +266 -0
- package/src/router/guardrail.mjs +120 -0
- package/src/router/planner.mjs +168 -0
- package/src/router/registry.mjs +271 -0
- package/src/router/resolver.mjs +331 -0
- package/src/server-http.mjs +296 -0
- package/src/syllogise.mjs +0 -0
- package/src/tui/app.mjs +63 -14
|
@@ -0,0 +1,120 @@
|
|
|
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.
|
|
7
|
+
//
|
|
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.
|
|
27
|
+
//
|
|
28
|
+
// Pure over its inputs + ctx.resolve (the binding oracle). No network, no Date.now.
|
|
29
|
+
|
|
30
|
+
import { capabilityByName, preconditionsOf, PRECOND } from "./registry.mjs";
|
|
31
|
+
import { hallucinationsIn } from "../../agentbench/grade.mjs";
|
|
32
|
+
|
|
33
|
+
/** Validate a proposed tool_use. Returns a glass-box verdict:
|
|
34
|
+
* { ok, tool, denied:[{reason,detail}], steps:[{pred,ok,...}], provenance }
|
|
35
|
+
* - ok=false with a `default-deny`/`undeclared`/`unknown-arg`/`missing-arg`
|
|
36
|
+
* denial is a STRUCTURAL rejection (no graph needed).
|
|
37
|
+
* - ok=false with an `unresolved` step is a BINDING rejection (a `resolves`
|
|
38
|
+
* precondition whose term matched no entity, or matched ambiguously).
|
|
39
|
+
* - ok=true means the call is RESOLVABLE + well-formed (NOT proven antecedent-
|
|
40
|
+
* correct — see the file header).
|
|
41
|
+
* `declaredNames` may be null to skip the declared-set check (validate against
|
|
42
|
+
* the registry alone); pass it to also enforce the case/session toolset.
|
|
43
|
+
* `ctx.resolve(term)` is the resolveObject oracle; omit it to skip binding proof
|
|
44
|
+
* (structural-only validation). */
|
|
45
|
+
export function guard(toolUse, declaredNames = null, ctx = {}) {
|
|
46
|
+
const name = toolUse?.name;
|
|
47
|
+
const input = toolUse && typeof toolUse.input === "object" && toolUse.input ? toolUse.input : {};
|
|
48
|
+
const denied = [];
|
|
49
|
+
const steps = [];
|
|
50
|
+
|
|
51
|
+
// 1. DEFAULT-DENY — unknown/unregistered tool is an automatic reject. Reuse the
|
|
52
|
+
// grader's hallucination check as the single source of truth for structural
|
|
53
|
+
// well-formedness (unknown-tool / undeclared / unknown-arg / missing-arg).
|
|
54
|
+
const declaredList = declaredNames ? [...declaredNames] : null;
|
|
55
|
+
const cap = capabilityByName(name);
|
|
56
|
+
if (!cap) {
|
|
57
|
+
denied.push({ reason: "default-deny", detail: `"${name ?? "(none)"}" is not a registered capability` });
|
|
58
|
+
return { ok: false, tool: name ?? null, denied, steps, provenance: "registry default-deny" };
|
|
59
|
+
}
|
|
60
|
+
// structural well-formedness against the registry (and the declared set when
|
|
61
|
+
// given — an undeclared-but-registered tool is still a policy denial).
|
|
62
|
+
const structural = hallucinationsIn({ name, input }, declaredList ?? [name]);
|
|
63
|
+
for (const p of structural) {
|
|
64
|
+
// when no declared set is supplied, an "undeclared" finding is not a real
|
|
65
|
+
// denial (we synthesised [name] as the set) — filter it out.
|
|
66
|
+
if (!declaredList && p.reason === "undeclared") continue;
|
|
67
|
+
denied.push(p);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. PRECONDITION CHECK — the STRIPS safety gate, step by step (the proof).
|
|
71
|
+
for (const pre of preconditionsOf(name)) {
|
|
72
|
+
if (pre.pred === PRECOND.graphLoaded) {
|
|
73
|
+
// graph presence is the harness's responsibility; if a resolver is wired we
|
|
74
|
+
// treat graph-loaded as satisfied (resolveObject would throw without one).
|
|
75
|
+
steps.push({ step: "precondition", pred: pre.pred, ok: true });
|
|
76
|
+
} else if (pre.pred === PRECOND.anyPresent) {
|
|
77
|
+
const ok = pre.params.some((k) => input[k] !== undefined && input[k] !== null && String(input[k]).trim() !== "");
|
|
78
|
+
steps.push({ step: "precondition", pred: pre.pred, params: pre.params, ok });
|
|
79
|
+
if (!ok) denied.push({ reason: "missing-arg", detail: `${name} needs one of ${pre.params.join("|")}` });
|
|
80
|
+
} else if (pre.pred === PRECOND.resolves) {
|
|
81
|
+
const term = input[pre.param];
|
|
82
|
+
const present = term !== undefined && term !== null && String(term).trim() !== "";
|
|
83
|
+
if (!present) {
|
|
84
|
+
// a missing required arg is already flagged structurally; record the step.
|
|
85
|
+
steps.push({ step: "precondition", pred: pre.pred, param: pre.param, value: null, ok: false });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// DELEGATE to resolveObject (the binding oracle). No oracle wired → we can
|
|
89
|
+
// only assert the arg is PRESENT, not that it binds (structural mode).
|
|
90
|
+
if (!ctx.resolve) {
|
|
91
|
+
steps.push({ step: "precondition", pred: pre.pred, param: pre.param, value: term, ok: true, note: "structural-only (no resolver wired)" });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const r = ctx.resolve(String(term));
|
|
95
|
+
const resolvedOk = Boolean(r && r.match && !r.ambiguous);
|
|
96
|
+
steps.push({
|
|
97
|
+
step: "precondition", pred: pre.pred, param: pre.param, value: term,
|
|
98
|
+
ok: resolvedOk,
|
|
99
|
+
...(r && r.match ? { boundTo: r.match.label, boundClass: r.match.class ?? null, tier: r.tier ?? null } : {}),
|
|
100
|
+
...(r && r.ambiguous ? { ambiguous: true } : {}),
|
|
101
|
+
});
|
|
102
|
+
if (!resolvedOk) {
|
|
103
|
+
denied.push({
|
|
104
|
+
reason: "unresolved",
|
|
105
|
+
detail: r && r.ambiguous
|
|
106
|
+
? `${name}.${pre.param}="${term}" is ambiguous (narrow it)`
|
|
107
|
+
: `${name}.${pre.param}="${term}" resolves to no graph entity`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ok = denied.length === 0;
|
|
114
|
+
return { ok, tool: name, denied, steps, provenance: ok ? "resolvable (NOT proven antecedent-correct)" : "denied" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 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;
|
|
120
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/router/planner.mjs — Stage 3 of the capability router
|
|
2
|
+
// (PLAN_CAPABILITY_ROUTER.md): THE PLANNER. Compose a bounded, ordered plan of
|
|
3
|
+
// tool calls for a multi-step request, over the SAME operators Stage 1 resolves
|
|
4
|
+
// single-shot. Pure-JS POP/HTN + a Steel & Ho monitor-and-replan loop under a
|
|
5
|
+
// HARD budget — sound/complete INSIDE the declared operator model, honest-refuse
|
|
6
|
+
// (escalate) for novelty outside it. Deterministic, no-LLM, glass-box.
|
|
7
|
+
//
|
|
8
|
+
// THE MODEL, mapped to the literature:
|
|
9
|
+
// - HTN decomposition (NONLIN/SHOP2): a compound request is decomposed into an
|
|
10
|
+
// ORDERED list of sub-goals by declared METHODS — the sequencing connectives
|
|
11
|
+
// ("... then ...", "... and then ...") and the two closed recipes we author:
|
|
12
|
+
// the CONDITIONAL method ("if <check>, <action> [instead]") and the
|
|
13
|
+
// RELATIVE-FILTER method ("of the <set> <rel> X, which are <Y>"). Each leaf
|
|
14
|
+
// sub-goal is resolved by Stage 1 (resolveOne) — the primitive operator.
|
|
15
|
+
// - POP causal links (partial-order planning): each step's proof records the
|
|
16
|
+
// PRODUCER -> CONDITION -> CONSUMER link. An independent step's producer is
|
|
17
|
+
// the grounded graph (graph-loaded); a THREADED step (one whose entity came
|
|
18
|
+
// from a prior step via anaphora — "its subclasses", "describe it") records
|
|
19
|
+
// the prior STEP as its producer. That link IS the proof chain (grade.mjs's
|
|
20
|
+
// connectedness check reads it), never a flat ok-list. Least commitment: we
|
|
21
|
+
// only order what the connectives actually order.
|
|
22
|
+
// - Steel & Ho monitor-and-replan: after each call we read the tool_result;
|
|
23
|
+
// a failed sub-goal (an unresolvable entity / an operator that errors) forces
|
|
24
|
+
// an honest STOP (refuse/escalate) rather than pressing on with a broken
|
|
25
|
+
// chain. Bounded depth + a hard step counter GUARANTEE termination — no
|
|
26
|
+
// unbounded search can ever wedge the caller (the harness also caps us).
|
|
27
|
+
//
|
|
28
|
+
// THE OPEN-WORLD BOUNDARY, named honestly: novelty the declared methods + operators
|
|
29
|
+
// do not cover (a sub-goal that resolves to nothing, a connective we do not model)
|
|
30
|
+
// is REFUSED/ESCALATED, not guessed. Sound/complete is claimed only INSIDE the
|
|
31
|
+
// declared world.
|
|
32
|
+
|
|
33
|
+
import { resolveOne, extractEntity } from "./resolver.mjs";
|
|
34
|
+
|
|
35
|
+
// Hard budget — the planner may emit at most this many steps; a request that
|
|
36
|
+
// decomposes to more is REFUSED (escalate) rather than searched. Guarantees
|
|
37
|
+
// termination independent of the harness backstop.
|
|
38
|
+
export const MAX_STEPS = 8;
|
|
39
|
+
|
|
40
|
+
const PRONOUN_RE = /\b(?:it|its|them|those|these|that|their)\b/i;
|
|
41
|
+
|
|
42
|
+
/** HTN decomposition — turn a request into an ORDERED list of leaf sub-goals.
|
|
43
|
+
* Returns { method, segments:[{ text, role, thread }] }:
|
|
44
|
+
* - role "check" — a conditional antecedent (a test whose call still emits)
|
|
45
|
+
* - role "action" — a plain sub-goal
|
|
46
|
+
* - thread:true — the segment carries an anaphor to bind from a prior step
|
|
47
|
+
* A single segment (no connective) means "not multi-step" (the driver hands
|
|
48
|
+
* those to resolveOne directly). Pure. */
|
|
49
|
+
export function decompose(request) {
|
|
50
|
+
const raw = String(request || "").trim();
|
|
51
|
+
|
|
52
|
+
// METHOD 1 — the CONDITIONAL recipe: "if <check>, <action> [instead]".
|
|
53
|
+
const cond = raw.match(/^if\s+(.+?),\s*(.+?)(?:\s+instead)?$/i);
|
|
54
|
+
if (cond) {
|
|
55
|
+
return {
|
|
56
|
+
method: "conditional",
|
|
57
|
+
segments: [
|
|
58
|
+
{ text: cond[1].trim(), role: "check", thread: PRONOUN_RE.test(cond[1]) },
|
|
59
|
+
{ text: cond[2].trim(), role: "action", thread: PRONOUN_RE.test(cond[2]) },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// METHOD 2 — the RELATIVE-FILTER recipe: "of the <set> <rel> X, which are <Y>".
|
|
65
|
+
// Decomposes to [produce the <set> (the <rel> over X), filter it by <Y>].
|
|
66
|
+
const rel = raw.match(/^of\s+the\s+(.+?),\s*which\s+(?:are\s+)?(.+?)$/i);
|
|
67
|
+
if (rel) {
|
|
68
|
+
return {
|
|
69
|
+
method: "relative-filter",
|
|
70
|
+
segments: [
|
|
71
|
+
{ text: rel[1].trim(), role: "action", thread: false },
|
|
72
|
+
{ text: `which are ${rel[2].trim()}`, role: "action", thread: true },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// METHOD 3 — SEQUENCING: split on the ordered connectives. Least commitment:
|
|
78
|
+
// we only split where a connective actually is.
|
|
79
|
+
const parts = raw.split(/\s*(?:,\s*then\s+|,\s+and\s+then\s+|\s+and\s+then\s+|\s+then\s+|,\s+|\s+and\s+)\s*/i)
|
|
80
|
+
.map((s) => s.replace(/^(?:then\s+|and\s+then\s+|and\s+|also\s+|check\s+|next\s+)/i, "").trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
const segments = parts.map((text) => ({ text, role: "action", thread: PRONOUN_RE.test(text) }));
|
|
83
|
+
return { method: segments.length > 1 ? "sequence" : "single", segments };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** True iff the request is multi-step (the planner owns it); else the driver
|
|
87
|
+
* routes it to the single-shot resolver. */
|
|
88
|
+
export function isMultiStep(request) {
|
|
89
|
+
return decompose(request).segments.length > 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Substitute a bound anaphor: replace a bare pronoun with the prior step's
|
|
93
|
+
* entity label so the leaf resolver can bind it ("its subclasses" + Widget ->
|
|
94
|
+
* "Widget subclasses"; "describe it" + fnAlpha -> "describe fnAlpha"). */
|
|
95
|
+
function bindAnaphor(text, lastEntity) {
|
|
96
|
+
if (!lastEntity) return text;
|
|
97
|
+
return text.replace(PRONOUN_RE, lastEntity);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A conditional ANTECEDENT that tests a SPECIFIC entity's coverage ("X has no
|
|
101
|
+
* tests", "fnAlpha is untested") is the CHECK operator tmct_tests_for over that
|
|
102
|
+
* entity — NOT the no-arg tmct_untested (which lists the whole codebase). Rewrite
|
|
103
|
+
* it to the terse "tests <entity>" command form so the leaf resolver binds the
|
|
104
|
+
* entity. A check with no coverage predicate is left untouched. */
|
|
105
|
+
function rewriteCheck(text, lastEntity) {
|
|
106
|
+
if (!/\b(?:untested|tested|no\s+tests?|has\s+no\s+tests?|tests?|coverage|covered)\b/i.test(text)) return text;
|
|
107
|
+
const entity = extractEntity(text) || lastEntity;
|
|
108
|
+
return entity ? `tests ${entity}` : text;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const refuse = (why, driver) => ({ calls: [], refused: true, terminated: true, proof: [], driver, why });
|
|
112
|
+
|
|
113
|
+
/** Plan + execute a multi-step request. Returns a loopResult
|
|
114
|
+
* { calls, refused, terminated, proof, why, driver, observed }
|
|
115
|
+
* with a POP causal-link proof chain. Steel & Ho: each step is monitored; a
|
|
116
|
+
* failed sub-goal STOPS the plan honestly (refuse/escalate). Bounded by
|
|
117
|
+
* MAX_STEPS + a hard step counter. `driver` labels the row.
|
|
118
|
+
*
|
|
119
|
+
* ctx: { dispatch(name,input)->{ok,text,resolved?}, resolve(term)->resolveObject } */
|
|
120
|
+
export async function plan(request, declaredNames, ctx, { driver = "resolver-0.8.0" } = {}) {
|
|
121
|
+
const { method, segments } = decompose(request);
|
|
122
|
+
if (segments.length > MAX_STEPS) {
|
|
123
|
+
return refuse(`plan would need ${segments.length} steps (> budget ${MAX_STEPS}) — escalate`, driver);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const calls = [];
|
|
127
|
+
const proof = [];
|
|
128
|
+
const why = [`HTN method: ${method} — ${segments.length} sub-goal(s)`];
|
|
129
|
+
let lastEntity = null; // the most-recent bound entity label (for anaphora threading)
|
|
130
|
+
let steps = 0;
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
133
|
+
if (steps >= MAX_STEPS) return refuse("step budget exhausted mid-plan — escalate", driver);
|
|
134
|
+
steps += 1;
|
|
135
|
+
const seg = segments[i];
|
|
136
|
+
let text = seg.thread ? bindAnaphor(seg.text, lastEntity) : seg.text;
|
|
137
|
+
if (seg.role === "check") text = rewriteCheck(text, lastEntity);
|
|
138
|
+
|
|
139
|
+
const r = await resolveOne(text, declaredNames, ctx, { execute: true });
|
|
140
|
+
if (r.refused) {
|
|
141
|
+
// Steel & Ho: an unresolvable sub-goal breaks the causal chain — STOP
|
|
142
|
+
// honestly (escalate), never emit a partial/guessed plan.
|
|
143
|
+
return refuse(`sub-goal ${i + 1} ("${text}") did not resolve: ${r.reason}`, driver);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
calls.push(r.selected);
|
|
147
|
+
// POP causal link: the producer is the prior step when this step THREADED an
|
|
148
|
+
// anaphor from it; otherwise the grounded graph. Its condition is the arg the
|
|
149
|
+
// step needed. This is the "why step i" edge, not a flat ok.
|
|
150
|
+
const producer = seg.thread && i > 0 ? `step-${i}` : "graph";
|
|
151
|
+
const boundLabel = r.resolved?.label ?? Object.values(r.selected.input || {})[0] ?? null;
|
|
152
|
+
proof.push({ step: "causal-link", producer, condition: boundLabel, consumer: `step-${i + 1}:${r.selected.name}`, role: seg.role, ok: true });
|
|
153
|
+
for (const s of r.proof) proof.push({ ...s, ofStep: i + 1 });
|
|
154
|
+
|
|
155
|
+
if (r.resolved?.label) lastEntity = r.resolved.label;
|
|
156
|
+
why.push(...(r.why || []).map((w) => `[${i + 1}] ${w}`));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
calls,
|
|
161
|
+
refused: false,
|
|
162
|
+
terminated: true,
|
|
163
|
+
proof,
|
|
164
|
+
driver,
|
|
165
|
+
why,
|
|
166
|
+
observed: `plan(${method}): ${calls.map((c) => c.name).join(" -> ")}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// src/router/registry.mjs — Stage 0 of the capability router (PLAN_CAPABILITY_ROUTER.md).
|
|
2
|
+
//
|
|
3
|
+
// Each tmct tool is modelled as a STRIPS/PDDL operator declared as DATA:
|
|
4
|
+
// a `Capability` with typed `Parameter`s, `Precondition`s, and `Effect`s
|
|
5
|
+
// (an add-list / delete-list). This is the direct mapping the reference note
|
|
6
|
+
// docs/references/planning/STRIPS_PDDL.md calls "the most direct in the whole
|
|
7
|
+
// set": a capability IS a STRIPS operator expressed in tmct's OWL vocabulary.
|
|
8
|
+
//
|
|
9
|
+
// - Preconditions are the SAFETY GATE — a capability will not fire unless its
|
|
10
|
+
// preconditions are provably satisfied (Stage 4, the guardrail, reads these).
|
|
11
|
+
// This is why the router REFUSES rather than emitting an unsafe call, the
|
|
12
|
+
// same discipline as tmct's honest miss.
|
|
13
|
+
// - Effects are the PROOF CHAIN — for a read-only query tool the effect is
|
|
14
|
+
// EPISTEMIC (it makes a fact KNOWN to the agent), never a world mutation, so
|
|
15
|
+
// every graph-query capability has an EMPTY delete-list (the STRIPS closed-
|
|
16
|
+
// world assumption: what is not deleted is unchanged, and a query changes
|
|
17
|
+
// nothing in the world). Stage 1 (the resolver) backward-chains from a goal
|
|
18
|
+
// `(knows <topic> ?x)` to the capability whose add-list achieves it.
|
|
19
|
+
//
|
|
20
|
+
// This module is PURE: plain frozen data + pure accessor functions, NO I/O. The
|
|
21
|
+
// tool NAMES + parameter ARG KEYS are the exact ones src/server.mjs `dispatchTool`
|
|
22
|
+
// reads (verified against its switch), so a bound call this registry validates is
|
|
23
|
+
// directly dispatchable. Stage 1 (resolver) and Stage 4 (guardrail) consume this
|
|
24
|
+
// substrate; nothing here imports the graph or the network.
|
|
25
|
+
|
|
26
|
+
// ---- OWL-labelled vocabulary (tmct's style: urn:tmct:… prefixes) ------------
|
|
27
|
+
// The registry declares its OWN vocabulary the way every tmct graph artifact
|
|
28
|
+
// does (see the fixture's `prefixes` block + the schema-doc individuals). A
|
|
29
|
+
// capability is a `cap:Capability` individual; its parts are `cap:Parameter`,
|
|
30
|
+
// `cap:Precondition`, `cap:Effect`. Parameter TYPES range over the same seon/mgx
|
|
31
|
+
// entity classes the code graph already speaks (Module, Class, Function, …).
|
|
32
|
+
|
|
33
|
+
export const PREFIXES = Object.freeze({
|
|
34
|
+
cap: "urn:tmct:cap#", // the capability/operator vocabulary (this module)
|
|
35
|
+
mgx: "urn:tmct:mgx#", // tmct's code-graph predicates (imports/calls/tests/…)
|
|
36
|
+
seon: "http://se-on.org/ontologies/seon.owl#", // software-evolution ontology classes
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// The OWL classes this registry mints individuals of.
|
|
40
|
+
export const VOCAB = Object.freeze({
|
|
41
|
+
Capability: "cap:Capability", // rdf:type of a declared tool/operator
|
|
42
|
+
Parameter: "cap:Parameter",
|
|
43
|
+
Precondition: "cap:Precondition",
|
|
44
|
+
Effect: "cap:Effect",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Parameter entity-KINDS — the seon/mgx classes a slot ranges over. `Query` and
|
|
48
|
+
// `Kind`/`Package` are free-text / enum slots (no graph resolution); the rest
|
|
49
|
+
// name a graph entity the guardrail must prove RESOLVES before the call fires.
|
|
50
|
+
export const KINDS = Object.freeze({
|
|
51
|
+
Symbol: "seon:CodeEntity", // any code symbol: function/method/class/module/attribute
|
|
52
|
+
Module: "seon:Module",
|
|
53
|
+
Class: "seon:ClassDefinition",
|
|
54
|
+
Query: "cap:FreeText", // lexical search string — no resolution precondition
|
|
55
|
+
Kind: "cap:KindFilter", // enum: function|class|method|… (search filter)
|
|
56
|
+
Package: "cap:PackageName", // optional architecture-scope filter
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Precondition PREDICATE tags (the small closed vocabulary a precondition uses).
|
|
60
|
+
export const PRECOND = Object.freeze({
|
|
61
|
+
graphLoaded: "cap:graph-loaded", // a graph artifact is present + parseable
|
|
62
|
+
resolves: "cap:resolves", // { param, as } — the slot binds to an entity of kind `as`
|
|
63
|
+
anyPresent: "cap:any-present", // { params } — at least one of these slots is provided
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---- capability builder (returns PLAIN FROZEN data) -------------------------
|
|
67
|
+
|
|
68
|
+
/** A parameter slot. `arg` is the EXACT dispatchTool key (never invented). */
|
|
69
|
+
const param = (name, kind, { arg = name, required = true, note = "" } = {}) =>
|
|
70
|
+
Object.freeze({ type: VOCAB.Parameter, name, kind, arg, required, note });
|
|
71
|
+
|
|
72
|
+
/** graph-loaded precondition — every graph-query capability carries it. */
|
|
73
|
+
const graphLoaded = () => Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.graphLoaded });
|
|
74
|
+
/** resolves(param, as) — the named slot must bind to a graph entity of kind `as`. */
|
|
75
|
+
const resolves = (paramName, as) =>
|
|
76
|
+
Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.resolves, param: paramName, as });
|
|
77
|
+
/** any-present(params) — search-style disjunction (query OR kind must be given). */
|
|
78
|
+
const anyPresent = (params) =>
|
|
79
|
+
Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.anyPresent, params: Object.freeze([...params]) });
|
|
80
|
+
|
|
81
|
+
/** An epistemic add-effect: after the call the agent KNOWS `topic` about `?of`. */
|
|
82
|
+
const knows = (topic, ofParam = null) =>
|
|
83
|
+
Object.freeze({ type: VOCAB.Effect, pred: "cap:knows", topic, of: ofParam ? `?${ofParam}` : null });
|
|
84
|
+
|
|
85
|
+
/** Declare one capability as frozen STRIPS data. Read-only query tools pass an
|
|
86
|
+
* empty delete-list (`del: []`) — the closed-world "queries mutate nothing". */
|
|
87
|
+
function capability({ name, label, question, params = [], preconditions = [], add = [], del = [] }) {
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
type: VOCAB.Capability,
|
|
90
|
+
name, // the dispatchTool tool name — directly callable
|
|
91
|
+
label, // human label (the slash-command verb)
|
|
92
|
+
question, // one-line "what question does this answer"
|
|
93
|
+
readOnly: true, // every capability here is query-only
|
|
94
|
+
parameters: Object.freeze(params),
|
|
95
|
+
preconditions: Object.freeze(preconditions),
|
|
96
|
+
effects: Object.freeze({ add: Object.freeze(add), del: Object.freeze(del) }),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---- the registry — the read-only graph-query tools as operators ------------
|
|
101
|
+
// Enumerated from src/server.mjs `dispatchTool` (the query-only, bounded-output
|
|
102
|
+
// slice). Arg keys verified against the switch: describe/callers/callees/tests/
|
|
103
|
+
// history/… take `symbol`; impact/exports take `module`; members/subclasses take
|
|
104
|
+
// `class`; search takes `query` (+ optional kind/name/decorator); architecture
|
|
105
|
+
// takes an optional `package`; untested takes nothing.
|
|
106
|
+
|
|
107
|
+
const CAPABILITIES = Object.freeze([
|
|
108
|
+
capability({
|
|
109
|
+
name: "tmct_search", label: "search", question: "lexical search across the graph",
|
|
110
|
+
params: [
|
|
111
|
+
param("query", KINDS.Query, { required: false, note: "required unless a kind filter is given" }),
|
|
112
|
+
param("kind", KINDS.Kind, { required: false }),
|
|
113
|
+
param("name", KINDS.Query, { required: false }),
|
|
114
|
+
param("decorator", KINDS.Query, { required: false }),
|
|
115
|
+
],
|
|
116
|
+
preconditions: [graphLoaded(), anyPresent(["query", "kind"])],
|
|
117
|
+
add: [knows("matches", "query")],
|
|
118
|
+
}),
|
|
119
|
+
capability({
|
|
120
|
+
name: "tmct_describe", label: "describe", question: "a symbol's definition, kind and relations",
|
|
121
|
+
params: [param("symbol", KINDS.Symbol)],
|
|
122
|
+
preconditions: [graphLoaded(), resolves("symbol", KINDS.Symbol)],
|
|
123
|
+
add: [knows("description", "symbol")],
|
|
124
|
+
}),
|
|
125
|
+
capability({
|
|
126
|
+
name: "tmct_signature", label: "signature", question: "a symbol's signature only",
|
|
127
|
+
params: [param("symbol", KINDS.Symbol)],
|
|
128
|
+
preconditions: [graphLoaded(), resolves("symbol", KINDS.Symbol)],
|
|
129
|
+
add: [knows("signature", "symbol")],
|
|
130
|
+
}),
|
|
131
|
+
capability({
|
|
132
|
+
name: "tmct_impact", label: "impact", question: "what a change to this module reaches (impact closure)",
|
|
133
|
+
params: [param("module", KINDS.Module)],
|
|
134
|
+
preconditions: [graphLoaded(), resolves("module", KINDS.Module)],
|
|
135
|
+
add: [knows("impact", "module")],
|
|
136
|
+
}),
|
|
137
|
+
capability({
|
|
138
|
+
name: "tmct_members", label: "members", question: "the methods/attributes of a class",
|
|
139
|
+
params: [param("class", KINDS.Class, { arg: "class" })],
|
|
140
|
+
preconditions: [graphLoaded(), resolves("class", KINDS.Class)],
|
|
141
|
+
add: [knows("members", "class")],
|
|
142
|
+
}),
|
|
143
|
+
capability({
|
|
144
|
+
name: "tmct_subclasses", label: "subclasses", question: "the subclasses of a class",
|
|
145
|
+
params: [param("class", KINDS.Class, { arg: "class" })],
|
|
146
|
+
preconditions: [graphLoaded(), resolves("class", KINDS.Class)],
|
|
147
|
+
add: [knows("subclasses", "class")],
|
|
148
|
+
}),
|
|
149
|
+
capability({
|
|
150
|
+
name: "tmct_exports", label: "exports", question: "a module's public exports",
|
|
151
|
+
params: [param("module", KINDS.Module)],
|
|
152
|
+
preconditions: [graphLoaded(), resolves("module", KINDS.Module)],
|
|
153
|
+
add: [knows("exports", "module")],
|
|
154
|
+
}),
|
|
155
|
+
capability({
|
|
156
|
+
name: "tmct_callers", label: "callers", question: "functions that call this symbol",
|
|
157
|
+
params: [param("symbol", KINDS.Symbol)],
|
|
158
|
+
preconditions: [graphLoaded(), resolves("symbol", KINDS.Symbol)],
|
|
159
|
+
add: [knows("callers", "symbol")],
|
|
160
|
+
}),
|
|
161
|
+
capability({
|
|
162
|
+
name: "tmct_callees", label: "callees", question: "functions this symbol calls",
|
|
163
|
+
params: [param("symbol", KINDS.Symbol)],
|
|
164
|
+
preconditions: [graphLoaded(), resolves("symbol", KINDS.Symbol)],
|
|
165
|
+
add: [knows("callees", "symbol")],
|
|
166
|
+
}),
|
|
167
|
+
capability({
|
|
168
|
+
name: "tmct_calls", label: "calls", question: "the call edges out of this symbol/module",
|
|
169
|
+
params: [param("symbol", KINDS.Symbol)],
|
|
170
|
+
preconditions: [graphLoaded(), resolves("symbol", KINDS.Symbol)],
|
|
171
|
+
add: [knows("calls", "symbol")],
|
|
172
|
+
}),
|
|
173
|
+
capability({
|
|
174
|
+
name: "tmct_tests_for", label: "tests", question: "the tests covering this symbol",
|
|
175
|
+
params: [param("symbol", KINDS.Symbol)],
|
|
176
|
+
preconditions: [graphLoaded(), resolves("symbol", KINDS.Symbol)],
|
|
177
|
+
add: [knows("tests", "symbol")],
|
|
178
|
+
}),
|
|
179
|
+
capability({
|
|
180
|
+
name: "tmct_untested", label: "untested", question: "symbols with no covering test",
|
|
181
|
+
params: [],
|
|
182
|
+
preconditions: [graphLoaded()],
|
|
183
|
+
add: [knows("untested", null)],
|
|
184
|
+
}),
|
|
185
|
+
capability({
|
|
186
|
+
name: "tmct_history", label: "history", question: "the commit history of this symbol",
|
|
187
|
+
params: [param("symbol", KINDS.Symbol)],
|
|
188
|
+
preconditions: [graphLoaded(), resolves("symbol", KINDS.Symbol)],
|
|
189
|
+
add: [knows("history", "symbol")],
|
|
190
|
+
}),
|
|
191
|
+
capability({
|
|
192
|
+
name: "tmct_cochanges", label: "cochanges", question: "symbols that change together with this one",
|
|
193
|
+
params: [param("symbol", KINDS.Symbol)],
|
|
194
|
+
preconditions: [graphLoaded(), resolves("symbol", KINDS.Symbol)],
|
|
195
|
+
add: [knows("cochanges", "symbol")],
|
|
196
|
+
}),
|
|
197
|
+
capability({
|
|
198
|
+
name: "tmct_architecture", label: "arch", question: "the architecture overview (optional package filter)",
|
|
199
|
+
params: [param("package", KINDS.Package, { required: false })],
|
|
200
|
+
preconditions: [graphLoaded()],
|
|
201
|
+
add: [knows("architecture", "package")],
|
|
202
|
+
}),
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
// A frozen name→capability index (built once).
|
|
206
|
+
const BY_NAME = Object.freeze(
|
|
207
|
+
CAPABILITIES.reduce((m, c) => { m[c.name] = c; return m; }, Object.create(null)),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// ---- closed-world / DEFAULT-DENY --------------------------------------------
|
|
211
|
+
// The registry is a deliberate, DOCUMENTED STRICT SUBSET of the src/server.mjs
|
|
212
|
+
// `dispatchTool` switch. The model is CLOSED-WORLD default-deny: a tool name
|
|
213
|
+
// that is NOT a registered capability is treated as UNKNOWN — the guardrail /
|
|
214
|
+
// AGENTBENCH grader rejects it as a hallucination (`hallucinationsIn` →
|
|
215
|
+
// "unknown-tool"), so a planner/shim that emits an UNREGISTERED tool is an
|
|
216
|
+
// AUTOMATIC FAIL, exactly as if it invented a tool that does not exist. This is
|
|
217
|
+
// the safety posture: only what is declared (with real preconditions) may fire.
|
|
218
|
+
//
|
|
219
|
+
// The following dispatch tools are INTENTIONALLY UNREGISTERED — they emit
|
|
220
|
+
// UNBOUNDED raw output (a source snippet / a whole edit-context bundle), which
|
|
221
|
+
// is the most hallucination-prone surface and NOT a clean STRIPS query with a
|
|
222
|
+
// bounded epistemic effect. Registering them would require modelling
|
|
223
|
+
// output-size + file-read preconditions we have not committed to; until then,
|
|
224
|
+
// default-deny keeps them OUT of the router's provable envelope by design (not
|
|
225
|
+
// by omission). Recorded here so the exclusion is a decision, not an accident:
|
|
226
|
+
export const EXCLUDED_FROM_REGISTRY = Object.freeze({
|
|
227
|
+
tmct_context: "unbounded edit-context bundle (multi-file); needs a size/budget precondition",
|
|
228
|
+
tmct_context_more: "unbounded context continuation; same as tmct_context",
|
|
229
|
+
tmct_snippet: "raw source-file read (reads the filesystem); needs a file-read + span precondition",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/** The full registry as a plain frozen object (facts + index), for callers that
|
|
233
|
+
* want the whole substrate. Prefer the accessors below for lookups. */
|
|
234
|
+
export const REGISTRY = Object.freeze({
|
|
235
|
+
prefixes: PREFIXES,
|
|
236
|
+
vocab: VOCAB,
|
|
237
|
+
kinds: KINDS,
|
|
238
|
+
precond: PRECOND,
|
|
239
|
+
capabilities: CAPABILITIES,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ---- pure accessors ---------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/** All declared capabilities (the operator set). */
|
|
245
|
+
export function capabilities() { return CAPABILITIES; }
|
|
246
|
+
|
|
247
|
+
/** The capability named `n`, or undefined. */
|
|
248
|
+
export function capabilityByName(n) { return BY_NAME[n]; }
|
|
249
|
+
|
|
250
|
+
/** True iff `n` names a declared capability. */
|
|
251
|
+
export function isCapability(n) { return Boolean(BY_NAME[n]); }
|
|
252
|
+
|
|
253
|
+
/** The parameter slots of capability `n` (empty array if unknown/no-arg). */
|
|
254
|
+
export function parametersOf(n) { return BY_NAME[n]?.parameters ?? []; }
|
|
255
|
+
|
|
256
|
+
/** The preconditions of capability `n` (the safety gate the guardrail checks). */
|
|
257
|
+
export function preconditionsOf(n) { return BY_NAME[n]?.preconditions ?? []; }
|
|
258
|
+
|
|
259
|
+
/** The effects of capability `n` — `{ add, del }` (the proof-chain contribution). */
|
|
260
|
+
export function effectsOf(n) { return BY_NAME[n]?.effects ?? { add: [], del: [] }; }
|
|
261
|
+
|
|
262
|
+
/** The set of arg keys capability `n` accepts (for the guardrail's unknown-arg
|
|
263
|
+
* check). Returns a Set of strings. */
|
|
264
|
+
export function argKeysOf(n) {
|
|
265
|
+
return new Set(parametersOf(n).map((p) => p.arg));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The required arg keys of `n` (params with required:true). */
|
|
269
|
+
export function requiredArgsOf(n) {
|
|
270
|
+
return parametersOf(n).filter((p) => p.required).map((p) => p.arg);
|
|
271
|
+
}
|