@polycode-projects/the-mechanical-code-talker 2.9.6 → 2.10.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.
@@ -1,10 +1,14 @@
1
- // import-file.mjs — `tmct import --file <definition.txt>`: teach a plain-text
2
- // definition file, one sentence at a time, through the SAME recognizers the
3
- // live chat uses (runTurn) — no separate parser, no guessing.
1
+ // import-file.mjs — `tmct import --file <definition.txt|facts.jsonl>`: teach a
2
+ // definition file into a repo's memory. A plain-text file is taught one
3
+ // sentence at a time through the SAME recognizers the live chat uses (runTurn)
4
+ // — no separate parser, no guessing. A JSONL file (the shape `tmct extract` and
5
+ // `tmct memory --export` emit — one {subject, predicate, object, provenance}
6
+ // object per line) loads each fact straight into the store, so an exported
7
+ // triple-store round-trips back in with its provenance intact.
4
8
  //
5
9
  // The report is loud on purpose: a definition file that half-teaches produces
6
10
  // a planner that finds wrong plans or no plans with no visible cause, so every
7
- // sentence's outcome is printed and any decline makes the caller exit non-zero.
11
+ // line's outcome is printed and any decline makes the caller exit non-zero.
8
12
  //
9
13
  // `#` lines are comments (skipped, counted, never "declined") — a definition
10
14
  // file carries its own example prompts this way.
@@ -13,10 +17,34 @@ import { readFile } from "node:fs/promises";
13
17
  import { basename, resolve } from "node:path";
14
18
 
15
19
  import { runTurn, uuidv7 } from "./chat.mjs";
16
- import { loadMemory, readFactRows, appendFact, openConfiguredMemoryBackend } from "../adapters/memory/core.mjs";
20
+ import { loadMemory, readFactRows, appendFact, appendFacts, openConfiguredMemoryBackend } from "../adapters/memory/core.mjs";
17
21
  import { loadConfig } from "../adapters/config.mjs";
18
22
  import { splitSentencesPreservingPaths } from "./sentences.mjs";
19
23
 
24
+ /** A body line as a stored fact, or null when it is not a JSONL fact object.
25
+ * A fact line JSON-parses to an object carrying string subject/predicate/
26
+ * object; anything else (a plain sentence, malformed JSON, a JSON array) is
27
+ * not one. */
28
+ function parseFactLine(line) {
29
+ const trimmed = line.trim();
30
+ if (!trimmed.startsWith("{")) return null;
31
+ let record;
32
+ try {
33
+ record = JSON.parse(trimmed);
34
+ } catch {
35
+ return null;
36
+ }
37
+ if (!record || typeof record !== "object") return null;
38
+ const { subject, predicate, object } = record;
39
+ if (typeof subject !== "string" || typeof predicate !== "string" || typeof object !== "string") return null;
40
+ if (!subject || !predicate || !object) return null;
41
+ return {
42
+ subject, predicate, object,
43
+ provenance: typeof record.provenance === "string" ? record.provenance : "",
44
+ quantifier: typeof record.quantifier === "string" ? record.quantifier : "",
45
+ };
46
+ }
47
+
20
48
  /**
21
49
  * Teach every sentence of `filePath` into `repoRoot`'s memory store.
22
50
  *
@@ -33,14 +61,48 @@ export async function importDefinitionFile(repoRoot, filePath, { env = process.e
33
61
 
34
62
  const lines = text.split("\n");
35
63
  const commentLines = lines.filter((l) => l.trim().startsWith("#"));
36
- const body = lines.filter((l) => !l.trim().startsWith("#")).join("\n");
37
- const sentences = splitSentencesPreservingPaths(body);
64
+ const bodyLines = lines.filter((l) => !l.trim().startsWith("#"));
65
+ const body = bodyLines.join("\n");
66
+
67
+ // A file whose every non-blank body line is a JSONL fact object is a
68
+ // triple-store dump, not prose — import each fact directly, keeping its own
69
+ // provenance, rather than pushing "{...}" through the sentence recognizer
70
+ // (which would decline it). This is the return leg for `tmct memory --export`
71
+ // and the browser pages' fact download.
72
+ const nonBlankBody = bodyLines.filter((l) => l.trim());
73
+ const factLines = nonBlankBody.map(parseFactLine);
74
+ const isFactFile = nonBlankBody.length > 0 && factLines.every(Boolean);
38
75
 
39
76
  // An injected handle (a caller mid-session, or a build script pinned to the
40
77
  // in-memory backend) is used as-is and left open — the caller owns it.
41
78
  const opened = injectedMemoryDir ? null : await openConfiguredMemoryBackend(root, env);
42
79
  const memoryDir = injectedMemoryDir ?? opened.dir;
43
80
  const close = opened ? opened.close : async () => {};
81
+
82
+ if (isFactFile) {
83
+ const importReport = [`${basename(abs)} — ${factLines.length} fact(s), ${commentLines.length} comment line(s) skipped`, ""];
84
+ // One batched write for the whole dump — a triple export can carry every
85
+ // seed fact, so a per-fact loop (load+write each) would be quadratic.
86
+ try {
87
+ await appendFacts(memoryDir, factLines.map((fact) => ({
88
+ subject: fact.subject, predicate: fact.predicate, object: fact.object,
89
+ provenance: fact.provenance || sourceTag, quantifier: fact.quantifier,
90
+ })));
91
+ } finally {
92
+ await close();
93
+ }
94
+ const imported = factLines.map((fact) => `${fact.subject} ${fact.predicate} ${fact.object}`);
95
+ // A triple dump can carry tens of thousands of facts, so the per-line echo
96
+ // is capped — enough to show the shape, not a wall that overflows a caller's
97
+ // output buffer.
98
+ const LIST_CAP = 20;
99
+ for (const rendered of imported.slice(0, LIST_CAP)) importReport.push(` imported — ${rendered}`);
100
+ if (imported.length > LIST_CAP) importReport.push(` … and ${imported.length - LIST_CAP} more fact(s)`);
101
+ importReport.push("", `${imported.length} fact(s) imported, 0 declined, ${commentLines.length} comment line(s) skipped`);
102
+ return { sentences: factLines.length, taught: imported, declined: [], comments: commentLines.length, report: importReport.join("\n") };
103
+ }
104
+
105
+ const sentences = splitSentencesPreservingPaths(body);
44
106
  const config = loadConfig(env, root);
45
107
 
46
108
  const taught = [];
@@ -515,10 +515,15 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
515
515
  // the query-only wording even when the live bundle is offered — the dock's
516
516
  // own script swaps it for a teach-aware placeholder the moment it confirms
517
517
  // tmctLedger actually loaded (never claimed ahead of that confirmation).
518
+ // The example term skips anything under 3 characters — the SAME floor the
519
+ // dock's own miss-tips apply below — so a graph whose highest-degree term
520
+ // is a short stopword-shaped fragment (init:large corpora carry plenty:
521
+ // "os", "ip", "s3") never becomes the one example a visitor sees.
518
522
  const termSet = new Set((terms || []).map((t) => t.term));
523
+ const exampleTerm = (terms || []).find((t) => t && t.term && t.term.length >= 3);
519
524
  const placeholder = termSet.has("ishmael")
520
525
  ? "who is the grandfather of ishmael"
521
- : (terms && terms.length ? `ask the graph… e.g. what is ${terms[0].term}` : "ask the graph…");
526
+ : (exampleTerm ? `ask the graph… e.g. what is ${exampleTerm.term}` : "ask the graph…");
522
527
  const dockHtml = hasMemChat
523
528
  ? `<div class="chat">
524
529
  <div class="chatlog" id="chatlog" aria-live="polite"></div>
@@ -346,6 +346,10 @@ ${THEME_TOKENS_CSS}
346
346
  negative margin equal to this padding, so it reads as one welded unit,
347
347
  not a label floating inside a box. */
348
348
  .hud, .chat, .tuning { background: var(--chrome-face); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-raised); border-radius: 2px; padding: .6rem .75rem; }
349
+ /* the tuning strip matches the board's own width (not the wider stage-left
350
+ column it sits in) so the two line up as one visual stack, the same way
351
+ the board-frame below it is already centered at a fixed width. */
352
+ .tuning { width: ${BOARD_PX}px; max-width: 100%; margin: 0 auto; box-sizing: border-box; }
349
353
  .hud h2, .chat h2, .tuning h2 {
350
354
  font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 600;
351
355
  margin: -.6rem -.75rem .5rem; padding: .42rem .75rem;
@@ -357,12 +361,22 @@ ${THEME_TOKENS_CSS}
357
361
  at once now, so the agent count (and a naive card list's own height)
358
362
  can jump sharply; the panel must never grow the page underneath it. */
359
363
  .hud-list { max-height: 420px; overflow-y: auto; }
360
- .hud-row { display: flex; flex-direction: column; gap: .1rem; padding: .4rem 0; border-top: 1px solid var(--chrome-edge-lo); box-shadow: inset 0 1px 0 var(--chrome-edge-hi); }
364
+ .hud-row { display: flex; align-items: flex-start; gap: .6rem; padding: .4rem 0; border-top: 1px solid var(--chrome-edge-lo); box-shadow: inset 0 1px 0 var(--chrome-edge-hi); }
361
365
  .hud-row:first-of-type { border-top: none; box-shadow: none; }
366
+ .hud-row.clickable { cursor: pointer; }
367
+ .hud-row.clickable:hover, .hud-row.clickable:focus-visible { background: var(--chrome-brass-soft); }
368
+ .hud-main { display: flex; flex-direction: column; gap: .1rem; flex: 1 1 auto; min-width: 0; }
362
369
  .hud-id { font-family: ${MONO_STACK}; font-size: .74rem; font-weight: 600; }
363
370
  .hud-id.spider { color: var(--taught); } .hud-id.fly { color: var(--fly); } .hud-id.egg { color: var(--muted); }
364
371
  .hud-goal { font-size: .85rem; }
365
372
  .hud-plan, .hud-belief { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); margin-top: .25rem; line-height: 1.4; padding-left: .5rem; border-left: 2px solid var(--chrome-brass); }
373
+ /* the click-expand facts panel (§28): beside the clicked spider/fly's own
374
+ row, never a separate popover or a second panel elsewhere on the page —
375
+ the same believedCellOf/beliefSnapshotFor read path spider-fly.mjs
376
+ already computes every tick for planning, rendered here as full
377
+ sentences instead of the compact believes:-line above. */
378
+ .hud-detail { flex: 1 1 auto; min-width: 0; font-family: ${MONO_STACK}; font-size: .64rem; line-height: 1.5; color: var(--chrome-well-ink); background: var(--chrome-well); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-inset); border-radius: 2px; padding: .35rem .5rem; }
379
+ .hud-detail-title { text-transform: uppercase; letter-spacing: .06em; opacity: .75; margin-bottom: .2rem; }
366
380
  /* A stat-readout track: an inset "LCD" well, filled with a segmented pip
367
381
  texture (repeating-linear-gradient) instead of a smooth gradient bar —
368
382
  discrete resource units, the same reading-at-a-glance language a 90s
@@ -457,7 +471,7 @@ ${THEME_TOKENS_CSS}
457
471
  <body>
458
472
  <main>
459
473
  <div class="eyebrow">tmct &middot; spider and fly</div>
460
- <h1>A spider in its web, a fly on the board — each planning against the other</h1>
474
+ <h1>Multiple competing planning agents</h1>
461
475
  <div class="stage">
462
476
  <div class="stage-left">
463
477
  <div class="tuning" id="tuning">
@@ -543,6 +557,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
543
557
  const spriteLayer = el("spriteLayer");
544
558
  const threadTip = el("threadTip");
545
559
  const hudEl = el("hud");
560
+ // Which agent ids currently show their expanded observed-facts panel —
561
+ // survives renderHud() rebuilding the list's innerHTML every tick, since
562
+ // that's the only way this state can persist across a full re-render.
563
+ const expandedAgents = new Set();
546
564
  const chatlogEl = el("chatlog");
547
565
  const chatformEl = el("chatform");
548
566
  const chatqEl = el("chatq");
@@ -783,21 +801,59 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
783
801
  return '<div class="hud-belief">believes: ' + text + "</div>";
784
802
  }
785
803
 
804
+ // The full text rendering behind the click-expand panel: one sentence per
805
+ // other candidate the observer currently has a belief about, or "has not
806
+ // been observed" for a candidate it doesn't — the same belief map
807
+ // beliefLineHtml already compresses into a single line, spelled out in
808
+ // full beside the clicked agent instead of abbreviated above it.
809
+ function observedFactsHtml(observerId, belief) {
810
+ const entries = Object.entries(belief || {});
811
+ const lines = entries.length
812
+ ? entries.map(([id, cell]) => '<div class="hud-detail-line">'
813
+ + (cell ? esc(id) + " is at " + esc(cell) + "." : esc(id) + " has not been observed.")
814
+ + "</div>").join("")
815
+ : '<div class="hud-detail-line">nothing else is on the board yet.</div>';
816
+ return '<div class="hud-detail"><div class="hud-detail-title">' + esc(observerId) + " observes</div>" + lines + "</div>";
817
+ }
818
+
786
819
  function renderHud() {
787
820
  const ids = Object.keys(lastAgents).sort();
788
821
  if (!ids.length) { hudEl.innerHTML = '<div class="hud-empty">no agents on the board.</div>'; return; }
789
822
  hudEl.innerHTML = ids.map((id) => {
790
823
  const cls = classOfAgentId(id);
791
824
  const a = lastAgents[id];
792
- return '<div class="hud-row"><span class="hud-id ' + esc(cls) + '">' + esc(id) + '</span>'
825
+ const clickable = cls === "spider" || cls === "fly";
826
+ const expanded = clickable && expandedAgents.has(id);
827
+ const main = '<div class="hud-main"><span class="hud-id ' + esc(cls) + '">' + esc(id) + '</span>'
793
828
  + '<span class="hud-goal">' + esc(goalById[id] || "watching\\u2026") + "</span>"
794
829
  + massBarHtml(cls, a.mass)
795
830
  + planLineHtml(a.plan)
796
831
  + beliefLineHtml(a.belief)
797
832
  + "</div>";
833
+ const attrs = clickable ? ' role="button" tabindex="0" aria-expanded="' + (expanded ? "true" : "false") + '"' : "";
834
+ return '<div class="hud-row' + (clickable ? " clickable" : "") + '" data-agent-id="' + esc(id) + '"' + attrs + '>'
835
+ + main + (expanded ? observedFactsHtml(id, a.belief) : "")
836
+ + "</div>";
798
837
  }).join("");
799
838
  }
800
839
 
840
+ function toggleAgentExpansion(id) {
841
+ if (!id) return;
842
+ if (expandedAgents.has(id)) expandedAgents.delete(id); else expandedAgents.add(id);
843
+ renderHud();
844
+ }
845
+ hudEl.addEventListener("click", (event) => {
846
+ const row = event.target.closest(".hud-row.clickable");
847
+ if (row) toggleAgentExpansion(row.dataset.agentId);
848
+ });
849
+ hudEl.addEventListener("keydown", (event) => {
850
+ if (event.key !== "Enter" && event.key !== " ") return;
851
+ const row = event.target.closest(".hud-row.clickable");
852
+ if (!row) return;
853
+ event.preventDefault();
854
+ toggleAgentExpansion(row.dataset.agentId);
855
+ });
856
+
801
857
  const threadGeometry = {
802
858
  parseCellId: (id) => tmctSpiderFly.parseCellId(id),
803
859
  cellId: (x, y) => tmctSpiderFly.cellId(x, y),
@@ -636,8 +636,11 @@ function stepPlan(fromCell, toCell) {
636
636
  * never ground truth: showing the observer's own honest gap
637
637
  * between belief and reality (visibly widened by a deceiving pill or a fed
638
638
  * false fact) is the whole point of that panel. Returns a plain
639
- * `{ [candidateId]: cellId | null }` map. */
640
- function beliefSnapshotFor(observerSubject, observerCell, candidateIds, state, opts) {
639
+ * `{ [candidateId]: cellId | null }` map. Exported as the read path for
640
+ * what one agent can currently observe — every caller (this file's own
641
+ * tick loop, a viz panel, a future chat lane) reads the same computation,
642
+ * never a re-derived copy of it. */
643
+ export function beliefSnapshotFor(observerSubject, observerCell, candidateIds, state, opts) {
641
644
  const belief = {};
642
645
  for (const candidateId of candidateIds) {
643
646
  if (candidateId === observerSubject) continue;
@@ -18,6 +18,7 @@
18
18
  // without it.
19
19
  import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
20
20
  import { createInMemoryStore, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
21
+ import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
21
22
  import { provenanceTagToSource } from "../../domain/memory/trust.mjs";
22
23
  import { parseEntities } from "../../domain/codegraph.mjs";
23
24
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
@@ -147,4 +148,14 @@ export async function memoryStats(memoryDir) {
147
148
  return { total: rows.length, bandCounts, taught };
148
149
  }
149
150
 
150
- globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, normFactTerm, vocabExampleHint, memoryStats, openPersistedStore };
151
+ /**
152
+ * The session's whole triple store as JSONL — one
153
+ * { subject, predicate, object, provenance } object per line, the same shape
154
+ * `tmct extract` and `tmct memory --export` emit. Reads the live memory the
155
+ * same way memoryStats does; the page offers it as a download.
156
+ */
157
+ export async function exportFactsJsonl(memoryDir) {
158
+ return serializeFactsJsonl(await loadMemory(memoryDir));
159
+ }
160
+
161
+ globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, normFactTerm, vocabExampleHint, memoryStats, openPersistedStore, exportFactsJsonl };
@@ -0,0 +1,79 @@
1
+ // code-explorer-browser-entry.mjs — the esbuild entry for the code explorer's
2
+ // LIVE chat dock (public/code-explorer.bundle.js / electron/renderer/
3
+ // code-explorer.bundle.js). It mirrors ledger-browser-entry.mjs, but seeds the
4
+ // full runTurn engine from a CODE graph instead of a memory payload: the dock
5
+ // answers "what does X import" / "which functions call Y" over the loaded
6
+ // graph, the same compositional shapes the hint rail suggests.
7
+ //
8
+ // The graph enters through source.mjs's provider seam — the same seam the CLI
9
+ // and HTTP surfaces read — so runTurn's symbol-grain lanes see the whole
10
+ // payload, while parseEntities builds the coarse graph the flat lanes read.
11
+ // Gitignored, built fresh by scripts/build-code-explorer-bundle.mjs; the page
12
+ // degrades to a static view when it is absent (renderCodeExplorerHtml's own
13
+ // contract), so nothing here is ever published.
14
+ import { runTurn } from "../../services/chat.mjs";
15
+ import { createInMemoryStore, normFactTerm } from "../../adapters/memory/core.mjs";
16
+ import { parseEntities } from "../../domain/codegraph.mjs";
17
+ import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
18
+ import { registerWinkModel } from "../../adapters/wink-model.mjs";
19
+ import * as source from "../../adapters/source.mjs";
20
+ import { computeCodeExplorerData, computeCodeLedger } from "../../services/code-explorer-viz.mjs";
21
+ import { generateCodeHints } from "../../domain/code-explorer-hints.mjs";
22
+
23
+ /**
24
+ * A browser code-explorer session over the real turn engine. Registers the
25
+ * loaded payload with source.mjs's provider seam, parses the coarse graph, and
26
+ * dispatches every turn through the same runTurn the CLI runs. Teaches land in
27
+ * an in-memory store so a taught fact never touches disk. Returns
28
+ * { sessionId, turn }, the createChatSession shape the page's dock expects.
29
+ */
30
+ export function createCodeExplorerSession({ graphPayload } = {}) {
31
+ const payload = graphPayload || { individuals: [], objectProperties: [] };
32
+ source.registerProvider(() => payload);
33
+
34
+ const graph = parseEntities(payload);
35
+ const memoryDir = createInMemoryStore();
36
+ const lexicon = loadLexicon();
37
+ const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
38
+ // A stable virtual path: the provider answers every fetch, so no file is
39
+ // ever read, but the code lanes still do path math (join/dirname) on it.
40
+ const config = { graphFile: "graph.json" };
41
+
42
+ let focus = null;
43
+ let last = null;
44
+ let planState = null;
45
+
46
+ return {
47
+ sessionId,
48
+ async turn(line) {
49
+ let result;
50
+ try {
51
+ result = await runTurn(line, {
52
+ config, source, graph, focus, last, memoryDir, sessionId,
53
+ env: {}, lexicon, vocabHint: "", planState,
54
+ });
55
+ } catch (e) {
56
+ const message = e instanceof Error ? e.message : String(e);
57
+ return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null };
58
+ }
59
+ focus = result.focus;
60
+ last = result.last;
61
+ if ("planState" in result) planState = result.planState;
62
+ return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null };
63
+ },
64
+ };
65
+ }
66
+
67
+ // Exposed for the page's inline client: the re-derivation helpers so a graph
68
+ // swapped through the desktop picker re-renders without duplicating logic, plus
69
+ // the wink loader hook registerWinkModel and normFactTerm the dock shares with
70
+ // the ledger page.
71
+ globalThis.tmctCodeExplorer = {
72
+ createCodeExplorerSession,
73
+ computeCodeExplorerData,
74
+ computeCodeLedger,
75
+ generateCodeHints,
76
+ parseEntities,
77
+ normFactTerm,
78
+ registerWinkModel,
79
+ };