@polycode-projects/the-mechanical-code-talker 2.10.5 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +2 -2
  2. package/corpus/sprites/src/sprite-facts.jsonl +18 -0
  3. package/corpus/worlds/manifest.json +5 -5
  4. package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
  5. package/corpus/worlds/src/ashcombe-hall.jsonl +27 -0
  6. package/data/sprites/book-icon.toml +12 -0
  7. package/data/sprites/cellar-icon.toml +12 -0
  8. package/data/sprites/drawing-room-icon.toml +13 -0
  9. package/data/sprites/garden-icon.toml +12 -0
  10. package/data/sprites/kitchen-icon.toml +13 -0
  11. package/data/sprites/library-icon.toml +12 -0
  12. package/data/sprites/pan-icon.toml +11 -0
  13. package/data/sprites/study-icon.toml +12 -0
  14. package/data/templates/responses.jsonl +1 -0
  15. package/package.json +5 -2
  16. package/src/adapters/corpus/wikipedia-live.mjs +182 -26
  17. package/src/adapters/corpus/worlds-pack.mjs +8 -2
  18. package/src/adapters/toml-config.mjs +6 -0
  19. package/src/domain/ask-vocab.mjs +17 -0
  20. package/src/domain/ask.mjs +51 -1
  21. package/src/domain/grammar/ace.mjs +43 -3
  22. package/src/domain/interpret/normalize.mjs +6 -2
  23. package/src/domain/interpret/strategies/grammar.mjs +47 -17
  24. package/src/domain/interpret/strategies/keywords.mjs +53 -4
  25. package/src/domain/memory/trust.mjs +11 -0
  26. package/src/domain/router/registry.mjs +8 -1
  27. package/src/domain/worlds-pack.mjs +50 -0
  28. package/src/services/adventure-autoplay.mjs +5 -2
  29. package/src/services/adventure-viz.mjs +301 -33
  30. package/src/services/adventure.mjs +162 -14
  31. package/src/services/chat-page-viz.mjs +265 -189
  32. package/src/services/chat-session.mjs +15 -5
  33. package/src/services/chat.mjs +471 -79
  34. package/src/services/code-explorer-viz.mjs +183 -75
  35. package/src/services/extract-facts.mjs +118 -28
  36. package/src/services/ingest-viz.mjs +328 -79
  37. package/src/services/ledger-viz.mjs +99 -0
  38. package/src/services/memory-panel-viz.mjs +159 -0
  39. package/src/services/research.mjs +266 -0
  40. package/src/services/sentences.mjs +19 -0
  41. package/src/services/spider-fly-viz.mjs +21 -5
  42. package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
  43. package/src/surfaces/web/chat-browser-entry.mjs +28 -11
  44. package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
  45. package/src/surfaces/web/ingest-browser-entry.mjs +123 -41
  46. package/src/surfaces/web/ledger-browser-entry.mjs +10 -4
  47. package/src/surfaces/web/memory-ask-browser.bundle.js +116 -116
  48. package/src/surfaces/web/memory-stats.mjs +53 -0
@@ -162,7 +162,7 @@ export async function unclaimedAdventureOpening(line, { env }) {
162
162
  * the meta opening speaks. */
163
163
  async function resumedPosition(memoryDir) {
164
164
  try {
165
- const state = foldWorldState(readFactRows(await loadMemory(memoryDir)));
165
+ const state = foldWorldState(worldActionRows(readFactRows(await loadMemory(memoryDir))));
166
166
  if (!state.turnCount) return null;
167
167
  return state.placements.get("player")?.object ?? null;
168
168
  } catch {
@@ -176,15 +176,35 @@ const SNAPSHOT_RE = /^(.+)@turn(\d+)$/;
176
176
  const PLACEMENT_PREDICATES = new Set([
177
177
  "mgx:currently-in", "mgx:located-in", "mgx:fixed-in", "mgx:stands-locked-in", "mgx:hidden-in",
178
178
  ]);
179
+ // Supplemental positional relations: where a thing sits WITHIN its room,
180
+ // never where its room is. Folded like placements (newest per subject) but
181
+ // kept apart, since visibility and movement key on the placement, not on
182
+ // which surface a thing rests against.
183
+ const POSITION_PREDICATES = new Set(["mgx:on-top-of", "mgx:on-plane", "mgx:under"]);
179
184
  const OPEN_PREDICATE = "mgx:is-open";
180
185
  const EXIT_PREDICATE_RE = /^mgx:has-exit-([a-z]+)$/;
181
186
 
187
+ /** The rows a live world's STATE fold may see: those the world itself wrote
188
+ * (provenance empty, or `world:*` — the loaded shard and its @turn
189
+ * snapshots), never a taught assert (`teach:chat:*`) or a merged corpus. The
190
+ * game world changes only through actions; a locative fact the player TAUGHT
191
+ * mid-game is their own note, and must not silently move a prop. Digest and
192
+ * background-colour paths keep the unfiltered rows — a taught fact still
193
+ * reads back as prose, it just never folds into the playable state. */
194
+ export function worldActionRows(rows) {
195
+ return (rows || []).filter((r) => {
196
+ const prov = String(r.provenance || "").trim();
197
+ return prov === "" || prov.startsWith("world:");
198
+ });
199
+ }
200
+
182
201
  /** Fold fact rows into the CURRENT world state: per subject, the newest
183
202
  * placement (base row = turn 0, @turnN snapshots override), the newest
184
203
  * open/closed state, the exit map, and the turn counter (the largest @turnN
185
204
  * suffix written so far — derived, never stored). Pure. */
186
205
  export function foldWorldState(factRows) {
187
206
  const placements = new Map(); // subject -> { predicate, object, turn }
207
+ const positions = new Map(); // subject -> { predicate, object, turn }
188
208
  const openness = new Map(); // subject -> { open, turn }
189
209
  const exits = new Map(); // room -> Map(direction -> room)
190
210
  let turnCount = 0;
@@ -198,6 +218,11 @@ export function foldWorldState(factRows) {
198
218
  if (!prior || turn >= prior.turn) placements.set(base, { predicate: row.predicate, object: row.object, turn });
199
219
  continue;
200
220
  }
221
+ if (POSITION_PREDICATES.has(row.predicate)) {
222
+ const prior = positions.get(base);
223
+ if (!prior || turn >= prior.turn) positions.set(base, { predicate: row.predicate, object: row.object, turn });
224
+ continue;
225
+ }
201
226
  if (row.predicate === OPEN_PREDICATE) {
202
227
  const prior = openness.get(base);
203
228
  if (!prior || turn >= prior.turn) openness.set(base, { open: row.object === "true", turn });
@@ -209,7 +234,46 @@ export function foldWorldState(factRows) {
209
234
  exits.get(row.subject).set(exit[1], row.object);
210
235
  }
211
236
  }
212
- return { placements, openness, exits, turnCount };
237
+ return { placements, positions, openness, exits, turnCount };
238
+ }
239
+
240
+ /** A subject's CURRENT within-room position, or null. A position goes stale
241
+ * the moment the subject is placed somewhere new: taking the lamp writes a
242
+ * later-turn placement, so its turn-0 `on-top-of desk` no longer holds and
243
+ * no extra write is needed to retract it. */
244
+ export function currentPosition(state, subject) {
245
+ const pos = state.positions.get(subject);
246
+ if (!pos) return null;
247
+ const place = state.placements.get(subject);
248
+ if (place && pos.turn < place.turn) return null;
249
+ return pos;
250
+ }
251
+
252
+ /** The default surface a subject's class implies, walking its rdf:type and
253
+ * rdfs:subClassOf edges for an `mgx:default-plane` fact. A non-floor plane
254
+ * (wall, ceiling) wins over floor wherever both are reachable — a portrait
255
+ * typed both `furniture` (floor) and, via `painting`, wall reads as hanging
256
+ * on the wall. Returns the plane, or null when no class default applies.
257
+ * Pure. */
258
+ function classDefaultPlane(rows, subject) {
259
+ const edgesFrom = (node) => (rows || [])
260
+ .filter((r) => r.subject === node && (r.predicate === "rdf:type" || r.predicate === "rdfs:subClassOf"))
261
+ .map((r) => r.object);
262
+ const planeOf = (node) => (rows || [])
263
+ .find((r) => r.subject === node && r.predicate === "mgx:default-plane")?.object ?? null;
264
+ const seen = new Set([subject]);
265
+ const queue = edgesFrom(subject);
266
+ let fallback = null;
267
+ while (queue.length) {
268
+ const node = queue.shift();
269
+ if (seen.has(node)) continue;
270
+ seen.add(node);
271
+ const plane = planeOf(node);
272
+ if (plane && plane !== "floor") return plane;
273
+ if (plane && !fallback) fallback = plane;
274
+ queue.push(...edgesFrom(node));
275
+ }
276
+ return fallback;
213
277
  }
214
278
 
215
279
  // ---- the world interpreter ---------------------------------------------------
@@ -384,11 +448,20 @@ async function writeWorldTurn(memoryDir, world, k, facts, cache) {
384
448
  const VIEW_EXCLUDED_PREDICATES = new Set([
385
449
  "mgx:hidden-in", "mgx:is-open", "mgx:is-npc", "mgx:is-container",
386
450
  "mgx:unlocks-with", "mgx:acts-on-turn", "mgx:acts-toward",
387
- // mgx:is-objective (PLAN_GAMES_UPLIFT_V2.md Part B) is an internal marker
388
- // for auto-play's goal inference the same information the opening
389
- // narration already tells a human player in prose, never meant to surface
390
- // as a raw, unphrased triple ("Letter mgx:is-objective true.") itself.
451
+ // is-objective is an internal marker for auto-play's goal inference — the
452
+ // same information the opening narration already tells a human player in
453
+ // prose, never meant to surface as a raw, unphrased triple ("Letter
454
+ // mgx:is-objective true.") itself.
391
455
  "mgx:is-objective",
456
+ // Staff knowledge is the whole puzzle: a room look must never leak
457
+ // "Gardener knows-where letter" or the game is spoiled. It reaches the
458
+ // player only through the talk lane, which resolves each pointer live.
459
+ "mgx:knows-where", "mgx:knows-objective", "mgx:knows-about",
460
+ // Class-schema facts describe the ontology, not the scene. default-contains
461
+ // is already materialized into real instances at load; default-plane and
462
+ // subClassOf drive positional rendering by their own readers, and read as
463
+ // raw triples if they land in room prose.
464
+ "mgx:default-contains", "mgx:default-plane", "rdfs:subClassOf",
392
465
  ]);
393
466
 
394
467
  const sentenceCase = (term) => String(term).charAt(0).toUpperCase() + String(term).slice(1);
@@ -450,6 +523,18 @@ export function worldDigestRows(rows, state) {
450
523
  }[place.predicate];
451
524
  if (phrase) push(subject, phrase, place.object);
452
525
  }
526
+ // Where a placed thing sits within its room — an instance position (the
527
+ // lamp on the desk) if one is current, else a notable class default (a
528
+ // portrait on the wall). Floor is the unremarkable default the room view
529
+ // already assumes, so it is left unsaid.
530
+ const POSITION_PHRASE = { "mgx:on-top-of": "is on the", "mgx:on-plane": "is on the", "mgx:under": "is under the" };
531
+ for (const [subject, place] of state.placements) {
532
+ if (place.predicate === "mgx:hidden-in" || place.object === "player") continue;
533
+ const pos = currentPosition(state, subject);
534
+ if (pos && POSITION_PHRASE[pos.predicate]) { push(subject, POSITION_PHRASE[pos.predicate], pos.object); continue; }
535
+ const plane = classDefaultPlane(rows, subject);
536
+ if (plane && plane !== "floor") push(subject, "is usually on the", plane);
537
+ }
453
538
  for (const row of rows || []) {
454
539
  if (SNAPSHOT_RE.test(row.subject)) continue; // folded above
455
540
  // Room text comes from the world source only. A merged corpus overlaps a
@@ -459,6 +544,7 @@ export function worldDigestRows(rows, state) {
459
544
  // only drops rows a non-world source provably owns.
460
545
  if (isNonWorldSourced(row)) continue;
461
546
  if (PLACEMENT_PREDICATES.has(row.predicate)) continue; // folded above
547
+ if (POSITION_PREDICATES.has(row.predicate)) continue; // folded above
462
548
  if (VIEW_EXCLUDED_PREDICATES.has(row.predicate)) continue;
463
549
  const exit = EXIT_PREDICATE_RE.exec(row.predicate);
464
550
  if (exit) { push(row.subject, `has an exit ${exit[1]} to the`, row.object); continue; }
@@ -537,10 +623,54 @@ function containerDatatypeState(state, object) {
537
623
  return rows;
538
624
  }
539
625
 
626
+ /** The knowledge a person shares when talked to, resolved against the LIVE
627
+ * world fold this turn — never a frozen string, so it stays true as the
628
+ * world changes and honest when it doesn't know. `knows-where` reveals a
629
+ * thing's current location, a hidden one included: talking to the staff is
630
+ * the sanctioned way to learn a hiding place, while the where-is aside keeps
631
+ * declining. `knows-objective` states the quest. `knows-about` topics come
632
+ * back for the caller to digest. Pure. */
633
+ export function personKnowledgeLines(rows, state, person) {
634
+ const lines = [];
635
+ for (const objective of factObjects(rows, person, "mgx:knows-objective")) {
636
+ lines.push(`the ${objective} is what you're after — find it and carry it out of the house.`);
637
+ }
638
+ for (const thing of factObjects(rows, person, "mgx:knows-where")) {
639
+ const place = state.placements.get(thing);
640
+ if (!place) continue;
641
+ lines.push(isTyped(rows, thing, "person")
642
+ ? `you'll find the ${thing} in the ${place.object}.`
643
+ : `the ${thing} is in the ${place.object}.`);
644
+ }
645
+ return { lines, aboutTopics: factObjects(rows, person, "mgx:knows-about") };
646
+ }
647
+
648
+ /** What a person can report from where they stand this turn — derived each
649
+ * turn, never stored: who and what shares their room, each container's
650
+ * open/locked status, and what unlocks a locked one there. Pure. */
651
+ export function personRoomReport(rows, state, person) {
652
+ const room = state.placements.get(person)?.object ?? null;
653
+ if (!room) return "";
654
+ const here = [...state.placements.keys()]
655
+ .filter((s) => s !== person && s !== "player" && visibleRoomOf(s, { rows, state }) === room)
656
+ .sort();
657
+ const parts = [];
658
+ if (here.length) parts.push(`here in the ${room}: the ${here.join(", the ")}.`);
659
+ for (const thing of here) {
660
+ if (!isContainer(rows, thing)) continue;
661
+ parts.push(containerStatusPhrase(thing, { state }));
662
+ const key = factObjects(rows, thing, "mgx:unlocks-with")[0];
663
+ if (key && state.placements.get(thing)?.predicate === "mgx:stands-locked-in") {
664
+ parts.push(`the ${thing} needs the ${key} to open.`);
665
+ }
666
+ }
667
+ return parts.join(" ");
668
+ }
669
+
540
670
  async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
541
671
  const memory = await loadMemory(memoryDir);
542
672
  const rows = readFactRows(memory);
543
- const state = foldWorldState(rows);
673
+ const state = foldWorldState(worldActionRows(rows));
544
674
  const here = state.placements.get("player")?.object ?? null;
545
675
  const noteFor = (detail) => `ADVENTURE — ${detail}`;
546
676
 
@@ -608,6 +738,24 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
608
738
  );
609
739
  }
610
740
  const person = isTyped(rows, object, "person");
741
+ // Talking to a person is the game's reveal channel: the staff share what
742
+ // they know (a hiding place, the quest, a topic) and report their own
743
+ // room, all resolved from the live fold this turn.
744
+ if (cmd.verb === "talk" && person) {
745
+ const { lines, aboutTopics } = personKnowledgeLines(rows, state, object);
746
+ const aboutLines = [];
747
+ for (const topic of aboutTopics) {
748
+ const digested = await worldDigest(topic, { memoryDir, memory, rows, state, graph });
749
+ if (digested) aboutLines.push(digested);
750
+ }
751
+ const report = personRoomReport(rows, state, object);
752
+ const said = [...lines, ...aboutLines, report].filter(Boolean).join(" ");
753
+ return answer(
754
+ said ? `the ${object} says: ${said}` : `the ${object} has nothing to tell you right now.`,
755
+ noteFor(`talk — the ${object}'s live knowledge (knows-where/objective/about from the current fold) and a derived room report`),
756
+ { goal: `talk to the ${object}` },
757
+ );
758
+ }
611
759
  const digest = await worldDigest(object, { memoryDir, memory, rows, state, graph });
612
760
  const body = digest ?? `nothing more about the ${object} is written down yet.`;
613
761
  const containerNote = !person && isContainer(rows, object) ? ` ${containerStatusPhrase(object, { state })}` : "";
@@ -649,7 +797,7 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
649
797
  // player is never left to retype "look" to see what just changed.
650
798
  const freshMemory = await loadMemory(memoryDir);
651
799
  const freshRows = readFactRows(freshMemory);
652
- const freshState = foldWorldState(freshRows);
800
+ const freshState = foldWorldState(worldActionRows(freshRows));
653
801
  const relookDigest = await worldDigest(playerRoomAfter, { memoryDir, memory: freshMemory, rows: freshRows, state: freshState, graph });
654
802
  const actions = roomAffordances(freshRows, freshState, playerRoomAfter);
655
803
  const relook = `you are in the ${playerRoomAfter}. ${relookDigest ?? "Nothing more about it is written down yet."}${affordanceSuffix(actions)}`;
@@ -874,13 +1022,13 @@ async function worldWhereAnswer(line, { memoryDir }) {
874
1022
  const thing = normFactTerm(m[1]);
875
1023
  let rows;
876
1024
  try { rows = readFactRows(await loadMemory(memoryDir)); } catch { return null; }
877
- const state = foldWorldState(rows);
1025
+ const state = foldWorldState(worldActionRows(rows));
878
1026
  const place = state.placements.get(thing);
879
1027
  if (!place) return null;
880
1028
  if (place.predicate === "mgx:hidden-in") {
881
1029
  return answer(
882
- `nothing you've seen says where the ${thing} is.`,
883
- `ADVENTURE — where-aside: ${thing} is hidden; declined without naming the hiding place`,
1030
+ `nothing you've seen says where the ${thing} is. Someone in the house may know — try talking to the staff.`,
1031
+ `ADVENTURE — where-aside: ${thing} is hidden; declined without naming the hiding place, pointed at the talk lane`,
884
1032
  { miss: true, goal: `locate the ${thing}` },
885
1033
  );
886
1034
  }
@@ -916,7 +1064,7 @@ async function worldOpennessAnswer(line, { memoryDir }) {
916
1064
  const askedOpen = /^open$/i.test(m[2]);
917
1065
  let rows;
918
1066
  try { rows = readFactRows(await loadMemory(memoryDir)); } catch { return null; }
919
- const state = foldWorldState(rows);
1067
+ const state = foldWorldState(worldActionRows(rows));
920
1068
  const openness = state.openness.get(thing);
921
1069
  if (!openness) return null;
922
1070
  const matches = askedOpen ? openness.open : !openness.open;
@@ -947,7 +1095,7 @@ async function worldContextAnswer(line, { memoryDir }) {
947
1095
  if (!asksWhere && !asksOptions && !asksQuest) return null;
948
1096
  let rows;
949
1097
  try { rows = readFactRows(await loadMemory(memoryDir)); } catch { return null; }
950
- const state = foldWorldState(rows);
1098
+ const state = foldWorldState(worldActionRows(rows));
951
1099
  const here = state.placements.get("player")?.object ?? null;
952
1100
 
953
1101
  if (asksWhere) {
@@ -982,7 +1130,7 @@ async function worldContextAnswer(line, { memoryDir }) {
982
1130
  async function inventoryAnswer({ memoryDir, graph }) {
983
1131
  const memory = await loadMemory(memoryDir);
984
1132
  const rows = readFactRows(memory);
985
- const state = foldWorldState(rows);
1133
+ const state = foldWorldState(worldActionRows(rows));
986
1134
  const carried = [...state.placements]
987
1135
  .filter(([, p]) => p.predicate === "mgx:located-in" && p.object === "player")
988
1136
  .map(([thing]) => thing)