@polycode-projects/the-mechanical-code-talker 5.0.5 → 5.0.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/README.md +78 -19
- package/bin/tmct.mjs +63 -2
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +23 -0
- package/src/domain/ask-vocab.mjs +19 -0
- package/src/domain/ask.mjs +10 -5
- package/src/domain/codegraph.mjs +23 -9
- package/src/domain/game-config.mjs +12 -0
- package/src/domain/interpret/strategies/keywords.mjs +30 -1
- package/src/domain/memory/capability.mjs +15 -11
- package/src/domain/router/drive.mjs +36 -17
- package/src/domain/router/resolver.mjs +63 -17
- package/src/domain/spider-fly-world.mjs +2 -2
- package/src/domain/sprite-templates.mjs +19 -7
- package/src/domain/syllogise.mjs +16 -6
- package/src/domain/town-square-world.mjs +1 -1
- package/src/services/adventure-viz.mjs +5 -2
- package/src/services/adventure.mjs +8 -1
- package/src/services/chat-page-viz.mjs +123 -25
- package/src/services/chat-session.mjs +60 -10
- package/src/services/chat.mjs +328 -36
- package/src/services/code-explorer-viz.mjs +3 -2
- package/src/services/extract-facts.mjs +47 -7
- package/src/services/ingest-viz.mjs +113 -29
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +44 -0
- package/src/services/mud-viz.mjs +21 -3
- package/src/services/mudiii-scene.mjs +407 -36
- package/src/services/mudiii-turn.mjs +65 -9
- package/src/services/mudiii-viz.mjs +810 -157
- package/src/services/p2p-room.mjs +1 -1
- package/src/services/plan-viz.mjs +26 -4
- package/src/services/predator-prey.mjs +141 -37
- package/src/services/research-viz.mjs +17 -23
- package/src/services/spider-fly-turn.mjs +7 -1
- package/src/services/spider-fly-viz.mjs +13 -5
- package/src/services/sprite-catalog-viz.mjs +3 -2
- package/src/services/viz-theme.mjs +20 -0
- package/src/services/viz-ticker.mjs +15 -2
- package/src/surfaces/http/server-http.mjs +90 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
- package/src/surfaces/web/mud-browser-entry.mjs +33 -1
- package/src/surfaces/web/mudiii-browser-entry.mjs +70 -34
- package/src/surfaces/web/tmct-surface.mjs +18 -6
- package/src/tools/handlers/tmct-ask.mjs +15 -2
- package/src/tools/server.mjs +31 -2
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Pure and import-free of core.mjs, exactly like trust.mjs beside it.
|
|
20
20
|
|
|
21
|
-
import { findIsaChain, SUBCLASS_PREDICATE, TYPE_PREDICATE } from "../syllogise.mjs";
|
|
21
|
+
import { findIsaChain, buildSubClassSuccessors, SUBCLASS_PREDICATE, TYPE_PREDICATE } from "../syllogise.mjs";
|
|
22
22
|
|
|
23
23
|
/** The negative-polarity CURIE prefix. A separate prefix, never an
|
|
24
24
|
* "mgx:not-<lemma>" mint: chat.mjs's predicatePhrase reads "mgx:not-fly" as
|
|
@@ -78,10 +78,14 @@ const byTrustThenName = (a, b) => (b.trust || 0) - (a.trust || 0) || String(a.su
|
|
|
78
78
|
|
|
79
79
|
const asSet = (v) => (v instanceof Set ? v : new Set(Array.isArray(v) ? v : [v]));
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
// The ⊑ adjacency is built here, once per call, rather than inside each chase:
|
|
82
|
+
// every caller below chases many subjects over the same edges, and rebuilding
|
|
83
|
+
// the index per chase makes one small search cost the whole edge set.
|
|
84
|
+
const isaEdgesOf = (facts) => {
|
|
85
|
+
const typeEdges = facts.filter((f) => f.predicate === TYPE_PREDICATE).map((f) => [f.subject, f.object]);
|
|
86
|
+
const subClassEdges = facts.filter((f) => f.predicate === SUBCLASS_PREDICATE).map((f) => [f.subject, f.object]);
|
|
87
|
+
return { typeEdges, subClassSucc: buildSubClassSuccessors(subClassEdges) };
|
|
88
|
+
};
|
|
85
89
|
|
|
86
90
|
/** The shortest isa chain from any spelling of `subjects` up to `target`, or
|
|
87
91
|
* null. The chase is corpus-INCLUSIVE on purpose, and that is a considered
|
|
@@ -90,11 +94,11 @@ const isaEdgesOf = (facts) => ({
|
|
|
90
94
|
* would be fabrication. Here the premise being chased to — "bird can fly" — is
|
|
91
95
|
* itself corpus data on a fresh install, so a taught-only chase would find
|
|
92
96
|
* nothing to inherit and the whole feature would never fire. */
|
|
93
|
-
function shortestChainTo(subjects, target, typeEdges,
|
|
97
|
+
function shortestChainTo(subjects, target, typeEdges, subClassSucc, maxHops) {
|
|
94
98
|
let best = null;
|
|
95
99
|
for (const s of subjects) {
|
|
96
100
|
if (s === target) return [];
|
|
97
|
-
const chain = findIsaChain(s, new Set([target]), typeEdges,
|
|
101
|
+
const chain = findIsaChain(s, new Set([target]), typeEdges, subClassSucc, { maxHops });
|
|
98
102
|
if (chain && (!best || chain.length < best.length)) best = chain;
|
|
99
103
|
}
|
|
100
104
|
return best;
|
|
@@ -126,7 +130,7 @@ export function resolveCapabilityPolarity(subject, object, facts, { maxHops = 3
|
|
|
126
130
|
const subjects = asSet(subject);
|
|
127
131
|
const objects = asSet(object);
|
|
128
132
|
const rows = Array.isArray(facts) ? facts : [];
|
|
129
|
-
const { typeEdges,
|
|
133
|
+
const { typeEdges, subClassSucc } = isaEdgesOf(rows);
|
|
130
134
|
|
|
131
135
|
const carriers = rows.filter(
|
|
132
136
|
(f) => (f.predicate === CAPABLE_OF_PREDICATE || f.predicate === NEG_CAPABLE_OF_PREDICATE) && objects.has(f.object),
|
|
@@ -139,7 +143,7 @@ export function resolveCapabilityPolarity(subject, object, facts, { maxHops = 3
|
|
|
139
143
|
candidates.push({ fact, polarity, hops: 0, chain: null });
|
|
140
144
|
continue;
|
|
141
145
|
}
|
|
142
|
-
const chain = shortestChainTo(subjects, fact.subject, typeEdges,
|
|
146
|
+
const chain = shortestChainTo(subjects, fact.subject, typeEdges, subClassSucc, maxHops);
|
|
143
147
|
if (chain && chain.length) candidates.push({ fact, polarity, hops: chain.length, chain });
|
|
144
148
|
}
|
|
145
149
|
|
|
@@ -212,14 +216,14 @@ export function capabilityBaseRate(subject, object, facts, { maxHops = 3 } = {})
|
|
|
212
216
|
};
|
|
213
217
|
|
|
214
218
|
const split = siblings.map(capabilityOf);
|
|
215
|
-
const { typeEdges,
|
|
219
|
+
const { typeEdges, subClassSucc } = isaEdgesOf(rows);
|
|
216
220
|
return {
|
|
217
221
|
klass,
|
|
218
222
|
kinds: siblings.length,
|
|
219
223
|
positive: split.filter((s) => s.polarity === "positive"),
|
|
220
224
|
negative: split.filter((s) => s.polarity === "negative"),
|
|
221
225
|
unknown: split.filter((s) => s.polarity === "unknown"),
|
|
222
|
-
chain: shortestChainTo(subjects, klass, typeEdges,
|
|
226
|
+
chain: shortestChainTo(subjects, klass, typeEdges, subClassSucc, maxHops),
|
|
223
227
|
};
|
|
224
228
|
}
|
|
225
229
|
|
|
@@ -56,11 +56,14 @@ const refuse = (why, driver) => ({ calls: [], refused: true, terminated: true, p
|
|
|
56
56
|
|
|
57
57
|
/** Execute the MEMBER-FILTER HTN method ("which methods of X end up calling Y")
|
|
58
58
|
* — the per-member hop the single-shot resolver cannot emit on its own. Step 1
|
|
59
|
-
* grounds members(X) via resolveOne; then, per CALLABLE member (sorted,
|
|
60
|
-
*
|
|
61
|
-
* membersReaching (the bounded transitive callsSymbol
|
|
62
|
-
* the graph, never parsed from text
|
|
63
|
-
*
|
|
59
|
+
* grounds members(X) via resolveOne; then, per CALLABLE member (sorted), one
|
|
60
|
+
* tmct_callees hop, so long as the whole list fits the planner's MAX_STEPS
|
|
61
|
+
* budget. The fold is membersReaching (the bounded transitive callsSymbol
|
|
62
|
+
* closure), computed over the graph, never parsed from text — it covers every
|
|
63
|
+
* member whether or not the hops were emitted, so a class with more members
|
|
64
|
+
* than the plan budget allows gets the same answer with a fold step in the
|
|
65
|
+
* proof in place of the hop chain. Honest refuses: no tmct_callees in the
|
|
66
|
+
* declared toolset, an unbindable filter target, an ungrounded member list. */
|
|
64
67
|
async function memberFilterDrive(request, tools, ctx, segments) {
|
|
65
68
|
const [setSeg, filterSeg] = segments;
|
|
66
69
|
if (!tools.includes("tmct_callees")) {
|
|
@@ -81,29 +84,45 @@ async function memberFilterDrive(request, tools, ctx, segments) {
|
|
|
81
84
|
const proof = [{ step: "causal-link", producer: "graph", condition: classInd.label, consumer: `step-1:${r1.selected.name}`, role: "action", ok: true }];
|
|
82
85
|
for (const s of r1.proof) proof.push({ ...s, ofStep: 1 });
|
|
83
86
|
const why = [
|
|
84
|
-
`HTN method: member-filter — enumerate members(${classInd.label}),
|
|
87
|
+
`HTN method: member-filter — enumerate members(${classInd.label}), fold by bounded transitive reach of ${target.label} over every callable member, and hop tmct_callees per member while that hop chain fits the plan budget`,
|
|
85
88
|
...(r1.why || []).map((w) => `[1] ${w}`),
|
|
86
89
|
];
|
|
87
90
|
|
|
88
91
|
const members = memberIndividuals(ctx.graph, classInd)
|
|
89
92
|
.filter((m) => CALLABLE_MEMBER_CLASSES.has(m.class))
|
|
90
93
|
.sort((a, b) => String(a.label).localeCompare(String(b.label)));
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
// MAX_STEPS bounds the emitted PLAN, not the answer. The fold below reads the
|
|
95
|
+
// graph's own callsSymbol edges in one pass over every member at any member
|
|
96
|
+
// count, so a member list too long to walk hop by hop still gets folded — it
|
|
97
|
+
// just gets no per-member hops. All of them or none: emitting the first seven
|
|
98
|
+
// of ninety-nine would read as "these are the members I checked", which is a
|
|
99
|
+
// trace of work nobody did.
|
|
100
|
+
const hopsFitTheBudget = 1 + members.length <= MAX_STEPS;
|
|
101
|
+
if (hopsFitTheBudget) {
|
|
102
|
+
for (let i = 0; i < members.length; i += 1) {
|
|
103
|
+
const m = members[i];
|
|
104
|
+
const res = await ctx.dispatch("tmct_callees", { symbol: m.label });
|
|
105
|
+
if (!res.ok) return refuse(`the callees hop for ${m.label} did not ground: ${res.error}`, ROUTER_DRIVER);
|
|
106
|
+
calls.push({ name: "tmct_callees", input: { symbol: m.label } });
|
|
107
|
+
proof.push({ step: "causal-link", producer: "step-1", condition: m.label, consumer: `step-${i + 2}:tmct_callees`, role: "member-filter", ok: true });
|
|
108
|
+
why.push(`[${i + 2}] callees hop over ${m.label} (a member step 1 produced)`);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
proof.push({
|
|
112
|
+
step: "graph-fold", producer: "step-1", condition: target.label,
|
|
113
|
+
consumer: `fold:membersReaching(${classInd.label})`, role: "member-filter",
|
|
114
|
+
members: members.length, hops: 0, ok: true,
|
|
115
|
+
});
|
|
116
|
+
why.push(`[2] ${members.length} callable members needs a ${1 + members.length}-step plan against a budget of ${MAX_STEPS}, so no per-member callees hop was dispatched — the fold reads the same callsSymbol edges those hops would have reported, over every one of the ${members.length}`);
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
const composed = membersReaching(ctx.graph, classInd, target.label);
|
|
120
|
+
const trace = hopsFitTheBudget
|
|
121
|
+
? calls.map((c) => c.name).join(" -> ")
|
|
122
|
+
: `${calls.map((c) => c.name).join(" -> ")} + graph fold over ${members.length} members`;
|
|
104
123
|
return {
|
|
105
124
|
calls, refused: false, terminated: true, proof, driver: ROUTER_DRIVER, why, composed,
|
|
106
|
-
observed: `plan(member-filter): ${
|
|
125
|
+
observed: `plan(member-filter): ${trace} => {${composed.join(", ")}}`,
|
|
107
126
|
};
|
|
108
127
|
}
|
|
109
128
|
|
|
@@ -36,6 +36,17 @@ const SUPERLATIVE_RE = new RegExp(
|
|
|
36
36
|
"i",
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
+
// A PAST-TENSE report of an action already taken ("I ran the impact of X",
|
|
40
|
+
// "someone called tmct_impact then tmct_untested"). The frames below key on topic
|
|
41
|
+
// words with no tense test, so `\bimpacts?\b` fires on a narration exactly as it
|
|
42
|
+
// does on an instruction and the router re-runs what the speaker said they had
|
|
43
|
+
// already done. Closed and past-tense only, in both the subject and the verb: a
|
|
44
|
+
// present-tense request ("run the impact of X", "I need the impact of X", "if I
|
|
45
|
+
// run impact then the untested scan") must keep binding, so no bare
|
|
46
|
+
// run/check/list here and no subjectless verb.
|
|
47
|
+
const NARRATED_ACTION_RE =
|
|
48
|
+
/\b(?:i|we|you|they|someone|somebody)\s+(?:just\s+|already\s+|first\s+|then\s+|earlier\s+|previously\s+)?(?:ran|looked|listed|checked|called|did|used|inspected|described|viewed|examined|dumped|traced|printed|queried|opened|walked|pulled)\b/i;
|
|
49
|
+
|
|
39
50
|
// ---- the ask-kind -> epistemic-topic MAPPING (the Stage-1 core) --------------
|
|
40
51
|
// Keyed `${shape}:${kind}` off parseQuery's output. Every topic must be achievable by
|
|
41
52
|
// exactly one registered capability (the bidirectional conformance test proves it).
|
|
@@ -218,6 +229,10 @@ export function mapParse(parse) {
|
|
|
218
229
|
/** Match an imperative FRAME. Returns { name, arg|noArg, term, topic, source:"frame", why }
|
|
219
230
|
* or null. Backward-chains the frame's topic to a capability. */
|
|
220
231
|
export function mapFrame(request) {
|
|
232
|
+
// A narrated trace is a report of work already done. Claiming it here dispatches
|
|
233
|
+
// that work a second time and calls the data an answer to "what am I trying to
|
|
234
|
+
// do", so the frames decline and the request goes on to the goal-reasoner.
|
|
235
|
+
if (NARRATED_ACTION_RE.test(request)) return null;
|
|
221
236
|
for (const f of FRAMES) {
|
|
222
237
|
if (!f.re.test(request)) continue;
|
|
223
238
|
if (f.skipIfSuperlative && SUPERLATIVE_RE.test(request)) continue;
|
|
@@ -345,6 +360,40 @@ function testModuleTie(graph, matchInd, candidateInd) {
|
|
|
345
360
|
|| (e.subject === candidateInd.id && e.object === matchInd.id));
|
|
346
361
|
}
|
|
347
362
|
|
|
363
|
+
/** The tied-read check EVERY binding path runs, so one term cannot mean two
|
|
364
|
+
* things down one route and one thing down another. A tie is either
|
|
365
|
+
* resolveObject's own score tie (r.ambiguous) or a same-tier candidate the
|
|
366
|
+
* graph's `tests` edge ties to the match. Returns the enumerate-or-refuse
|
|
367
|
+
* answer — a refusal carrying one dispatched read per tied reading — or null
|
|
368
|
+
* when the term names exactly one entity. `siblingTie` is off for a memory-graph
|
|
369
|
+
* resolution, which has no code-graph tests-edge notion to check. */
|
|
370
|
+
async function tiedReadRefusal(r, { term, capName, arg, ctx, execute, siblingTie = true }) {
|
|
371
|
+
const sibling = (siblingTie && !r.ambiguous)
|
|
372
|
+
? (r.candidates || []).find((c) => testModuleTie(ctx.graph, r.match, c))
|
|
373
|
+
: null;
|
|
374
|
+
if (!r.ambiguous && !sibling) return null;
|
|
375
|
+
const pool = r.ambiguous ? [r.match, ...(r.candidates || [])].slice(0, 4) : [r.match, sibling];
|
|
376
|
+
const candidateResults = await dispatchEachCandidate(pool, capName, arg, ctx, execute);
|
|
377
|
+
const extra = candidateResults
|
|
378
|
+
? { candidateResults, candidateCalls: pool.map((c) => ({ name: capName, input: { [arg]: c.label } })) }
|
|
379
|
+
: undefined;
|
|
380
|
+
return REFUSE(`"${term}" is ambiguous (${pool.map((m) => m.label).join(", ")}) — narrow it`, extra);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** The arg key of a bound call's code-graph ENTITY slot, read off the registry's
|
|
384
|
+
* own `resolves` precondition rather than a list kept here. A free-text slot
|
|
385
|
+
* (tmct_search's query — declared with no resolves precondition) and a
|
|
386
|
+
* memory-graph slot both return null, so neither is checked against the code
|
|
387
|
+
* graph. Null when the call names no code-graph entity at all. */
|
|
388
|
+
function codeGraphEntityArg(capName, input) {
|
|
389
|
+
for (const pre of preconditionsOf(capName)) {
|
|
390
|
+
if (pre.pred !== PRECOND.resolves || MEMORY_KINDS.includes(pre.as)) continue;
|
|
391
|
+
const arg = parametersOf(capName).find((p) => p.name === pre.param)?.arg ?? pre.param;
|
|
392
|
+
if (input[arg] != null) return arg;
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
348
397
|
/** Select a capability for a request and BIND its arguments — the full resolver.
|
|
349
398
|
* Order: command register -> NL parse -> imperative frame. Delegates entity binding to
|
|
350
399
|
* ctx.resolve and, unless `execute:false`, grounds it via ctx.dispatch. Returns
|
|
@@ -388,7 +437,18 @@ export async function resolveOne(request, declaredNames, ctx, { execute = true }
|
|
|
388
437
|
// the two binding oracles never mix on one pick.
|
|
389
438
|
let input = pick.input ? { ...pick.input } : {};
|
|
390
439
|
let resolved = null;
|
|
391
|
-
if (
|
|
440
|
+
if (pick.input) {
|
|
441
|
+
// A command pick arrives already bound, so it skips the binding step below.
|
|
442
|
+
// The tie check is not part of binding: a term naming two graph entities has
|
|
443
|
+
// to refuse on this route too, or the terse command form answers what the NL
|
|
444
|
+
// and frame routes both decline.
|
|
445
|
+
const entityArg = ctx.resolve ? codeGraphEntityArg(pick.name, input) : null;
|
|
446
|
+
if (entityArg) {
|
|
447
|
+
const term = String(input[entityArg]);
|
|
448
|
+
const tied = await tiedReadRefusal(ctx.resolve(term), { term, capName: pick.name, arg: entityArg, ctx, execute });
|
|
449
|
+
if (tied) return tied;
|
|
450
|
+
}
|
|
451
|
+
} else if (!pick.noArg) {
|
|
392
452
|
const term = String(pick.term || "").trim();
|
|
393
453
|
if (!term) return REFUSE(`the ${pick.topic} intent named no entity to bind`);
|
|
394
454
|
const memoryBound = isMemoryTermSlot(pick.name, pick.arg);
|
|
@@ -400,22 +460,8 @@ export async function resolveOne(request, declaredNames, ctx, { execute = true }
|
|
|
400
460
|
? `the memory graph holds no facts mentioning "${term}" (honest miss)`
|
|
401
461
|
: `"${term}" does not resolve to any graph entity (honest miss)`);
|
|
402
462
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// its test module — a grain neither side's raw score alone reveals as tied).
|
|
406
|
-
// resolveMemoryTerm's exact-match tiers have no code-graph tests-edge notion, so
|
|
407
|
-
// the sibling tie-check only applies on the code-graph resolution path.
|
|
408
|
-
const sibling = (!memoryBound && !r.ambiguous)
|
|
409
|
-
? (r.candidates || []).find((c) => testModuleTie(ctx.graph, r.match, c))
|
|
410
|
-
: null;
|
|
411
|
-
if (r.ambiguous || sibling) {
|
|
412
|
-
const pool = r.ambiguous ? [r.match, ...(r.candidates || [])].slice(0, 4) : [r.match, sibling];
|
|
413
|
-
const candidateResults = await dispatchEachCandidate(pool, pick.name, pick.arg, ctx, execute);
|
|
414
|
-
const extra = candidateResults
|
|
415
|
-
? { candidateResults, candidateCalls: pool.map((c) => ({ name: pick.name, input: { [pick.arg]: c.label } })) }
|
|
416
|
-
: undefined;
|
|
417
|
-
return REFUSE(`"${term}" is ambiguous (${pool.map((m) => m.label).join(", ")}) — narrow it`, extra);
|
|
418
|
-
}
|
|
463
|
+
const tied = await tiedReadRefusal(r, { term, capName: pick.name, arg: pick.arg, ctx, execute, siblingTie: !memoryBound });
|
|
464
|
+
if (tied) return tied;
|
|
419
465
|
resolved = r.match;
|
|
420
466
|
// The frame's own optional slots ride alongside the bound entity — declared
|
|
421
467
|
// params of the same capability, so hallucinationsIn still validates them.
|
|
@@ -37,8 +37,8 @@ export const SPIDER_MASS_DECREMENT_PER_TURN = 0.5;
|
|
|
37
37
|
* the town square's own roles object, minus the food entry: nothing inert
|
|
38
38
|
* lies on a spider-and-fly board, and a null food role is what says so. */
|
|
39
39
|
export const SPIDER_FLY_ROLES = Object.freeze({
|
|
40
|
-
predator: Object.freeze({ role: "predator", kind: "spider", idPrefix: "spider" }),
|
|
41
|
-
prey: Object.freeze({ role: "prey", kind: "fly", idPrefix: "fly" }),
|
|
40
|
+
predator: Object.freeze({ role: "predator", kind: "spider", idPrefix: "spider", hunts: "prey" }),
|
|
41
|
+
prey: Object.freeze({ role: "prey", kind: "fly", idPrefix: "fly", hunts: null }),
|
|
42
42
|
food: null,
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -244,12 +244,22 @@ function withUnfilledPlaceholdersDropped(template, svg) {
|
|
|
244
244
|
/** Resolve ONE class term (no ancestor walk here — the caller repeats this
|
|
245
245
|
* at every level of the chain) against the template set, in specificity
|
|
246
246
|
* order: fully-specific match variant > parameterized template filled with
|
|
247
|
-
* an observed value > plain class template
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
247
|
+
* an observed value > plain class template > a parameterized template with
|
|
248
|
+
* no fact to fill it, its own unfilled placeholders dropped. Among
|
|
249
|
+
* satisfied match variants the most demanding one wins (bestSatisfiedVariant).
|
|
250
|
+
* A match variant that declares its own `[parameters.*]` is filled from them
|
|
251
|
+
* first, so a facing profile also carrying `[face]`/`[parameters.emotion]`
|
|
252
|
+
* renders the mood the instance's own mgx:feels fact names. Returns the SVG
|
|
253
|
+
* string, or null when nothing at this level matches.
|
|
254
|
+
*
|
|
255
|
+
* The last step matters for a class whose only art IS a parameterized
|
|
256
|
+
* template (e.g. lamp/cabinet at the sprite tier: one `[parameters.material]`
|
|
257
|
+
* file, no plain sibling) — with no fact to fill it, `parameterizedFillAll`
|
|
258
|
+
* correctly declines rather than guessing a material, but the class still
|
|
259
|
+
* has real dedicated art. Dropping the unfilled tokens and returning that
|
|
260
|
+
* art (the same treatment a satisfied match variant's own unfilled
|
|
261
|
+
* placeholders already get) means an untaught instance renders as ITS OWN
|
|
262
|
+
* class, not as whatever a less-specific ancestor happens to resolve to. */
|
|
253
263
|
function resolveAtTerm(term, propertyFacts, templates) {
|
|
254
264
|
const candidates = templatesForClass(term, templates);
|
|
255
265
|
const matched = bestSatisfiedVariant(candidates, propertyFacts);
|
|
@@ -263,7 +273,9 @@ function resolveAtTerm(term, propertyFacts, templates) {
|
|
|
263
273
|
if (filled) return filled;
|
|
264
274
|
}
|
|
265
275
|
const plain = candidates.find((t) => !t.match && !t.parameters);
|
|
266
|
-
|
|
276
|
+
if (plain) return plain.svg;
|
|
277
|
+
const parameterizedOnly = candidates.find((t) => !t.match && t.parameters);
|
|
278
|
+
return parameterizedOnly ? withUnfilledPlaceholdersDropped(parameterizedOnly, parameterizedOnly.svg) : null;
|
|
267
279
|
}
|
|
268
280
|
|
|
269
281
|
function resolveSpriteAssetRaw(className, factRows, propertyFacts, templates, spriteRegistry, rootFallback) {
|
package/src/domain/syllogise.mjs
CHANGED
|
@@ -1700,6 +1700,21 @@ export async function retractSubClassOf(repoDir, subject, object, {
|
|
|
1700
1700
|
};
|
|
1701
1701
|
}
|
|
1702
1702
|
|
|
1703
|
+
/** subject -> Set(objects) over a subClassOf edge list, the adjacency
|
|
1704
|
+
* `findIsaChain` walks. Building it is O(edges); a search from one subject is
|
|
1705
|
+
* O(that subject's own reachable set). A caller chasing MANY subjects over the
|
|
1706
|
+
* same edges should build this once and hand it to `findIsaChain` in place of
|
|
1707
|
+
* the edge list, or every small search pays for the whole graph again. */
|
|
1708
|
+
export function buildSubClassSuccessors(subClassEdges) {
|
|
1709
|
+
const subSucc = new Map();
|
|
1710
|
+
for (const [a, b] of subClassEdges || []) {
|
|
1711
|
+
if (!a || !b || a === b) continue;
|
|
1712
|
+
if (!subSucc.has(a)) subSucc.set(a, new Set());
|
|
1713
|
+
subSucc.get(a).add(b);
|
|
1714
|
+
}
|
|
1715
|
+
return subSucc;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1703
1718
|
/**
|
|
1704
1719
|
* PROOF SEARCH (not a third rule — a bounded rooted chase for a single "does
|
|
1705
1720
|
* `subj` reach one of `targets`?" query). Walks OUTWARD from `subj` only,
|
|
@@ -1714,12 +1729,7 @@ export async function retractSubClassOf(repoDir, subject, object, {
|
|
|
1714
1729
|
*/
|
|
1715
1730
|
export function findIsaChain(subj, targets, typeEdges, subClassEdges, { maxHops = 6 } = {}) {
|
|
1716
1731
|
const targetSet = targets instanceof Set ? targets : new Set(targets || []);
|
|
1717
|
-
const subSucc =
|
|
1718
|
-
for (const [a, b] of subClassEdges || []) {
|
|
1719
|
-
if (!a || !b || a === b) continue;
|
|
1720
|
-
if (!subSucc.has(a)) subSucc.set(a, new Set());
|
|
1721
|
-
subSucc.get(a).add(b);
|
|
1722
|
-
}
|
|
1732
|
+
const subSucc = subClassEdges instanceof Map ? subClassEdges : buildSubClassSuccessors(subClassEdges);
|
|
1723
1733
|
|
|
1724
1734
|
let frontier = [];
|
|
1725
1735
|
for (const [x, c] of typeEdges || []) {
|
|
@@ -225,7 +225,7 @@ const TOWN_SQUARE_MARKET = layout({
|
|
|
225
225
|
|
|
226
226
|
/** The chapel corner: an L of buildings in the north-west, a fence line across
|
|
227
227
|
* the south, three oaks. The only shipped layout with two predators, so it is
|
|
228
|
-
* where the
|
|
228
|
+
* where two hunters working the same board show up. */
|
|
229
229
|
const TOWN_SQUARE_CHAPEL = layout({
|
|
230
230
|
name: "town-square-chapel",
|
|
231
231
|
gridSize: 14,
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
// an edit implies run through the browser bundle's own `session.applyEdit`
|
|
93
93
|
// (adventure-browser-entry.mjs), never here — this module only renders and
|
|
94
94
|
// reads.
|
|
95
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel, rowsForWorld, wordBeforeCursor } from "./viz-theme.mjs";
|
|
95
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel, rowsForWorld, wordBeforeCursor, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
|
|
96
96
|
import { createTicker, createSerialQueue } from "./viz-ticker.mjs";
|
|
97
97
|
import { directedGridLayout } from "./viz-room-graph.mjs";
|
|
98
98
|
import { worldDigestRows, roomAffordances, foldWorldState } from "./adventure.mjs";
|
|
@@ -628,7 +628,9 @@ ${THEME_TOKENS_CSS}
|
|
|
628
628
|
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
629
629
|
.mono { font-family: ${MONO_STACK}; }
|
|
630
630
|
main { max-width: 920px; margin: 0 auto; padding: 1.4rem 1.2rem 2.2rem; }
|
|
631
|
+
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
631
632
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .12em; text-transform: uppercase; color: var(--gilt); }
|
|
633
|
+
${EYEBROW_LINKS_CSS}
|
|
632
634
|
.titlebar { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: .3rem 0 1rem; }
|
|
633
635
|
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
634
636
|
button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
@@ -993,8 +995,9 @@ ${THEME_TOKENS_CSS}
|
|
|
993
995
|
</head>
|
|
994
996
|
<body>
|
|
995
997
|
<main>
|
|
998
|
+
<h1 class="visually-hidden">the adventure</h1>
|
|
996
999
|
<div class="titlebar">
|
|
997
|
-
<div class="eyebrow"
|
|
1000
|
+
<div class="eyebrow">${demoEyebrowHtml("adventure", "the adventure")}</div>
|
|
998
1001
|
<button id="editModeBtn" type="button" class="mode-toggle" disabled>edit the world</button>
|
|
999
1002
|
</div>
|
|
1000
1003
|
<p class="page-note" id="pageNote">${escapeHtml(scenarioList[0].worldPayload.opening || "")}</p>
|
|
@@ -1893,7 +1893,14 @@ export async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache
|
|
|
1893
1893
|
);
|
|
1894
1894
|
}
|
|
1895
1895
|
const digest = await worldDigest(object, { memoryDir, memory, rows, state, graph, actingSubject });
|
|
1896
|
-
|
|
1896
|
+
// A thing that IS here with nothing written about it, and a word that only
|
|
1897
|
+
// turns up in the room's own prose, are different answers. Sharing one line
|
|
1898
|
+
// let "look at the door" reply "nothing more about the door is written down
|
|
1899
|
+
// yet" in a house whose world model has no door at all, which reads as
|
|
1900
|
+
// confirmation that a door is standing there.
|
|
1901
|
+
const body = digest ?? (notHere
|
|
1902
|
+
? `there's no ${object} here — the word turns up in what's written about this place, but nothing by that name is in the scene.`
|
|
1903
|
+
: `nothing more about the ${object} is written down yet.`);
|
|
1897
1904
|
const containerNote = !person && isContainer(rows, object) ? ` ${containerStatusPhrase(object, { state })}` : "";
|
|
1898
1905
|
// Framing follows the VERB the player typed, not the object's type: talking
|
|
1899
1906
|
// to a lamp still reads as an attempted conversation (nothing replies, but
|