@polycode-projects/the-mechanical-code-talker 0.7.0 → 0.8.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.
- package/ROADMAP.md +113 -11
- package/bin/tmct.mjs +45 -0
- package/package.json +3 -1
- package/src/ask-vocab.mjs +1 -1
- package/src/ask.mjs +94 -2
- package/src/chat.mjs +86 -19
- 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 +28 -1
- 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/guardrail.mjs +120 -0
- package/src/router/planner.mjs +168 -0
- package/src/router/registry.mjs +271 -0
- package/src/router/resolver.mjs +293 -0
- package/src/server-http.mjs +296 -0
- package/src/syllogise.mjs +0 -0
- package/src/tui/app.mjs +63 -14
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// src/router/resolver.mjs — Stage 1 of the capability router (PLAN_CAPABILITY_ROUTER.md):
|
|
2
|
+
// THE RESOLVER. Turn a request into a SELECTED registry capability with bound
|
|
3
|
+
// arguments, by unification + backward chaining over capabilities-as-facts
|
|
4
|
+
// (a mini-Datalog/SLD step, exactly the "open-condition satisfaction" the plan
|
|
5
|
+
// names). Deterministic, no-LLM, glass-box: every choice is provable.
|
|
6
|
+
//
|
|
7
|
+
// THE CORE is the ask-kind -> capability MAPPING. A request becomes an epistemic
|
|
8
|
+
// GOAL `(knows <topic> ?of)`; each capability's add-effect declares which topic
|
|
9
|
+
// it achieves (registry.mjs `knows(topic, of)`); backward chaining finds the
|
|
10
|
+
// capability whose add-list unifies with the goal and binds ?of to the request's
|
|
11
|
+
// object term. Three fact sources feed the SAME backward-chaining step:
|
|
12
|
+
//
|
|
13
|
+
// 1. THE COMMAND REGISTER (server-http.mjs `selectTool`, the terse verbs
|
|
14
|
+
// "describe X" / "callers X" / "untested") — exact + unambiguous, tried
|
|
15
|
+
// FIRST because a terse command mis-parses through the NL grammar (e.g.
|
|
16
|
+
// "callees Widget.render" keyword-spots to shape:reverse/kind:calls, which
|
|
17
|
+
// would wrongly route to callers). A literal command verb is ground truth.
|
|
18
|
+
// 2. THE NL PARSE (ask.mjs `parseQuery` -> {shape, kind, entityType, object}) —
|
|
19
|
+
// the relational grammar ("which functions call X", "what does X export").
|
|
20
|
+
// NL_INTENTS maps a {shape,kind} to the epistemic TOPIC; backwardChain maps
|
|
21
|
+
// the topic to the capability. This is the Stage-1 deliverable proper.
|
|
22
|
+
// 3. IMPERATIVE INTENT FRAMES (Stage 2, this module's FRAMES table) — curated
|
|
23
|
+
// phrasings the relational grammar does not carry ("blast radius of X",
|
|
24
|
+
// "who calls X", "search for X"): a regex -> {topic, arg}. Same backward
|
|
25
|
+
// chaining (topic -> capability), same resolveObject binding.
|
|
26
|
+
//
|
|
27
|
+
// ENTITY BINDING is DELEGATED to `resolveObject` (ask.mjs — the tiered lemma/
|
|
28
|
+
// fuzzy binding oracle with honest ambiguity). This module NEVER re-implements
|
|
29
|
+
// resolution: the registry's `resolves(param, as)` precondition maps exactly to
|
|
30
|
+
// a resolveObject call, and an ambiguous / no-match term is an HONEST REFUSE,
|
|
31
|
+
// never a guess. On any no-fit -> refuse.
|
|
32
|
+
//
|
|
33
|
+
// Pure-ish: mapParse/mapFrame/backwardChain/commandCapability are pure; resolveOne
|
|
34
|
+
// is async only because it consults ctx.resolve (the graph binding oracle) and
|
|
35
|
+
// ctx.dispatch (executes the grounded call). No network, no Date.now.
|
|
36
|
+
|
|
37
|
+
import { parseQuery } from "../ask.mjs";
|
|
38
|
+
import { selectTool } from "../server-http.mjs";
|
|
39
|
+
import {
|
|
40
|
+
capabilities, capabilityByName, preconditionsOf, effectsOf, PRECOND,
|
|
41
|
+
} from "./registry.mjs";
|
|
42
|
+
import { hallucinationsIn } from "../../agentbench/grade.mjs";
|
|
43
|
+
|
|
44
|
+
// ---- the ask-kind -> epistemic-topic MAPPING (the Stage-1 core) --------------
|
|
45
|
+
// Keyed `${shape}:${kind}` off parseQuery's simple-clause output. The VALUE is
|
|
46
|
+
// the epistemic TOPIC a capability's add-effect must achieve; backwardChain then
|
|
47
|
+
// unifies that topic with the registry (capabilities-as-facts). Every entry's
|
|
48
|
+
// topic MUST be achievable by exactly one registered capability (the bidirectional
|
|
49
|
+
// conformance test proves it). `arg` is the parameter grain the object binds to.
|
|
50
|
+
export const NL_INTENTS = Object.freeze({
|
|
51
|
+
"reverse:calls": { topic: "callers", arg: "symbol" }, // "which fns call X" -> callers of X
|
|
52
|
+
"forward:calls": { topic: "callees", arg: "symbol" }, // "what does X call" -> callees of X
|
|
53
|
+
"reverse:tests": { topic: "tests", arg: "symbol" }, // "which tests cover X"
|
|
54
|
+
"forward:tests": { topic: "tests", arg: "symbol" }, // "what tests X"
|
|
55
|
+
"reverse:inherits": { topic: "subclasses", arg: "class" }, // "which classes extend X"
|
|
56
|
+
"forward:reexports": { topic: "exports", arg: "module" }, // "what does X export"
|
|
57
|
+
"forward:contains": { topic: "members", arg: "class" }, // "what does X contain" -> members of X
|
|
58
|
+
"when:touches": { topic: "history", arg: "symbol" }, // "when did X change" -> commit history
|
|
59
|
+
"forward:cochange": { topic: "cochanges", arg: "symbol" }, // "what changes with X"
|
|
60
|
+
"reverse:cochange": { topic: "cochanges", arg: "symbol" },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ---- ask-vocab RELATION kinds with NO capability — the HONEST ceiling --------
|
|
64
|
+
// Every ask-vocab.mjs RELATIONS key must be either mapped (above) or listed here
|
|
65
|
+
// with a reason (the bidirectional conformance test enforces the partition — no
|
|
66
|
+
// silent gap). These are relations tmct's grammar SPEAKS but the read-only
|
|
67
|
+
// graph-query registry has no operator for: routing them anywhere would be a
|
|
68
|
+
// mis-route, so the resolver REFUSES (never a guess).
|
|
69
|
+
export const UNMAPPED_KINDS = Object.freeze({
|
|
70
|
+
imports: "no importer/imports query tool in the registry (there is no tmct_imports); refusing beats mis-routing to calls",
|
|
71
|
+
uses: "a query-side UNION (imports+calls+callsSymbol) with no single capability; a router that must emit ONE call cannot honour it — refuse",
|
|
72
|
+
defines: "Module->symbol `defines` has no dedicated capability (tmct_members is Class-scoped `contains`); refuse rather than answer a different grain",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ---- capabilities the NL surface cannot reach today (named, not accidental) ---
|
|
76
|
+
// A declared capability with no NL/command/frame path is a ROUTING GAP. The
|
|
77
|
+
// conformance test FAILS on an untagged gap; a genuinely-unreachable cap must be
|
|
78
|
+
// tagged HERE with the Stage it needs, so the ceiling is honest rather than a
|
|
79
|
+
// silent low-completion refuse. (Coordinator reinforcement 2.)
|
|
80
|
+
export const NOT_NL_REACHABLE = Object.freeze({
|
|
81
|
+
tmct_calls: "the raw call-edge dump collides with tmct_callees on every NL phrasing (\"what does X call\" -> callees) and has no command verb; distinguishing it needs a Stage-2 intent frame we have not authored",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ---- imperative intent FRAMES (Stage 2 — fills what the relational grammar and
|
|
85
|
+
// the command register both miss). regex -> { topic, arg | noArg }. `arg` names
|
|
86
|
+
// the parameter grain; the entity is pulled by extractEntity (or, for search, the
|
|
87
|
+
// residual query text). Ordered: first match wins. Every frame's topic is
|
|
88
|
+
// backward-chained to a capability just like an NL intent, so the frame table
|
|
89
|
+
// adds PHRASINGS, never a new routing path. ----
|
|
90
|
+
export const FRAMES = Object.freeze([
|
|
91
|
+
{ re: /\buntested\b|\bwithout\s+(?:a\s+)?tests?\b|\bhas\s+no\s+tests?\b|\bneeds?\s+(?:a\s+)?tests?\b/i, topic: "untested", noArg: true },
|
|
92
|
+
{ re: /\bblast\s*radius\b|\bimpacts?\b|\bimpacted\b|what\s+(?:a\s+)?change.*(?:reach|affect|touch)|what\s+(?:depends?\s+on|dependents?)\b/i, topic: "impact", arg: "module" },
|
|
93
|
+
{ re: /\bcallees?\b|what\s+does\s+\S+\s+call\b/i, topic: "callees", arg: "symbol" },
|
|
94
|
+
{ re: /\bcallers?\b|who\s+calls\b|what\s+calls\b/i, topic: "callers", arg: "symbol" },
|
|
95
|
+
{ re: /\btests?\b.*\b(?:for|cover|covering|of)\b|which\s+tests?\b|covers?\b|covered\b|test\s+coverage\b/i, topic: "tests", arg: "symbol" },
|
|
96
|
+
{ re: /\bcochang|change[- ]coupl/i, topic: "cochanges", arg: "symbol" },
|
|
97
|
+
{ re: /\bexports?\b|\bpublic\s+api\b/i, topic: "exports", arg: "module" },
|
|
98
|
+
{ re: /\bsubclasses?\b|children\s+of\b|\bextends?\b/i, topic: "subclasses", arg: "class" },
|
|
99
|
+
{ re: /\bmembers?\b|\bmethods?\s+of\b|\battributes?\s+of\b/i, topic: "members", arg: "class" },
|
|
100
|
+
{ re: /\bhistory\b|who\s+changed\b|commits?\s+(?:that\s+)?touch/i, topic: "history", arg: "symbol" },
|
|
101
|
+
{ re: /\bsignature\b/i, topic: "signature", arg: "symbol" },
|
|
102
|
+
{ re: /\bdescribe\b|what\s+is\b|tell\s+me\s+about\b|definition\s+of\b/i, topic: "description", arg: "symbol" },
|
|
103
|
+
{ re: /\bsearch\b|\bfind\b|look\s+for\b/i, topic: "matches", arg: "query" },
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
// ---- backward chaining (the SLD/Datalog step) --------------------------------
|
|
107
|
+
|
|
108
|
+
/** Backward-chain a goal `(knows <topic> ?of)` to the registered capability whose
|
|
109
|
+
* add-effect ACHIEVES it. Pure over the registry; returns the capability or null.
|
|
110
|
+
* This is the whole selection primitive — a capability is chosen ONLY because its
|
|
111
|
+
* declared effect unifies with the request's epistemic goal, never by name. */
|
|
112
|
+
export function backwardChain(topic) {
|
|
113
|
+
for (const cap of capabilities()) {
|
|
114
|
+
if (effectsOf(cap.name).add.some((e) => e.topic === topic)) return cap;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The stopword set for the imperative-frame entity extractor (Stage-2 slot
|
|
120
|
+
// filling). Deliberately generous: a wrong pick is caught by the resolveObject
|
|
121
|
+
// miss -> honest refuse, never emitted.
|
|
122
|
+
const STOP = new Set([
|
|
123
|
+
"what", "whats", "which", "who", "whom", "does", "do", "did", "is", "are", "the", "a", "an",
|
|
124
|
+
"of", "for", "to", "in", "on", "by", "me", "us", "tell", "about", "show", "list", "give", "get",
|
|
125
|
+
"assess", "check", "then", "and", "or", "instead", "if", "end", "up", "its", "it", "this", "that",
|
|
126
|
+
"call", "calls", "called", "caller", "callers", "callee", "callees", "test", "tests", "tested",
|
|
127
|
+
"cover", "covers", "covering", "describe", "description", "define", "definition", "impact",
|
|
128
|
+
"member", "members", "method", "methods", "attribute", "attributes", "subclass", "subclasses",
|
|
129
|
+
"export", "exports", "history", "commit", "commits", "signature", "search", "find", "look",
|
|
130
|
+
"module", "modules", "class", "classes", "function", "functions", "symbol", "symbols",
|
|
131
|
+
"untested", "blast", "radius", "change", "changes", "changing", "reach", "reaches", "affect", "affects",
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
/** Pull one entity token from a request (imperative-frame slot-filling). Prefer a
|
|
135
|
+
* path/dotted/CamelCase token, else the last non-stopword identifier. "" if none. */
|
|
136
|
+
export function extractEntity(request) {
|
|
137
|
+
const tokens = String(request).match(/[A-Za-z_][A-Za-z0-9_./-]*/g) || [];
|
|
138
|
+
const pool = tokens.filter((t) => !STOP.has(t.toLowerCase()));
|
|
139
|
+
const strong = pool.filter((t) => /[./]/.test(t) || /^[A-Z][a-z]/.test(t) || /\.[a-z]+$/.test(t));
|
|
140
|
+
const pick = strong.length ? strong : pool;
|
|
141
|
+
return pick.length ? pick[pick.length - 1] : "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The residual search query: strip the leading search/find verb + a filler "for". */
|
|
145
|
+
function searchQuery(request) {
|
|
146
|
+
return String(request).replace(/^\s*(?:search|find|look\s+for|look)\s+(?:for\s+)?/i, "").trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- request -> capability + raw term (PURE, no binding yet) -----------------
|
|
150
|
+
|
|
151
|
+
/** Map a parseQuery simple-clause parse to a capability selection, or a refusal,
|
|
152
|
+
* or null (not an NL shape this resolver routes). Returns one of:
|
|
153
|
+
* { name, arg, term, topic, source:"nl", why:[...] } — a selection
|
|
154
|
+
* { refuse:true, reason } — an honest no-fit
|
|
155
|
+
* null — parse absent / not simple
|
|
156
|
+
* Backward-chains topic -> capability so selection is provable, never by name. */
|
|
157
|
+
export function mapParse(parse) {
|
|
158
|
+
if (!parse || parse.node || !parse.shape) return null; // compositional/absent -> not here
|
|
159
|
+
const { shape, kind } = parse;
|
|
160
|
+
// yes/no + locational/vocabulary shapes have no read-only query operator
|
|
161
|
+
if (shape === "ask") return { refuse: true, reason: "a yes/no `does X <verb> Y` question has no capability that emits it (the registry answers WHAT, not WHETHER)" };
|
|
162
|
+
if (shape === "where" || shape === "meta" || shape === "mentions") {
|
|
163
|
+
return { refuse: true, reason: `the "${shape}" shape (location/vocabulary/prose) has no read-only graph-query capability` };
|
|
164
|
+
}
|
|
165
|
+
const intent = NL_INTENTS[`${shape}:${kind}`];
|
|
166
|
+
if (!intent) {
|
|
167
|
+
const reason = UNMAPPED_KINDS[kind] || `no capability maps the ${shape}/${kind} intent`;
|
|
168
|
+
return { refuse: true, reason };
|
|
169
|
+
}
|
|
170
|
+
const cap = backwardChain(intent.topic);
|
|
171
|
+
if (!cap) return { refuse: true, reason: `backward chaining found no capability achieving (knows ${intent.topic})` };
|
|
172
|
+
return {
|
|
173
|
+
name: cap.name, arg: intent.arg, term: String(parse.object || "").trim(), topic: intent.topic, source: "nl",
|
|
174
|
+
why: [`parse ${shape}/${kind} => goal (knows ${intent.topic} ?${intent.arg})`, `backward-chain => ${cap.name} (its add-effect achieves ${intent.topic})`],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Match an imperative FRAME. Returns { name, arg|noArg, term, topic, source:"frame", why }
|
|
179
|
+
* or null. Backward-chains the frame's topic to a capability. */
|
|
180
|
+
export function mapFrame(request) {
|
|
181
|
+
for (const f of FRAMES) {
|
|
182
|
+
if (!f.re.test(request)) continue;
|
|
183
|
+
const cap = backwardChain(f.topic);
|
|
184
|
+
if (!cap) continue;
|
|
185
|
+
if (f.noArg) return { name: cap.name, noArg: true, topic: f.topic, source: "frame", why: [`imperative frame => goal (knows ${f.topic})`, `backward-chain => ${cap.name}`] };
|
|
186
|
+
const term = f.arg === "query" ? searchQuery(request) : extractEntity(request);
|
|
187
|
+
return { name: cap.name, arg: f.arg, term, topic: f.topic, source: "frame", why: [`imperative frame => goal (knows ${f.topic} ?${f.arg})`, `backward-chain => ${cap.name}`] };
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The command register (server-http.mjs selectTool) filtered to the registry.
|
|
193
|
+
* Returns { name, input, source:"command", why } or null. selectTool binds the
|
|
194
|
+
* arg from the terse command; we re-clean a search query (its raw join keeps a
|
|
195
|
+
* stray "for"). Never reaches outside the declared set. */
|
|
196
|
+
export function commandCapability(request, declaredNames) {
|
|
197
|
+
const sel = selectTool(request, new Set(declaredNames));
|
|
198
|
+
if (!sel || !capabilityByName(sel.name)) return null;
|
|
199
|
+
const input = { ...sel.input };
|
|
200
|
+
if (sel.name === "tmct_search" && input.query != null) input.query = searchQuery(request) || String(input.query);
|
|
201
|
+
return { name: sel.name, input, source: "command", why: [`command register: "${String(request).trim().split(/\s+/)[0]}" => ${sel.name}`] };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---- the full single-call resolver (async — binds + grounds) -----------------
|
|
205
|
+
|
|
206
|
+
/** Build the glass-box proof chain for a grounded single call: its preconditions
|
|
207
|
+
* (graphLoaded + resolves(param) with the BOUND value + any-present) then the
|
|
208
|
+
* epistemic add-effect. Dispatch has SUCCEEDED, so `resolves` steps are ok. */
|
|
209
|
+
export function proofFor(name, input) {
|
|
210
|
+
const steps = [];
|
|
211
|
+
for (const pre of preconditionsOf(name)) {
|
|
212
|
+
if (pre.pred === PRECOND.graphLoaded) steps.push({ step: "precondition", pred: pre.pred, ok: true });
|
|
213
|
+
else if (pre.pred === PRECOND.resolves) steps.push({ step: "precondition", pred: pre.pred, param: pre.param, value: input[pre.param] ?? null, ok: true });
|
|
214
|
+
else if (pre.pred === PRECOND.anyPresent) steps.push({ step: "precondition", pred: pre.pred, params: pre.params, ok: pre.params.some((k) => input[k]) });
|
|
215
|
+
}
|
|
216
|
+
for (const eff of effectsOf(name).add) steps.push({ step: "effect", pred: eff.pred, topic: eff.topic, of: eff.of });
|
|
217
|
+
return steps;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const REFUSE = (why) => ({ selected: null, refused: true, reason: why });
|
|
221
|
+
|
|
222
|
+
/** Select a capability for a request and BIND its arguments — the full resolver.
|
|
223
|
+
* Order: command register -> NL parse -> imperative frame. On a bound selection
|
|
224
|
+
* it delegates entity binding to ctx.resolve (resolveObject) and, unless
|
|
225
|
+
* `execute:false`, grounds it via ctx.dispatch. Returns
|
|
226
|
+
* { selected:{name,input}, proof, why, resolved, observed? } — a grounded call
|
|
227
|
+
* { selected:null, refused:true, reason } — an honest refusal
|
|
228
|
+
* NEVER emits an ungrounded / ambiguous / undeclared call. */
|
|
229
|
+
export async function resolveOne(request, declaredNames, ctx, { execute = true } = {}) {
|
|
230
|
+
const declared = new Set(declaredNames);
|
|
231
|
+
|
|
232
|
+
// 1. command register (exact) 2. NL parse 3. imperative frame. An NL-parse
|
|
233
|
+
// REFUSAL is NOT terminal: a shape with no relational operator ("what is the
|
|
234
|
+
// impact of X", "list what covers X") can still be an imperative FRAME hit,
|
|
235
|
+
// so we fall through and only surface the NL reason if the frame misses too.
|
|
236
|
+
let pick = commandCapability(request, declared);
|
|
237
|
+
let nlRefuse = null;
|
|
238
|
+
if (!pick) {
|
|
239
|
+
const mapped = mapParse(parseQuery(request));
|
|
240
|
+
if (mapped && !mapped.refuse) pick = mapped;
|
|
241
|
+
else if (mapped && mapped.refuse) nlRefuse = mapped.reason;
|
|
242
|
+
}
|
|
243
|
+
if (!pick) pick = mapFrame(request);
|
|
244
|
+
if (!pick) return REFUSE(nlRefuse || "no command, NL parse, or imperative frame selects a capability");
|
|
245
|
+
let why = pick.why ?? [];
|
|
246
|
+
if (!declared.has(pick.name)) return REFUSE(`selected ${pick.name} but it is not in the declared toolset`);
|
|
247
|
+
|
|
248
|
+
// build the bound input. A command pick already carries a bound input; an NL /
|
|
249
|
+
// frame pick carries a raw term that we BIND via resolveObject (the oracle).
|
|
250
|
+
let input = pick.input ? { ...pick.input } : {};
|
|
251
|
+
let resolved = null;
|
|
252
|
+
if (!pick.input && !pick.noArg) {
|
|
253
|
+
const term = String(pick.term || "").trim();
|
|
254
|
+
if (!term) return REFUSE(`the ${pick.topic} intent named no entity to bind`);
|
|
255
|
+
// DELEGATE binding to resolveObject — the resolves(param,as) precondition.
|
|
256
|
+
const r = ctx.resolve ? ctx.resolve(term) : { match: { label: term }, ambiguous: false };
|
|
257
|
+
if (!r || !r.match) return REFUSE(`"${term}" does not resolve to any graph entity (honest miss)`);
|
|
258
|
+
if (r.ambiguous) return REFUSE(`"${term}" is ambiguous (${[r.match, ...(r.candidates || [])].slice(0, 4).map((m) => m.label).join(", ")}) — narrow it`);
|
|
259
|
+
resolved = r.match;
|
|
260
|
+
input = { [pick.arg]: r.match.label };
|
|
261
|
+
why = [...why, `resolveObject: "${term}" => ${r.match.label} (${r.match.class || "?"}, tier ${r.tier})`];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const call = { name: pick.name, input };
|
|
265
|
+
// the same zero-hallucination gate the grader enforces — self-check before emit.
|
|
266
|
+
const problems = hallucinationsIn(call, [...declared]);
|
|
267
|
+
if (problems.length) return REFUSE(`bound call did not validate: ${problems.map((p) => p.reason).join(",")}`);
|
|
268
|
+
|
|
269
|
+
// ground it: a ToolError (unresolvable entity) is an honest miss -> refuse.
|
|
270
|
+
if (execute && ctx.dispatch) {
|
|
271
|
+
const res = await ctx.dispatch(pick.name, input);
|
|
272
|
+
if (!res.ok) return REFUSE(`unresolvable at dispatch: ${res.error}`);
|
|
273
|
+
return { selected: call, proof: proofFor(pick.name, input), why, resolved: res.resolved ?? resolved, observed: String(res.text ?? "").slice(0, 240) };
|
|
274
|
+
}
|
|
275
|
+
return { selected: call, proof: proofFor(pick.name, input), why, resolved };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---- reachability (used by the bidirectional conformance test + docs) ---------
|
|
279
|
+
|
|
280
|
+
/** The epistemic topics some NL intent or imperative frame can reach. */
|
|
281
|
+
export function nlReachableTopics() {
|
|
282
|
+
const topics = new Set();
|
|
283
|
+
for (const v of Object.values(NL_INTENTS)) topics.add(v.topic);
|
|
284
|
+
for (const f of FRAMES) topics.add(f.topic);
|
|
285
|
+
return topics;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Every capability an NL intent or frame can reach (by name), via backwardChain. */
|
|
289
|
+
export function reachableCapabilityNames() {
|
|
290
|
+
const names = new Set();
|
|
291
|
+
for (const topic of nlReachableTopics()) { const c = backwardChain(topic); if (c) names.add(c.name); }
|
|
292
|
+
return names;
|
|
293
|
+
}
|