@polycode-projects/the-mechanical-code-talker 2.10.2 → 2.10.3
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.3",
|
|
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.",
|
package/src/domain/domain.mjs
CHANGED
|
@@ -5,9 +5,18 @@
|
|
|
5
5
|
// arrives as data from the memory store's fact/Rule rows. Plugs into
|
|
6
6
|
// planning.mjs's findActionPath as its applyActions.
|
|
7
7
|
|
|
8
|
+
import { provenanceTagToSource } from "./memory/trust.mjs";
|
|
9
|
+
|
|
8
10
|
const MEMBER_EDGE_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
9
11
|
const SNAPSHOT_RE = /^(.+)@step(\d+)$/;
|
|
10
12
|
|
|
13
|
+
/** The class-member list a grounding step ranges over. The default "all" is
|
|
14
|
+
* every typed member; "taught" restricts to instances a person taught, the
|
|
15
|
+
* scope the plan lane uses so a merged corpus never widens a quantified goal
|
|
16
|
+
* or the movable set. Falls back to nothing for an unknown scope. */
|
|
17
|
+
const membersInScope = (domain, cls, scope) =>
|
|
18
|
+
(scope === "taught" ? domain.taughtClassMembers : domain.classMembers)?.[cls] || [];
|
|
19
|
+
|
|
11
20
|
/** Trim a taught term defensively: some teach frames keep a sentence's
|
|
12
21
|
* trailing punctuation in the captured object. */
|
|
13
22
|
const normTerm = (value) => String(value ?? "").trim().replace(/[.!?]+$/, "");
|
|
@@ -110,18 +119,34 @@ export function compileDomain(factRows, ruleRows) {
|
|
|
110
119
|
|
|
111
120
|
// Class membership from typing edges. A member is a subject with a typing
|
|
112
121
|
// edge into the class and no typing edge pointing at itself (a leaf).
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
const membersFromEdges = (typingEdges) => {
|
|
123
|
+
const hasIncoming = new Set(typingEdges.map((r) => r.object));
|
|
124
|
+
const members = {};
|
|
125
|
+
for (const edge of typingEdges) {
|
|
126
|
+
if (hasIncoming.has(edge.subject)) continue;
|
|
127
|
+
(members[edge.object] ??= []).push(edge.subject);
|
|
128
|
+
}
|
|
129
|
+
for (const list of Object.values(members)) {
|
|
130
|
+
list.sort();
|
|
131
|
+
// de-dup while keeping order
|
|
132
|
+
for (let i = list.length - 1; i > 0; i -= 1) if (list[i] === list[i - 1]) list.splice(i, 1);
|
|
133
|
+
}
|
|
134
|
+
return members;
|
|
135
|
+
};
|
|
136
|
+
const typingRows = (factRows || []).filter((r) => MEMBER_EDGE_PREDICATES.has(normTerm(r.predicate)));
|
|
137
|
+
const edges = typingRows.map(normRow);
|
|
138
|
+
const classMembers = membersFromEdges(edges);
|
|
139
|
+
// The same membership, restricted to instances a person taught (an operator
|
|
140
|
+
// assert or a teach-lane frame). A planner that quantifies over a class —
|
|
141
|
+
// "every X" — ranges over the taught individuals only: a merged corpus can
|
|
142
|
+
// type dozens of unrelated things into that class, and sweeping those into
|
|
143
|
+
// the goal makes it unsatisfiable and the search fruitless. classMembers
|
|
144
|
+
// keeps every member for the readers that want them all.
|
|
145
|
+
const taughtEdges = edges.filter((_, i) => {
|
|
146
|
+
const kind = provenanceTagToSource(typingRows[i].provenance)?.kind;
|
|
147
|
+
return kind === "operator" || kind === "teach";
|
|
148
|
+
});
|
|
149
|
+
const taughtClassMembers = membersFromEdges(taughtEdges);
|
|
125
150
|
|
|
126
151
|
// A class-bound word (an effect role or constraint term that is neither
|
|
127
152
|
// "subject" nor "target") is substituted by its class's sole member at
|
|
@@ -158,7 +183,7 @@ export function compileDomain(factRows, ruleRows) {
|
|
|
158
183
|
.filter((r) => !dynamicPredicates.has(r.predicate) && !MEMBER_EDGE_PREDICATES.has(r.predicate))
|
|
159
184
|
.sort(rowSort);
|
|
160
185
|
|
|
161
|
-
return { actions, classMembers, dynamicPredicates, ordering };
|
|
186
|
+
return { actions, classMembers, taughtClassMembers, dynamicPredicates, ordering };
|
|
162
187
|
}
|
|
163
188
|
|
|
164
189
|
const domainIndividuals = (domain) => {
|
|
@@ -312,13 +337,16 @@ function constraintViolated(action, nextState, domain) {
|
|
|
312
337
|
}
|
|
313
338
|
|
|
314
339
|
/** Every legal grounded action from `state`, with its successor.
|
|
315
|
-
* Deterministic: actions, signatures, and members are walked sorted.
|
|
316
|
-
|
|
340
|
+
* Deterministic: actions, signatures, and members are walked sorted. `scope`
|
|
341
|
+
* ("all" | "taught") picks which class members ground the moves — the plan
|
|
342
|
+
* lane passes "taught" so corpus members of a taught class never enter the
|
|
343
|
+
* movable set. */
|
|
344
|
+
export function movesFromRules(state, domain, { budget = 5000, scope = "all" } = {}) {
|
|
317
345
|
let groundings = 0;
|
|
318
346
|
for (const action of domain.actions) {
|
|
319
347
|
for (const sig of action.signatures) {
|
|
320
|
-
groundings += (domain
|
|
321
|
-
(domain
|
|
348
|
+
groundings += membersInScope(domain, sig.subjectClass, scope).length *
|
|
349
|
+
membersInScope(domain, sig.targetClass, scope).length;
|
|
322
350
|
}
|
|
323
351
|
}
|
|
324
352
|
if (groundings > budget) throw new PlanBudgetError(groundings, budget);
|
|
@@ -327,8 +355,8 @@ export function movesFromRules(state, domain, { budget = 5000 } = {}) {
|
|
|
327
355
|
for (const action of domain.actions) {
|
|
328
356
|
const [verb, particle] = action.name.split(/\s+/);
|
|
329
357
|
for (const sig of action.signatures) {
|
|
330
|
-
for (const subject of domain
|
|
331
|
-
for (const target of domain
|
|
358
|
+
for (const subject of membersInScope(domain, sig.subjectClass, scope)) {
|
|
359
|
+
for (const target of membersInScope(domain, sig.targetClass, scope)) {
|
|
332
360
|
if (subject === target) continue;
|
|
333
361
|
let ok = true;
|
|
334
362
|
for (const precond of action.preconds) {
|
|
@@ -360,7 +388,7 @@ export function movesFromRules(state, domain, { budget = 5000 } = {}) {
|
|
|
360
388
|
* state predicate. Satisfaction is a transitive walk along the goal
|
|
361
389
|
* predicate: a stacked member reaches the goal object through its support
|
|
362
390
|
* chain, which a direct row lookup cannot see. */
|
|
363
|
-
export function compileGoal(goalSpecs, domain) {
|
|
391
|
+
export function compileGoal(goalSpecs, domain, { scope = "all" } = {}) {
|
|
364
392
|
const specs = (goalSpecs || []).map((g) => ({
|
|
365
393
|
universal: Boolean(g.universal),
|
|
366
394
|
term: normTerm(g.term),
|
|
@@ -369,7 +397,7 @@ export function compileGoal(goalSpecs, domain) {
|
|
|
369
397
|
}));
|
|
370
398
|
const checks = [];
|
|
371
399
|
for (const spec of specs) {
|
|
372
|
-
const members = spec.universal ? domain
|
|
400
|
+
const members = spec.universal ? membersInScope(domain, spec.term, scope) : [spec.term];
|
|
373
401
|
if (spec.universal && members.length === 0) {
|
|
374
402
|
throw new Error(`the goal names "${spec.term}" as a class, but it has no known members`);
|
|
375
403
|
}
|
|
@@ -408,9 +408,21 @@ const BACKGROUND_FACT_PHRASES = {
|
|
|
408
408
|
"mgx:atLocation": "is found in",
|
|
409
409
|
};
|
|
410
410
|
|
|
411
|
+
/** True when a fact row is provably owned by a NON-world source (a merged
|
|
412
|
+
* corpus, a reference pack, a taught assert) rather than the loaded world. A
|
|
413
|
+
* row with no provenance is not "non-world" — a hand-built digest view carries
|
|
414
|
+
* none, and the room-look filter keeps those. World facts and their @turn
|
|
415
|
+
* snapshots tag as `world:<name>[:turnN]`. */
|
|
416
|
+
function isNonWorldSourced(row) {
|
|
417
|
+
const prov = String(row.provenance || "").trim();
|
|
418
|
+
return prov !== "" && !prov.startsWith("world:");
|
|
419
|
+
}
|
|
420
|
+
|
|
411
421
|
/** The digest's fact view: current placements (folded), exits, typing and
|
|
412
422
|
* every other surviving fact, with phrase predicates and sentence-cased
|
|
413
|
-
* subjects so the pipeline's sentence splitter sees real sentences.
|
|
423
|
+
* subjects so the pipeline's sentence splitter sees real sentences. Room text
|
|
424
|
+
* is world-sourced only — a merged corpus's overlap on a room's own vocabulary
|
|
425
|
+
* never leaks into the description. Pure. */
|
|
414
426
|
export function worldDigestRows(rows, state) {
|
|
415
427
|
const out = [];
|
|
416
428
|
const seen = new Set();
|
|
@@ -440,6 +452,12 @@ export function worldDigestRows(rows, state) {
|
|
|
440
452
|
}
|
|
441
453
|
for (const row of rows || []) {
|
|
442
454
|
if (SNAPSHOT_RE.test(row.subject)) continue; // folded above
|
|
455
|
+
// Room text comes from the world source only. A merged corpus overlaps a
|
|
456
|
+
// room's own vocabulary ("library rdfs:subClassOf literary study"), and
|
|
457
|
+
// without this those rows leak into the room description as stray sentences.
|
|
458
|
+
// A row with no provenance (a hand-built test view) is kept — the filter
|
|
459
|
+
// only drops rows a non-world source provably owns.
|
|
460
|
+
if (isNonWorldSourced(row)) continue;
|
|
443
461
|
if (PLACEMENT_PREDICATES.has(row.predicate)) continue; // folded above
|
|
444
462
|
if (VIEW_EXCLUDED_PREDICATES.has(row.predicate)) continue;
|
|
445
463
|
const exit = EXIT_PREDICATE_RE.exec(row.predicate);
|
|
@@ -909,6 +927,58 @@ async function worldOpennessAnswer(line, { memoryDir }) {
|
|
|
909
927
|
);
|
|
910
928
|
}
|
|
911
929
|
|
|
930
|
+
// The in-game orientation asides — "where am I", "what can I do", "what is the
|
|
931
|
+
// quest/goal". Without a world-state answer these fall through to the ordinary
|
|
932
|
+
// lanes and misroute: "where am I" reads "I" as a module name, "what can I do"
|
|
933
|
+
// walls, and "what is the goal" answers from corpus vocabulary about the word
|
|
934
|
+
// "goal". A live world answers each from its own fold first.
|
|
935
|
+
const WORLD_WHERE_AM_I_RE = /^where\s+am\s+i(?:\s+now)?[?.!\s]*$/i;
|
|
936
|
+
const WORLD_OPTIONS_RE = /^(?:what\s+can\s+i\s+do(?:\s+(?:here|now))?|what\s+are\s+my\s+options|what\s+(?:should|do)\s+i\s+do(?:\s+(?:here|now))?|what\s+now)[?.!\s]*$/i;
|
|
937
|
+
const WORLD_QUEST_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+|my\s+)?(?:quest|goal|objective|mission|aim)|what\s+am\s+i\s+(?:trying\s+to\s+do|(?:supposed|meant)\s+to\s+do)|what\s+do\s+i\s+do\s+here)[?.!\s]*$/i;
|
|
938
|
+
|
|
939
|
+
/** The in-game orientation asides, answered from the world fold: the player's
|
|
940
|
+
* room, the room's real affordances, and the world's objective. Null when the
|
|
941
|
+
* line is none of them, so an ordinary question keeps its lane. */
|
|
942
|
+
async function worldContextAnswer(line, { memoryDir }) {
|
|
943
|
+
const l = String(line).trim();
|
|
944
|
+
const asksWhere = WORLD_WHERE_AM_I_RE.test(l);
|
|
945
|
+
const asksOptions = WORLD_OPTIONS_RE.test(l);
|
|
946
|
+
const asksQuest = WORLD_QUEST_RE.test(l);
|
|
947
|
+
if (!asksWhere && !asksOptions && !asksQuest) return null;
|
|
948
|
+
let rows;
|
|
949
|
+
try { rows = readFactRows(await loadMemory(memoryDir)); } catch { return null; }
|
|
950
|
+
const state = foldWorldState(rows);
|
|
951
|
+
const here = state.placements.get("player")?.object ?? null;
|
|
952
|
+
|
|
953
|
+
if (asksWhere) {
|
|
954
|
+
return here
|
|
955
|
+
? answer(`you are in the ${here}.`, "ADVENTURE — where-am-I aside: the player's own room from the current world fold", { goal: "check where you are" })
|
|
956
|
+
: answer("the world has no written player position yet.", "ADVENTURE — where-am-I aside: no player placement", { miss: true, goal: "check where you are" });
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (asksOptions) {
|
|
960
|
+
const actions = here ? roomAffordances(rows, state, here) : [];
|
|
961
|
+
return answer(
|
|
962
|
+
actions.length ? `you can: ${actions.join(", ")}.` : `nothing obvious here — say "look" to look around${here ? ` the ${here}` : ""}.`,
|
|
963
|
+
`ADVENTURE — options aside: the ${here}'s roomAffordances, the same list "look" appends`,
|
|
964
|
+
{ goal: "see what you can do here" },
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const objectiveId = rows.find((r) => r.predicate === "mgx:is-objective" && r.object === "true")?.subject ?? null;
|
|
969
|
+
return objectiveId
|
|
970
|
+
? answer(
|
|
971
|
+
`your goal is to find the ${objectiveId} and pick it up.`,
|
|
972
|
+
`ADVENTURE — quest aside: the world's objective (${objectiveId}), named without spoiling where it is`,
|
|
973
|
+
{ goal: `find the ${objectiveId}` },
|
|
974
|
+
)
|
|
975
|
+
: answer(
|
|
976
|
+
`this world sets no explicit goal — explore it, and say "look" to see your options.`,
|
|
977
|
+
"ADVENTURE — quest aside: no objective marker in this world",
|
|
978
|
+
{ goal: "explore the world" },
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
912
982
|
async function inventoryAnswer({ memoryDir, graph }) {
|
|
913
983
|
const memory = await loadMemory(memoryDir);
|
|
914
984
|
const rows = readFactRows(memory);
|
|
@@ -1033,5 +1103,7 @@ export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "
|
|
|
1033
1103
|
if (whereAside) return whereAside;
|
|
1034
1104
|
const opennessAside = await worldOpennessAnswer(line, { memoryDir });
|
|
1035
1105
|
if (opennessAside) return opennessAside;
|
|
1106
|
+
const contextAside = await worldContextAnswer(line, { memoryDir });
|
|
1107
|
+
if (contextAside) return contextAside;
|
|
1036
1108
|
return null; // a mid-game aside — the ordinary lanes answer, world untouched
|
|
1037
1109
|
}
|
package/src/services/chat.mjs
CHANGED
|
@@ -2555,6 +2555,10 @@ const GOAL_CONJUNCT_RE = new RegExp(
|
|
|
2555
2555
|
// three only REPORT.
|
|
2556
2556
|
const PLAN_WHAT_NEXT_RE = /^(?:what(?:'s|\s+is)?|whats)\s+the\s+next\s+move[?.!\s]*$/i;
|
|
2557
2557
|
const PLAN_MOVE_COUNT_RE = /^how\s+many\s+moves(?:\s+(?:are\s+(?:there|left)|remain(?:ing)?|left|to\s+go|in\s+the\s+plan|total))?[?.!\s]*$/i;
|
|
2558
|
+
// "what is the goal" while a goal is held — a read-back off planState, so a
|
|
2559
|
+
// mid-plan aside never falls to the child-pack lane and answers from corpus
|
|
2560
|
+
// vocabulary about the word "goal".
|
|
2561
|
+
const PLAN_GOAL_READBACK_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+|my\s+)?goal|remind\s+me\s+(?:of\s+|what\s+)?the\s+goal(?:\s+is)?|what\s+am\s+i\s+solving\s+for|what\s+goal(?:'s|\s+is)\s+(?:set|held))[?.!\s]*$/i;
|
|
2558
2562
|
// "is that really the minimum number of moves?" / "could there be a shorter
|
|
2559
2563
|
// plan than that?" — a confirmation of the planner's own optimality claim,
|
|
2560
2564
|
// not a request to count anything (without this it fell to the unrelated
|
|
@@ -5798,8 +5802,38 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
5798
5802
|
"mgx:similarTo": "is similar to",
|
|
5799
5803
|
"mgx:relatedTo": "is related to",
|
|
5800
5804
|
"mgx:symbolOf": "is a symbol of",
|
|
5805
|
+
// A loaded adventure world's placement predicates, so a describe read-back of
|
|
5806
|
+
// a visible prop reads as English ("lamp is in the study") instead of the
|
|
5807
|
+
// mechanical -s fold garbling them ("lamp locateds in study"). The world's
|
|
5808
|
+
// SECRET/mechanics predicates (a hidden object's location, the objective
|
|
5809
|
+
// marker, the lock/open/NPC internals) are kept out of the describe lane
|
|
5810
|
+
// entirely by WORLD_INTERNAL_PREDICATES below, so they never render at all.
|
|
5811
|
+
"mgx:currently-in": "is in",
|
|
5812
|
+
"mgx:located-in": "is in",
|
|
5813
|
+
"mgx:fixed-in": "is fixed in",
|
|
5814
|
+
"mgx:stands-locked-in": "stands locked in",
|
|
5815
|
+
"mgx:works-in": "works in",
|
|
5801
5816
|
};
|
|
5802
5817
|
|
|
5818
|
+
/** The world-mechanics predicates the generic describe read-back must never
|
|
5819
|
+
* surface: a hidden object's location and the objective marker spoil the
|
|
5820
|
+
* puzzle, and the lock/container/open/NPC-schedule flags are datatype internals
|
|
5821
|
+
* the adventure's own readers answer in-game. Mirrors adventure.mjs's own
|
|
5822
|
+
* VIEW_EXCLUDED_PREDICATES — the same discipline the room-look digest uses. */
|
|
5823
|
+
const WORLD_INTERNAL_PREDICATES = new Set([
|
|
5824
|
+
"mgx:hidden-in", "mgx:is-objective", "mgx:unlocks-with",
|
|
5825
|
+
"mgx:is-npc", "mgx:acts-on-turn", "mgx:acts-toward",
|
|
5826
|
+
"mgx:is-container", "mgx:is-open",
|
|
5827
|
+
]);
|
|
5828
|
+
|
|
5829
|
+
/** The world PLACEMENT predicates carry curated phrases above so they render as
|
|
5830
|
+
* English, but they must stay OUT of the query-marker families derived from
|
|
5831
|
+
* FACT_PREDICATE_PHRASES — "what is in the study" is a members-of-class query,
|
|
5832
|
+
* not a reverse placement lookup, and "is in" is far too broad an anchor. */
|
|
5833
|
+
const WORLD_PLACEMENT_PREDICATES = new Set([
|
|
5834
|
+
"mgx:currently-in", "mgx:located-in", "mgx:fixed-in", "mgx:stands-locked-in", "mgx:works-in",
|
|
5835
|
+
]);
|
|
5836
|
+
|
|
5803
5837
|
/** The MECHANICAL fallback for a predicate this table has no curated entry
|
|
5804
5838
|
* for — specifically generalVerbTeach's minted "mgx:<lemma>" predicates
|
|
5805
5839
|
* ("mgx:eat", "mgx:drive", …) — the mechanical INVERSE of singularizeSurface's
|
|
@@ -5889,6 +5923,7 @@ function relationRoleWord(predicate) {
|
|
|
5889
5923
|
// (no curated second table) — the single-letter "a" is excluded, too short
|
|
5890
5924
|
// to anchor on without risking eating a genuine multi-word subject.
|
|
5891
5925
|
const TRAILING_PREDICATE_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
5926
|
+
.filter(([predicate]) => !WORLD_PLACEMENT_PREDICATES.has(predicate))
|
|
5892
5927
|
.map(([predicate, phrase]) => {
|
|
5893
5928
|
const m = /^(?:is|are)\s+(.+)$/i.exec(phrase);
|
|
5894
5929
|
return m ? { predicate, marker: m[1].trim().toLowerCase() } : null;
|
|
@@ -6654,7 +6689,7 @@ const REVERSE_PREDICATE_EXCLUDE = new Set([
|
|
|
6654
6689
|
"mgx:ownedBy", "owl:disjointWith", "mgx:hasProperty", "mgx:receivesAction",
|
|
6655
6690
|
]);
|
|
6656
6691
|
const REVERSE_PREDICATE_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
6657
|
-
.filter(([predicate]) => !REVERSE_PREDICATE_EXCLUDE.has(predicate))
|
|
6692
|
+
.filter(([predicate]) => !REVERSE_PREDICATE_EXCLUDE.has(predicate) && !WORLD_PLACEMENT_PREDICATES.has(predicate))
|
|
6658
6693
|
.map(([predicate, phrase]) => ({
|
|
6659
6694
|
predicate,
|
|
6660
6695
|
re: new RegExp(`^what\\s+${escapeRegex(phrase)}\\s+(.+?)[?.!\\s]*$`, "i"),
|
|
@@ -6680,7 +6715,7 @@ const FORWARD_YESNO_EXCLUDE = new Set([
|
|
|
6680
6715
|
"mgx:ownedBy",
|
|
6681
6716
|
]);
|
|
6682
6717
|
const FORWARD_YESNO_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
6683
|
-
.filter(([predicate]) => !FORWARD_YESNO_EXCLUDE.has(predicate))
|
|
6718
|
+
.filter(([predicate]) => !FORWARD_YESNO_EXCLUDE.has(predicate) && !WORLD_PLACEMENT_PREDICATES.has(predicate))
|
|
6684
6719
|
.map(([predicate, phrase]) => {
|
|
6685
6720
|
let re;
|
|
6686
6721
|
if (phrase === "can be") {
|
|
@@ -7003,8 +7038,11 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7003
7038
|
const variants = factTermVariants(normFactTerm, subject);
|
|
7004
7039
|
// factRows (trust+sourceIds-bearing), not the plain memoryFacts shape — the
|
|
7005
7040
|
// bias-weighted ranking below needs each hit's sourceIds to resolve which
|
|
7006
|
-
// bundle it came from (memory/bias.mjs's biasForRow).
|
|
7007
|
-
|
|
7041
|
+
// bundle it came from (memory/bias.mjs's biasForRow). A live world's secret
|
|
7042
|
+
// and mechanics predicates are dropped so "what is the letter" never reads
|
|
7043
|
+
// back where it's hidden or that it's the objective (WORLD_INTERNAL_PREDICATES).
|
|
7044
|
+
const subjectHits = (await factRows(memoryDir, cache))
|
|
7045
|
+
.filter((f) => variants.has(f.subject) && !WORLD_INTERNAL_PREDICATES.has(f.predicate));
|
|
7008
7046
|
let hits = predicate ? subjectHits.filter((f) => f.predicate === predicate) : subjectHits;
|
|
7009
7047
|
if (!hits.length) {
|
|
7010
7048
|
// The subject itself is known, but not under this specific relation —
|
|
@@ -7533,6 +7571,12 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7533
7571
|
hits = rows.filter((f) => overlaps(f.subject) || overlaps(f.object));
|
|
7534
7572
|
}
|
|
7535
7573
|
}
|
|
7574
|
+
// A live adventure world's mechanics never leak through the describe lane:
|
|
7575
|
+
// "what is the letter" must not read back where it's hidden or that it's the
|
|
7576
|
+
// objective, and those datatype internals render as garbled non-English
|
|
7577
|
+
// besides. The adventure's own where/openness readers answer the legitimate
|
|
7578
|
+
// in-game questions from the world fold.
|
|
7579
|
+
hits = hits.filter((f) => !WORLD_INTERNAL_PREDICATES.has(f.predicate));
|
|
7536
7580
|
// A genuinely empty result here is a real miss: "what do you know about
|
|
7537
7581
|
// the last commit" needs a TEACH-OFFER, not a bare wall — added as a LATE
|
|
7538
7582
|
// runTurn-level addition, below, alongside the sibling "what is X" offer,
|
|
@@ -10684,7 +10728,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
10684
10728
|
if (wantsLegal) {
|
|
10685
10729
|
let moves;
|
|
10686
10730
|
try {
|
|
10687
|
-
moves = movesFromRules(state, domain);
|
|
10731
|
+
moves = movesFromRules(state, domain, { scope: "taught" });
|
|
10688
10732
|
} catch (err) {
|
|
10689
10733
|
if (err instanceof PlanBudgetError) {
|
|
10690
10734
|
return { text: `too many possible moves to enumerate here (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "list the legal moves (budget exceeded)", note: "plan lane — budget decline" };
|
|
@@ -10774,7 +10818,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
10774
10818
|
}
|
|
10775
10819
|
let isGoal;
|
|
10776
10820
|
try {
|
|
10777
|
-
isGoal = compileGoal(goals, domain);
|
|
10821
|
+
isGoal = compileGoal(goals, domain, { scope: "taught" });
|
|
10778
10822
|
} catch (err) {
|
|
10779
10823
|
return { text: `I can't compile that goal: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence (uncompilable goal)", note: "plan lane — goal compile decline" };
|
|
10780
10824
|
}
|
|
@@ -10782,7 +10826,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
10782
10826
|
const maxDepth = gameConfig?.planning?.maxDepth ?? DEFAULT_GAME_CONFIG.planning.maxDepth;
|
|
10783
10827
|
let found;
|
|
10784
10828
|
try {
|
|
10785
|
-
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain), { maxDepth, stateKey: stateKeyFor });
|
|
10829
|
+
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain, { scope: "taught" }), { maxDepth, stateKey: stateKeyFor });
|
|
10786
10830
|
} catch (err) {
|
|
10787
10831
|
if (err instanceof PlanBudgetError) {
|
|
10788
10832
|
return { text: `the search space is too large (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "plan a move sequence (budget exceeded)", note: "plan lane — budget decline" };
|
|
@@ -10883,7 +10927,7 @@ async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
|
10883
10927
|
const factRows = readFactRows(payload);
|
|
10884
10928
|
const domain = compileDomain(factRows, readRuleRows(payload));
|
|
10885
10929
|
const finalState = stateFromFacts(factRows, domain);
|
|
10886
|
-
const holds = compileGoal(ps.goals, domain)(finalState);
|
|
10930
|
+
const holds = compileGoal(ps.goals, domain, { scope: "taught" })(finalState);
|
|
10887
10931
|
planHolder.state = { ...planHolder.state, done: true };
|
|
10888
10932
|
return {
|
|
10889
10933
|
text: holds
|
|
@@ -10940,6 +10984,18 @@ async function planFollowUpAnswer(query, { memoryDir, planHolder, pendingPager =
|
|
|
10940
10984
|
};
|
|
10941
10985
|
}
|
|
10942
10986
|
|
|
10987
|
+
if (PLAN_GOAL_READBACK_RE.test(q)) {
|
|
10988
|
+
if (!ps || !(ps.goalTexts?.length || ps.goals?.length)) return null;
|
|
10989
|
+
const goalText = ps.goalTexts?.length ? ps.goalTexts.join("; ") : "the goal you set";
|
|
10990
|
+
const status = activePlan
|
|
10991
|
+
? ` A plan is ready — ${ps.actions.length} move${ps.actions.length === 1 ? "" : "s"}; say "next" to step through it.`
|
|
10992
|
+
: ' Say "solve it" when the board is taught.';
|
|
10993
|
+
return {
|
|
10994
|
+
text: `the goal is that ${goalText}.${status}`,
|
|
10995
|
+
deduced: "read back the held goal",
|
|
10996
|
+
note: "PLAN FOLLOW-UP — goal read-back from the held planState",
|
|
10997
|
+
};
|
|
10998
|
+
}
|
|
10943
10999
|
if (PLAN_WHAT_NEXT_RE.test(q)) {
|
|
10944
11000
|
if (!activePlan) return null;
|
|
10945
11001
|
if (ps.done || ps.cursor >= ps.actions.length) {
|
|
@@ -381,6 +381,68 @@ async function runToldFactTurn(match, { planHolder, memoryDir, cache, gameConfig
|
|
|
381
381
|
});
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
// ---- in-game orientation asides ---------------------------------------------
|
|
385
|
+
//
|
|
386
|
+
// "where is the spider", "where am I", "what can I do", "what is the goal" —
|
|
387
|
+
// while the board is live these must answer from the board, not fall through to
|
|
388
|
+
// the code-graph lanes, where "where is the spider" reads "spider" as a module
|
|
389
|
+
// name and "what is the goal" answers from corpus vocabulary. There is no
|
|
390
|
+
// player piece here (both agents move on their own), so "where am I" reports
|
|
391
|
+
// the watcher stance and where the pieces stand.
|
|
392
|
+
|
|
393
|
+
const SF_WHERE_AGENT_RE = /^where(?:'s|\s+is|\s+are)\s+(?:the\s+)?(spider|fly)(?:-\d+)?(?:\s+now)?[?.!\s]*$/i;
|
|
394
|
+
const SF_WHERE_AM_I_RE = /^where\s+am\s+i(?:\s+now)?[?.!\s]*$/i;
|
|
395
|
+
const SF_OPTIONS_RE = /^(?:what\s+can\s+i\s+do(?:\s+(?:here|now))?|what\s+are\s+my\s+options|what\s+(?:should|do)\s+i\s+do(?:\s+(?:here|now))?|what\s+now)[?.!\s]*$/i;
|
|
396
|
+
const SF_GOAL_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+|my\s+)?(?:goal|objective|point|quest|aim)|what\s+are\s+they\s+(?:doing|trying\s+to\s+do)|what\s+am\s+i\s+(?:trying\s+to\s+do|(?:supposed|meant)\s+to\s+do))[?.!\s]*$/i;
|
|
397
|
+
|
|
398
|
+
const WATCHER_STANCE = 'you have no piece here — both agents move on their own. Watch, say "tick" to advance, or address one, e.g. "@spider the fly is east".';
|
|
399
|
+
|
|
400
|
+
const positionsOfKind = (kind, state) =>
|
|
401
|
+
liveIdsOfKind(kind, state).map((id) => `${id} at ${state.placements.get(id).cell}`);
|
|
402
|
+
|
|
403
|
+
async function spiderFlyContextAnswer(line, { memoryDir }) {
|
|
404
|
+
const l = String(line).trim();
|
|
405
|
+
const whereAgent = l.match(SF_WHERE_AGENT_RE);
|
|
406
|
+
const asksWhereMe = SF_WHERE_AM_I_RE.test(l);
|
|
407
|
+
const asksOptions = SF_OPTIONS_RE.test(l);
|
|
408
|
+
const asksGoal = SF_GOAL_RE.test(l);
|
|
409
|
+
if (!whereAgent && !asksWhereMe && !asksOptions && !asksGoal) return null;
|
|
410
|
+
let state;
|
|
411
|
+
try { state = foldSpiderFlyState(readFactRows(await loadMemory(memoryDir))); } catch { return null; }
|
|
412
|
+
|
|
413
|
+
if (whereAgent) {
|
|
414
|
+
const kind = whereAgent[1].toLowerCase();
|
|
415
|
+
const positions = positionsOfKind(kind, state);
|
|
416
|
+
return {
|
|
417
|
+
text: positions.length ? `${positions.join("; ")}.` : `there's no live ${kind} on the board right now.`,
|
|
418
|
+
lane: "game-answer",
|
|
419
|
+
note: `SPIDER-FLY — where-aside: ${kind} positions from the current board fold`,
|
|
420
|
+
goal: `find the ${kind}`,
|
|
421
|
+
miss: !positions.length,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (asksWhereMe) {
|
|
426
|
+
return { text: WATCHER_STANCE, lane: "game-inform", note: "SPIDER-FLY — where-am-I aside: the watcher stance (no player piece)", goal: "understand your role" };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (asksOptions) {
|
|
430
|
+
return {
|
|
431
|
+
text: 'say "tick" to advance a turn, or address an agent — e.g. "@spider the fly is east" or "@spider the fly is at cell-7-3" to plant a belief. Say "stop watching" to end.',
|
|
432
|
+
lane: "game-inform",
|
|
433
|
+
note: "SPIDER-FLY — options aside: the live game's own commands",
|
|
434
|
+
goal: "see what you can do",
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
text: "the spider hunts the fly; the fly tries to stay clear. You watch it play out — plant a belief to nudge one, or say \"tick\" to advance.",
|
|
440
|
+
lane: "game-inform",
|
|
441
|
+
note: "SPIDER-FLY — goal aside: the game's predator/prey objective",
|
|
442
|
+
goal: "understand the game",
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
384
446
|
// ---- the lane ------------------------------------------------------------
|
|
385
447
|
|
|
386
448
|
/**
|
|
@@ -468,5 +530,8 @@ export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache =
|
|
|
468
530
|
return runTickAndRender({ planHolder, memoryDir, cache, toldFacts: [], gameConfig });
|
|
469
531
|
}
|
|
470
532
|
|
|
533
|
+
const contextAside = await spiderFlyContextAnswer(line, { memoryDir });
|
|
534
|
+
if (contextAside) return contextAside;
|
|
535
|
+
|
|
471
536
|
return null; // an unaddressed aside — the ordinary lanes answer, board untouched
|
|
472
537
|
}
|