@polycode-projects/the-mechanical-code-talker 2.7.4 → 2.7.7
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/package.json +1 -1
- package/src/adapters/memory/core.mjs +1 -0
- package/src/domain/router/drive.mjs +43 -17
- package/src/domain/router/planner.mjs +54 -4
- package/src/domain/router/resolver.mjs +88 -14
- package/src/services/adventure.mjs +6 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +94 -85
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
|
@@ -712,6 +712,7 @@ function sourceIdFor(desc) {
|
|
|
712
712
|
case "teach": return { id: desc.sessionId ? `${TEACH_SOURCE_ID}:${desc.sessionId}` : TEACH_SOURCE_ID, type: "teach" };
|
|
713
713
|
case "provider": return { id: `src:provider:${desc.name}`, type: "provider" };
|
|
714
714
|
case "corpus": return { id: `src:corpus:${desc.name}`, type: "corpus" };
|
|
715
|
+
case "corpusWeak": return { id: `src:corpus-weak:${desc.name}`, type: "corpusWeak" };
|
|
715
716
|
// One Source per pack article (the @revid stays in the article segment),
|
|
716
717
|
// so two facts from the same article corroborate nothing extra.
|
|
717
718
|
case "reference": return { id: `src:reference:${desc.pack}:${desc.article}`, type: "reference" };
|
|
@@ -6,20 +6,29 @@
|
|
|
6
6
|
// build a { dispatch, resolve, graph } context against the repo's actual code
|
|
7
7
|
// graph, then run a request through resolver -> planner -> goal-reasoner.
|
|
8
8
|
//
|
|
9
|
-
// Single-shot -> the resolver. A
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// (
|
|
19
|
-
//
|
|
20
|
-
//
|
|
9
|
+
// Single-shot -> the resolver. A single-shot bound argument that TIES between
|
|
10
|
+
// two same-tier candidates (a source module and its own test module, the
|
|
11
|
+
// graph's own `tests` edge — never a raw score tie alone) never picks one
|
|
12
|
+
// arbitrarily: the resolver dispatches a read per tied candidate and refuses
|
|
13
|
+
// with `candidateResults` carrying both, the enumerate-or-refuse discipline
|
|
14
|
+
// the chat surface's own ambiguous-entity refusal already uses. A compound
|
|
15
|
+
// "... then ..."/"if ..."/"of the ..., which are ..." request -> the planner,
|
|
16
|
+
// HTN-decomposed into an ordered call sequence with a POP causal-link proof
|
|
17
|
+
// chain, then folded into ONE composed answer via the same set-algebra the HTN
|
|
18
|
+
// method names (relative-filter -> intersect; conditional -> fallback/guard).
|
|
19
|
+
// A "<primary>, and if <it came up empty>, <fallback> instead" request
|
|
20
|
+
// decomposes to the RECOVER method instead: the primary dispatches, its own
|
|
21
|
+
// structured result is OBSERVED, and the fallback dispatches only when that
|
|
22
|
+
// observation is empty (`recovered:true`) — never both, and never neither. A
|
|
23
|
+
// refused WORLD goal ("make every disk rest on peg-c") is tried against the
|
|
24
|
+
// taught capability records next (runTaughtPlan — selected by backward
|
|
25
|
+
// chaining, grounded by pure simulation, never dispatched). A request none of
|
|
26
|
+
// those ground escalates to the closed-world goal-reasoner — a maintenance-
|
|
27
|
+
// invariant deduction (coverage-gap / cochange-risk), never a keyword guess.
|
|
28
|
+
// Anything no stage grounds is an honest refuse, the same "grounded or an
|
|
29
|
+
// honest miss" contract as every other tmct answer path.
|
|
21
30
|
|
|
22
|
-
import { resolveOne, backwardChainWorld } from "./resolver.mjs";
|
|
31
|
+
import { resolveOne, backwardChainWorld, resolveMemoryTerm } from "./resolver.mjs";
|
|
23
32
|
import { plan, isMultiStep, decompose, MAX_STEPS } from "./planner.mjs";
|
|
24
33
|
import { goalReason } from "./goal-reasoner.mjs";
|
|
25
34
|
import { capabilities } from "./registry.mjs";
|
|
@@ -136,7 +145,12 @@ export async function runResolverPlan(request, tools, ctx) {
|
|
|
136
145
|
|
|
137
146
|
const r = await resolveOne(request, tools, ctx, { execute: true });
|
|
138
147
|
if (r.refused) {
|
|
139
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
calls: [], refused: true, terminated: true, proof: [], driver: ROUTER_DRIVER, why: r.reason,
|
|
150
|
+
// the tied-candidate composer's answer: one dispatched read per tied
|
|
151
|
+
// candidate, riding the refusal rather than an arbitrary pick.
|
|
152
|
+
...(r.candidateCalls ? { candidateResults: r.candidateCalls } : {}),
|
|
153
|
+
};
|
|
140
154
|
}
|
|
141
155
|
return {
|
|
142
156
|
calls: [r.selected], refused: false, terminated: true, proof: r.proof,
|
|
@@ -232,10 +246,17 @@ export async function runTaughtPlan(request, tools, ctx) {
|
|
|
232
246
|
* agentbench's driver-resolver.mjs + driver-goal.mjs composition, with no
|
|
233
247
|
* agentbench/ dependency (agentbench/ is dev-only, never shipped). Returns a
|
|
234
248
|
* loopResult:
|
|
235
|
-
* `{ calls, refused, terminated, proof, why, driver, composed?, observed? }`.
|
|
249
|
+
* `{ calls, refused, terminated, proof, why, driver, composed?, observed?, candidateResults? }`.
|
|
250
|
+
*
|
|
251
|
+
* A C1 refusal that already carries `candidateResults` (the tied-candidate
|
|
252
|
+
* composer's enumerate-or-refuse answer) is TERMINAL — it stands as-is rather
|
|
253
|
+
* than escalating further, the same way a grounded C1 answer stands. Escalating
|
|
254
|
+
* it would silently trade a complete "both tied readings, dispatched" answer
|
|
255
|
+
* for whatever the taught/goal lanes make of the same refusal (typically a
|
|
256
|
+
* plainer refuse with no candidates at all). */
|
|
236
257
|
export async function runCapabilityPlan(request, tools, ctx) {
|
|
237
258
|
const c1 = await runResolverPlan(request, tools, ctx);
|
|
238
|
-
if (!c1.refused) return c1;
|
|
259
|
+
if (!c1.refused || c1.candidateResults) return c1;
|
|
239
260
|
const taught = await runTaughtPlan(request, tools, ctx);
|
|
240
261
|
if (taught) return taught.refused ? { ...taught, c1Why: c1.why } : taught;
|
|
241
262
|
const c2 = await goalReason(request, tools, ctx, { driver: GOAL_DRIVER });
|
|
@@ -266,7 +287,11 @@ export async function runCapabilityPlan(request, tools, ctx) {
|
|
|
266
287
|
* an already-registered name is skipped) and runTaughtPlan simulates over the
|
|
267
288
|
* same store, re-reading it per request via ctx.readTaughtStore. The new
|
|
268
289
|
* registrations' unregister disposers ride the ctx as `ctx.disposers`; the
|
|
269
|
-
* caller runs them when the ctx is done.
|
|
290
|
+
* caller runs them when the ctx is done. The same `memoryDir` also opens
|
|
291
|
+
* `ctx.resolveMemoryTerm` — resolveOne's binding oracle for a memoryTerm slot
|
|
292
|
+
* (tmct_related's `term`), re-reading the store's fact rows per request
|
|
293
|
+
* through resolveMemoryTerm (resolver.mjs), the memory-graph sibling of
|
|
294
|
+
* `resolve` above. */
|
|
270
295
|
export async function buildCapabilityPlanCtx({
|
|
271
296
|
config, source, tel = null, graph = null, memoryDir = null,
|
|
272
297
|
dispatchTool, isToolError = () => false, selectTool = null,
|
|
@@ -294,6 +319,7 @@ export async function buildCapabilityPlanCtx({
|
|
|
294
319
|
const memory = await loadMemory(memoryDir);
|
|
295
320
|
return { factRows: readFactRows(memory), ruleRows: readRuleRows(memory) };
|
|
296
321
|
};
|
|
322
|
+
ctx.resolveMemoryTerm = async (term) => resolveMemoryTerm(readFactRows(await loadMemory(memoryDir)), term);
|
|
297
323
|
ctx.disposers = registerTaughtActions(readRuleRows(await loadMemory(memoryDir)));
|
|
298
324
|
}
|
|
299
325
|
return ctx;
|
|
@@ -12,6 +12,13 @@ export const MAX_STEPS = 8;
|
|
|
12
12
|
|
|
13
13
|
const PRONOUN_RE = /\b(?:it|its|them|those|these|that|their)\b/i;
|
|
14
14
|
|
|
15
|
+
// The RECOVER method's check clause must itself name an EMPTY outcome (none/
|
|
16
|
+
// nothing/no/not any/empty) — a closed, curated cue list, not a general "any if
|
|
17
|
+
// clause is a guard" reading. This is what keeps METHOD 1b narrow: "X, and if Y,
|
|
18
|
+
// Z instead" without an emptiness cue in Y falls through to plain sequencing
|
|
19
|
+
// unchanged, exactly as it did before this method existed.
|
|
20
|
+
const RECOVER_EMPTY_CUE_RE = /\bnone\b|\bnothing\b|\bno\b|\bnot\s+any\b|\bempty\b/i;
|
|
21
|
+
|
|
15
22
|
/** HTN decomposition — turn a request into an ORDERED list of leaf sub-goals.
|
|
16
23
|
* Returns { method, segments:[{ text, role, thread }] }:
|
|
17
24
|
* - role "check" — a conditional antecedent (a test whose call still emits)
|
|
@@ -34,6 +41,25 @@ export function decompose(request) {
|
|
|
34
41
|
};
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
// METHOD 1b — the RECOVER recipe: "<primary>, and if <the primary came up
|
|
45
|
+
// empty>, <fallback> [instead]". Unlike METHOD 1 (which always dispatches
|
|
46
|
+
// both sides and folds the answer), the check clause here names no separate
|
|
47
|
+
// call at all — it is read as an emptiness GUARD on the primary's own
|
|
48
|
+
// result, observed at execution rather than dispatched. Requires an
|
|
49
|
+
// emptiness cue in the check clause, so an ordinary "X, and if Y, Z instead"
|
|
50
|
+
// whose Y doesn't name an empty outcome keeps falling through to plain
|
|
51
|
+
// sequencing (METHOD 4), unchanged from before this method existed.
|
|
52
|
+
const recover = raw.match(/^(.+?),\s*(?:and\s+)?if\s+(.+?),\s*(.+?)(?:\s+instead)?$/i);
|
|
53
|
+
if (recover && RECOVER_EMPTY_CUE_RE.test(recover[2])) {
|
|
54
|
+
return {
|
|
55
|
+
method: "recover",
|
|
56
|
+
segments: [
|
|
57
|
+
{ text: recover[1].trim(), role: "action", thread: false },
|
|
58
|
+
{ text: recover[3].trim(), role: "action", thread: PRONOUN_RE.test(recover[3]) },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
// METHOD 2 — the RELATIVE-FILTER recipe: "of the <set> <rel> X, which are <Y>".
|
|
38
64
|
// Decomposes to [produce the <set> (the <rel> over X), filter it by <Y>].
|
|
39
65
|
const rel = raw.match(/^of\s+the\s+(.+?),\s*which\s+(?:are\s+)?(.+?)$/i);
|
|
@@ -101,11 +127,15 @@ function rewriteCheck(text, lastEntity) {
|
|
|
101
127
|
const refuse = (why, driver) => ({ calls: [], refused: true, terminated: true, proof: [], driver, why });
|
|
102
128
|
|
|
103
129
|
/** Plan + execute a multi-step request. Returns a loopResult
|
|
104
|
-
* { calls, refused, terminated, proof, why, driver, observed }
|
|
130
|
+
* { calls, refused, terminated, proof, why, driver, observed, recovered? }
|
|
105
131
|
* with a POP causal-link proof chain. Each step is monitored; a failed sub-goal stops the
|
|
106
|
-
* plan honestly. Bounded by MAX_STEPS.
|
|
132
|
+
* plan honestly. Bounded by MAX_STEPS. A `recover` method OBSERVES its primary step's
|
|
133
|
+
* own structured result before deciding the fallback: `recovered:true` marks a plan
|
|
134
|
+
* whose fallback fired because the primary came up empty; a non-empty primary stops the
|
|
135
|
+
* plan after just that one call (the fallback is never dispatched) and carries no
|
|
136
|
+
* `recovered` key at all.
|
|
107
137
|
*
|
|
108
|
-
* ctx: { dispatch(name,input)->{ok,text,resolved?}, resolve(term)->resolveObject } */
|
|
138
|
+
* ctx: { dispatch(name,input)->{ok,text,resolved?,result?}, resolve(term)->resolveObject } */
|
|
109
139
|
export async function plan(request, declaredNames, ctx, { driver = "resolver-0.8.0" } = {}) {
|
|
110
140
|
const { method, segments } = decompose(request);
|
|
111
141
|
if (segments.length > MAX_STEPS) {
|
|
@@ -117,6 +147,7 @@ export async function plan(request, declaredNames, ctx, { driver = "resolver-0.8
|
|
|
117
147
|
const why = [`HTN method: ${method} — ${segments.length} sub-goal(s)`];
|
|
118
148
|
let lastEntity = null; // the most-recent bound entity label (for anaphora threading)
|
|
119
149
|
let steps = 0;
|
|
150
|
+
let recovered = false;
|
|
120
151
|
|
|
121
152
|
for (let i = 0; i < segments.length; i += 1) {
|
|
122
153
|
if (steps >= MAX_STEPS) return refuse("step budget exhausted mid-plan — escalate", driver);
|
|
@@ -127,7 +158,10 @@ export async function plan(request, declaredNames, ctx, { driver = "resolver-0.8
|
|
|
127
158
|
|
|
128
159
|
const r = await resolveOne(text, declaredNames, ctx, { execute: true });
|
|
129
160
|
if (r.refused) {
|
|
130
|
-
return
|
|
161
|
+
return {
|
|
162
|
+
...refuse(`sub-goal ${i + 1} ("${text}") did not resolve: ${r.reason}`, driver),
|
|
163
|
+
...(r.candidateCalls ? { candidateResults: r.candidateCalls } : {}),
|
|
164
|
+
};
|
|
131
165
|
}
|
|
132
166
|
|
|
133
167
|
calls.push(r.selected);
|
|
@@ -140,6 +174,21 @@ export async function plan(request, declaredNames, ctx, { driver = "resolver-0.8
|
|
|
140
174
|
|
|
141
175
|
if (r.resolved?.label) lastEntity = r.resolved.label;
|
|
142
176
|
why.push(...(r.why || []).map((w) => `[${i + 1}] ${w}`));
|
|
177
|
+
|
|
178
|
+
// RECOVER's guard: OBSERVE the primary's own structured result (a fresh,
|
|
179
|
+
// read-only re-dispatch — the same idiom composeResult uses to fold a
|
|
180
|
+
// threaded plan) before deciding the fallback. A non-empty primary already
|
|
181
|
+
// answered the request, so the loop stops here — the fallback is never
|
|
182
|
+
// dispatched, closing the bug this method exists to fix (the primary used
|
|
183
|
+
// to double-emit because the guard was never actually observed). An empty
|
|
184
|
+
// primary marks the plan `recovered` and lets the fallback segment run.
|
|
185
|
+
if (method === "recover" && i === 0) {
|
|
186
|
+
const res = await ctx.dispatch(r.selected.name, r.selected.input || {});
|
|
187
|
+
const primaryEmpty = res.ok && Array.isArray(res.result) && res.result.length === 0;
|
|
188
|
+
why.push(`[guard] observed ${r.selected.name} => ${primaryEmpty ? "empty — recovering with the fallback" : "non-empty — the primary already answers this; no fallback dispatched"}`);
|
|
189
|
+
if (!primaryEmpty) break;
|
|
190
|
+
recovered = true;
|
|
191
|
+
}
|
|
143
192
|
}
|
|
144
193
|
|
|
145
194
|
return {
|
|
@@ -149,6 +198,7 @@ export async function plan(request, declaredNames, ctx, { driver = "resolver-0.8
|
|
|
149
198
|
proof,
|
|
150
199
|
driver,
|
|
151
200
|
why,
|
|
201
|
+
...(recovered ? { recovered: true } : {}),
|
|
152
202
|
observed: `plan(${method}): ${calls.map((c) => c.name).join(" -> ")}`,
|
|
153
203
|
};
|
|
154
204
|
}
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { parseQuery } from "../ask.mjs";
|
|
18
18
|
import { SUPERLATIVE_EXTREMES } from "../ask-vocab.mjs";
|
|
19
|
+
import { buildSkosConceptView } from "../skos-view.mjs";
|
|
20
|
+
import { edgesOfKind } from "../codegraph.mjs";
|
|
19
21
|
import {
|
|
20
22
|
capabilities, capabilityByName, preconditionsOf, effectsOf, PRECOND,
|
|
21
23
|
} from "./registry.mjs";
|
|
@@ -56,10 +58,9 @@ export const UNMAPPED_KINDS = Object.freeze({
|
|
|
56
58
|
|
|
57
59
|
// ---- capabilities the NL surface cannot reach today (named, not accidental) ---
|
|
58
60
|
// A declared capability with no NL/command/frame path is a routing gap and must be tagged
|
|
59
|
-
// here with the reason.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
61
|
+
// here with the reason. Empty today: the synonym/related FRAME below reaches tmct_related
|
|
62
|
+
// via resolveMemoryTerm, the memory-graph sibling of resolveObject's code-graph binding.
|
|
63
|
+
export const NOT_NL_REACHABLE = Object.freeze({});
|
|
63
64
|
|
|
64
65
|
// ---- imperative intent FRAMES (fills what the relational grammar and command register
|
|
65
66
|
// both miss). regex -> { topic, arg | noArg }. Ordered: first match wins.
|
|
@@ -81,6 +82,12 @@ export const FRAMES = Object.freeze([
|
|
|
81
82
|
{ re: /\bmembers?\b|\bmethods?\s+of\b|\battributes?\s+of\b/i, topic: "members", arg: "class" },
|
|
82
83
|
{ re: /\bhistory\b|who\s+changed\b|commits?\s+(?:that\s+)?touch/i, topic: "history", arg: "symbol" },
|
|
83
84
|
{ re: /\bsignature\b/i, topic: "signature", arg: "symbol" },
|
|
85
|
+
// tmct_related: the SKOS synonym/related-concept surface over the memory graph
|
|
86
|
+
// — "another word for X" / "a synonym for X" / "synonyms of X" / "what's
|
|
87
|
+
// related to X". `arg: "term"` is the one memory-graph-bound slot in this
|
|
88
|
+
// table (see resolveMemoryTerm below); every other frame's arg binds against
|
|
89
|
+
// the code graph via ctx.resolve.
|
|
90
|
+
{ re: /\bsynonyms?\b|\banother\s+word\s+for\b|\brelated\s+(?:words?|concepts?|to)\b/i, topic: "related", arg: "term" },
|
|
84
91
|
{ re: /\bdescribe\b|\bexplain\b|what\s+is\b|tell\s+me\s+about\b|definition\s+of\b/i, topic: "description", arg: "symbol" },
|
|
85
92
|
{ re: /\bsearch\b|\bfind\b|look\s+for\b/i, topic: "matches", arg: "query" },
|
|
86
93
|
]);
|
|
@@ -122,6 +129,7 @@ const STOP = new Set([
|
|
|
122
129
|
"module", "modules", "class", "classes", "function", "functions", "symbol", "symbols",
|
|
123
130
|
"untested", "blast", "radius", "change", "changes", "changing", "reach", "reaches", "affect", "affects",
|
|
124
131
|
"explain", "edge", "edges", "graph", "outgoing", "site", "sites", "invoke", "invokes", "run", "runs", "execute", "executes",
|
|
132
|
+
"word", "words", "another", "synonym", "synonyms", "related", "relate", "relates", "like", "concept", "concepts",
|
|
125
133
|
]);
|
|
126
134
|
|
|
127
135
|
/** Pull one entity token from a request (imperative-frame slot-filling). Prefer a
|
|
@@ -178,7 +186,14 @@ export function mapFrame(request) {
|
|
|
178
186
|
if (!cap) continue;
|
|
179
187
|
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}`] };
|
|
180
188
|
const term = f.arg === "query" ? searchQuery(request) : extractEntity(request);
|
|
181
|
-
|
|
189
|
+
// memoryTerm: this slot binds against the memory graph's SKOS concept view
|
|
190
|
+
// (resolveMemoryTerm below), not the code graph resolveObject resolves
|
|
191
|
+
// every other frame's arg against — "term" is the one param kind this is
|
|
192
|
+
// true for (tmct_related's own memory-facts-gated param).
|
|
193
|
+
return {
|
|
194
|
+
name: cap.name, arg: f.arg, term, topic: f.topic, source: "frame", memoryTerm: f.arg === "term",
|
|
195
|
+
why: [`imperative frame => goal (knows ${f.topic} ?${f.arg})`, `backward-chain => ${cap.name}`],
|
|
196
|
+
};
|
|
182
197
|
}
|
|
183
198
|
return null;
|
|
184
199
|
}
|
|
@@ -198,16 +213,37 @@ export function commandCapability(request, declaredNames, selectTool) {
|
|
|
198
213
|
return { name: sel.name, input, source: "command", why: [`command register: "${String(request).trim().split(/\s+/)[0]}" => ${sel.name}`] };
|
|
199
214
|
}
|
|
200
215
|
|
|
216
|
+
/** Resolve a term against the memory graph's SKOS concept view
|
|
217
|
+
* (skos-view.mjs's buildSkosConceptView) — the memory-graph sibling of
|
|
218
|
+
* resolveObject, for a param whose precondition is memory-facts rather than
|
|
219
|
+
* a code-graph `resolves`. Same `{ match, ambiguous }` shape resolveObject
|
|
220
|
+
* returns, so resolveOne's generic binding step treats both the same way:
|
|
221
|
+
* `match.label` is the RAW queried term (not the concept's canonicalised
|
|
222
|
+
* prefLabel), because tmct_related's own lookup (relatedForTerm) re-derives
|
|
223
|
+
* the concept from whatever term it's given — the bound call should carry
|
|
224
|
+
* what the user actually asked about. No code-graph fallback: a term the
|
|
225
|
+
* store holds no synonym/related facts for is an honest miss, never a guess.
|
|
226
|
+
* `rows` is a loadMemory+readFactRows payload — the same trust-bearing rows
|
|
227
|
+
* skosRelatedAnswer (chat.mjs) and tmct_related (the tool handler) read. */
|
|
228
|
+
export function resolveMemoryTerm(rows, term) {
|
|
229
|
+
const t = String(term || "").trim();
|
|
230
|
+
if (!t) return { match: null };
|
|
231
|
+
const view = buildSkosConceptView(rows);
|
|
232
|
+
if (!view.conceptIdForTerm(t)) return { match: null };
|
|
233
|
+
return { match: { label: t, class: "skos:Concept" }, ambiguous: false, tier: "memory-concept" };
|
|
234
|
+
}
|
|
235
|
+
|
|
201
236
|
// ---- the full single-call resolver (async — binds + grounds) -----------------
|
|
202
237
|
|
|
203
238
|
/** Build the glass-box proof chain for a grounded single call: its preconditions then the
|
|
204
|
-
* epistemic add-effect. Dispatch has succeeded, so `resolves` steps are ok. */
|
|
239
|
+
* epistemic add-effect. Dispatch has succeeded, so `resolves`/`memoryFacts` steps are ok. */
|
|
205
240
|
function proofFor(name, input) {
|
|
206
241
|
const steps = [];
|
|
207
242
|
for (const pre of preconditionsOf(name)) {
|
|
208
243
|
if (pre.pred === PRECOND.graphLoaded) steps.push({ step: "precondition", pred: pre.pred, ok: true });
|
|
209
244
|
else if (pre.pred === PRECOND.resolves) steps.push({ step: "precondition", pred: pre.pred, param: pre.param, value: input[pre.param] ?? null, ok: true });
|
|
210
245
|
else if (pre.pred === PRECOND.anyPresent) steps.push({ step: "precondition", pred: pre.pred, params: pre.params, ok: pre.params.some((k) => input[k]) });
|
|
246
|
+
else if (pre.pred === PRECOND.memoryFacts) steps.push({ step: "precondition", pred: pre.pred, ok: true });
|
|
211
247
|
}
|
|
212
248
|
for (const eff of effectsOf(name).add) steps.push({ step: "effect", pred: eff.pred, topic: eff.topic, of: eff.of });
|
|
213
249
|
return steps;
|
|
@@ -238,11 +274,29 @@ async function dispatchEachCandidate(pool, capName, arg, ctx, execute) {
|
|
|
238
274
|
return results;
|
|
239
275
|
}
|
|
240
276
|
|
|
277
|
+
/** A tie the raw tier-3 SCORE never flags: a bare basename that binds to a source
|
|
278
|
+
* module also names its own test module, because the graph's own `tests` edge
|
|
279
|
+
* connects the two directly (not a filename guess — a real edge dispatchTool
|
|
280
|
+
* already reads). resolveObject's scoring ranks the source module far ahead on
|
|
281
|
+
* string similarity ("b" is an exact stem match; "b.test" is not), so this is
|
|
282
|
+
* never `ambiguous:true` on its own, yet "b" genuinely names either reading. Two
|
|
283
|
+
* individuals with no such edge are near-miss neighbours, not a genuine tie. */
|
|
284
|
+
function testModuleTie(graph, matchInd, candidateInd) {
|
|
285
|
+
if (!graph || !matchInd || !candidateInd) return false;
|
|
286
|
+
return edgesOfKind(graph, "tests").some((e) =>
|
|
287
|
+
(e.subject === matchInd.id && e.object === candidateInd.id)
|
|
288
|
+
|| (e.subject === candidateInd.id && e.object === matchInd.id));
|
|
289
|
+
}
|
|
290
|
+
|
|
241
291
|
/** Select a capability for a request and BIND its arguments — the full resolver.
|
|
242
292
|
* Order: command register -> NL parse -> imperative frame. Delegates entity binding to
|
|
243
293
|
* ctx.resolve and, unless `execute:false`, grounds it via ctx.dispatch. Returns
|
|
244
294
|
* { selected:{name,input}, proof, why, resolved, observed? } — a grounded call
|
|
245
|
-
* { selected:null, refused:true, reason, candidateResults? }
|
|
295
|
+
* { selected:null, refused:true, reason, candidateResults?, candidateCalls? } — an
|
|
296
|
+
* honest refusal. candidateResults carries the FULL dispatched {candidate,result}
|
|
297
|
+
* pair per tied reading; candidateCalls is its {name,input} call-only twin, the
|
|
298
|
+
* shape a caller composes into a loopResult's own top-level `candidateResults`
|
|
299
|
+
* (the tied-candidate composer's enumerate-or-refuse answer).
|
|
246
300
|
* Never emits an ungrounded / ambiguous / undeclared call. */
|
|
247
301
|
export async function resolveOne(request, declaredNames, ctx, { execute = true } = {}) {
|
|
248
302
|
const declared = new Set(declaredNames);
|
|
@@ -272,22 +326,42 @@ export async function resolveOne(request, declaredNames, ctx, { execute = true }
|
|
|
272
326
|
if (!declared.has(pick.name)) return REFUSE(`selected ${pick.name} but it is not in the declared toolset`);
|
|
273
327
|
|
|
274
328
|
// A command pick already carries a bound input; an NL/frame pick carries a raw term
|
|
275
|
-
// we bind via resolveObject
|
|
329
|
+
// we bind via resolveObject (code graph) or, for a memoryTerm slot, resolveMemoryTerm
|
|
330
|
+
// (the memory graph's SKOS concept view) — the two binding oracles never mix on one pick.
|
|
276
331
|
let input = pick.input ? { ...pick.input } : {};
|
|
277
332
|
let resolved = null;
|
|
278
333
|
if (!pick.input && !pick.noArg) {
|
|
279
334
|
const term = String(pick.term || "").trim();
|
|
280
335
|
if (!term) return REFUSE(`the ${pick.topic} intent named no entity to bind`);
|
|
281
|
-
const r =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
336
|
+
const r = pick.memoryTerm
|
|
337
|
+
? (ctx.resolveMemoryTerm ? await ctx.resolveMemoryTerm(term) : { match: null })
|
|
338
|
+
: (ctx.resolve ? ctx.resolve(term) : { match: { label: term }, ambiguous: false });
|
|
339
|
+
if (!r || !r.match) {
|
|
340
|
+
return REFUSE(pick.memoryTerm
|
|
341
|
+
? `"${term}" has no synonym/related facts in the memory graph (honest miss)`
|
|
342
|
+
: `"${term}" does not resolve to any graph entity (honest miss)`);
|
|
343
|
+
}
|
|
344
|
+
// A tied read: either resolveObject's own score-tie (r.ambiguous), or a same-tier
|
|
345
|
+
// candidate the graph's own `tests` edge ties to the match (a source module and
|
|
346
|
+
// its test module — a grain neither side's raw score alone reveals as tied).
|
|
347
|
+
// resolveMemoryTerm's SKOS concept view has no code-graph tests-edge notion, so the
|
|
348
|
+
// sibling tie-check only applies on the code-graph resolution path.
|
|
349
|
+
const sibling = (!pick.memoryTerm && !r.ambiguous)
|
|
350
|
+
? (r.candidates || []).find((c) => testModuleTie(ctx.graph, r.match, c))
|
|
351
|
+
: null;
|
|
352
|
+
if (r.ambiguous || sibling) {
|
|
353
|
+
const pool = r.ambiguous ? [r.match, ...(r.candidates || [])].slice(0, 4) : [r.match, sibling];
|
|
285
354
|
const candidateResults = await dispatchEachCandidate(pool, pick.name, pick.arg, ctx, execute);
|
|
286
|
-
|
|
355
|
+
const extra = candidateResults
|
|
356
|
+
? { candidateResults, candidateCalls: pool.map((c) => ({ name: pick.name, input: { [pick.arg]: c.label } })) }
|
|
357
|
+
: undefined;
|
|
358
|
+
return REFUSE(`"${term}" is ambiguous (${pool.map((m) => m.label).join(", ")}) — narrow it`, extra);
|
|
287
359
|
}
|
|
288
360
|
resolved = r.match;
|
|
289
361
|
input = { [pick.arg]: r.match.label };
|
|
290
|
-
why = [...why,
|
|
362
|
+
why = [...why, pick.memoryTerm
|
|
363
|
+
? `resolveMemoryTerm: "${term}" mints a memory-graph SKOS concept (tier ${r.tier})`
|
|
364
|
+
: `resolveObject: "${term}" => ${r.match.label} (${r.match.class || "?"}, tier ${r.tier})`];
|
|
291
365
|
}
|
|
292
366
|
|
|
293
367
|
const call = { name: pick.name, input };
|
|
@@ -474,7 +474,12 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
|
|
|
474
474
|
|
|
475
475
|
if (cmd.verb === "examine" || cmd.verb === "talk") {
|
|
476
476
|
const object = cmd.object;
|
|
477
|
-
|
|
477
|
+
// A carried object has no room to be "visible in" (visibleRoomOf returns
|
|
478
|
+
// null for anything held by the player) — examine still applies to it,
|
|
479
|
+
// the same way "what am I carrying" already reads inventory contents.
|
|
480
|
+
// talk has no carried exception: NPCs are never portable.
|
|
481
|
+
const carried = cmd.verb === "examine" && carriedByPlayer(state, object);
|
|
482
|
+
if (!carried && visibleRoomOf(object, { rows, state }) !== here) {
|
|
478
483
|
return answer(
|
|
479
484
|
`I don't see a ${object} here.`,
|
|
480
485
|
noteFor(`${cmd.verb} — ${object} isn't visible in the ${here}; declined, hidden things stay hidden`),
|
|
@@ -9675,6 +9675,8 @@ ${bodyText}` : graphText, tier });
|
|
|
9675
9675
|
return { id: `src:provider:${desc.name}`, type: "provider" };
|
|
9676
9676
|
case "corpus":
|
|
9677
9677
|
return { id: `src:corpus:${desc.name}`, type: "corpus" };
|
|
9678
|
+
case "corpusWeak":
|
|
9679
|
+
return { id: `src:corpus-weak:${desc.name}`, type: "corpusWeak" };
|
|
9678
9680
|
// One Source per pack article (the @revid stays in the article segment),
|
|
9679
9681
|
// so two facts from the same article corroborate nothing extra.
|
|
9680
9682
|
case "reference":
|
|
@@ -10580,6 +10582,94 @@ CREATE INDEX IF NOT EXISTS edges_by_prop ON edges(prop);
|
|
|
10580
10582
|
}
|
|
10581
10583
|
});
|
|
10582
10584
|
|
|
10585
|
+
// src/domain/skos-view.mjs
|
|
10586
|
+
function buildSkosConceptView(rows, { conceptBase = "concept:", relationMap = DEFAULT_RELATION_MAP } = {}) {
|
|
10587
|
+
const synonymPreds = new Set(relationMap.synonym || []);
|
|
10588
|
+
const relatedPreds = new Set(relationMap.related || []);
|
|
10589
|
+
const parent = /* @__PURE__ */ new Map();
|
|
10590
|
+
const ensure = (t) => {
|
|
10591
|
+
if (!parent.has(t)) parent.set(t, t);
|
|
10592
|
+
};
|
|
10593
|
+
const find = (x) => {
|
|
10594
|
+
let r = x;
|
|
10595
|
+
while (parent.get(r) !== r) r = parent.get(r);
|
|
10596
|
+
while (parent.get(x) !== r) {
|
|
10597
|
+
const next = parent.get(x);
|
|
10598
|
+
parent.set(x, r);
|
|
10599
|
+
x = next;
|
|
10600
|
+
}
|
|
10601
|
+
return r;
|
|
10602
|
+
};
|
|
10603
|
+
const union = (a, b) => {
|
|
10604
|
+
const ra = find(a), rb = find(b);
|
|
10605
|
+
if (ra === rb) return;
|
|
10606
|
+
if (ra < rb) parent.set(rb, ra);
|
|
10607
|
+
else parent.set(ra, rb);
|
|
10608
|
+
};
|
|
10609
|
+
const relatedRaw = [];
|
|
10610
|
+
for (const r of rows) {
|
|
10611
|
+
const p = r.predicate;
|
|
10612
|
+
if (!synonymPreds.has(p) && !relatedPreds.has(p)) continue;
|
|
10613
|
+
const s = normFactTerm(r.subject), o = normFactTerm(r.object);
|
|
10614
|
+
if (!s || !o) continue;
|
|
10615
|
+
ensure(s);
|
|
10616
|
+
ensure(o);
|
|
10617
|
+
if (synonymPreds.has(p)) union(s, o);
|
|
10618
|
+
else relatedRaw.push({ s, o });
|
|
10619
|
+
}
|
|
10620
|
+
const iriFor = (rep) => conceptBase + rep.replace(/ /g, "_");
|
|
10621
|
+
const componentTerms = /* @__PURE__ */ new Map();
|
|
10622
|
+
for (const t of parent.keys()) {
|
|
10623
|
+
const rep = find(t);
|
|
10624
|
+
if (!componentTerms.has(rep)) componentTerms.set(rep, /* @__PURE__ */ new Set());
|
|
10625
|
+
componentTerms.get(rep).add(t);
|
|
10626
|
+
}
|
|
10627
|
+
const concepts = [];
|
|
10628
|
+
for (const [rep, terms] of componentTerms) {
|
|
10629
|
+
const sorted = [...terms].sort();
|
|
10630
|
+
concepts.push({ id: iriFor(rep), prefLabel: rep, altLabels: sorted.filter((t) => t !== rep) });
|
|
10631
|
+
}
|
|
10632
|
+
concepts.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
10633
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10634
|
+
const related = [];
|
|
10635
|
+
for (const { s, o } of relatedRaw) {
|
|
10636
|
+
const cs = iriFor(find(s)), co = iriFor(find(o));
|
|
10637
|
+
if (cs === co) continue;
|
|
10638
|
+
const key = cs < co ? `${cs}\0${co}` : `${co}\0${cs}`;
|
|
10639
|
+
if (seen.has(key)) continue;
|
|
10640
|
+
seen.add(key);
|
|
10641
|
+
related.push({ subject: cs, object: co });
|
|
10642
|
+
}
|
|
10643
|
+
related.sort((a, b) => `${a.subject}${a.object}` < `${b.subject}${b.object}` ? -1 : 1);
|
|
10644
|
+
const conceptIdForTerm = (term) => {
|
|
10645
|
+
const t = normFactTerm(term);
|
|
10646
|
+
return parent.has(t) ? iriFor(find(t)) : null;
|
|
10647
|
+
};
|
|
10648
|
+
return { concepts, related, conceptIdForTerm, namespace: SKOS_NS };
|
|
10649
|
+
}
|
|
10650
|
+
function relatedForTerm(rows, term, options = {}) {
|
|
10651
|
+
const view = buildSkosConceptView(rows, options);
|
|
10652
|
+
const conceptId = view.conceptIdForTerm(term);
|
|
10653
|
+
if (!conceptId) return null;
|
|
10654
|
+
const byId = new Map(view.concepts.map((c) => [c.id, c]));
|
|
10655
|
+
const concept = byId.get(conceptId);
|
|
10656
|
+
const queried = normFactTerm(term);
|
|
10657
|
+
const synonyms = [concept.prefLabel, ...concept.altLabels].filter((label) => label !== queried);
|
|
10658
|
+
const related = view.related.filter((r) => r.subject === conceptId || r.object === conceptId).map((r) => byId.get(r.subject === conceptId ? r.object : r.subject)).filter(Boolean);
|
|
10659
|
+
return { conceptId, prefLabel: concept.prefLabel, altLabels: concept.altLabels, synonyms, related };
|
|
10660
|
+
}
|
|
10661
|
+
var SKOS_NS, DEFAULT_RELATION_MAP;
|
|
10662
|
+
var init_skos_view = __esm({
|
|
10663
|
+
"src/domain/skos-view.mjs"() {
|
|
10664
|
+
init_hash();
|
|
10665
|
+
SKOS_NS = "http://www.w3.org/2004/02/skos/core#";
|
|
10666
|
+
DEFAULT_RELATION_MAP = {
|
|
10667
|
+
synonym: ["mgx:synonym"],
|
|
10668
|
+
related: ["mgx:relatedTo", "mgx:similarTo"]
|
|
10669
|
+
};
|
|
10670
|
+
}
|
|
10671
|
+
});
|
|
10672
|
+
|
|
10583
10673
|
// adapter-stub-ask-nlp.mjs:../adapters/ask-nlp.mjs
|
|
10584
10674
|
var nlpAdapter;
|
|
10585
10675
|
var init_ask_nlp = __esm({
|
|
@@ -22968,91 +23058,7 @@ ${hint}` : ""}${cand}`;
|
|
|
22968
23058
|
|
|
22969
23059
|
// src/tools/handlers/tmct-related.mjs
|
|
22970
23060
|
init_config();
|
|
22971
|
-
|
|
22972
|
-
// src/domain/skos-view.mjs
|
|
22973
|
-
init_hash();
|
|
22974
|
-
var SKOS_NS = "http://www.w3.org/2004/02/skos/core#";
|
|
22975
|
-
var DEFAULT_RELATION_MAP = {
|
|
22976
|
-
synonym: ["mgx:synonym"],
|
|
22977
|
-
related: ["mgx:relatedTo", "mgx:similarTo"]
|
|
22978
|
-
};
|
|
22979
|
-
function buildSkosConceptView(rows, { conceptBase = "concept:", relationMap = DEFAULT_RELATION_MAP } = {}) {
|
|
22980
|
-
const synonymPreds = new Set(relationMap.synonym || []);
|
|
22981
|
-
const relatedPreds = new Set(relationMap.related || []);
|
|
22982
|
-
const parent = /* @__PURE__ */ new Map();
|
|
22983
|
-
const ensure = (t) => {
|
|
22984
|
-
if (!parent.has(t)) parent.set(t, t);
|
|
22985
|
-
};
|
|
22986
|
-
const find = (x) => {
|
|
22987
|
-
let r = x;
|
|
22988
|
-
while (parent.get(r) !== r) r = parent.get(r);
|
|
22989
|
-
while (parent.get(x) !== r) {
|
|
22990
|
-
const next = parent.get(x);
|
|
22991
|
-
parent.set(x, r);
|
|
22992
|
-
x = next;
|
|
22993
|
-
}
|
|
22994
|
-
return r;
|
|
22995
|
-
};
|
|
22996
|
-
const union = (a, b) => {
|
|
22997
|
-
const ra = find(a), rb = find(b);
|
|
22998
|
-
if (ra === rb) return;
|
|
22999
|
-
if (ra < rb) parent.set(rb, ra);
|
|
23000
|
-
else parent.set(ra, rb);
|
|
23001
|
-
};
|
|
23002
|
-
const relatedRaw = [];
|
|
23003
|
-
for (const r of rows) {
|
|
23004
|
-
const p = r.predicate;
|
|
23005
|
-
if (!synonymPreds.has(p) && !relatedPreds.has(p)) continue;
|
|
23006
|
-
const s = normFactTerm(r.subject), o = normFactTerm(r.object);
|
|
23007
|
-
if (!s || !o) continue;
|
|
23008
|
-
ensure(s);
|
|
23009
|
-
ensure(o);
|
|
23010
|
-
if (synonymPreds.has(p)) union(s, o);
|
|
23011
|
-
else relatedRaw.push({ s, o });
|
|
23012
|
-
}
|
|
23013
|
-
const iriFor = (rep) => conceptBase + rep.replace(/ /g, "_");
|
|
23014
|
-
const componentTerms = /* @__PURE__ */ new Map();
|
|
23015
|
-
for (const t of parent.keys()) {
|
|
23016
|
-
const rep = find(t);
|
|
23017
|
-
if (!componentTerms.has(rep)) componentTerms.set(rep, /* @__PURE__ */ new Set());
|
|
23018
|
-
componentTerms.get(rep).add(t);
|
|
23019
|
-
}
|
|
23020
|
-
const concepts = [];
|
|
23021
|
-
for (const [rep, terms] of componentTerms) {
|
|
23022
|
-
const sorted = [...terms].sort();
|
|
23023
|
-
concepts.push({ id: iriFor(rep), prefLabel: rep, altLabels: sorted.filter((t) => t !== rep) });
|
|
23024
|
-
}
|
|
23025
|
-
concepts.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
23026
|
-
const seen = /* @__PURE__ */ new Set();
|
|
23027
|
-
const related = [];
|
|
23028
|
-
for (const { s, o } of relatedRaw) {
|
|
23029
|
-
const cs = iriFor(find(s)), co = iriFor(find(o));
|
|
23030
|
-
if (cs === co) continue;
|
|
23031
|
-
const key = cs < co ? `${cs}\0${co}` : `${co}\0${cs}`;
|
|
23032
|
-
if (seen.has(key)) continue;
|
|
23033
|
-
seen.add(key);
|
|
23034
|
-
related.push({ subject: cs, object: co });
|
|
23035
|
-
}
|
|
23036
|
-
related.sort((a, b) => `${a.subject}${a.object}` < `${b.subject}${b.object}` ? -1 : 1);
|
|
23037
|
-
const conceptIdForTerm = (term) => {
|
|
23038
|
-
const t = normFactTerm(term);
|
|
23039
|
-
return parent.has(t) ? iriFor(find(t)) : null;
|
|
23040
|
-
};
|
|
23041
|
-
return { concepts, related, conceptIdForTerm, namespace: SKOS_NS };
|
|
23042
|
-
}
|
|
23043
|
-
function relatedForTerm(rows, term, options = {}) {
|
|
23044
|
-
const view = buildSkosConceptView(rows, options);
|
|
23045
|
-
const conceptId = view.conceptIdForTerm(term);
|
|
23046
|
-
if (!conceptId) return null;
|
|
23047
|
-
const byId = new Map(view.concepts.map((c) => [c.id, c]));
|
|
23048
|
-
const concept = byId.get(conceptId);
|
|
23049
|
-
const queried = normFactTerm(term);
|
|
23050
|
-
const synonyms = [concept.prefLabel, ...concept.altLabels].filter((label) => label !== queried);
|
|
23051
|
-
const related = view.related.filter((r) => r.subject === conceptId || r.object === conceptId).map((r) => byId.get(r.subject === conceptId ? r.object : r.subject)).filter(Boolean);
|
|
23052
|
-
return { conceptId, prefLabel: concept.prefLabel, altLabels: concept.altLabels, synonyms, related };
|
|
23053
|
-
}
|
|
23054
|
-
|
|
23055
|
-
// src/tools/handlers/tmct-related.mjs
|
|
23061
|
+
init_skos_view();
|
|
23056
23062
|
init_memory_fallthrough();
|
|
23057
23063
|
async function tmct_related(args, { config }) {
|
|
23058
23064
|
const term = requiredArg(args, "term");
|
|
@@ -23396,6 +23402,9 @@ ${JSON.stringify(envelope, null, 2)}`;
|
|
|
23396
23402
|
"game-inform": "inform"
|
|
23397
23403
|
});
|
|
23398
23404
|
|
|
23405
|
+
// src/services/chat.mjs
|
|
23406
|
+
init_skos_view();
|
|
23407
|
+
|
|
23399
23408
|
// src/domain/worlds-pack.mjs
|
|
23400
23409
|
var WORLD_NAME_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
23401
23410
|
var WORLD_RULE_KINDS = Object.freeze([
|