@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.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 (65) hide show
  1. package/README.md +2 -1
  2. package/corpus/sprites/src/sprite-facts.jsonl +375 -8
  3. package/package.json +1 -1
  4. package/src/adapters/memory/core.mjs +20 -0
  5. package/src/domain/ask-vocab.mjs +71 -0
  6. package/src/domain/ask.mjs +168 -0
  7. package/src/domain/game-config.mjs +11 -0
  8. package/src/domain/mud-facts.mjs +15 -0
  9. package/src/domain/router/drive.mjs +35 -9
  10. package/src/domain/router/registry.mjs +24 -4
  11. package/src/domain/router/resolver.mjs +102 -40
  12. package/src/domain/scene-compose.mjs +117 -0
  13. package/src/domain/spider-fly-world.mjs +36 -0
  14. package/src/domain/sprite-facts.mjs +0 -0
  15. package/src/domain/sprite-request.mjs +156 -0
  16. package/src/domain/sprite-templates.mjs +161 -14
  17. package/src/services/adventure-editor.mjs +8 -14
  18. package/src/services/adventure-viz.mjs +119 -150
  19. package/src/services/adventure.mjs +97 -35
  20. package/src/services/chat-page-viz.mjs +64 -48
  21. package/src/services/chat.mjs +102 -34
  22. package/src/services/code-explorer-viz.mjs +52 -50
  23. package/src/services/ingest-viz.mjs +32 -74
  24. package/src/services/ledger-viz.mjs +87 -70
  25. package/src/services/memory-panel-viz.mjs +38 -0
  26. package/src/services/mud-editor.mjs +10 -15
  27. package/src/services/mud-turn.mjs +6 -6
  28. package/src/services/mud-viz.mjs +119 -225
  29. package/src/services/p2p-room.mjs +90 -23
  30. package/src/services/plan-pddl.mjs +3 -1
  31. package/src/services/plan-viz.mjs +13 -12
  32. package/src/services/research-viz.mjs +25 -67
  33. package/src/services/spider-fly-turn.mjs +14 -22
  34. package/src/services/spider-fly-viz.mjs +97 -136
  35. package/src/services/spider-fly.mjs +69 -11
  36. package/src/services/sprite-catalog-viz.mjs +274 -224
  37. package/src/services/viz-boot.mjs +71 -0
  38. package/src/services/viz-room-graph.mjs +203 -0
  39. package/src/services/viz-theme.mjs +75 -1
  40. package/src/services/viz-ticker.mjs +22 -0
  41. package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
  42. package/src/surfaces/web/chat-browser-entry.mjs +51 -107
  43. package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
  44. package/src/surfaces/web/engine-surface.mjs +82 -0
  45. package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
  46. package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
  47. package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
  48. package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
  49. package/src/surfaces/web/memory-stats.mjs +11 -0
  50. package/src/surfaces/web/mud-browser-entry.mjs +70 -49
  51. package/src/surfaces/web/plan-browser-entry.mjs +39 -50
  52. package/src/surfaces/web/research-browser-entry.mjs +48 -46
  53. package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
  54. package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
  55. package/src/surfaces/web/tmct-surface.mjs +147 -0
  56. package/src/surfaces/web/turn-session.mjs +124 -0
  57. package/src/tools/definitions.mjs +30 -0
  58. package/src/tools/handlers/index.mjs +6 -3
  59. package/src/tools/handlers/kit.mjs +19 -2
  60. package/src/tools/handlers/tmct-ask.mjs +11 -6
  61. package/src/tools/handlers/tmct-ingest.mjs +5 -1
  62. package/src/tools/handlers/tmct-related.mjs +4 -4
  63. package/src/tools/handlers/tmct-sprite.mjs +147 -0
  64. package/src/tools/memory-fallthrough.mjs +9 -2
  65. package/src/tools/server.mjs +37 -6
@@ -953,9 +953,35 @@ async function answerMemoryClassQuery(memoryDir, query) {
953
953
  // literal quantifier lookup. Placed ahead of it in runTurn: HOW_MANY_ARE_RE
954
954
  // reads "there" as a second noun and answers "I was never told a quantifier",
955
955
  // stealing the phrasing before a member count ever runs.
956
- const TAUGHT_CLASS_COUNT_RE = /^how\s+many\s+([a-z][\w-]*)\s*(.*)$/i;
956
+ const TAUGHT_CLASS_COUNT_RE = /^how\s+many\s+([a-z][\w-]*(?:\s+[a-z][\w-]*)*)\s*(.*)$/i;
957
+
958
+ /** The longest leading run of `nounRun`'s words that names a class something was
959
+ * actually taught about, as `{asked, tail, members}` — its taught members and
960
+ * whatever words are left over, joined onto `trailing` as the restrictor tail.
961
+ * A class name is a noun PHRASE, not a word ("sprite class", "body of water"),
962
+ * so the run is tried longest-first and the shortest reading wins only when no
963
+ * longer one is on record. That ordering is what keeps a single-word class
964
+ * carrying a restrictor ("list the animals in the graph") reading exactly as it
965
+ * did when only the first word was ever considered. */
966
+ async function longestTaughtClassInRun(memoryDir, nounRun, trailing, biasByBundle, cache) {
967
+ const words = String(nounRun || "").trim().split(/\s+/).filter(Boolean);
968
+ if (!words.length) return null;
969
+ if (COUNT_NOUNS[words[0].toLowerCase()]) return null; // a real graph-countable class — the code lanes own it
970
+ let normFactTerm;
971
+ try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
972
+ const rows = await factRows(memoryDir, cache);
973
+ const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
974
+ for (let take = words.length; take >= 1; take -= 1) {
975
+ const asked = words.slice(0, take).join(" ").toLowerCase();
976
+ const variants = factTermVariants(normFactTerm, asked);
977
+ const members = rankByBiasThenTrust(isa.filter((f) => variants.has(f.object)), biasByBundle);
978
+ if (!members.length) continue; // nothing taught under this reading — try a shorter one
979
+ return { asked, tail: [words.slice(take).join(" "), String(trailing || "")].filter(Boolean).join(" ").trim(), members };
980
+ }
981
+ return null;
982
+ }
957
983
 
958
- /** Count the taught members of a class named by a plain noun ("how many animals
984
+ /** Count the taught members of a class named by a noun phrase ("how many animals
959
985
  * are there" → every "X is a kind of animal"). Declines (null) for a real
960
986
  * code-countable class (answerCount owns it) or a class nothing was taught
961
987
  * about, so structural counts and the quantifier lane are unaffected. */
@@ -963,22 +989,16 @@ async function answerTaughtClassCount(memoryDir, query, biasByBundle = {}, cache
963
989
  if (!memoryDir) return null;
964
990
  const m = String(query).trim().match(TAUGHT_CLASS_COUNT_RE);
965
991
  if (!m) return null;
966
- if (!DYNAMIC_TAIL_OK_RE.test((m[2] || "").trim())) return null;
967
- const asked = m[1].toLowerCase();
968
- if (COUNT_NOUNS[asked]) return null; // a real graph-countable class — answerCount owns it
969
- let normFactTerm;
970
- try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
971
- const rows = await factRows(memoryDir, cache);
972
- const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
973
- const variants = factTermVariants(normFactTerm, asked);
974
- const members = rankByBiasThenTrust(isa.filter((f) => variants.has(f.object)), biasByBundle);
975
- if (!members.length) return null; // nothing taught under this class name — later lanes own it
992
+ const hit = await longestTaughtClassInRun(memoryDir, m[1], m[2], biasByBundle, cache);
993
+ if (!hit) return null;
994
+ if (!DYNAMIC_TAIL_OK_RE.test(hit.tail)) return null;
976
995
  // A member whose SUBJECT is itself a countable graph class ("every class is a
977
996
  // component") is an asserted-vocabulary cardinality, not a member enumeration —
978
997
  // countFromFacts counts the real class, so defer to it rather than tallying the
979
998
  // one class-level fact.
980
- if (members.some((f) => COUNT_NOUNS[String(f.subject).toLowerCase()])) return null;
981
- return `${members.length} ${members.length === 1 ? asked.replace(/s$/, "") : asked}.`;
999
+ if (hit.members.some((f) => COUNT_NOUNS[String(f.subject).toLowerCase()])) return null;
1000
+ const n = hit.members.length;
1001
+ return `${n} ${n === 1 ? hit.asked.replace(/s$/, "") : hit.asked}.`;
982
1002
  }
983
1003
 
984
1004
  // "list all animals" / "list the animals" — enumerate a taught class's members,
@@ -986,9 +1006,9 @@ async function answerTaughtClassCount(memoryDir, query, biasByBundle = {}, cache
986
1006
  // leftovers: at scale the definition lane fills its cap with forward corpus facts
987
1007
  // before the reverse-membership listing ever shows, and the conversational
988
1008
  // orientation lane claims the bare "list …" phrasing before factReadBack runs.
989
- const MEMBERSHIP_LIST_RE = /^(?:list|show(?:\s+me)?)\s+(?:all\s+|the\s+)?([a-z][\w-]*)\s*(.*)$/i;
1009
+ const MEMBERSHIP_LIST_RE = /^(?:list|show(?:\s+me)?)\s+(?:all\s+|the\s+)?([a-z][\w-]*(?:\s+[a-z][\w-]*)*)\s*(.*)$/i;
990
1010
 
991
- /** List the taught members of a class named by a plain noun ("list all animals"
1011
+ /** List the taught members of a class named by a noun phrase ("list all animals"
992
1012
  * → every "X is a kind of animal"). Declines (null) for a code-countable class
993
1013
  * or a class nothing was taught about; declines with a message for a real
994
1014
  * restrictor tail rather than answering as if it weren't there. */
@@ -996,16 +1016,9 @@ async function answerMembershipList(memoryDir, query, biasByBundle = {}, cache =
996
1016
  if (!memoryDir) return null;
997
1017
  const m = String(query).trim().match(MEMBERSHIP_LIST_RE);
998
1018
  if (!m) return null;
999
- const asked = m[1].toLowerCase();
1000
- if (COUNT_NOUNS[asked]) return null; // a real graph-countable class — the code list lane owns it
1001
- const tail = (m[2] || "").trim();
1002
- let normFactTerm;
1003
- try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
1004
- const rows = await factRows(memoryDir, cache);
1005
- const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
1006
- const variants = factTermVariants(normFactTerm, asked);
1007
- const members = rankByBiasThenTrust(isa.filter((f) => variants.has(f.object)), biasByBundle);
1008
- if (!members.length) return null; // nothing taught under this class name — later lanes own it
1019
+ const hit = await longestTaughtClassInRun(memoryDir, m[1], m[2], biasByBundle, cache);
1020
+ if (!hit) return null;
1021
+ const { asked, tail, members } = hit;
1009
1022
  if (!DYNAMIC_TAIL_OK_RE.test(tail)) {
1010
1023
  return {
1011
1024
  text: `I can list the ${asked}, but not the "${tail}" part of that question — `
@@ -2114,7 +2127,7 @@ export { moduleCountOf };
2114
2127
  /** A KNOWN-empty code graph: a loaded graph object with 0 modules. A null graph
2115
2128
  * (a bare runTurn that wasn't handed one) is "unknown", NOT empty — the empty
2116
2129
  * orientation/greeting only fires when we actually hold an empty graph. */
2117
- const noCodeGraph = (graph) => !!graph && moduleCountOf(graph) === 0;
2130
+ export const noCodeGraph = (graph) => !!graph && moduleCountOf(graph) === 0;
2118
2131
 
2119
2132
  /** LIVE orientation examples: the example queries on the orientation card name
2120
2133
  * entities from the LOADED graph — the sorted-first Module label and the
@@ -7160,6 +7173,16 @@ const LOCATIVE_FACT_PREDICATE_RE = /^mgx:[a-z]+-(?:on|in|at|inside|under|below|a
7160
7173
  * to the ordinary BARE_WHATIS_RE handling untouched. */
7161
7174
  const WHAT_IS_PREP_FACT_RE = new RegExp(`^what(?:'s|\\s+is|\\s+are)\\s+(${PREP_SRC})\\s+(.+?)\\s*[?.!]*$`, "i");
7162
7175
 
7176
+ /** "what parameters does a person sprite take" / "what materials does a bed
7177
+ * accept" — the object-fronted property question, where the property noun
7178
+ * leads and the verb closes. It reads the same folded verb-plus-noun predicates
7179
+ * the teach path already mints (mgx:take-parameter, mgx:accept-material,
7180
+ * mgx:offer-variant), so the predicate is recovered from the sentence's own two
7181
+ * ends rather than from a table of known property words: m[1] is the noun,
7182
+ * m[2] the subject, m[3] the verb. Consumed by factAnswer's (a-pre6) reader,
7183
+ * which diverts only on a real stored hit. */
7184
+ const OBJECT_FRONTED_PROPERTY_RE = /^what\s+([a-z][\w-]*)\s+(?:does|do)\s+(?:an?\s+|the\s+)?(.+?)\s+([a-z][a-z-]*)\s*[?.!]*$/i;
7185
+
7163
7186
  // CAN_ASK_RE's remaining paraphrase-ladder siblings, all over the same
7164
7187
  // mgx:capableOf facts:
7165
7188
  // - DO_VERB_ASK_RE: the do-support yes/no ("do birds fly", "does a dog
@@ -7666,6 +7689,35 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
7666
7689
  }
7667
7690
  }
7668
7691
 
7692
+ // (a-pre6) "what parameters does a person sprite take" — the object-fronted
7693
+ // property question over a folded verb-plus-noun predicate. Both ends of the
7694
+ // sentence carry half the predicate the teach path minted, so the verb and the
7695
+ // noun are rejoined into mgx:<verb>-<noun> and looked up directly; the noun is
7696
+ // tried through its own plural variants, since a question asks for "parameters"
7697
+ // where the stored predicate names one "parameter". Checked before (a) for the
7698
+ // same reason as its siblings above — the leading "what …" would otherwise be
7699
+ // claimed as one literal term to define — and hit-gated the same way, so a
7700
+ // sentence of this shape with nothing on record falls through untouched.
7701
+ const propertyQ = q.match(OBJECT_FRONTED_PROPERTY_RE);
7702
+ if (propertyQ) {
7703
+ const verb = propertyQ[3].toLowerCase();
7704
+ const subjectVariants = factTermVariants(normFactTerm, propertyQ[2]);
7705
+ const predicates = new Set(
7706
+ [...factTermVariants(normFactTerm, propertyQ[1])].map((noun) => normFactPredicate(`mgx:${verb}-${noun.replace(/\s+/g, "-")}`)),
7707
+ );
7708
+ const hits = (await factRows(memoryDir, cache)).filter(
7709
+ (f) => predicates.has(normFactPredicate(f.predicate)) && subjectVariants.has(normFactTerm(f.subject)),
7710
+ );
7711
+ if (hits.length) {
7712
+ const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
7713
+ const lines = ranked.map(renderFactLine);
7714
+ const shown = lines.slice(0, FACT_ANSWER_CAP);
7715
+ const rest = lines.slice(FACT_ANSWER_CAP);
7716
+ const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
7717
+ return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
7718
+ }
7719
+ }
7720
+
7669
7721
  // (a) meta-shaped questions ("what is a module", "what does cache mean") — the
7670
7722
  // parsed object term, matched against fact SUBJECTS; consulted for hits (append
7671
7723
  // alongside the schema-docs answer) and misses (facts answer alone) alike.
@@ -12526,11 +12578,16 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
12526
12578
  let envelope = null;
12527
12579
  try {
12528
12580
  let text;
12529
- if (graph && (focus?.id || prev.length)) {
12530
- // Direct ask() when EITHER a focus is set (thread it as contextId so "it"
12531
- // binds) OR the previous turn produced a set to refer back to (thread it as
12532
- // `prev` for the anaphora node). Builds the SAME delimited envelope dispatchTool
12533
- // emits, so the parse below is identical either way.
12581
+ if (graph?.individuals?.length || (graph && (focus?.id || prev.length))) {
12582
+ // Direct ask() whenever the caller HANDED US a graph with something in it.
12583
+ // The focus/prev pair is threaded through it (contextId so "it" binds, prev
12584
+ // for the anaphora node), but neither is what earns the direct call: a
12585
+ // caller that passes a real graph means that graph, and the tmct_ask branch
12586
+ // below reads the CONFIG's graph instead — which an in-process session (a
12587
+ // page's own world facts, say) has no file for. Gating on history alone
12588
+ // refused every cold turn against a perfectly good graph and only started
12589
+ // answering once a focus happened to be set. Builds the SAME delimited
12590
+ // envelope dispatchTool emits, so the parse below is identical either way.
12534
12591
  const { ask } = await import("../domain/ask.mjs");
12535
12592
  const r = ask(graph, askQuery, { contextId: effectiveContextId, prev });
12536
12593
  text = `${r.content}${ASK_ENVELOPE_DELIM}${JSON.stringify(r.tmct_ask, null, 2)}`;
@@ -14084,9 +14141,20 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
14084
14141
  if (name === "plan") {
14085
14142
  note(trace, "goal: plan/execute a compound or maintenance-goal request over the graph (the capability router)");
14086
14143
  if (!argText) return mk("/plan needs a request, e.g. `/plan of the modules impacted by X, which are untested`.", { miss: true });
14087
- if (!graph) return mk("no graph loaded /plan needs a code graph to plan over.", { miss: true });
14144
+ // A KNOWN-EMPTY code graph is nothing to plan over, and it is what every
14145
+ // memory-graph page and an un-pointed CLI session actually holds: each of
14146
+ // its parameter slots would bind against an index with no entities in it.
14147
+ // Where there is a memory store, hand the planner that instead — that is
14148
+ // buildCapabilityPlanCtx's memory-only mode, where world facts bind and a
14149
+ // code-graph capability refuses by naming the graph it hasn't got. Only a
14150
+ // graph with something in it plans as a code graph. `source` travels with
14151
+ // it: this turn reuses what it already holds and never loads one mid-turn.
14152
+ const planGraph = graph && !noCodeGraph(graph) ? graph : null;
14153
+ if (!planGraph && !memoryDir) return mk("no graph loaded — /plan needs a code graph or a memory store to plan over.", { miss: true });
14088
14154
  const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } = await import("../domain/router/drive.mjs");
14089
- const planCtx = await buildCapabilityPlanCtx({ ...capabilityPlanDeps(), config, source, tel, graph, memoryDir });
14155
+ const planCtx = await buildCapabilityPlanCtx({
14156
+ ...capabilityPlanDeps(), config, source: planGraph ? source : null, tel, graph: planGraph, memoryDir,
14157
+ });
14090
14158
  try {
14091
14159
  const result = await runCapabilityPlan(argText, declaredCapabilityNames(), planCtx);
14092
14160
  if (result.refused) {
@@ -2,11 +2,14 @@
2
2
  // a title bar (graph source, Open graph…/Open repo…), an explorer sidebar
3
3
  // reading each import/call/contains edge back as a plain sentence, a chat
4
4
  // centre over the same graph with a rail of suggested questions, and a status
5
- // bar carrying the graph's own counts. The chat session seeds BOTH the loaded
6
- // code graph and chat.html's general-knowledge bands (./chat-seed.json,
7
- // fetched lazily at runtime), so one conversation answers "what is a queue"
8
- // and "what imports src/core/model.mjs" alike — and degrades to graph-only
9
- // when the seed asset is unavailable.
5
+ // bar carrying the graph's own counts. Clicking a term asks the engine what
6
+ // relates to it (askRelatedFacts, over tmct_ask) and renders the answer, so the
7
+ // sidebar and the chat put the same question to the same place.
8
+ //
9
+ // The chat session seeds BOTH the loaded code graph and chat.html's
10
+ // general-knowledge bands (./chat-seed.json, fetched lazily at runtime), so one
11
+ // conversation answers "what is a queue" and "what imports src/core/model.mjs"
12
+ // alike — and degrades to graph-only when the seed asset is unavailable.
10
13
  //
11
14
  // The derivations are pure so the shell, the packaging scripts, and the unit
12
15
  // tests all share one code path; renderCodeExplorerHtml builds one
@@ -18,24 +21,14 @@
18
21
 
19
22
  import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
20
23
  import { generateCodeHints } from "../domain/code-explorer-hints.mjs";
24
+ import { phraseForRelation } from "../domain/ask-vocab.mjs";
25
+ import { fetchWithProgress } from "./memory-panel-viz.mjs";
21
26
 
22
- // Third-person verb for each stored relation kind, symbol grain folded onto its
23
- // coarse sibling. A kind with no row here reads back as itself, never breaking
24
- // the sentence.
25
- const EDGE_PHRASE = new Map([
26
- ["imports", "imports"],
27
- ["calls", "calls"], ["callsSymbol", "calls"],
28
- ["contains", "contains"],
29
- ["defines", "defines"],
30
- ["inherits", "inherits from"],
31
- ["tests", "tests"],
32
- ["touches", "touches"], ["touchesSymbol", "touches"],
33
- ["cochange", "co-changes with"],
34
- ["reexports", "re-exports"],
35
- ]);
36
-
27
+ // Third-person verb for each stored relation kind, symbol grain folded onto
28
+ // its coarse sibling derived from ask-vocab.mjs's own RELATIONS table
29
+ // rather than a second hand-curated relation-verb table.
37
30
  export function edgePhrase(kind) {
38
- return EDGE_PHRASE.get(String(kind || "")) || String(kind || "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
31
+ return phraseForRelation(kind);
39
32
  }
40
33
 
41
34
  // Both packagers (build-electron-app.mjs, build-demo-site.mjs) place
@@ -117,7 +110,7 @@ export function computeCodeExplorerData(payload, opts = {}) {
117
110
  const CLIENT_JS = String.raw`
118
111
  (function () {
119
112
  var DATA = window.__CODE_EXPLORER__;
120
- var api = window.tmctCodeExplorer || null;
113
+ var api = window.tmct ? window.tmct.page : null;
121
114
  var els = {
122
115
  focus: document.getElementById("focus-name"),
123
116
  ledger: document.getElementById("ledger"),
@@ -133,11 +126,7 @@ const CLIENT_JS = String.raw`
133
126
  };
134
127
  var session = null;
135
128
 
136
- function esc(s) {
137
- return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
138
- return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
139
- });
140
- }
129
+ var esc = ${escapeHtml.toString()};
141
130
 
142
131
  function renderStats(data) {
143
132
  var s = data.ledger.stats;
@@ -155,11 +144,37 @@ const CLIENT_JS = String.raw`
155
144
  els.factTotal.textContent = (edges + seedState.facts).toLocaleString();
156
145
  }
157
146
 
158
- function renderFocusRows(data) {
147
+ // "What relates to the focus", put to the engine. askRelatedFacts asks
148
+ // tmct_ask one question per relation kind the loaded graph carries, in both
149
+ // directions, and hands back the answers' own typed rows — the same ask()
150
+ // this page's chat turns reach, so the panel and the conversation answer one
151
+ // question one way. Null on the static page, which has no engine to ask.
152
+ function askRelated(focus) {
153
+ if (!api || !api.askRelatedFacts || !focus) return null;
154
+ try {
155
+ return api.askRelatedFacts(DATA.payload, focus);
156
+ } catch (e) {
157
+ console.warn("tmct code explorer: the related-facts ask failed, splitting the row list instead", e);
158
+ return null;
159
+ }
160
+ }
161
+
162
+ function rowKey(r) { return JSON.stringify([r.s, r.kind, r.o]); }
163
+
164
+ function renderFocusRows(data, related) {
159
165
  var focus = data.focus;
160
166
  var rows = data.ledger.rows;
161
- var near = rows.filter(function (r) { return r.s === focus || r.o === focus; });
162
- var rest = rows.filter(function (r) { return r.s !== focus && r.o !== focus; });
167
+ // The engine's answer whenever it grounded one. The row list's own split is
168
+ // what is left when there is no engine (the static page) or when every
169
+ // question came back parsed as something else — an identifier that is
170
+ // itself a relation verb reads as a question about the verb.
171
+ var grounded = Boolean(related && related.grounded);
172
+ var near = grounded ? related.rows : rows.filter(function (r) { return r.s === focus || r.o === focus; });
173
+ var nearKeys = {};
174
+ near.forEach(function (r) { nearKeys[rowKey(r)] = true; });
175
+ // Whatever the neighbourhood did not already name, in degree order: a bulk
176
+ // view of the rest of the graph, which is a fold and not a question.
177
+ var rest = rows.filter(function (r) { return !nearKeys[rowKey(r)]; });
163
178
  var ordered = near.concat(rest);
164
179
  els.ledger.innerHTML = ordered.map(function (r) {
165
180
  var hot = (r.s === focus || r.o === focus) ? " row-focus" : "";
@@ -168,7 +183,9 @@ const CLIENT_JS = String.raw`
168
183
  + '<span class="verb">' + esc(r.phrase) + '</span> '
169
184
  + '<button class="term" data-term="' + esc(r.o) + '">' + esc(r.o) + '</button>'
170
185
  + '</li>';
171
- }).join("") || '<li class="row muted">no edges in this graph.</li>';
186
+ }).join("") || (grounded
187
+ ? '<li class="row muted">nothing in this graph relates to ' + esc(focus) + '.</li>'
188
+ : '<li class="row muted">no edges in this graph.</li>');
172
189
  els.focus.textContent = focus || "—";
173
190
  }
174
191
 
@@ -188,7 +205,7 @@ const CLIENT_JS = String.raw`
188
205
 
189
206
  function mountView(data) {
190
207
  renderStats(data);
191
- renderFocusRows(data);
208
+ renderFocusRows(data, askRelated(data.focus));
192
209
  renderHints(data);
193
210
  }
194
211
 
@@ -233,23 +250,7 @@ const CLIENT_JS = String.raw`
233
250
  function seedNote(text) { if (els.seedStatus) els.seedStatus.textContent = text; }
234
251
  function mbText(n) { return (n / 1048576).toFixed(1); }
235
252
 
236
- async function fetchTextWithProgress(url, onProgress) {
237
- var res = await fetch(url);
238
- if (!res.ok) throw new Error("HTTP " + res.status);
239
- var total = Number(res.headers.get("content-length")) || 0;
240
- if (!res.body || !res.body.getReader) return res.text();
241
- var reader = res.body.getReader();
242
- var chunks = [];
243
- var loaded = 0;
244
- for (;;) {
245
- var step = await reader.read();
246
- if (step.done) break;
247
- chunks.push(step.value);
248
- loaded += step.value.byteLength;
249
- onProgress(loaded, total);
250
- }
251
- return new Blob(chunks).text();
252
- }
253
+ var fetchWithProgress = ${fetchWithProgress.toString()};
253
254
 
254
255
  async function loadSeed() {
255
256
  try {
@@ -258,9 +259,10 @@ const CLIENT_JS = String.raw`
258
259
  seedNote("loading general knowledge…");
259
260
  text = await window.tmctDesktop.readSeed();
260
261
  } else {
261
- text = await fetchTextWithProgress("./chat-seed.json" + SEED_QUERY, function (loaded, total) {
262
+ var seedBlob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY, function (loaded, total) {
262
263
  seedNote("loading general knowledge… " + mbText(loaded) + (total ? " of " + mbText(total) : "") + " MB");
263
264
  });
265
+ text = await seedBlob.text();
264
266
  }
265
267
  if (text) {
266
268
  seedState.payload = JSON.parse(text);
@@ -2,7 +2,7 @@
2
2
  // self-contained document shaped exactly like chat-page-viz.mjs's own
3
3
  // page-builder — one inlined <style> importing viz-theme.mjs's shared tokens,
4
4
  // behaviour as an inlined IIFE — running the ingest engine
5
- // (ingest-browser.bundle.js's globalThis.tmctIngest) by same-origin relative
5
+ // (ingest-browser.bundle.js's globalThis.tmct) by same-origin relative
6
6
  // paths.
7
7
  //
8
8
  // The page's own chrome is a two-pane translate-tool layout: mode pills
@@ -26,46 +26,20 @@
26
26
  // input. scripts/build-demo-site.mjs calls it directly and writes the result
27
27
  // to public/ingest.html, after ingest-browser.bundle.js already exists.
28
28
  import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
29
- import { bandLabelFor, statsSummaryLine, clearSiteAssetCaches, fetchWithProgress, renderStatsPanelInto } from "./memory-panel-viz.mjs";
29
+ import {
30
+ bandLabelFor,
31
+ statsSummaryLine,
32
+ clearSiteAssetCaches,
33
+ fetchWithProgress,
34
+ renderStatsPanelInto,
35
+ loadProgressLine,
36
+ factTripleParts,
37
+ } from "./memory-panel-viz.mjs";
38
+ import { loadWinkVendor } from "./viz-boot.mjs";
39
+ import { cloneMemoryPayload } from "../adapters/memory/core.mjs";
30
40
 
31
41
  const DEFAULT_TITLE = "the-mechanical-code-talker — ingest";
32
42
 
33
- /** One grounded fact as a canonical triple line — subject, predicate, object,
34
- * each in its own cell so the pane can align them. Pure and
35
- * `.toString()`-splice safe (no outer refs): the inline script splices this
36
- * in and calls it per row. */
37
- export function factTripleParts(fact) {
38
- return {
39
- subject: String((fact && fact.subject) || ""),
40
- predicate: String((fact && fact.predicate) || ""),
41
- object: String((fact && fact.object) || ""),
42
- provenance: String((fact && fact.provenance) || ""),
43
- };
44
- }
45
-
46
- /** The boot statusline while the big assets stream in — the same aggregator
47
- * chat-page-viz.mjs's own loadProgressLine is (kept as this page's own copy
48
- * rather than a shared import — the two pages' boot lines diverge slightly
49
- * and neither is a collaborator the other calls). `parts` is an array of
50
- * { loaded, total } byte counts (total 0 when the response carried no
51
- * Content-Length); with no usable total the line shows loaded bytes alone
52
- * rather than inventing a denominator. Self-contained, `.toString()`-splice
53
- * safe. */
54
- export function loadProgressLine(parts) {
55
- const mb = (n) => (n / 1048576).toFixed(1);
56
- let loaded = 0;
57
- let total = 0;
58
- let totalKnown = true;
59
- for (const p of parts || []) {
60
- loaded += (p && p.loaded) || 0;
61
- if (p && p.total > 0) total += p.total;
62
- else totalKnown = false;
63
- }
64
- return totalKnown && total > 0
65
- ? "loading the engine… " + mb(loaded) + " MB / " + mb(total) + " MB"
66
- : "loading the engine… " + mb(loaded) + " MB";
67
- }
68
-
69
43
  /** The self-contained ingest page. Pure — the same output for the same
70
44
  * `title` every time; every piece of state (the session, each grounded fact)
71
45
  * is computed live in the browser once the sibling ingest bundle loads. */
@@ -159,7 +133,7 @@ ${THEME_TOKENS_CSS}
159
133
  the right of the ingest column (a real layout column, not an overlay) —
160
134
  the same class names and breakpoint chat-page-viz.mjs's own docked panel
161
135
  uses, re-rendered after boot and after every ingest from
162
- window.tmctIngest's own memoryStats(). */
136
+ window.tmct's own memoryStats(). */
163
137
  .statsPanel { flex: 0 0 300px; max-width: 300px; overflow-y: auto; border-left: 1px solid var(--line); padding: 1.1rem 1.2rem 1.6rem; font-family: ${MONO_STACK}; font-size: .74rem; line-height: 1.55; display: flex; flex-direction: column; }
164
138
  .statsPanel h2 { font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin: 1.3rem 0 .5rem; }
165
139
  .statsPanel h2:first-child { margin-top: 0; }
@@ -385,8 +359,8 @@ ${THEME_TOKENS_CSS}
385
359
  // ---- memory stats: the docked panel, same convention as chat.html --------
386
360
  async function renderStatsPanel(stats) {
387
361
  if (!stats) {
388
- if (!session || !window.tmctIngest.memoryStats) return;
389
- try { stats = await window.tmctIngest.memoryStats(session.memoryDir); }
362
+ if (!session || !window.tmct.page.memoryStats) return;
363
+ try { stats = await window.tmct.page.memoryStats(session.memoryDir); }
390
364
  catch { return; }
391
365
  }
392
366
  factPillValueEl.textContent = Number(stats.total || 0).toLocaleString();
@@ -440,30 +414,14 @@ ${THEME_TOKENS_CSS}
440
414
  console.warn("tmct ingest: chat-seed.json unavailable — starting unseeded", err);
441
415
  }
442
416
  }
443
- function cloneSeed() {
444
- if (!seedPayload) return null;
445
- try { return structuredClone(seedPayload); } catch { return JSON.parse(JSON.stringify(seedPayload)); }
446
- }
447
- function newSession() {
448
- return window.tmctIngest.createIngestSession({ seedPayload: cloneSeed(), vocabSeeded: Boolean(seedPayload) });
417
+ const cloneMemoryPayload = ${cloneMemoryPayload.toString()};
418
+ async function newSession() {
419
+ return window.tmct.open({ seedPayload: cloneMemoryPayload(seedPayload), vocabSeeded: Boolean(seedPayload) });
449
420
  }
450
421
 
451
422
  // ---- engine boot ---------------------------------------------------------
452
- const WINK_LOAD_TIMEOUT_MS = 8000;
453
- async function tryLoadWink() {
454
- let settled = false;
455
- const timeout = new Promise((_, reject) => setTimeout(() => { if (!settled) reject(new Error("wink load stalled")); }, WINK_LOAD_TIMEOUT_MS));
456
- try {
457
- const mod = await Promise.race([import("./vendor/wink.js"), timeout]);
458
- settled = true;
459
- window.tmctIngest.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
460
- return "loaded";
461
- } catch (err) {
462
- settled = true;
463
- console.warn("tmct ingest: the wink vendor asset failed to load; the recognizer needs it to split and parse sentences", err);
464
- return "unavailable";
465
- }
466
- }
423
+ const loadWinkVendor = ${loadWinkVendor.toString()};
424
+ const tryLoadWink = loadWinkVendor({ register: (factory) => window.tmct.page.registerWinkModel(factory) });
467
425
 
468
426
  // The deploy's own version, read off the service worker file the build
469
427
  // already stamps — the only same-origin place the number exists at runtime
@@ -509,10 +467,10 @@ ${THEME_TOKENS_CSS}
509
467
  clearTimeout(saveTimer);
510
468
  saveTimer = null;
511
469
  if (persist) await persist.clear();
512
- session = newSession();
470
+ session = await newSession();
513
471
  clearFactsPane();
514
472
  updateIngestEnabled();
515
- const stats = await window.tmctIngest.memoryStats(session.memoryDir);
473
+ const stats = await window.tmct.page.memoryStats(session.memoryDir);
516
474
  statusEl.textContent = "forgot everything taught on this device. Back to the fresh seed (" + statsSummaryLine(stats, bandLabelFor) + ").";
517
475
  await renderStatsPanel(stats);
518
476
  }
@@ -528,9 +486,9 @@ ${THEME_TOKENS_CSS}
528
486
  await fetchSeedIfWanted();
529
487
  clearTimeout(saveTimer);
530
488
  saveTimer = null;
531
- session = newSession();
489
+ session = await newSession();
532
490
  clearFactsPane();
533
- const stats = await window.tmctIngest.memoryStats(session.memoryDir);
491
+ const stats = await window.tmct.page.memoryStats(session.memoryDir);
534
492
  statusEl.textContent = statsSummaryLine(stats, bandLabelFor) + ". Ready.";
535
493
  await renderStatsPanel(stats);
536
494
  updateIngestEnabled();
@@ -538,7 +496,7 @@ ${THEME_TOKENS_CSS}
538
496
  });
539
497
 
540
498
  async function boot() {
541
- if (!window.tmctIngest) {
499
+ if (!window.tmct) {
542
500
  statusEl.textContent = "the ingest engine didn't load. This page needs its build step (npm run demo:build)";
543
501
  return;
544
502
  }
@@ -549,16 +507,16 @@ ${THEME_TOKENS_CSS}
549
507
  fetchSiteVersion().then((v) => { siteVersion = v; }),
550
508
  ]);
551
509
  progressActive = false;
552
- if (window.tmctIngest.openPersistedStore) {
553
- persist = window.tmctIngest.openPersistedStore({ storeKey: "ingest", stamp: siteVersion + ":" + seedFacts + ":" + SEED_STAMP });
510
+ if (window.tmct.page.openPersistedStore) {
511
+ persist = window.tmct.page.openPersistedStore({ storeKey: "ingest", stamp: siteVersion + ":" + seedFacts + ":" + SEED_STAMP });
554
512
  }
555
513
  const savedRecord = persist ? await persist.load() : null;
556
514
  session = savedRecord && savedRecord.payload
557
- ? window.tmctIngest.createIngestSession({ seedPayload: savedRecord.payload, vocabSeeded: true })
558
- : newSession();
515
+ ? await window.tmct.open({ seedPayload: savedRecord.payload, vocabSeeded: true })
516
+ : await newSession();
559
517
  setMode(false);
560
518
  updateIngestEnabled();
561
- const stats = await window.tmctIngest.memoryStats(session.memoryDir);
519
+ const stats = await window.tmct.page.memoryStats(session.memoryDir);
562
520
  const winkPart = winkStatus === "loaded"
563
521
  ? "wink-nlp: loaded"
564
522
  : "wink-nlp unavailable. The recognizer can't split sentences without it";
@@ -611,10 +569,10 @@ ${THEME_TOKENS_CSS}
611
569
 
612
570
  // ---- download the canonical facts as JSONL -------------------------------
613
571
  downloadBtn.addEventListener("click", async () => {
614
- if (!session || !window.tmctIngest.exportFactsJsonl) return;
572
+ if (!session || !window.tmct.page.exportFactsJsonl) return;
615
573
  let jsonl;
616
574
  try {
617
- jsonl = await window.tmctIngest.exportFactsJsonl(session.memoryDir);
575
+ jsonl = await window.tmct.page.exportFactsJsonl(session.memoryDir);
618
576
  } catch (err) {
619
577
  statusEl.textContent = "couldn't build the download (" + (err && err.message ? err.message : err) + ")";
620
578
  return;