@polycode-projects/the-mechanical-code-talker 4.1.1 → 4.1.2

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 (55) hide show
  1. package/README.md +31 -18
  2. package/bin/tmct.mjs +3 -0
  3. package/data/templates/responses.jsonl +3 -0
  4. package/package.json +2 -1
  5. package/src/adapters/memory/core.mjs +1358 -196
  6. package/src/adapters/memory/inspect.mjs +11 -0
  7. package/src/adapters/memory/shacl.mjs +38 -0
  8. package/src/adapters/p2p/webrtc-transport.mjs +28 -5
  9. package/src/domain/ask-vocab.mjs +39 -0
  10. package/src/domain/ask.mjs +183 -34
  11. package/src/domain/grammar/assert.mjs +8 -2
  12. package/src/domain/hanoi-board.mjs +232 -0
  13. package/src/domain/ingest-facts.mjs +120 -0
  14. package/src/domain/interpret/normalize.mjs +49 -0
  15. package/src/domain/memory/compaction.mjs +284 -0
  16. package/src/domain/memory/resolution.mjs +171 -0
  17. package/src/domain/memory/trust.mjs +175 -5
  18. package/src/domain/memory-facts.mjs +139 -0
  19. package/src/domain/p2p/facts.mjs +21 -0
  20. package/src/domain/p2p/peer-id.mjs +15 -0
  21. package/src/domain/p2p/provenance-relabel.mjs +13 -2
  22. package/src/domain/p2p/sync-filter.mjs +5 -1
  23. package/src/domain/p2p/wire.mjs +7 -4
  24. package/src/domain/scene-compose.mjs +2 -2
  25. package/src/domain/sprite-facts.mjs +0 -0
  26. package/src/services/adventure-viz.mjs +5 -1
  27. package/src/services/adventure.mjs +70 -44
  28. package/src/services/chat-page-viz.mjs +381 -310
  29. package/src/services/chat.mjs +273 -155
  30. package/src/services/code-explorer-viz.mjs +141 -54
  31. package/src/services/index.mjs +1 -1
  32. package/src/services/ingest-viz.mjs +134 -9
  33. package/src/services/ledger-viz.mjs +7 -4
  34. package/src/services/memory-panel-viz.mjs +8 -3
  35. package/src/services/mud-turn.mjs +11 -8
  36. package/src/services/mud-viz.mjs +441 -206
  37. package/src/services/p2p-room.mjs +110 -23
  38. package/src/services/plan-viz.mjs +63 -4
  39. package/src/services/research-viz.mjs +18 -7
  40. package/src/services/share-overlay-viz.mjs +623 -0
  41. package/src/services/spider-fly-viz.mjs +2 -2
  42. package/src/services/sprite-catalog-viz.mjs +303 -78
  43. package/src/surfaces/web/adventure-browser-entry.mjs +27 -5
  44. package/src/surfaces/web/chat-browser-entry.mjs +37 -10
  45. package/src/surfaces/web/code-explorer-browser-entry.mjs +4 -3
  46. package/src/surfaces/web/ingest-browser-entry.mjs +73 -12
  47. package/src/surfaces/web/ledger-browser-entry.mjs +32 -7
  48. package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
  49. package/src/surfaces/web/mud-browser-entry.mjs +38 -7
  50. package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
  51. package/src/surfaces/web/plan-browser-entry.mjs +33 -2
  52. package/src/surfaces/web/research-browser-entry.mjs +11 -19
  53. package/src/surfaces/web/sprites-browser-entry.mjs +39 -8
  54. package/src/surfaces/web/tmct-surface.mjs +12 -0
  55. package/src/surfaces/web/turn-session.mjs +10 -3
@@ -30,6 +30,7 @@ import { vocabExampleHint } from "../../services/chat.mjs";
30
30
  import { createInMemoryStore, normFactTerm, loadMemory, readFactRows, applySeedPayload } from "../../adapters/memory/core.mjs";
31
31
  import { splitSentencesPreservingPaths } from "../../services/sentences.mjs";
32
32
  import { parseEntities } from "../../domain/codegraph.mjs";
33
+ import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
33
34
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
34
35
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
35
36
  import { setDigestStructures } from "../../adapters/corpus/digest-bank.mjs";
@@ -72,9 +73,14 @@ import { graphAsk, enginePlan } from "./engine-surface.mjs";
72
73
  * is module-scope, last write wins, and every session created after this one
73
74
  * shares it.
74
75
  *
75
- * Returns { memoryDir, sessionId, graph, turn }. `turn(line)` resolves to
76
- * { answer, end, record, plan } and threads focus/last/planState between
77
- * calls exactly as the CLI session does.
76
+ * Returns { memoryDir, sessionId, graph, codeGraph, refreshGraph, turn }.
77
+ * `turn(line)` resolves to { answer, end, record, plan } and threads
78
+ * focus/last/planState between calls exactly as the CLI session does.
79
+ *
80
+ * The two graphs are separate on purpose. `codeGraph` is the known-empty index
81
+ * the turn engine reads, and `graph` is this session's memory store projected
82
+ * for `ask()` — a question about taught facts has a real graph to traverse
83
+ * while a code-structure question keeps its honest no-code-graph refusal.
78
84
  */
79
85
  export function createChatSession({ seedPayload = null, vocabSeeded = false, liveReference = false, onLiveLookup = null, synthesisBudget = 12, digestStructures = null } = {}) {
80
86
  setDigestStructures(digestStructures || []);
@@ -82,10 +88,24 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
82
88
  applySeedPayload(memoryDir, seedPayload);
83
89
 
84
90
  // A known-empty code graph: code-structure questions get the same honest
85
- // no-code-graph answer an un-pointed CLI session gives, never a crash.
86
- const graph = parseEntities({ individuals: [], objectProperties: [] });
91
+ // no-code-graph answer an un-pointed CLI session gives, never a crash. The
92
+ // turn engine keeps reading THIS one, so the identity-led greeting, the teach
93
+ // pointer and the zero module counts a page earns from an empty index all
94
+ // stay exactly as they were.
95
+ const codeGraph = parseEntities({ individuals: [], objectProperties: [] });
96
+
97
+ // What `tmct.ask()` traverses is a different graph: this session's own memory
98
+ // store, projected through memoryFactGraphPayload. Rebuilt on demand rather
99
+ // than once at open, because every teach, ingest and research turn grows the
100
+ // store — the same reason spider-fly rebuilds its board before each turn.
101
+ let memoryGraph = parseEntities({ individuals: [], objectProperties: [] });
102
+ async function refreshGraph() {
103
+ memoryGraph = parseEntities(memoryFactGraphPayload(readFactRows(await loadMemory(memoryDir))));
104
+ return memoryGraph;
105
+ }
106
+
87
107
  const lexicon = loadLexicon();
88
- const vocabHint = vocabExampleHint(vocabSeeded);
108
+ const vocabHint = vocabExampleHint(vocabSeeded, "browser");
89
109
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
90
110
 
91
111
  // Four-state, like the CLI: false (off), true (rescue on a miss),
@@ -99,10 +119,10 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
99
119
  let synthesisBudgetOn = Number.isFinite(synthesisBudget) ? synthesisBudget : 12;
100
120
 
101
121
  const session = createTurnSession({
102
- memoryDir, graph, lexicon, sessionId, vocabHint,
122
+ memoryDir, graph: codeGraph, lexicon, sessionId, vocabHint,
103
123
  buildExtraOptions: () => ({
104
124
  liveReference: liveReferenceOn, onLiveLookup,
105
- uiContext: "browser", synthesisBudget: synthesisBudgetOn,
125
+ synthesisBudget: synthesisBudgetOn,
106
126
  }),
107
127
  // `result.liveReference` mirrors a `/wiki on|off|supplement|always`
108
128
  // command back into this session's own toggle state.
@@ -114,7 +134,9 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
114
134
  return {
115
135
  memoryDir,
116
136
  sessionId,
117
- graph,
137
+ get graph() { return memoryGraph; },
138
+ codeGraph,
139
+ refreshGraph,
118
140
  get liveReference() { return liveReferenceOn; },
119
141
  /** The page's toggle seam: set the live Wikipedia mode for every later turn
120
142
  * (the `/wiki on|off|supplement|always` command sets the same state). */
@@ -155,7 +177,12 @@ export async function researchedFactRows(memoryDir) {
155
177
  // and paste-ingest controls run.
156
178
  publishTmctSurface({
157
179
  open: createChatSession,
158
- ask: graphAsk,
180
+ // The memory projection is rebuilt first, so a direct tmct.ask() sees every
181
+ // fact taught, pasted or researched since the last one.
182
+ ask: async (request, options, session) => {
183
+ await session.refreshGraph();
184
+ return graphAsk(request, options, session);
185
+ },
159
186
  plan: enginePlan,
160
187
  page: {
161
188
  registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider,
@@ -53,7 +53,7 @@ export function createCodeExplorerSession({ graphPayload, seedPayload = null, vo
53
53
  // the write path recounts — teach turns must work on any seed.
54
54
  if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
55
55
  const lexicon = loadLexicon();
56
- const vocabHint = vocabExampleHint(vocabSeeded);
56
+ const vocabHint = vocabExampleHint(vocabSeeded, "browser");
57
57
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
58
58
  // A stable virtual path: the provider answers every fetch, so no file is
59
59
  // ever read, but the code lanes still do path math (join/dirname) on it.
@@ -233,8 +233,9 @@ export function askRelatedFacts(graphPayload, term) {
233
233
  // `tmct.page` keeps the re-derivation helpers a graph swapped through the
234
234
  // desktop picker re-renders with, plus the wink and term-normalizing seams the
235
235
  // chat shares with the ledger page. `createCodeExplorerSession` sits there
236
- // too: this page's client script is still one raw-text block, and it reaches
237
- // its factory through the bag rather than through `tmct.open()`.
236
+ // too: the page keeps its own session slot, created lazily on the first turn
237
+ // once the general-knowledge seed has landed, rather than the one `tmct.open`
238
+ // would install eagerly — so it calls the factory straight from the bag.
238
239
  publishTmctSurface({
239
240
  open: createCodeExplorerSession,
240
241
  ask: graphAsk,
@@ -29,16 +29,24 @@ import { splitSentencesPreservingPaths, stripCitationResidue } from "../../servi
29
29
  import { clauseCandidates, optimisticTriples } from "../../services/extract-facts.mjs";
30
30
  import { touchedFactRows } from "../../domain/memory/touched-facts.mjs";
31
31
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
32
+ import { parseEntities } from "../../domain/codegraph.mjs";
33
+ import { ingestFactGraphPayload, ingestedFactRows } from "../../domain/ingest-facts.mjs";
32
34
  import { registerWinkModel, winkInstance } from "../../adapters/wink-model.mjs";
33
35
  import { memoryStats, exportFactsJsonl } from "./memory-stats.mjs";
34
36
  import { openPersistedStore } from "./idb-persist.mjs";
37
+ import { createTurnSession } from "./turn-session.mjs";
35
38
  import { publishTmctSurface } from "./tmct-surface.mjs";
39
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
36
40
 
37
41
  // The pronoun subjects a bounded carry substitutes with the last unique
38
42
  // grounded subject in the SAME paragraph. Reset at every blank line, so a
39
43
  // fresh paragraph never resolves against a stale antecedent.
40
44
  const PRONOUN_LEAD_RE = /^(?:they|it|these|those|this)\b\s*/i;
41
45
 
46
+ // The turn kinds that move the fact store, so the projected graph a later
47
+ // question traverses has to be rebuilt before it answers.
48
+ const STORE_WRITING_TURNS = new Set(["assert", "retract"]);
49
+
42
50
  /**
43
51
  * The single recognizer seam. Splits `text` into paragraphs (blank-line
44
52
  * separated, so the pronoun carry never bridges a topic break) and each
@@ -171,35 +179,88 @@ export async function groundTextToFacts(text, { memoryDir, sessionId, lexicon, v
171
179
  * the page can then export. `seedPayload` (optional) pre-loads a graph the
172
180
  * recognizer can recall and link against.
173
181
  *
174
- * Returns { memoryDir, sessionId, ingest }. `ingest(text, { onFact,
175
- * optimistic })` is the one call the page makes; it drives groundTextToFacts
176
- * against this session's store and returns its { sentences, recognized,
177
- * skipped, facts } summary.
182
+ * Returns { memoryDir, sessionId, graph, refreshGraph, askableClasses, ingest,
183
+ * turn }. `ingest(text, { onFact, optimistic })` is the call the ingest panes
184
+ * make; it drives groundTextToFacts against this session's store and returns
185
+ * its { sentences, recognized, skipped, facts } summary. `turn(line)` is the
186
+ * ask dock's own entry point — the FULL chat turn engine (the exact dispatch
187
+ * the CLI runs) over the same store, so a question it can't ground gets the
188
+ * same refusal the CLI gives.
189
+ *
190
+ * `graph` is what a question actually traverses: the ingested rows projected
191
+ * through ingest-facts.mjs, rebuilt by `refreshGraph()` whenever the store has
192
+ * moved since the last one. The rebuild is gated on a dirty flag rather than
193
+ * run per call because a seeded store holds tens of thousands of rows, and
194
+ * re-reading all of them to answer a second question about the same two
195
+ * sentences is the one cost this page has to avoid.
178
196
  */
179
197
  export function createIngestSession({ seedPayload = null, vocabSeeded = false } = {}) {
180
198
  const memoryDir = createInMemoryStore();
181
199
  applySeedPayload(memoryDir, seedPayload);
182
200
 
183
201
  const lexicon = loadLexicon();
184
- const vocabHint = vocabExampleHint(vocabSeeded);
202
+ const vocabHint = vocabExampleHint(vocabSeeded, "browser");
185
203
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
186
204
 
205
+ let graph = parseEntities({ individuals: [], objectProperties: [] });
206
+ let storeMovedSinceGraph = true;
207
+ async function refreshGraph() {
208
+ if (!storeMovedSinceGraph) return graph;
209
+ const rows = ingestedFactRows(readFactRows(await loadMemory(memoryDir)));
210
+ graph = parseEntities(ingestFactGraphPayload(rows));
211
+ storeMovedSinceGraph = false;
212
+ return graph;
213
+ }
214
+
215
+ // An empty graph is not the same as no graph to runTurn: a non-null one
216
+ // makes it read an ordinary "the number of X" phrase as a graph count query
217
+ // instead of running the teach cascade, so before anything is ingested the
218
+ // dock keeps the plain conversational reading.
219
+ const graphForTurn = () => (graph.individuals.length ? graph : null);
220
+
221
+ const turnSession = createTurnSession({
222
+ memoryDir, lexicon, sessionId, vocabHint,
223
+ buildExtraOptions: () => ({ graph: graphForTurn() }),
224
+ captureExtraState: (result) => {
225
+ if (STORE_WRITING_TURNS.has(result?.record?.via) && !result.record.miss) storeMovedSinceGraph = true;
226
+ },
227
+ });
228
+
187
229
  return {
188
230
  memoryDir,
189
231
  sessionId,
190
- ingest(text, { onFact = null, optimistic = false } = {}) {
191
- return groundTextToFacts(text, { memoryDir, sessionId, lexicon, vocabHint, onFact, optimistic });
232
+ get graph() { return graph; },
233
+ refreshGraph,
234
+ /** The class nouns this graph can actually list, for a page building its
235
+ * own "try asking" hint out of real data rather than invented examples. */
236
+ askableClasses() {
237
+ return [...new Set(graph.individuals.map((i) => i.class).filter(Boolean))].sort();
238
+ },
239
+ async ingest(text, { onFact = null, optimistic = false } = {}) {
240
+ const summary = await groundTextToFacts(text, { memoryDir, sessionId, lexicon, vocabHint, onFact, optimistic });
241
+ if (summary.recognized) storeMovedSinceGraph = true;
242
+ return summary;
243
+ },
244
+ async turn(line) {
245
+ await refreshGraph();
246
+ return turnSession.turn(line);
192
247
  },
193
248
  };
194
249
  }
195
250
 
196
- // This page grounds pasted prose into facts; it holds no conversation, so
197
- // `tmct.turn` is unwired and says so, and the one call the page makes is
198
- // `tmct.session.ingest(text)`. `tmct.page` keeps the recognizer entry point
199
- // the page also drives directly, the wink seam, the stats fold behind its
200
- // counters, its IndexedDB wrapper and its JSONL export.
251
+ // The page's own two calls are `tmct.session.ingest(text)` and, from the ask
252
+ // dock, `tmct.turn(line)`. `tmct.ask` puts one question straight to the graph
253
+ // this session projects from what it has ingested, rebuilding it first so a
254
+ // fact grounded a moment ago is already visible. `tmct.page` keeps the
255
+ // recognizer entry point the page also drives directly, the wink seam, the
256
+ // stats fold behind its counters, its IndexedDB wrapper and its JSONL export.
201
257
  publishTmctSurface({
202
258
  open: createIngestSession,
259
+ ask: async (request, options, session) => {
260
+ await session.refreshGraph();
261
+ return graphAsk(request, options, session);
262
+ },
263
+ plan: enginePlan,
203
264
  page: {
204
265
  groundTextToFacts, exportFactsJsonl, registerWinkModel, normFactTerm,
205
266
  memoryStats, openPersistedStore,
@@ -17,9 +17,10 @@
17
17
  // never published — only the hosted demo site's public/ledger.html links to
18
18
  // it, as an optional sibling script the page degrades honestly without.
19
19
  import { vocabExampleHint } from "../../services/chat.mjs";
20
- import { createInMemoryStore, normFactTerm, applySeedPayload } from "../../adapters/memory/core.mjs";
20
+ import { createInMemoryStore, normFactTerm, applySeedPayload, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
21
21
  import { splitSentencesPreservingPaths } from "../../services/sentences.mjs";
22
22
  import { parseEntities } from "../../domain/codegraph.mjs";
23
+ import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
23
24
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
24
25
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
25
26
  import { registerResearchProvider } from "../../adapters/corpus/wikipedia-live.mjs";
@@ -37,7 +38,12 @@ import { graphAsk, enginePlan } from "./engine-surface.mjs";
37
38
  * a fact taught through the dock extends the SAME graph the page renders
38
39
  * from, never a disconnected one.
39
40
  *
40
- * Returns { memoryDir, sessionId, graph, turn }, identical to createChatSession.
41
+ * Returns { memoryDir, sessionId, graph, codeGraph, refreshGraph, turn },
42
+ * identical to createChatSession — including the two-graph split: `codeGraph`
43
+ * is the known-empty index the turn engine reads, and `graph` is the ledger's
44
+ * own store projected for `ask()`, so a question about the facts on the page
45
+ * traverses those facts while a code-structure question still refuses honestly.
46
+ *
41
47
  * ledger-viz.mjs's own inline script calls computeLedgerDataFromPayload
42
48
  * (re-exported below) on `memoryDir.payload` after a turn whose record shows
43
49
  * a successful write (`via: "assert"` or `via: "retract"`, `miss: false`) to
@@ -48,13 +54,27 @@ export function createLedgerSession({ seedPayload = null, vocabSeeded = false }
48
54
  const memoryDir = createInMemoryStore();
49
55
  applySeedPayload(memoryDir, seedPayload);
50
56
 
51
- const graph = parseEntities({ individuals: [], objectProperties: [] });
57
+ const codeGraph = parseEntities({ individuals: [], objectProperties: [] });
58
+
59
+ let memoryGraph = parseEntities({ individuals: [], objectProperties: [] });
60
+ async function refreshGraph() {
61
+ memoryGraph = parseEntities(memoryFactGraphPayload(readFactRows(await loadMemory(memoryDir))));
62
+ return memoryGraph;
63
+ }
64
+
52
65
  const lexicon = loadLexicon();
53
- const vocabHint = vocabExampleHint(vocabSeeded);
66
+ const vocabHint = vocabExampleHint(vocabSeeded, "browser");
54
67
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
55
68
 
56
- const session = createTurnSession({ memoryDir, graph, lexicon, sessionId, vocabHint });
57
- return { memoryDir, sessionId, graph, turn: session.turn };
69
+ const session = createTurnSession({ memoryDir, graph: codeGraph, lexicon, sessionId, vocabHint });
70
+ return {
71
+ memoryDir,
72
+ sessionId,
73
+ get graph() { return memoryGraph; },
74
+ codeGraph,
75
+ refreshGraph,
76
+ turn: session.turn,
77
+ };
58
78
  }
59
79
 
60
80
  // `tmct.page` keeps what the page's own script renders with and the engine has
@@ -64,7 +84,12 @@ export function createLedgerSession({ seedPayload = null, vocabSeeded = false }
64
84
  // paste-ingest and JSONL export.
65
85
  publishTmctSurface({
66
86
  open: createLedgerSession,
67
- ask: graphAsk,
87
+ // The memory projection is rebuilt first, so a direct tmct.ask() sees every
88
+ // fact the dock or the ingest panel has taught since the last one.
89
+ ask: async (request, options, session) => {
90
+ await session.refreshGraph();
91
+ return graphAsk(request, options, session);
92
+ },
68
93
  plan: enginePlan,
69
94
  page: {
70
95
  computeLedgerDataFromPayload, normFactTerm, registerWinkModel, registerResearchProvider,