@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.0.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 (68) hide show
  1. package/README.md +176 -8
  2. package/corpus/reference/index.json.gz +0 -0
  3. package/corpus/reference/manifest.json +8 -8
  4. package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
  5. package/corpus/sprites/src/sprite-facts.jsonl +34 -0
  6. package/corpus/worlds/index.json.gz +0 -0
  7. package/corpus/worlds/manifest.json +49 -9
  8. package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
  9. package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
  10. package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
  11. package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
  12. package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
  13. package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
  14. package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
  15. package/corpus/worlds/src/mud-hollow.jsonl +82 -0
  16. package/corpus/worlds/src/mud-warren.jsonl +124 -0
  17. package/corpus/worlds/src/spider-fly.jsonl +1 -1
  18. package/data/sprites/animal-icon.toml +11 -5
  19. package/data/sprites/book-icon.toml +9 -5
  20. package/data/sprites/cabinet-icon.toml +10 -5
  21. package/data/sprites/cellar-icon.toml +16 -6
  22. package/data/sprites/container-icon.toml +7 -3
  23. package/data/sprites/desk-icon.toml +8 -5
  24. package/data/sprites/dog-icon.toml +8 -5
  25. package/data/sprites/dog-with-colour-icon.toml +13 -12
  26. package/data/sprites/drawing-room-icon.toml +14 -5
  27. package/data/sprites/egg-icon.toml +6 -3
  28. package/data/sprites/fly-icon.toml +10 -5
  29. package/data/sprites/furniture-icon.toml +5 -3
  30. package/data/sprites/garden-icon.toml +10 -3
  31. package/data/sprites/key-icon.toml +3 -1
  32. package/data/sprites/kitchen-icon.toml +15 -6
  33. package/data/sprites/lamp-icon.toml +9 -4
  34. package/data/sprites/letter-icon.toml +7 -4
  35. package/data/sprites/library-icon.toml +14 -3
  36. package/data/sprites/pan-icon.toml +7 -3
  37. package/data/sprites/person-icon.toml +6 -2
  38. package/data/sprites/poodle-icon.toml +1 -1
  39. package/data/sprites/portable-icon.toml +6 -4
  40. package/data/sprites/portrait-icon.toml +7 -4
  41. package/data/sprites/room-icon.toml +11 -2
  42. package/data/sprites/spider-icon.toml +9 -3
  43. package/data/sprites/study-icon.toml +10 -2
  44. package/package.json +5 -4
  45. package/src/domain/grammar/ace.mjs +40 -4
  46. package/src/domain/grammar/lexicon-core.json +2 -1
  47. package/src/domain/grammar/lexicon.mjs +18 -0
  48. package/src/domain/reference-pack.mjs +31 -7
  49. package/src/domain/spider-fly-world.mjs +1 -1
  50. package/src/domain/sprite-templates.mjs +8 -6
  51. package/src/services/adventure-viz.mjs +130 -47
  52. package/src/services/adventure.mjs +440 -367
  53. package/src/services/chat-page-viz.mjs +37 -9
  54. package/src/services/chat.mjs +99 -18
  55. package/src/services/code-explorer-viz.mjs +52 -14
  56. package/src/services/extract-facts.mjs +4 -7
  57. package/src/services/ingest-viz.mjs +54 -26
  58. package/src/services/ledger-viz.mjs +105 -56
  59. package/src/services/memory-panel-viz.mjs +24 -0
  60. package/src/services/mud-viz.mjs +940 -21
  61. package/src/services/plan-viz.mjs +119 -61
  62. package/src/services/research-viz.mjs +151 -57
  63. package/src/services/spider-fly-turn.mjs +1 -1
  64. package/src/services/spider-fly-viz.mjs +67 -43
  65. package/src/services/sprite-catalog-viz.mjs +175 -51
  66. package/src/services/viz-theme.mjs +15 -0
  67. package/src/surfaces/web/memory-ask-browser.bundle.js +107 -107
  68. package/src/surfaces/web/mud-browser-entry.mjs +44 -2
@@ -32,7 +32,7 @@ import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-the
32
32
  import { provBucketFor } from "./ledger-viz.mjs";
33
33
  import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
34
34
  import { sessionLogTimeOfDay, sessionLogHeaderMarkdown, sessionLogTurnMarkdown } from "./session-log-format.mjs";
35
- import { bandLabelFor, statsSummaryLine, fetchWithProgress, renderStatsPanelInto } from "./memory-panel-viz.mjs";
35
+ import { bandLabelFor, statsSummaryLine, clearSiteAssetCaches, fetchWithProgress, renderStatsPanelInto } from "./memory-panel-viz.mjs";
36
36
 
37
37
  const DEFAULT_TITLE = "the-mechanical-code-talker — talk to it";
38
38
 
@@ -379,7 +379,7 @@ export function transcriptMarkdown(turns, meta, headerMd, turnMd) {
379
379
  * live digest-bank twin (see chat-browser-entry.mjs) rather than to a
380
380
  * client-side digest panel of this page's own; an empty list degrades to the
381
381
  * flat list exactly as before this page could digest at all. */
382
- export function renderChatHtml({ title = DEFAULT_TITLE, digestStructures = [] } = {}) {
382
+ export function renderChatHtml({ title = DEFAULT_TITLE, digestStructures = [], seedStamp = "" } = {}) {
383
383
  const digestStructuresJson = JSON.stringify(Array.isArray(digestStructures) ? digestStructures : []);
384
384
  const legendHtml = PROV_LEGEND.map(
385
385
  ([key, label]) => `<span class="legend-item"><i class="dot dot-${provKey(key)}"></i>${escapeHtml(label)}</span>`,
@@ -549,6 +549,13 @@ ${THEME_TOKENS_CSS}
549
549
  input and a round send button. The chrome's controls take the same shape,
550
550
  so the page's networking reads as ordinary chat furniture rather than as
551
551
  an instrument bolted on. */
552
+ /* the live fact count, in the topbar rather than the statusline: it is the
553
+ one number that says what this session actually knows, and a small line of
554
+ grey mono under the composer is where a wrong one goes unnoticed. Reads as
555
+ a pill like its neighbours, with the number itself at reading size. */
556
+ .fact-pill { display: inline-flex; align-items: baseline; gap: .34rem; font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); border: 1px solid var(--corpus-t1); border-radius: 99px; padding: .2rem .8rem; background: var(--corpus-soft); white-space: nowrap; }
557
+ .fact-pill .fact-pill-value { font-size: .96rem; letter-spacing: 0; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); }
558
+
552
559
  .chrome { display: flex; align-items: center; gap: .4rem; }
553
560
  .chrome-btn { font-family: ${SERIF_STACK}; font-size: .8rem; color: var(--muted); border: 1px solid var(--line); border-radius: 99px; padding: .22rem .75rem; background: var(--card); text-decoration: none; display: inline-flex; align-items: center; gap: .32rem; white-space: nowrap; line-height: 1.35; }
554
561
  .chrome-btn:hover { color: var(--ink); border-color: var(--ink); }
@@ -789,6 +796,10 @@ ${THEME_TOKENS_CSS}
789
796
  <span class="eyebrow">the-mechanical-code-talker</span>
790
797
  </div>
791
798
  <div class="chrome">
799
+ <span class="fact-pill" id="factPill" aria-live="polite"
800
+ title="every fact this session's memory holds right now — the starter memory it shipped with plus anything you have taught, researched or ingested">
801
+ <span class="fact-pill-value" id="factPillValue">&mdash;</span> facts
802
+ </span>
792
803
  <button type="button" class="state-pill" id="statePill" data-tone="idle"
793
804
  title="the shared-world connection; click to open the network panel">
794
805
  <i class="pill-dot"></i><span id="statePillWord">not shared</span>
@@ -880,6 +891,7 @@ ${THEME_TOKENS_CSS}
880
891
  const transcriptMarkdown = ${transcriptMarkdown.toString()};
881
892
  const bandLabelFor = ${bandLabelFor.toString()};
882
893
  const statsSummaryLine = ${statsSummaryLine.toString()};
894
+ const clearSiteAssetCaches = ${clearSiteAssetCaches.toString()};
883
895
  const fetchWithProgress = ${fetchWithProgress.toString()};
884
896
  const renderStatsPanelInto = ${renderStatsPanelInto.toString()};
885
897
  const createTicker = ${createTicker.toString()};
@@ -900,6 +912,7 @@ ${THEME_TOKENS_CSS}
900
912
  const inputEl = el("composerInput");
901
913
  const sendBtn = el("composerSend");
902
914
  const statusEl = el("status");
915
+ const factPillValueEl = el("factPillValue");
903
916
  const statsPanelEl = el("statsPanelStats");
904
917
  const researchedPanelEl = el("researchedPanel");
905
918
  const wikiModeFieldset = el("wikiMode");
@@ -1102,11 +1115,18 @@ ${THEME_TOKENS_CSS}
1102
1115
  }
1103
1116
  }
1104
1117
 
1118
+ // The build's own content hash for the seed. It rides in the URL this page
1119
+ // fetches the seed by, so the service worker's cache-first read can only
1120
+ // ever return the copy this page asked for. Empty in a build that had no
1121
+ // seed to hash (the desktop shell's own render).
1122
+ const SEED_STAMP = ${JSON.stringify(seedStamp)};
1123
+ const SEED_QUERY = SEED_STAMP ? "?b=" + SEED_STAMP : "";
1124
+
1105
1125
  let seedPayload = null;
1106
1126
  let seedFacts = 0;
1107
1127
  async function fetchSeed() {
1108
1128
  try {
1109
- const blob = await fetchWithProgress("./chat-seed.json", (loaded, total) => noteProgress("seed", loaded, total));
1129
+ const blob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY, (loaded, total) => noteProgress("seed", loaded, total));
1110
1130
  seedPayload = JSON.parse(await blob.text());
1111
1131
  seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
1112
1132
  } catch (err) {
@@ -1156,8 +1176,9 @@ ${THEME_TOKENS_CSS}
1156
1176
  // Best-effort IndexedDB (window.tmctChat.openPersistedStore): the whole
1157
1177
  // Backend-B payload snapshots after each teach turn, debounced so a burst
1158
1178
  // of teaching costs one multi-MB write, not one per fact. The stamp ties a
1159
- // snapshot to this deploy (site version) AND this seed (fact count) — either
1160
- // changing discards the snapshot in favour of the fresh seed.
1179
+ // snapshot to this deploy (site version) AND this seed (its fact count and
1180
+ // content hash) — any of them changing discards the snapshot in favour of
1181
+ // the fresh seed.
1161
1182
  let persist = null;
1162
1183
  let saveTimer = null;
1163
1184
  let restoredCount = 0;
@@ -1217,6 +1238,7 @@ ${THEME_TOKENS_CSS}
1217
1238
  try { stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir); }
1218
1239
  catch { return; }
1219
1240
  }
1241
+ factPillValueEl.textContent = Number(stats.total || 0).toLocaleString();
1220
1242
  renderStatsPanelInto(statsPanelEl, stats, {
1221
1243
  bandLabel: bandLabelFor,
1222
1244
  onForget: persist ? forgetEverything : null,
@@ -1598,14 +1620,20 @@ ${THEME_TOKENS_CSS}
1598
1620
  inputEl.focus();
1599
1621
  });
1600
1622
 
1601
- // "reset to seed" is the full re-initialisation: drop the persisted payload
1602
- // outright and reload, so boot re-seeds from the page's shipped seed as if on
1603
- // a first visit. Harder than "forget everything", which only swaps the live
1623
+ // "reset to seed" is the full re-initialisation: drop everything this device
1624
+ // holds and reload, so boot re-seeds from the page's shipped seed as if on a
1625
+ // first visit. Harder than "forget everything", which only swaps the live
1604
1626
  // session — this trusts nothing in memory and re-fetches the seed asset.
1627
+ //
1628
+ // Both stores go, not just the payload: what you taught lives in IndexedDB,
1629
+ // and the seed asset itself lives in the service worker's Cache Storage. A
1630
+ // reset that cleared only the first would re-seed straight back out of a
1631
+ // cached copy of an older seed, which is exactly what it promises not to do.
1605
1632
  el("reinitStore").addEventListener("click", async () => {
1606
1633
  clearTimeout(saveTimer);
1607
1634
  saveTimer = null;
1608
1635
  if (persist) await persist.clear();
1636
+ await clearSiteAssetCaches();
1609
1637
  window.location.reload();
1610
1638
  });
1611
1639
 
@@ -2186,7 +2214,7 @@ ${THEME_TOKENS_CSS}
2186
2214
  progressActive = false;
2187
2215
  window.tmctChat.registerReferencePackProvider(fetchPackProvider);
2188
2216
  if (window.tmctChat.openPersistedStore) {
2189
- persist = window.tmctChat.openPersistedStore({ storeKey: "chat", stamp: siteVersion + ":" + seedFacts });
2217
+ persist = window.tmctChat.openPersistedStore({ storeKey: "chat", stamp: siteVersion + ":" + seedFacts + ":" + SEED_STAMP });
2190
2218
  }
2191
2219
  const savedRecord = persist ? await persist.load() : null;
2192
2220
  const initialMode = readWikiMode();
@@ -7758,6 +7758,13 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
7758
7758
  // teach frame, in a store written before the spellings converged) is
7759
7759
  // found by the curated spelling it means.
7760
7760
  let hits = predicate ? subjectHits.filter((f) => normFactPredicate(f.predicate) === predicate) : subjectHits;
7761
+ // A bare "what is X" answered ONLY by corpus-weak ConceptNet association(s)
7762
+ // is the hedged "possibly: X is related to Y" guess, not a definition — treated
7763
+ // as no hit at all so the honest-miss cascade below (teach lane, reference-pack/
7764
+ // Wikipedia fallback) still gets a turn, instead of the hedge silently standing
7765
+ // in as "already answered". A predicate-specific ask ("X used for") is untouched:
7766
+ // scoped to the SAME undifferentiated shape the reference-pack fallback serves.
7767
+ if (!predicate && hits.length && !hits.some(isRealGrounding)) hits = [];
7761
7768
  if (!hits.length) {
7762
7769
  // The subject itself is known — as a fact subject, or as the standing
7763
7770
  // focus a pronoun just resolved to — but not under this specific
@@ -7777,7 +7784,9 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
7777
7784
  // ishmael"): surface those reverse relations rather than missing, the same
7778
7785
  // facts "what do you know about X" would list.
7779
7786
  if (!predicate) {
7780
- const objectHits = rankByBiasThenTrust((await factRows(memoryDir, cache)).filter((f) => variants.has(f.object)), biasByBundle);
7787
+ let objectHits = rankByBiasThenTrust((await factRows(memoryDir, cache)).filter((f) => variants.has(f.object)), biasByBundle);
7788
+ // Same corpus-weak-only demotion as the subject-side hits above.
7789
+ if (objectHits.length && !objectHits.some(isRealGrounding)) objectHits = [];
7781
7790
  if (objectHits.length) {
7782
7791
  const objLines = objectHits.map(renderFactLine);
7783
7792
  const objShown = objLines.slice(0, FACT_ANSWER_CAP);
@@ -8343,6 +8352,16 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
8343
8352
  // besides. The adventure's own where/openness readers answer the legitimate
8344
8353
  // in-game questions from the world fold.
8345
8354
  hits = hits.filter((f) => !WORLD_INTERNAL_PREDICATES.has(f.predicate));
8355
+ // A corpus-weak-only result set (every hit resolved ONLY through
8356
+ // ConceptNet's /r/RelatedTo tier) still composes its hedged "possibly"
8357
+ // lines below exactly as before — this reader's whole job is showing
8358
+ // those, weak or not, so they're never suppressed from the text. But the
8359
+ // result is flagged `weakOnly` so the shared honest-miss cascade (the
8360
+ // reference-pack/Wikipedia fallback, below in runAsk) still gets a turn
8361
+ // instead of the hedge silently standing in as "already answered" —
8362
+ // mirroring isRealGrounding's other two call sites without breaking this
8363
+ // one's own job.
8364
+ const weakOnly = hits.length > 0 && !hits.some(isRealGrounding);
8346
8365
  // A genuinely empty result here is a real miss: "what do you know about
8347
8366
  // the last commit" needs a TEACH-OFFER, not a bare wall — added as a LATE
8348
8367
  // runTurn-level addition, below, alongside the sibling "what is X" offer,
@@ -8393,11 +8412,16 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
8393
8412
  const header = `${hits.length} remembered fact${hits.length === 1 ? "" : "s"} about ${term}`
8394
8413
  + `${viaSubtype ? " (including its known subtypes)" : ""}:`;
8395
8414
  const { lines, grouped } = senseSplitFactList(hits, rows, variants, { indent: " " });
8396
- if (grouped) return { ...grouped, text: `${header}\n${grouped.text}` };
8415
+ if (grouped) return { ...grouped, text: `${header}\n${grouped.text}`, ...(weakOnly ? { weakOnly: true } : {}) };
8397
8416
  const shown = lines.slice(0, FACT_ANSWER_CAP);
8398
8417
  const rest = lines.slice(FACT_ANSWER_CAP);
8399
8418
  const extra = rest.length ? `\n …and ${rest.length} more — say 'more' to see them.` : "";
8400
- return { text: `${header}\n${shown.join("\n")}${extra}`, replace: true, ...(rest.length ? { pending: { items: rest.map((l) => l.trim()), noun: "facts" } } : {}) };
8419
+ return {
8420
+ text: `${header}\n${shown.join("\n")}${extra}`,
8421
+ replace: true,
8422
+ ...(rest.length ? { pending: { items: rest.map((l) => l.trim()), noun: "facts" } } : {}),
8423
+ ...(weakOnly ? { weakOnly: true } : {}),
8424
+ };
8401
8425
  }
8402
8426
  return null;
8403
8427
  }
@@ -10674,14 +10698,37 @@ async function curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon })
10674
10698
  return { text: `${def} (source: corpus/seon)`, term };
10675
10699
  }
10676
10700
 
10701
+ /** True when `f` carries a source better than the ConceptNet `/r/RelatedTo`
10702
+ * corpus-weak tier — or no recorded source type at all (the conservative,
10703
+ * pre-existing read for legacy rows). A fact resolved ONLY by corpus-weak
10704
+ * source(s) is the hedged "possibly: X is related to Y" guess, not an
10705
+ * actual definition; callers treat it as no grounding at all rather than an
10706
+ * answer, so it can never by itself skip the honest-miss cascade below
10707
+ * (the teach lane, the reference-pack/Wikipedia fallback). Shared by the
10708
+ * bare "what is X" reader, the "what do you know about X" reader (both in
10709
+ * factAnswerReaders) and the learn-on-miss gate (cleanMissPackKey/
10710
+ * cleanMissLiveKey) so none of them can ever disagree. */
10711
+ function isRealGrounding(f) {
10712
+ const types = f.sourceTypes || [];
10713
+ return types.length === 0 || types.some((t) => t !== "corpusWeak");
10714
+ }
10715
+
10677
10716
  // ---- learn-on-miss: the shipped child + reference packs behind the cleanest miss ----
10678
10717
 
10718
+ /** A remembered fact naming `term` is real grounding only when SOME source
10719
+ * behind it outranks the ConceptNet `/r/RelatedTo` corpus-weak tier — see
10720
+ * isRealGrounding just above. */
10721
+ function hasNonWeakGrounding(rows, variants) {
10722
+ return rows.some((f) => (variants.has(f.subject) || variants.has(f.object)) && isRealGrounding(f));
10723
+ }
10724
+
10679
10725
  /** The learn-on-miss gate, shared by the child-pack hook, the articled
10680
10726
  * reference hook and the bare-form fallback so the three can never disagree.
10681
10727
  * Passes only on the CLEANEST miss: a definition-shaped term the lexicon
10682
- * knows, resolving to no graph entity and no remembered fact then, and
10683
- * only then, may a pack provider be consulted. Null means the turn proceeds
10684
- * byte-identically to a pack-less run. */
10728
+ * knows, resolving to no graph entity and no fact grounded above the
10729
+ * corpus-weak tier — then, and only then, may a pack provider be
10730
+ * consulted. Null means the turn proceeds byte-identically to a pack-less
10731
+ * run. */
10685
10732
  async function cleanMissPackKey(term, { graph, memoryDir, lexicon, cache }) {
10686
10733
  if (!term || !memoryDir) return null;
10687
10734
  let key = null;
@@ -10695,7 +10742,7 @@ async function cleanMissPackKey(term, { graph, memoryDir, lexicon, cache }) {
10695
10742
  const variants = factTermVariants(normFactTerm, term);
10696
10743
  variants.add(key);
10697
10744
  const rows = await factRows(memoryDir, cache);
10698
- if (rows.some((f) => variants.has(f.subject) || variants.has(f.object))) return null;
10745
+ if (hasNonWeakGrounding(rows, variants)) return null;
10699
10746
  // A taught RULE that owns this term outranks any pack load: surfacing
10700
10747
  // unrelated conceptnet content over the user's own taught concept is worse
10701
10748
  // than the honest miss the decline leaves standing.
@@ -10723,10 +10770,10 @@ async function referencePackMissAnswer(term, { graph, memoryDir, lexicon, env, c
10723
10770
  }
10724
10771
 
10725
10772
  /** The LIVE variant of cleanMissPackKey — the same resolveEntity and
10726
- * remembered-fact checks, but through cleanMissLiveTerm, which drops the
10727
- * lexicon-membership wall: a word the lexicon has never met is exactly what
10728
- * the live lookup exists for. Null means the turn proceeds byte-identically
10729
- * to a live-off run. */
10773
+ * above-corpus-weak grounding checks, but through cleanMissLiveTerm, which
10774
+ * drops the lexicon-membership wall: a word the lexicon has never met is
10775
+ * exactly what the live lookup exists for. Null means the turn proceeds
10776
+ * byte-identically to a live-off run. */
10730
10777
  async function cleanMissLiveKey(term, { graph, memoryDir, lexicon, cache }) {
10731
10778
  if (!term || !memoryDir) return null;
10732
10779
  let key = null;
@@ -10738,7 +10785,7 @@ async function cleanMissLiveKey(term, { graph, memoryDir, lexicon, cache }) {
10738
10785
  const variants = factTermVariants(normFactTerm, term);
10739
10786
  variants.add(key);
10740
10787
  const rows = await factRows(memoryDir, cache);
10741
- if (rows.some((f) => variants.has(f.subject) || variants.has(f.object))) return null;
10788
+ if (hasNonWeakGrounding(rows, variants)) return null;
10742
10789
  return key;
10743
10790
  }
10744
10791
 
@@ -12641,6 +12688,13 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
12641
12688
  let via = "composed";
12642
12689
  let recordMiss = miss;
12643
12690
  let factPending = null; // a truncated fact listing's held remainder (for "more" paging)
12691
+ // Set when the memory-facts lane (W4, below) answered from a corpus-weak-only
12692
+ // hit set (factAnswer's KNOW_ABOUT_RE reader, `weakOnly`) — the hedged
12693
+ // "possibly" answer still stands as `answer`, but `via` is deliberately left
12694
+ // at "composed" so the honest-miss cascade further down still gets a turn,
12695
+ // and this flag lets the LEARN-ON-MISS PACKS lane resolve a reference-pack
12696
+ // term for THIS query shape (which metaTermOf alone never recognizes).
12697
+ let factLaneWeakOnly = false;
12644
12698
  // scm-svf1/cardinality-monotonicity/
12645
12699
  // cax-maxc0's LIVE proof chases (factReadBack) have no persisted Fact to
12646
12700
  // attach trust.mjs's entailed hook to, so they compute
@@ -13156,9 +13210,18 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
13156
13210
  // the empty-graph orientation pointer below stays off it — a pronoun
13157
13211
  // decline with an index pointer under it is two answers to one turn.
13158
13212
  if (fact.selfContainedMiss) selfContainedMiss = true;
13159
- if (!fact.miss) {
13213
+ // A `weakOnly` hit (the "what do you know about X" reader's
13214
+ // corpus-weak-only hedge) is shown as `answer` above like any other
13215
+ // fact-lane hit, but doesn't count as REAL grounding — via/recordMiss
13216
+ // stay untouched so the honest-miss cascade below (the reference-pack
13217
+ // lane in particular) still gets a turn at replacing the hedge with a
13218
+ // real answer, exactly the discipline isRealGrounding's other two call
13219
+ // sites already apply.
13220
+ if (!fact.miss && !fact.weakOnly) {
13160
13221
  via = "fact";
13161
13222
  recordMiss = false;
13223
+ } else if (fact.weakOnly) {
13224
+ factLaneWeakOnly = true;
13162
13225
  }
13163
13226
  if (fact.pending) factPending = fact.pending; // a truncated fact list → paginable remainder
13164
13227
  if (typeof fact.trust === "number") entailedTrust = fact.trust; // live-chase trust (see the `entailedTrust` declaration above)
@@ -13465,13 +13528,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
13465
13528
  // pack's article speak, cited as before. Both packs missing leaves the
13466
13529
  // honest miss byte-identical.
13467
13530
  if (miss && recordMiss && via === "composed" && memoryDir) {
13468
- const refTerm = metaTermOf(query, envelope);
13531
+ // metaTermOf only recognizes the "what is X"/"define X" shapes — a
13532
+ // "what do you know about X" query only reaches this lane at all when
13533
+ // its own reader (above) answered with a corpus-weak-only hedge
13534
+ // (factLaneWeakOnly), so the KNOW_ABOUT_RE term is resolved directly the
13535
+ // same way the TEACH-OFFER lane below already does for that shape.
13536
+ const refTerm = metaTermOf(query, envelope)
13537
+ || (factLaneWeakOnly ? expandContractions(String(query).trim()).match(KNOW_ABOUT_RE)?.[1]?.trim() : null);
13469
13538
  const key = refTerm ? await cleanMissPackKey(refTerm, { graph, memoryDir, lexicon, cache }) : null;
13470
13539
  const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache, synthesisBudget }) : null;
13471
13540
  if (learned) {
13472
13541
  const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle, cache, newFocus?.label))
13473
13542
  ?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
13474
- if (fact && !fact.miss) {
13543
+ if (fact && !fact.miss && !fact.weakOnly) {
13475
13544
  answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
13476
13545
  via = "fact";
13477
13546
  recordMiss = false;
@@ -13558,7 +13627,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
13558
13627
  const ent = knowAboutTerm ? null : await resolveEntity(graph, offerTerm);
13559
13628
  if (!ent) {
13560
13629
  const variants = factTermVariants(normFactTerm, offerTerm);
13561
- const known = (await memoryFacts(memoryDir)).some((f) => variants.has(f.subject) || variants.has(f.object));
13630
+ // A corpus-weak-only match (ConceptNet's /r/RelatedTo hedge) is not
13631
+ // real grounding — same isRealGrounding discipline as the bare
13632
+ // "what is X" reader and the learn-on-miss gate, so a term with
13633
+ // nothing but a loose association still gets the "teach me" offer
13634
+ // rather than being silently counted as already known.
13635
+ const known = hasNonWeakGrounding(await factRows(memoryDir, cache), variants);
13562
13636
  if (!known) teachOffer = unknownVocabTermOffer(cleanTerm);
13563
13637
  }
13564
13638
  }
@@ -14698,11 +14772,18 @@ const LIST_KNOWN_KINDS_RE = /^list\s+(?:the\s+|all\s+(?:the\s+)?)?(?!facts?\b|th
14698
14772
  // conditional over a NAMED subject or an arbitrary property is a rule, not a
14699
14773
  // subclass fact, and stays outside this frame.
14700
14774
  const UNIVERSAL_CONDITIONAL_RE = /^if\s+(?:something|somebody|someone|anything)\s+is\s+an?\s+([\w-]+)\s*,?\s*(?:then\s+)?(?:it|they)\s+(?:is|are)\s+an?\s+([\w-]+)[.!?\s]*$/i;
14701
- function rewriteVocabOpener(line) {
14775
+ function rewriteVocabOpener(line, { adventureLive = false } = {}) {
14702
14776
  let m = line.match(DESIRE_ABOUT_RE) || line.match(TELL_ABOUT_VARIANT_RE);
14703
14777
  if (m) return `tell me about ${m[1].trim()}`;
14704
14778
  m = line.match(UNIVERSAL_CONDITIONAL_RE);
14705
14779
  if (m) return `every ${m[1]} is ${indefiniteArticleFor(m[2])} ${m[2]}`;
14780
+ // "what <noun> do you know [about]" shares its surface form with a live
14781
+ // adventure's own personal-knowledge asides ("what food do you know
14782
+ // about") — those ask what the CHARACTER has learned in-world, not for
14783
+ // the taught CLASS's members, so this closed-set enumeration rewrite
14784
+ // stands down while a world is live and leaves the line for the
14785
+ // adventure lane to recognise (or not) on its own terms.
14786
+ if (adventureLive) return null;
14706
14787
  m = line.match(KNOWN_KINDS_RE) || line.match(LIST_KNOWN_KINDS_RE);
14707
14788
  if (m) {
14708
14789
  const noun = teachableSubjectOf(m[1]);
@@ -14977,7 +15058,7 @@ async function dispatchTurn(input, { config, source = defaultSource, graph = nul
14977
15058
  const indirectMatch = line.match(INDIRECT_REQUEST_RE);
14978
15059
  const indirectLine = indirectMatch ? indirectMatch[1].trim() : line;
14979
15060
  const preRewriteLine = rewriteEntryPointQuestion(indirectLine) || rewriteProveThat(indirectLine)
14980
- || rewriteVocabOpener(indirectLine) || indirectLine;
15061
+ || rewriteVocabOpener(indirectLine, { adventureLive: Boolean(planState?.adventure) }) || indirectLine;
14981
15062
  // rewriteUsesAsBaseFrame's discontiguous-frame rewrite: applied here, once,
14982
15063
  // before ANY dispatch lane sees the text. Null (no-op) for every turn that
14983
15064
  // doesn't match one of the four discontiguous shapes.
@@ -129,6 +129,7 @@ const CLIENT_JS = String.raw`
129
129
  dockNote: document.getElementById("dock-note"),
130
130
  source: document.getElementById("source-name"),
131
131
  seedStatus: document.getElementById("seed-status"),
132
+ factTotal: document.getElementById("fact-total-value"),
132
133
  };
133
134
  var session = null;
134
135
 
@@ -143,6 +144,15 @@ const CLIENT_JS = String.raw`
143
144
  var parts = [s.individuals + " individuals", s.edges + " edges"];
144
145
  var cls = s.classes.slice(0, 4).map(function (c) { return c[1] + " " + c[0]; });
145
146
  els.stats.textContent = parts.concat(cls).join(" · ");
147
+ renderFactTotal(data);
148
+ }
149
+
150
+ // Everything the chat can answer from: the graph's own edges plus the
151
+ // general-knowledge seed, which arrives later and re-renders this.
152
+ function renderFactTotal(data) {
153
+ if (!els.factTotal) return;
154
+ var edges = (data && data.ledger && data.ledger.stats && data.ledger.stats.edges) || 0;
155
+ els.factTotal.textContent = (edges + seedState.facts).toLocaleString();
146
156
  }
147
157
 
148
158
  function renderFocusRows(data) {
@@ -182,7 +192,13 @@ const CLIENT_JS = String.raw`
182
192
  renderHints(data);
183
193
  }
184
194
 
195
+ function clearEmptyState() {
196
+ var e = document.getElementById("chat-empty");
197
+ if (e) e.remove();
198
+ }
199
+
185
200
  function appendTurn(role, text) {
201
+ clearEmptyState();
186
202
  var div = document.createElement("div");
187
203
  div.className = "turn turn-" + role;
188
204
  div.innerHTML = '<span class="who">' + (role === "you" ? "you" : "tmct") + '</span><span class="said">' + esc(text) + '</span>';
@@ -190,10 +206,16 @@ const CLIENT_JS = String.raw`
190
206
  els.log.scrollTop = els.log.scrollHeight;
191
207
  }
192
208
 
193
- function appendNote(text) {
209
+ function renderEmptyState() {
194
210
  var div = document.createElement("div");
195
- div.className = "turn turn-note";
196
- div.textContent = text;
211
+ div.className = "chat-empty";
212
+ div.id = "chat-empty";
213
+ div.innerHTML = api
214
+ ? '<p class="chat-empty-head">Ask the graph, or ask it anything</p>'
215
+ + '<p>Ask about the code graph on the left, or about anything its general knowledge covers, like “what is a queue”.</p>'
216
+ + '<p class="chat-empty-hint">Try one of the questions below, or type your own.</p>'
217
+ : '<p class="chat-empty-head">Static view</p>'
218
+ + '<p>This page shows a fixed snapshot of the graph. The live chat is not available here.</p>';
197
219
  els.log.appendChild(div);
198
220
  }
199
221
 
@@ -203,6 +225,10 @@ const CLIENT_JS = String.raw`
203
225
  // readSeed under the desktop shell (a file:// page cannot fetch). The
204
226
  // status bar narrates the load; a missing or failing seed leaves the chat
205
227
  // graph-only and says so, never broken.
228
+ // The build's own content hash for the seed rides in the URL, so the service
229
+ // worker's cache-first read can only return the copy this page asked for.
230
+ // Empty under the desktop shell, which reads the seed off disk instead.
231
+ var SEED_QUERY = DATA.seedStamp ? "?b=" + DATA.seedStamp : "";
206
232
  var seedState = { status: api ? "loading" : "absent", payload: null, facts: 0 };
207
233
  function seedNote(text) { if (els.seedStatus) els.seedStatus.textContent = text; }
208
234
  function mbText(n) { return (n / 1048576).toFixed(1); }
@@ -232,7 +258,7 @@ const CLIENT_JS = String.raw`
232
258
  seedNote("loading general knowledge…");
233
259
  text = await window.tmctDesktop.readSeed();
234
260
  } else {
235
- text = await fetchTextWithProgress("./chat-seed.json", function (loaded, total) {
261
+ text = await fetchTextWithProgress("./chat-seed.json" + SEED_QUERY, function (loaded, total) {
236
262
  seedNote("loading general knowledge… " + mbText(loaded) + (total ? " of " + mbText(total) : "") + " MB");
237
263
  });
238
264
  }
@@ -241,13 +267,14 @@ const CLIENT_JS = String.raw`
241
267
  seedState.facts = (seedState.payload.individuals || []).filter(function (i) { return i.class === "Fact"; }).length;
242
268
  seedState.status = "ready";
243
269
  seedNote("general knowledge: " + seedState.facts + " facts");
270
+ renderFactTotal(DATA);
244
271
  return;
245
272
  }
246
273
  } catch (e) {
247
274
  console.warn("tmct code explorer: chat-seed unavailable, continuing graph-only", e);
248
275
  }
249
276
  seedState.status = "absent";
250
- seedNote("graph-only general knowledge unavailable");
277
+ seedNote("graph-only, general knowledge unavailable");
251
278
  }
252
279
  var seedPromise = api ? loadSeed() : Promise.resolve();
253
280
 
@@ -315,6 +342,7 @@ const CLIENT_JS = String.raw`
315
342
  window.__CODE_EXPLORER__ = DATA;
316
343
  if (updateSource && els.source) els.source.textContent = picked.name || "(loaded graph)";
317
344
  els.log.innerHTML = "";
345
+ renderEmptyState();
318
346
  mountView(DATA);
319
347
  }
320
348
  } finally { btn.disabled = false; }
@@ -324,12 +352,11 @@ const CLIENT_JS = String.raw`
324
352
  wirePicker("open-repo", "openRepo", true);
325
353
 
326
354
  if (!api) {
327
- if (els.dockNote) els.dockNote.textContent = "static view — the live chat is unavailable on this page.";
355
+ if (els.dockNote) els.dockNote.textContent = "static view, live chat unavailable here.";
328
356
  if (els.input) els.input.disabled = true;
329
357
  seedNote("static view");
330
- } else {
331
- appendNote("Ask about this code graph — or anything its general knowledge covers, like “what is a queue”.");
332
358
  }
359
+ renderEmptyState();
333
360
 
334
361
  mountView(DATA);
335
362
  })();
@@ -344,7 +371,7 @@ const CLIENT_JS = String.raw`
344
371
  * pointing at the desktop app's README section — the desktop shell itself
345
372
  * renders this page too and passes `false`, since it has nothing to point at.
346
373
  */
347
- export function renderCodeExplorerHtml(data, { bundleInline = "", bundleAvailable = false, winkLoaderInline = "", sourceName = "demo code graph", showDesktopLink = false } = {}) {
374
+ export function renderCodeExplorerHtml(data, { bundleInline = "", bundleAvailable = false, winkLoaderInline = "", sourceName = "demo code graph", showDesktopLink = false, seedStamp = "" } = {}) {
348
375
  const payloadJson = embedJson(data.payload);
349
376
  const dataJson = embedJson({ ledger: data.ledger, hints: data.hints, focus: data.focus, meta: data.meta });
350
377
 
@@ -369,6 +396,11 @@ body { margin: 0; overflow: hidden; background: var(--bg); color: var(--ink); fo
369
396
  .titlebar h1 { font-size: 0.95rem; margin: 0; font-weight: 600; letter-spacing: 0.02em; }
370
397
  .titlebar .sub { color: var(--muted); font-size: 0.76rem; font-family: ${MONO_STACK}; }
371
398
  .titlebar .sub a { color: var(--corpus); }
399
+ /* the fact count sits in the titlebar, not the footer status bar: it is what
400
+ the chat can actually answer from, and the footer is where a wrong number
401
+ goes unread. */
402
+ .titlebar .factpill { display: inline-flex; align-items: baseline; gap: 0.34rem; font-family: ${MONO_STACK}; font-size: 0.62rem; letter-spacing: 0.07em; text-transform: uppercase; color: var(--muted); border: 1px solid var(--entail); border-radius: 99px; padding: 0.14rem 0.7rem; background: var(--entail-soft); white-space: nowrap; }
403
+ .titlebar .factpill-value { font-size: 0.9rem; letter-spacing: 0; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); }
372
404
  .titlebar .pickers { margin-left: auto; display: flex; gap: 0.45rem; }
373
405
  button { font: inherit; cursor: pointer; }
374
406
  button:disabled { cursor: default; opacity: 0.5; }
@@ -397,13 +429,16 @@ ul.rows { list-style: none; margin: 0; padding: 0; }
397
429
  .turn-you .said { font-family: ${MONO_STACK}; font-size: 0.84rem; }
398
430
  .turn-tmct { border-left: 2px solid var(--entail); padding-left: 0.7rem; }
399
431
  .turn-tmct .said { white-space: pre-wrap; }
400
- .turn-note { color: var(--muted); font-style: italic; font-size: 0.85rem; }
401
- .suggest { flex: none; border-top: 1px solid var(--line); padding: 0.45rem 1.1rem 0.1rem; }
432
+ .chat-empty { margin: auto; max-width: 26rem; text-align: center; color: var(--muted); }
433
+ .chat-empty-head { color: var(--ink); font-weight: 600; font-size: 1.05rem; margin: 0 0 0.5rem; }
434
+ .chat-empty p { margin: 0.3rem 0; line-height: 1.55; font-size: 0.92rem; }
435
+ .chat-empty-hint { font-size: 0.82rem; }
436
+ .suggest { flex: none; border-top: 1px solid var(--line); padding: 0.65rem 1.1rem 0.4rem; }
402
437
  .suggest-label { font-family: ${MONO_STACK}; font-size: 0.66rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.09em; color: var(--muted); }
403
- .hints { display: flex; flex-wrap: wrap; gap: 0.35rem; max-height: 5.6rem; overflow-y: auto; padding: 0.35rem 0 0.45rem; }
438
+ .hints { display: flex; flex-wrap: wrap; gap: 0.4rem; max-height: 5.6rem; overflow-y: auto; padding: 0.45rem 0 0.55rem; }
404
439
  .hint { background: var(--bg); border: 1px solid var(--line); border-radius: 999px; padding: 0.24rem 0.7rem; font-size: 0.8rem; color: var(--ink); text-align: left; }
405
440
  .hint:hover { border-color: var(--entail); }
406
- #chat-form { flex: none; display: flex; gap: 0.5rem; padding: 0.55rem 1.1rem 0.8rem; border-top: 1px solid var(--line); }
441
+ #chat-form { flex: none; display: flex; gap: 0.5rem; padding: 0.75rem 1.1rem 0.9rem; border-top: 1px solid var(--line); }
407
442
  #chat-input { flex: 1; min-width: 0; font: inherit; font-size: 0.92rem; padding: 0.5rem 0.65rem; border: 1px solid var(--line); border-radius: 4px; background: var(--bg); color: var(--ink); }
408
443
  #chat-form button { background: var(--entail); color: var(--card); font-weight: 600; border: none; border-radius: 4px; padding: 0.5rem 1rem; font-size: 0.9rem; }
409
444
 
@@ -422,6 +457,9 @@ ul.rows { list-style: none; margin: 0; padding: 0; }
422
457
  <svg class="mark" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 0l1 2.3a5.8 5.8 0 0 1 1.9.8L13.3 2l.7.7-1.1 2.4c.4.6.6 1.2.8 1.9L16 8l-2.3 1a5.8 5.8 0 0 1-.8 1.9l1.1 2.4-.7.7-2.4-1.1a5.8 5.8 0 0 1-1.9.8L8 16l-1-2.3a5.8 5.8 0 0 1-1.9-.8L2.7 14l-.7-.7 1.1-2.4a5.8 5.8 0 0 1-.8-1.9L0 8l2.3-1c.2-.7.4-1.3.8-1.9L2 2.7l.7-.7 2.4 1.1A5.8 5.8 0 0 1 7 2.3L8 0zm0 5.2A2.8 2.8 0 1 0 8 10.8 2.8 2.8 0 0 0 8 5.2z"/></svg>
423
458
  <h1>tmct code explorer</h1>
424
459
  <span class="sub">source: <span id="source-name">${escapeHtml(sourceName)}</span></span>
460
+ <span class="factpill" id="fact-total" aria-live="polite" title="every fact this page's chat can draw on — the code graph's own edges plus the general-knowledge seed once it lands">
461
+ <span class="factpill-value" id="fact-total-value">&mdash;</span> facts loaded
462
+ </span>
425
463
  ${showDesktopLink ? `<span class="sub">Also available as a <a href="${DESKTOP_APP_URL}">desktop app</a>.</span>` : ""}
426
464
  <div class="pickers">
427
465
  <button id="open-graph">Open graph…</button>
@@ -456,7 +494,7 @@ ul.rows { list-style: none; margin: 0; padding: 0; }
456
494
  <span id="seed-status"></span>
457
495
  </footer>
458
496
  </div>
459
- <script>window.__CODE_EXPLORER__ = Object.assign({ payload: ${payloadJson} }, ${dataJson});</script>
497
+ <script>window.__CODE_EXPLORER__ = Object.assign({ payload: ${payloadJson}, seedStamp: ${JSON.stringify(seedStamp)} }, ${dataJson});</script>
460
498
  ${winkLoaderInline ? `<script>\n${embedScriptText(winkLoaderInline)}\n</script>` : ""}
461
499
  ${bundleInline ? `<script>\n${embedScriptText(bundleInline)}\n</script>` : ""}
462
500
  ${bundleAvailable && !bundleInline ? `<script src="./code-explorer.bundle.js"></script>` : ""}
@@ -63,6 +63,7 @@ import { normFactTerm } from "../domain/hash.mjs";
63
63
  import { winkInstance } from "../adapters/wink-model.mjs";
64
64
  import {
65
65
  loadLexicon, lookupNoun, lookupVerb, lookupAdjective, lookupProperName, predicateOf,
66
+ OF_CLASSIFIER_HEADS, OF_PARTITIVE_HEADS,
66
67
  } from "../domain/grammar/lexicon.mjs";
67
68
 
68
69
  export const USAGE = "usage: tmct extract <text-file>|--file <text-file> [--repo <path>] [--out <file.jsonl>] [--optimistic] [--canonical]";
@@ -111,14 +112,10 @@ const OPTIMISTIC_ENTITY_HOPS = 4;
111
112
  // read — the noun on the far side belongs to a different clause or to a
112
113
  // prepositional complement, not to "X is a Y".
113
114
  const COPULA_FRAME_BLOCKERS = new Set(["VERB", "AUX", "ADP", "SCONJ", "CCONJ"]);
114
- // Of-chain handling on a copula object: classifier heads read through to the
115
- // real class; partitive containers state composition and yield no isa.
116
- const COPULA_OF_READ_THROUGH = new Set(["type", "kind", "sort", "form", "class", "variety", "species", "breed", "genus"]);
117
115
  // Naming periphrases stay copular: "can be termed as a name", "is known as",
118
116
  // "is defined as" — the participle + "as" carries the same class claim the
119
117
  // bare copula does, unlike any other verb after "is".
120
118
  const COPULA_NAMING_PARTICIPLES = new Set(["termed", "known", "defined", "described", "referred", "called", "classified"]);
121
- const COPULA_PARTITIVE_HEADS = new Set(["body", "mass", "group", "collection", "set", "series", "number", "amount", "piece", "part", "lot", "pair", "bunch", "pile"]);
122
119
  // The relative pronouns that open a clause predicating about the SENTENCE
123
120
  // subject: "a mountain that has lava" is a fact about the volcano, so the
124
121
  // relative clause's verb binds to the copula's own subject, not to its object.
@@ -202,7 +199,7 @@ function optimisticTriplesPos(sentence, lexicon, nlp) {
202
199
  let k = g - 1;
203
200
  while (k >= 0 && !isNounish(k) && (ofChainSkip(k) || values[k]?.toLowerCase() === "of")) k -= 1;
204
201
  if (k < 0 || !isNounish(k)) return null; // no readable head — abstain
205
- if (COPULA_OF_READ_THROUGH.has(String(values[k]).toLowerCase())) return lo; // classifier reads through
202
+ if (OF_CLASSIFIER_HEADS.has(String(values[k]).toLowerCase())) return lo; // classifier reads through
206
203
  lo = runLoOf(k);
207
204
  }
208
205
  return lo;
@@ -271,8 +268,8 @@ function optimisticTriplesPos(sentence, lexicon, nlp) {
271
268
  const headWord = String(values[hi]).toLowerCase();
272
269
  const nextIsOf = values[hi + 1]?.toLowerCase() === "of";
273
270
  if (!nextIsOf) return { label: entityRunAt(j), hi };
274
- if (COPULA_OF_READ_THROUGH.has(headWord)) { i = hi + 1; j = hi + 1; continue; }
275
- if (COPULA_PARTITIVE_HEADS.has(headWord)) return null;
271
+ if (OF_CLASSIFIER_HEADS.has(headWord)) { i = hi + 1; j = hi + 1; continue; }
272
+ if (OF_PARTITIVE_HEADS.has(headWord)) return null;
276
273
  return { label: entityRunAt(j), hi };
277
274
  }
278
275
  return null;