@polycode-projects/the-mechanical-code-talker 5.0.2 → 5.0.4

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 (37) hide show
  1. package/corpus/worlds/manifest.json +9 -9
  2. package/corpus/worlds/shards/town-square-chapel.jsonl.gz +0 -0
  3. package/corpus/worlds/shards/town-square-market.jsonl.gz +0 -0
  4. package/corpus/worlds/shards/town-square.jsonl.gz +0 -0
  5. package/corpus/worlds/src/town-square-chapel.jsonl +1 -1
  6. package/corpus/worlds/src/town-square-market.jsonl +1 -1
  7. package/corpus/worlds/src/town-square.jsonl +1 -1
  8. package/data/mudiii-assets.json +35 -5
  9. package/package.json +1 -1
  10. package/src/adapters/memory/core.mjs +236 -18
  11. package/src/domain/ask-vocab.mjs +1 -1
  12. package/src/domain/ask.mjs +20 -10
  13. package/src/domain/game-config.mjs +10 -1
  14. package/src/domain/interpret/normalize.mjs +4 -0
  15. package/src/domain/memory/retraction.mjs +232 -0
  16. package/src/domain/p2p/sync-filter.mjs +9 -1
  17. package/src/domain/spider-fly-world.mjs +80 -35
  18. package/src/domain/syllogise.mjs +128 -75
  19. package/src/services/adventure-autoplay.mjs +2 -2
  20. package/src/services/adventure-viz.mjs +12 -7
  21. package/src/services/chat.mjs +42 -14
  22. package/src/services/mud-turn.mjs +1 -1
  23. package/src/services/mud-viz.mjs +11 -2
  24. package/src/services/mudiii-scene.mjs +199 -55
  25. package/src/services/mudiii-turn.mjs +82 -1
  26. package/src/services/mudiii-viz.mjs +17 -7
  27. package/src/services/p2p-room.mjs +130 -9
  28. package/src/services/predator-prey.mjs +524 -69
  29. package/src/services/spider-fly-turn.mjs +128 -56
  30. package/src/services/spider-fly-viz.mjs +65 -71
  31. package/src/services/world-teach.mjs +3 -3
  32. package/src/surfaces/web/adventure-browser-entry.mjs +12 -3
  33. package/src/surfaces/web/memory-ask-browser.bundle.js +118 -118
  34. package/src/surfaces/web/mud-browser-entry.mjs +10 -3
  35. package/src/surfaces/web/mudiii-browser-entry.mjs +3 -3
  36. package/src/surfaces/web/spider-fly-browser-entry.mjs +26 -22
  37. package/src/services/spider-fly.mjs +0 -943
@@ -1527,14 +1527,35 @@ function bestEnvironmentTrustOpts(provenance, environments, trustOfId) {
1527
1527
  * without it removal is still correct, the survivor's environments just stay
1528
1528
  * stale until the next syllogise pass.
1529
1529
  *
1530
+ * `sourceTags` narrows the target removal to the INVOKING PARTY's own
1531
+ * record(s) — a chat `/retract` names every provenance tag its own session
1532
+ * could have asserted the triple under here (the free-form teach lane and
1533
+ * the ACE-parsed assert lane both belong to one session, under two
1534
+ * different tags), so two sources who independently asserted the same
1535
+ * triple never let one's retraction erase the other's. Omitted (or empty),
1536
+ * retraction stays group-wide (every source's record for the triple), which
1537
+ * is what mud EDIT mode wants and what every pre-existing caller of this
1538
+ * function already gets.
1539
+ *
1530
1540
  * Returns { retracted, count, budget, depth, truncated, found } — `found` is
1531
- * false when `subject ⊑ object` was never a stored fact.
1541
+ * false when `subject ⊑ object` was never a stored fact. With `sourceTags`
1542
+ * set, two more fields describe what the SCOPED removal actually did:
1543
+ * `ownRecord` (false when the invoking party never asserted the triple
1544
+ * itself, so nothing of theirs existed to retract) and `stillStands` (true
1545
+ * when another source's record keeps the triple asserted after this one's
1546
+ * record is gone — in which case nothing entailed from it is cascaded,
1547
+ * because its premise never actually stopped holding).
1532
1548
  */
1533
1549
  export async function retractSubClassOf(repoDir, subject, object, {
1534
- budget = 50, depth = 32, maxEnvironments = DEFAULT_MAX_ENVIRONMENTS, store,
1550
+ budget = 50, depth = 32, maxEnvironments = DEFAULT_MAX_ENVIRONMENTS, store, sourceTags = [],
1535
1551
  } = {}) {
1536
1552
  const { loadMemory, readFactRows, removeFacts } = requireStore(store, ["loadMemory", "readFactRows", "removeFacts"], "retractSubClassOf");
1537
1553
  const appendFactsFn = typeof store?.appendFacts === "function" ? store.appendFacts : null;
1554
+ const factRecordIdForTag = typeof store?.factRecordIdForTag === "function" ? store.factRecordIdForTag : null;
1555
+ const scoped = (sourceTags || []).filter(Boolean);
1556
+ if (scoped.length && !factRecordIdForTag) {
1557
+ throw new TypeError("retractSubClassOf needs a store option carrying factRecordIdForTag (memory/core.mjs's) to scope a retraction to sourceTags");
1558
+ }
1538
1559
  const s = normFactTerm(subject);
1539
1560
  const o = normFactTerm(object);
1540
1561
  const targetId = factIdForTriple(s, SUBCLASS_PREDICATE, o);
@@ -1543,92 +1564,121 @@ export async function retractSubClassOf(repoDir, subject, object, {
1543
1564
  const byId = new Map(rows.map((r) => [r.id, r]));
1544
1565
  if (!byId.has(targetId)) return { retracted: [], count: 0, budget, depth, truncated: false, found: false };
1545
1566
 
1546
- // Only a purely-entailed fact ever carries a walkable justification
1547
- // a fact later independently taught is never a cascade candidate at all.
1548
- const entailedRows = rows.filter((r) => environmentsOf(r).length && isPurelyEntailed(r.provenance));
1549
- // premise id -> the entailed fact ids whose environments cite it. Built
1550
- // ONCE; each round's candidate set reads it for the facts the newest
1551
- // removals could actually touch backward relevance from the same
1552
- // structure a forward pass reads forward.
1553
- const citedBy = new Map();
1554
- for (const r of entailedRows) {
1555
- for (const env of environmentsOf(r)) {
1556
- for (const premiseId of env) {
1557
- if (!citedBy.has(premiseId)) citedBy.set(premiseId, new Set());
1558
- citedBy.get(premiseId).add(r.id);
1559
- }
1560
- }
1567
+ // The invoking party's own record(s) for the target one candidate source
1568
+ // id per tag it could have asserted under filtered down to whichever
1569
+ // ones the triple is ACTUALLY recorded under. Zero of them means it was
1570
+ // never this party's own assertion, whatever else stored it.
1571
+ const targetSourceIds = byId.get(targetId).sourceIds || [];
1572
+ const candidateSourceIds = scoped.map((tag) => factRecordIdForTag(targetId, tag).slice(targetId.length + 1));
1573
+ const ownSourceIds = scoped.length ? targetSourceIds.filter((sid) => candidateSourceIds.includes(sid)) : [];
1574
+ const ownsTarget = !scoped.length || ownSourceIds.length > 0;
1575
+ if (scoped.length && !ownsTarget) {
1576
+ return { retracted: [], count: 0, budget, depth, truncated: false, found: true, ownRecord: false, stillStands: true };
1561
1577
  }
1578
+ // Another source's record keeps the triple standing even after this
1579
+ // party's own record(s) go — the premise never actually broke, so no
1580
+ // cascade runs.
1581
+ const stillStands = scoped.length > 0 && ownSourceIds.length < targetSourceIds.length;
1562
1582
 
1563
1583
  const removed = new Set([targetId]);
1564
1584
  const order = [targetId]; // deterministic report order: target first, then removal order
1565
1585
  const reground = new Map(); // survivor fact id -> the environments to persist for it
1566
1586
  let truncated = false;
1567
- let round = 0;
1568
- let newlyRemoved = [targetId];
1569
- for (; round < depth; round += 1) {
1570
- const candidateIds = new Set();
1571
- for (const id of newlyRemoved) {
1572
- for (const cited of citedBy.get(id) || []) {
1573
- if (!removed.has(cited)) candidateIds.add(cited);
1587
+
1588
+ // A scoped retraction whose triple STILL STANDS through another source's
1589
+ // record never runs the cascade at all — its premise never actually broke,
1590
+ // so nothing entailed from it loses support.
1591
+ if (!stillStands) {
1592
+ // Only a purely-entailed fact ever carries a walkable justification —
1593
+ // a fact later independently taught is never a cascade candidate at all.
1594
+ const entailedRows = rows.filter((r) => environmentsOf(r).length && isPurelyEntailed(r.provenance));
1595
+ // premise id -> the entailed fact ids whose environments cite it. Built
1596
+ // ONCE; each round's candidate set reads it for the facts the newest
1597
+ // removals could actually touch — backward relevance from the same
1598
+ // structure a forward pass reads forward.
1599
+ const citedBy = new Map();
1600
+ for (const r of entailedRows) {
1601
+ for (const env of environmentsOf(r)) {
1602
+ for (const premiseId of env) {
1603
+ if (!citedBy.has(premiseId)) citedBy.set(premiseId, new Set());
1604
+ citedBy.get(premiseId).add(r.id);
1605
+ }
1574
1606
  }
1575
1607
  }
1576
- const candidates = [...candidateIds].map((id) => byId.get(id))
1577
- .sort((a, b) => a.subject.localeCompare(b.subject) || a.predicate.localeCompare(b.predicate) || a.object.localeCompare(b.object));
1578
- if (!candidates.length) break; // fixpoint — nothing cites what just fell
1579
-
1580
- // The surviving fact set for THIS round excludes every candidate's own
1581
- // row too, not just `removed` — otherwise a candidate could trivially
1582
- // "reach itself" through its own not-yet-deleted edge, or lean on a
1583
- // sibling candidate standing on the same broken premise.
1584
- const survivors = rows.filter((r) => !removed.has(r.id) && !candidateIds.has(r.id));
1585
- const survivorIds = new Set(survivors.map((r) => r.id));
1586
- const enumerateSupport = buildSupportEnumerator(survivors);
1587
- const stillDerivable = buildSurvivorDerivabilityCheck(survivors);
1588
1608
 
1589
- let progressed = false;
1590
- let hitBudget = false;
1591
- newlyRemoved = [];
1592
- for (const c of candidates) {
1593
- if (removed.size >= budget) { hitBudget = true; break; }
1594
- // FAST PATH: an environment whose every premise still stands keeps the
1595
- // fact — pure set membership, no re-derivation. When some environments
1596
- // broke, queue the pruned set so the next retraction still sees the
1597
- // survivor (the stale-justification fix).
1598
- const environments = environmentsOf(c);
1599
- const intact = environments.filter((env) => env.every((id) => survivorIds.has(id)));
1600
- if (intact.length) {
1601
- if (intact.length !== environments.length) reground.set(c.id, intact);
1602
- continue;
1609
+ let round = 0;
1610
+ let newlyRemoved = [targetId];
1611
+ for (; round < depth; round += 1) {
1612
+ const candidateIds = new Set();
1613
+ for (const id of newlyRemoved) {
1614
+ for (const cited of citedBy.get(id) || []) {
1615
+ if (!removed.has(cited)) candidateIds.add(cited);
1616
+ }
1603
1617
  }
1604
- // ENUMERATE: a fresh premise environment among the survivors re-grounds
1605
- // the fact under new citations.
1606
- const fresh = enumerateSupport(c, { maxEnvironments });
1607
- if (fresh.length) {
1608
- reground.set(c.id, fresh);
1609
- continue;
1618
+ const candidates = [...candidateIds].map((id) => byId.get(id))
1619
+ .sort((a, b) => a.subject.localeCompare(b.subject) || a.predicate.localeCompare(b.predicate) || a.object.localeCompare(b.object));
1620
+ if (!candidates.length) break; // fixpoint nothing cites what just fell
1621
+
1622
+ // The surviving fact set for THIS round excludes every candidate's own
1623
+ // row too, not just `removed` — otherwise a candidate could trivially
1624
+ // "reach itself" through its own not-yet-deleted edge, or lean on a
1625
+ // sibling candidate standing on the same broken premise.
1626
+ const survivors = rows.filter((r) => !removed.has(r.id) && !candidateIds.has(r.id));
1627
+ const survivorIds = new Set(survivors.map((r) => r.id));
1628
+ const enumerateSupport = buildSupportEnumerator(survivors);
1629
+ const stillDerivable = buildSurvivorDerivabilityCheck(survivors);
1630
+
1631
+ let progressed = false;
1632
+ let hitBudget = false;
1633
+ newlyRemoved = [];
1634
+ for (const c of candidates) {
1635
+ if (removed.size >= budget) { hitBudget = true; break; }
1636
+ // FAST PATH: an environment whose every premise still stands keeps the
1637
+ // fact — pure set membership, no re-derivation. When some environments
1638
+ // broke, queue the pruned set so the next retraction still sees the
1639
+ // survivor (the stale-justification fix).
1640
+ const environments = environmentsOf(c);
1641
+ const intact = environments.filter((env) => env.every((id) => survivorIds.has(id)));
1642
+ if (intact.length) {
1643
+ if (intact.length !== environments.length) reground.set(c.id, intact);
1644
+ continue;
1645
+ }
1646
+ // ENUMERATE: a fresh premise environment among the survivors re-grounds
1647
+ // the fact under new citations.
1648
+ const fresh = enumerateSupport(c, { maxEnvironments });
1649
+ if (fresh.length) {
1650
+ reground.set(c.id, fresh);
1651
+ continue;
1652
+ }
1653
+ // BOOLEAN BACKSTOP: the closure walk is the final authority — it sees
1654
+ // multi-hop support with no materialised direct edge to cite, so a
1655
+ // still-derivable fact is never removed on a stale citation alone (its
1656
+ // environments stay as they were).
1657
+ if (stillDerivable(c)) continue;
1658
+ removed.add(c.id);
1659
+ order.push(c.id);
1660
+ newlyRemoved.push(c.id);
1661
+ progressed = true;
1610
1662
  }
1611
- // BOOLEAN BACKSTOP: the closure walk is the final authority — it sees
1612
- // multi-hop support with no materialised direct edge to cite, so a
1613
- // still-derivable fact is never removed on a stale citation alone (its
1614
- // environments stay as they were).
1615
- if (stillDerivable(c)) continue;
1616
- removed.add(c.id);
1617
- order.push(c.id);
1618
- newlyRemoved.push(c.id);
1619
- progressed = true;
1663
+ if (hitBudget) { truncated = true; break; }
1664
+ if (!progressed) break; // every candidate this round survived fixpoint
1665
+ }
1666
+ if (!truncated && round >= depth) {
1667
+ // depth exhausted, not a natural fixpoint — honestly flag it if a pending
1668
+ // candidate (any surviving fact whose environment union still cites a
1669
+ // removed id) would have been checked next round.
1670
+ truncated = entailedRows.some((r) => !removed.has(r.id) && r.justification.some((j) => removed.has(j)));
1620
1671
  }
1621
- if (hitBudget) { truncated = true; break; }
1622
- if (!progressed) break; // every candidate this round survived — fixpoint
1623
- }
1624
- if (!truncated && round >= depth) {
1625
- // depth exhausted, not a natural fixpoint — honestly flag it if a pending
1626
- // candidate (any surviving fact whose environment union still cites a
1627
- // removed id) would have been checked next round.
1628
- truncated = entailedRows.some((r) => !removed.has(r.id) && r.justification.some((j) => removed.has(j)));
1629
1672
  }
1630
1673
 
1631
- const { removed: actuallyRemoved } = await removeFacts(repoDir, order);
1674
+ // The target's own position in `order` narrows to the invoking party's
1675
+ // record(s) when scoped; every cascade id stays group-wide — an entailed
1676
+ // fact is never itself "the invoking party's own", so there is no
1677
+ // per-source record to narrow it to.
1678
+ const removalIds = scoped.length
1679
+ ? [...ownSourceIds.map((sid) => `${targetId}@${sid}`), ...order.slice(1)]
1680
+ : order;
1681
+ const { removed: actuallyRemoved } = await removeFacts(repoDir, removalIds);
1632
1682
  if (appendFactsFn) {
1633
1683
  const regroundWrites = [...reground.entries()]
1634
1684
  .filter(([id]) => !removed.has(id))
@@ -1644,7 +1694,10 @@ export async function retractSubClassOf(repoDir, subject, object, {
1644
1694
  });
1645
1695
  if (regroundWrites.length) await appendFactsFn(repoDir, regroundWrites);
1646
1696
  }
1647
- return { retracted: actuallyRemoved, count: actuallyRemoved.length, budget, depth, truncated, found: true };
1697
+ return {
1698
+ retracted: actuallyRemoved, count: actuallyRemoved.length, budget, depth, truncated, found: true,
1699
+ ...(scoped.length ? { ownRecord: true, stillStands } : {}),
1700
+ };
1648
1701
  }
1649
1702
 
1650
1703
  /**
@@ -11,7 +11,7 @@
11
11
  // actually seen. `exposedRoomIds` is the set of rooms this auto-play run has
12
12
  // itself moved into (the opening room included from turn 0) — it is the sole
13
13
  // caller making every move, so it always knows this, and threads the set
14
- // forward turn to turn exactly like spider-fly.mjs threads its own agents
14
+ // forward turn to turn exactly like predator-prey.mjs threads its own agents
15
15
  // shape. `exposedFacts` turns that set into the actual filtered view: a fact
16
16
  // is exposed when its subject's CURRENT placement resolves into an exposed
17
17
  // room, when the subject is the player, or when the fact IS the world's
@@ -130,7 +130,7 @@ async function stepTowardThenAct({
130
130
  * and execute exactly the one move that goal implies through `adventureTurn`
131
131
  * — the same public entry point a real chat turn calls. Returns
132
132
  * `{ turn, goal, plan, done, stalled, exposedRoomIds }`, mirroring
133
- * spider-fly.mjs's own tick shape: `plan` is the remaining multi-step route a
133
+ * predator-prey.mjs's own tick shape: `plan` is the remaining multi-step route a
134
134
  * `findActionPath` search found this tick (or null when the move was a
135
135
  * single, immediate step — an adjacent unexposed exit, or a take), `goal` is
136
136
  * a short line describing what this tick did, `done` means the objective is
@@ -944,6 +944,8 @@ ${THEME_TOKENS_CSS}
944
944
  onto further lines, staying right-anchored rather than dropping the
945
945
  whole pill row under the heading. */
946
946
  .command-head .pills { flex: 1 1 auto; min-width: 0; justify-content: flex-end; margin-bottom: 0; }
947
+ .teach-toggle { display: flex; align-items: center; gap: .35rem; font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); margin: 0 0 .4rem; cursor: pointer; }
948
+ .teach-toggle input { accent-color: var(--gilt); }
947
949
  .chatask { display: flex; align-items: center; gap: .5rem; border-top: 1px solid var(--line); padding-top: .5rem; }
948
950
  .chatask .prompt { color: var(--taught); font-size: .78rem; font-family: ${MONO_STACK}; }
949
951
  .chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); padding: .32rem .55rem; min-width: 0; }
@@ -1042,6 +1044,10 @@ ${scenarioList.map((s, i) => ` <option value="${i}"${i === 0 ? " select
1042
1044
  <h2>What would you like to do</h2>
1043
1045
  <div class="pills" id="pills"></div>
1044
1046
  </div>
1047
+ <label class="teach-toggle" title="With this on, a sentence like &quot;Candle is in the study.&quot; writes a fact into the world instead of running as a command.">
1048
+ <input type="checkbox" id="teachToggle">
1049
+ teach mode
1050
+ </label>
1045
1051
  <form class="chatask" id="chatform">
1046
1052
  <span class="prompt mono">tmct&gt;</span>
1047
1053
  <input id="chatq" type="text" placeholder="go north" aria-label="Type a command, or ask a question" disabled>
@@ -1151,6 +1157,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1151
1157
  const pillsEl = el("pills");
1152
1158
  const chatformEl = el("chatform");
1153
1159
  const chatqEl = el("chatq");
1160
+ const teachToggleEl = el("teachToggle");
1154
1161
  const carryListEl = el("carryList");
1155
1162
  const mapWrapEl = el("mapWrap");
1156
1163
  const mapViewportEl = el("mapViewport");
@@ -1525,7 +1532,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1525
1532
  }
1526
1533
  function renderPills(rows, state, here) {
1527
1534
  const actions = pillsFor(rows, state, here).filter((a) => a.indexOf("go ") !== 0);
1528
- pillsEl.innerHTML = actions.map((a) => '<button type="button" class="pill">' + esc(a) + "</button>").join("");
1535
+ pillsEl.innerHTML = actions.map((a) => '<button type="button" class="pill" data-command="' + esc(a) + '">' + esc(a) + "</button>").join("");
1529
1536
  // The empty input teaches the grounded noun form off the same affordance
1530
1537
  // list the pills read — real props from THIS room, so a first-time player
1531
1538
  // types "examine lamp", not a pronoun with nothing to bind to yet.
@@ -1822,12 +1829,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1822
1829
 
1823
1830
  async function boot({ fresh = false } = {}) {
1824
1831
  const saved = !fresh && persist ? await persist.load() : null;
1825
- session = await tmct.open(
1826
- world(),
1827
- saved && saved.payload && saved.payload.memoryPayload
1828
- ? { restoredPayload: saved.payload.memoryPayload, restoredVisitedRoomIds: saved.payload.visitedRoomIds }
1829
- : {},
1830
- );
1832
+ const restoreOptions = saved && saved.payload && saved.payload.memoryPayload
1833
+ ? { restoredPayload: saved.payload.memoryPayload, restoredVisitedRoomIds: saved.payload.visitedRoomIds }
1834
+ : {};
1835
+ session = await tmct.open(world(), { ...restoreOptions, getTeachEnabled: () => teachToggleEl.checked });
1831
1836
  lastTicks = 0;
1832
1837
  const snap = await session.snapshot();
1833
1838
  redraw(snap);
@@ -40,7 +40,7 @@ import {
40
40
  stripTrailingScopeFiller, stripTrailingDiscourseTag, EDGE_NOUN_TO_METRIC, RELATIONS, LIST_TRIGGERS,
41
41
  locativePreposition,
42
42
  } from "../domain/ask-vocab.mjs";
43
- import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, expandContractions, normalizeQuery, stripFillerWords, escapeRegex, kindNounAnaphoraHint, datedTeachSuffix } from "../domain/interpret/normalize.mjs";
43
+ import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, expandContractions, normalizeQuery, stripFillerWords, escapeRegex, kindNounAnaphoraHint, datedTeachSuffix, QUESTION_LEAD_RE } from "../domain/interpret/normalize.mjs";
44
44
  import { setDefaultNlpAdapter } from "../domain/interpret/nlp-registry.mjs";
45
45
  import { setConstructionBanks } from "../domain/interpret/strategies/constructions.mjs";
46
46
  import { nlpAdapter } from "../adapters/ask-nlp.mjs";
@@ -2366,9 +2366,6 @@ const COMPARATIVE_ASK_RE = new RegExp(`^(?:is|are)\\s+(.+?)\\s+(${COMPARATIVE_SR
2366
2366
  * preposition into a minted predicate (the general-verb teach/query lanes
2367
2367
  * and the action-rule frames) — a single source so the set never forks. */
2368
2368
  const PREP_SRC = "on|in|at|onto|upon|under|over|beside|near|behind|above|below|inside|outside";
2369
- /** Interrogative / auxiliary leads that make an "X is a Y"-shaped line a QUESTION
2370
- * ("what is a cache", "is a module a component"), never a teach declarative. */
2371
- const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|did|can|could|should|would|will|has|have)\b/i;
2372
2369
  /** A plain declarative "X is a kind of Y" / "X is a Y" shape (subject-first, no
2373
2370
  * question lead — paired with QUESTION_LEAD_RE at every call site), tolerating
2374
2371
  * an infix "kind of"/"type of" (teachLane's own stripKindOf handles this same
@@ -4794,23 +4791,54 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
4794
4791
 
4795
4792
  // RETRACTION — "forget that X is a Y": wires the data-layer retraction
4796
4793
  // primitive (retractSubClassOf, src/domain/syllogise.mjs) up to chat-level
4797
- // phrasing.
4794
+ // phrasing. Scoped to THIS session's own assertion (sourceTags below) —
4795
+ // the same tags teachFact/assertSentence write under — so retracting
4796
+ // never erases a fact another source taught, only this session's own copy.
4798
4797
  //
4799
4798
  // TRIGGER, never itself the authority: RETRACT_FORGET_RE only recognizes
4800
4799
  // the SHAPE of a retraction sentence — it says nothing about whether
4801
- // subject⊑object was ever actually taught. retractSubClassOf is asked for
4802
- // real and is the only thing that decides:
4803
- // - found:truea real stored (or entailed) fact existed and was
4804
- // retracted (with its dependency-directed cascade) confirmed here.
4805
- // - found:false subject⊑object was never a stored fact, and this falls
4806
- // through to the rest of teachLane's ordinary cascade below rather than
4807
- // claiming a specific, possibly-wrong reason.
4800
+ // subject⊑object was ever actually taught, or taught by THIS session.
4801
+ // retractSubClassOf is asked for real and is the only thing that decides:
4802
+ // - found:falsesubject⊑object was never a stored fact, and this
4803
+ // falls through to the rest of teachLane's ordinary cascade below
4804
+ // rather than claiming a specific, possibly-wrong reason.
4805
+ // - found:true, ownRecord:false some OTHER source taught it; this
4806
+ // session has nothing of its own to withdraw.
4807
+ // - found:true, stillStands:true → this session's own record is gone,
4808
+ // but another source's record keeps the fact standing (no cascade —
4809
+ // its premise never actually broke).
4810
+ // - found:true, stillStands:false → this session held the only record;
4811
+ // the fact and its dependency-directed cascade are both gone.
4808
4812
  if (retractForgetMatch) {
4809
4813
  const { retractSubClassOf } = await import("../domain/syllogise.mjs");
4810
- const { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts, appendFacts: appendFactsForRetract } = await import("../adapters/memory/core.mjs");
4814
+ const { provenanceTag: aceProvenanceTag } = await import("../domain/grammar/assert.mjs");
4815
+ const {
4816
+ loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts,
4817
+ appendFacts: appendFactsForRetract, factRecordIdForTag,
4818
+ } = await import("../adapters/memory/core.mjs");
4811
4819
  const result = await retractSubClassOf(memoryDir, retractSubject, retractObject, {
4812
- store: { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts, appendFacts: appendFactsForRetract },
4820
+ // A session's own positive assertion of subject⊑object can have
4821
+ // landed under either lane: the free-form teach lane (teachFact) or
4822
+ // the ACE-parsed assert lane (assertSentence) — both tags name here.
4823
+ sourceTags: [teachProvenanceTag(sessionId), aceProvenanceTag({ sessionId })],
4824
+ store: {
4825
+ loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts,
4826
+ appendFacts: appendFactsForRetract, factRecordIdForTag,
4827
+ },
4813
4828
  });
4829
+ if (result.found && !result.ownRecord) {
4830
+ return {
4831
+ text: `"${retractSubject} is a kind of ${retractObject}" isn't something you taught me — it's on record from elsewhere, so there's nothing of yours to forget.`,
4832
+ via: "retract", miss: false,
4833
+ };
4834
+ }
4835
+ if (result.found && result.stillStands) {
4836
+ return {
4837
+ text: `noted — forgotten your own record of "${retractSubject} is a kind of ${retractObject}", `
4838
+ + "but it's still stored, told to me by someone else.",
4839
+ via: "retract", miss: false,
4840
+ };
4841
+ }
4814
4842
  if (result.found) {
4815
4843
  const extra = result.count - 1; // beyond the target fact itself
4816
4844
  return {
@@ -1,7 +1,7 @@
1
1
  // mud-turn.mjs — one acting character's whole turn in a mud world: investigate
2
2
  // the room it stands in, walk toward food it actually knows about, roll for
3
3
  // digging out of the room's frontier, and (when none of that came to anything)
4
- // set off for a room it has never stood in. The split mirrors spider-fly.mjs /
4
+ // set off for a room it has never stood in. The split mirrors predator-prey.mjs /
5
5
  // spider-fly-turn.mjs: adventure.mjs owns the read/fold/write primitives, this
6
6
  // file owns the per-tick decisions that drive them. Nothing here writes a fact
7
7
  // of its own — every change goes out through runWorldCommand, recordTold,
@@ -411,6 +411,10 @@ ${scenarioList.length > 1 ? ` <select id="scenarioSelect" class="deck-sel
411
411
  ${scenarioList.map((s, i) => ` <option value="${i}"${i === 0 ? " selected" : ""}>${escapeHtml(s.label || scenarioLabel(s.worldPayload?.name))}</option>`).join("\n")}
412
412
  </select>` : ""}
413
413
  <button type="button" id="editModeBtn" aria-pressed="false">edit</button>
414
+ <label class="deck-teach" title="With this on, a sentence like &quot;Candle is in the study.&quot; writes a fact into the world instead of running as a command.">
415
+ <input type="checkbox" id="teachToggle">
416
+ teach
417
+ </label>
414
418
  <button type="button" class="deck-info-btn" id="deckInfoBtn" aria-expanded="false" aria-controls="deckInfoPopup" aria-label="about this demo">?</button>
415
419
  <span class="mono deck-turns" id="globalTurnCount">turns: 0</span>
416
420
  </div>
@@ -634,6 +638,8 @@ const MUD_STYLE = `
634
638
  background: rgba(255,255,255,.5); color: var(--mud-ink);
635
639
  }
636
640
  .deck-select:hover { border-color: var(--burrow-glow); }
641
+ .deck-teach { display: flex; align-items: center; gap: .3rem; font-family: ${MONO_STACK}; font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--soil-mid); cursor: pointer; }
642
+ .deck-teach input[type="checkbox"] { accent-color: var(--burrow-glow); }
637
643
  .deck-play { background: var(--mud-ink) !important; color: var(--parchment); border-color: var(--mud-ink) !important; padding: .38rem 1.1rem !important; }
638
644
  .deck-play[aria-pressed="true"] { background: var(--burrow-glow) !important; border-color: var(--burrow-glow) !important; color: var(--mud-ink); }
639
645
  .deck-turns { margin-left: auto; font-size: .74rem; color: var(--soil-mid); }
@@ -1006,7 +1012,7 @@ const MUD_STYLE = `
1006
1012
  --lem-readout: var(--burrow-glow); --lem-readout-bg: var(--soil-deep);
1007
1013
  --lem-chalk: var(--soil-mid);
1008
1014
  }
1009
- .deck-slider, .deck h3 { color: var(--lem-chalk); }
1015
+ .deck-slider, .deck h3, .deck-teach { color: var(--lem-chalk); }
1010
1016
  .deck-slider input[type="range"] { accent-color: var(--burrow-glow); }
1011
1017
  .deck button, .pane-controls button {
1012
1018
  background: var(--lem-face); color: var(--mud-ink); border: 1px solid var(--soil-mid); border-radius: 3px;
@@ -3052,7 +3058,10 @@ function pageScript() {
3052
3058
  // scenario's cast paired with the LAST scenario's pane ids, which is what
3053
3059
  // a null pane-element lookup in renderAll means when it happens.
3054
3060
  for (let i = 0; i < cast.length; i += 1) slotOf[cast[i]] = slots[i];
3055
- const opened = await window.tmct.open(scenario().worldPayload, { characters: everyone(), epoch: nextEpoch });
3061
+ const opened = await window.tmct.open(scenario().worldPayload, {
3062
+ characters: everyone(), epoch: nextEpoch,
3063
+ getTeachEnabled: function () { return el("teachToggle").checked; },
3064
+ });
3056
3065
  if (seq !== bootSeq) return;
3057
3066
  session = opened;
3058
3067
  if (liveRoom) {