@polycode-projects/the-mechanical-code-talker 2.10.3 → 2.10.5
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.
- package/README.md +66 -10
- package/bin/tmct.mjs +5 -2
- package/package.json +3 -1
- package/src/adapters/memory/core.mjs +8 -1
- package/src/domain/cli-verbs.mjs +2 -0
- package/src/domain/memory/trust.mjs +21 -2
- package/src/domain/sense-split.mjs +203 -0
- package/src/services/chat-page-viz.mjs +87 -19
- package/src/services/chat-session.mjs +12 -7
- package/src/services/chat.mjs +320 -30
- package/src/services/code-explorer-viz.mjs +16 -2
- package/src/services/extract-facts.mjs +293 -81
- package/src/services/fold.mjs +1 -1
- package/src/services/ingest-viz.mjs +388 -0
- package/src/services/ledger-viz.mjs +110 -0
- package/src/services/session-log-format.mjs +64 -0
- package/src/services/sessions.mjs +56 -22
- package/src/services/spider-fly-turn.mjs +54 -1
- package/src/services/spider-fly-viz.mjs +21 -19
- package/src/surfaces/web/chat-browser-entry.mjs +10 -6
- package/src/surfaces/web/ingest-browser-entry.mjs +126 -0
- package/src/surfaces/web/ledger-browser-entry.mjs +15 -2
- package/src/surfaces/web/memory-ask-browser.bundle.js +135 -126
- package/src/tools/definitions.mjs +14 -0
- package/src/tools/handlers/index.mjs +2 -0
- package/src/tools/handlers/tmct-ingest.mjs +43 -0
- package/src/tools/server.mjs +5 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sessions.mjs — chat sessions as first-class temporal graph data, like commits.
|
|
2
2
|
//
|
|
3
3
|
// A `tmct chat` session leaves two artifacts under the target repo:
|
|
4
|
-
// .tmct/session-<uuidv7>.
|
|
4
|
+
// .tmct/session-<uuidv7>.md — the human-readable transcript (chat-session.mjs, session-log-format.mjs)
|
|
5
5
|
// .tmct/sessions/session-<uuidv7>.jsonl — the STRUCTURED sidecar this module owns:
|
|
6
6
|
// {"type":"session", id, started, repo, tmctVersion} (header line)
|
|
7
7
|
// {"type":"turn", ts, query, via, resolvedIds, answeredIds, miss} (one per turn, flushed)
|
|
@@ -207,7 +207,7 @@ async function recordSessionMemory(graphFile, record, repoDirOverride = null) {
|
|
|
207
207
|
|
|
208
208
|
let answers = new Map();
|
|
209
209
|
try {
|
|
210
|
-
answers = parseSessionLog(await readFile(join(repoDir, ".tmct", `session-${record.id}.
|
|
210
|
+
answers = parseSessionLog(await readFile(join(repoDir, ".tmct", `session-${record.id}.md`), "utf8"));
|
|
211
211
|
} catch { /* no transcript (direct API callers) — record the requests alone */ }
|
|
212
212
|
|
|
213
213
|
const utterances = [];
|
|
@@ -294,38 +294,72 @@ export function parseSessionJsonl(text) {
|
|
|
294
294
|
return { id: String(header.id), started: String(header.started || ""), ended, turns };
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
// A transcript turn
|
|
298
|
-
//
|
|
299
|
-
|
|
297
|
+
// A transcript turn heading (session-log-format.mjs's sessionLogTurnMarkdown):
|
|
298
|
+
// "### HH:MM:SS.mmm · turn N" — time of day only, no calendar date (the date
|
|
299
|
+
// lives once, in the header byline). The header byline itself carries that
|
|
300
|
+
// date: "*YYYY-MM-DD · started HH:MM:SS.mmm[ · repo ...]*".
|
|
301
|
+
const MD_TURN_HEADING_RE = /^### (\d{2}:\d{2}:\d{2}\.\d{3}) · turn \d+$/;
|
|
302
|
+
const MD_BYLINE_DATE_RE = /^\*(\d{4}-\d{2}-\d{2}) ·/;
|
|
300
303
|
|
|
301
304
|
export { turnKey };
|
|
302
305
|
|
|
306
|
+
/** The calendar date one UTC day after `dateStr` ("YYYY-MM-DD") — used to
|
|
307
|
+
* carry the transcript's running date forward across a midnight rollover
|
|
308
|
+
* (see parseSessionLog below). */
|
|
309
|
+
function nextUtcDate(dateStr) {
|
|
310
|
+
const d = new Date(`${dateStr}T00:00:00Z`);
|
|
311
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
312
|
+
return d.toISOString().slice(0, 10);
|
|
313
|
+
}
|
|
314
|
+
|
|
303
315
|
/**
|
|
304
|
-
* Parse a human-readable session transcript (.tmct/session-<id>.
|
|
316
|
+
* Parse a human-readable session transcript (.tmct/session-<id>.md) into a
|
|
305
317
|
* Map of turnKey(ts, query) → answer text. The transcript is the ONLY session
|
|
306
318
|
* artifact that carries the answer PROSE (the structured sidecar records ids,
|
|
307
|
-
* not text)
|
|
319
|
+
* not text).
|
|
320
|
+
*
|
|
321
|
+
* Each turn heading carries only a TIME of day (see MD_TURN_HEADING_RE above)
|
|
322
|
+
* — the calendar date is read once from the header byline and carried
|
|
323
|
+
* forward turn to turn, advancing a day whenever a heading's time reads
|
|
324
|
+
* EARLIER than the turn before it (a midnight rollover on a long session).
|
|
325
|
+
* The reconstructed `date + "T" + time + "Z"` is then byte-identical to the
|
|
326
|
+
* full ISO timestamp session-log-format.mjs's writer sliced the time out of,
|
|
327
|
+
* so it matches the sidecar's own `record.ts` under turnKey exactly.
|
|
308
328
|
*/
|
|
309
329
|
export function parseSessionLog(text) {
|
|
310
330
|
const lines = String(text ?? "").split("\n");
|
|
311
331
|
const answers = new Map();
|
|
312
|
-
let
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
332
|
+
let date = null;
|
|
333
|
+
let lastTime = null;
|
|
334
|
+
let i = 0;
|
|
335
|
+
while (i < lines.length) {
|
|
336
|
+
if (date === null) {
|
|
337
|
+
const dateMatch = lines[i].match(MD_BYLINE_DATE_RE);
|
|
338
|
+
if (dateMatch) date = dateMatch[1];
|
|
339
|
+
}
|
|
340
|
+
const heading = date && lines[i].match(MD_TURN_HEADING_RE);
|
|
341
|
+
if (heading) {
|
|
342
|
+
const time = heading[1];
|
|
343
|
+
if (lastTime !== null && time < lastTime) date = nextUtcDate(date);
|
|
344
|
+
lastTime = time;
|
|
345
|
+
let j = i + 1;
|
|
346
|
+
if (lines[j] === "") j += 1;
|
|
347
|
+
const queryLine = lines[j];
|
|
348
|
+
if (queryLine?.startsWith("> ")) {
|
|
349
|
+
j += 1;
|
|
350
|
+
if (lines[j] === "") j += 1;
|
|
351
|
+
if (lines[j] === "```text") {
|
|
352
|
+
j += 1;
|
|
353
|
+
const answerLines = [];
|
|
354
|
+
while (j < lines.length && lines[j] !== "```") { answerLines.push(lines[j]); j += 1; }
|
|
355
|
+
answers.set(turnKey(`${date}T${time}Z`, queryLine.slice(2)), answerLines.join("\n"));
|
|
356
|
+
i = j + 1;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
326
360
|
}
|
|
361
|
+
i += 1;
|
|
327
362
|
}
|
|
328
|
-
close();
|
|
329
363
|
return answers;
|
|
330
364
|
}
|
|
331
365
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import {
|
|
19
19
|
DIRECTION_DELTA, WORLD_NAME, cellId, parseCellId, inBounds, chebyshevDistance, oneStepDirectionBetween,
|
|
20
20
|
} from "../domain/spider-fly-world.mjs";
|
|
21
|
-
import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame } from "./spider-fly.mjs";
|
|
21
|
+
import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame, beliefSnapshotFor } from "./spider-fly.mjs";
|
|
22
22
|
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
23
23
|
import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
|
|
24
24
|
import { appendFacts, appendRule, loadMemory, readFactRows } from "../adapters/memory/core.mjs";
|
|
@@ -66,6 +66,12 @@ const SPIDER_FLY_TOLD_RE = new RegExp(
|
|
|
66
66
|
"i",
|
|
67
67
|
);
|
|
68
68
|
|
|
69
|
+
// The observable-facts read: "what does the fly see?" / "what can the
|
|
70
|
+
// spider see?" — a closed vocabulary shape, styled after the other
|
|
71
|
+
// game-lane regexes above, with the same optional numbered suffix
|
|
72
|
+
// (SPIDER_FLY_ADDRESS_LEAD_RE's own "-<n>") for a board past one of a kind.
|
|
73
|
+
const SPIDER_FLY_SEE_RE = /^what (?:does|can) the (spider|fly)(?:-(\d+))?\s+see[.!?\s]*$/i;
|
|
74
|
+
|
|
69
75
|
const WORLD_OPENING_FALLBACK =
|
|
70
76
|
"a spider waits in its web; a fly drifts in from the edge of the board. Neither is yours to move — watch, or address one by name in chat.";
|
|
71
77
|
|
|
@@ -342,6 +348,48 @@ async function runTickAndRender({ planHolder, memoryDir, cache, toldFacts = [],
|
|
|
342
348
|
};
|
|
343
349
|
}
|
|
344
350
|
|
|
351
|
+
// ---- the observable-facts read: "what does the fly see?" -----------------
|
|
352
|
+
|
|
353
|
+
/** One `[id, cellId | null]` belief entry as a sentence: `"spider-1 is at
|
|
354
|
+
* cell-3-4."` when observed/told, `"fly-2 has not been observed."`
|
|
355
|
+
* otherwise — the same wording spider-fly-viz.mjs's own click-expand panel
|
|
356
|
+
* (observedFactsHtml) renders, so the chat phrasing and the browser panel
|
|
357
|
+
* never disagree about what an agent can see. */
|
|
358
|
+
function observedFactSentence(id, believedCell) {
|
|
359
|
+
return believedCell ? `${id} is at ${believedCell}.` : `${id} has not been observed.`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** "what does the fly see?" / "what does the spider see?" rendered as plain
|
|
363
|
+
* text: the same beliefSnapshotFor read spider-fly.mjs's own tick loop and
|
|
364
|
+
* the browser panel already use, over the CURRENT board state — read-only,
|
|
365
|
+
* no tick runs, nothing is written. Candidates are every OTHER live agent
|
|
366
|
+
* of either kind; toldFacts is empty (a told position only ever arrives
|
|
367
|
+
* fresh alongside a tick — see runToldFactTurn — so there is none standing
|
|
368
|
+
* between ticks to read back here). */
|
|
369
|
+
async function spiderFlyBeliefAnswer(match, { memoryDir, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
370
|
+
const kind = match[1].toLowerCase();
|
|
371
|
+
const num = match[2];
|
|
372
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
373
|
+
const state = foldSpiderFlyState(rows);
|
|
374
|
+
const observerId = resolveAgentId(kind, num, state);
|
|
375
|
+
if (!observerId) return noSuchAgentAnswer(kind, "addressee");
|
|
376
|
+
const observerCell = parseCellId(state.placements.get(observerId).cell);
|
|
377
|
+
const candidateIds = [...liveIdsOfKind("spider", state), ...liveIdsOfKind("fly", state)];
|
|
378
|
+
const visionRadius = kind === "spider"
|
|
379
|
+
? gameConfig?.spiderFly?.spiderVisionRadius
|
|
380
|
+
: gameConfig?.spiderFly?.flyVisionRadius;
|
|
381
|
+
const belief = beliefSnapshotFor(observerId, observerCell, candidateIds, state, { visionRadius });
|
|
382
|
+
const entries = Object.entries(belief);
|
|
383
|
+
const text = entries.length
|
|
384
|
+
? `${observerId} sees: ${entries.map(([id, cell]) => observedFactSentence(id, cell)).join(" ")}`
|
|
385
|
+
: `${observerId} is alone on the board — nothing else to see.`;
|
|
386
|
+
return {
|
|
387
|
+
text,
|
|
388
|
+
lane: "game-inform",
|
|
389
|
+
note: `SPIDER-FLY — belief snapshot rendered for ${observerId} via beliefSnapshotFor (read-only, no tick run)`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
345
393
|
/** The addressed teach-frame turn: resolve the addressee and the belief
|
|
346
394
|
* subject, resolve the told cell, and run ONE tick with that told-fact fed
|
|
347
395
|
* in. Told-facts are NOT persisted on the session slot across turns — each
|
|
@@ -526,6 +574,11 @@ export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache =
|
|
|
526
574
|
return runToldFactTurn(told, { planHolder, memoryDir, cache, gameConfig });
|
|
527
575
|
}
|
|
528
576
|
|
|
577
|
+
const seeMatch = String(line).trim().match(SPIDER_FLY_SEE_RE);
|
|
578
|
+
if (seeMatch) {
|
|
579
|
+
return spiderFlyBeliefAnswer(seeMatch, { memoryDir, gameConfig });
|
|
580
|
+
}
|
|
581
|
+
|
|
529
582
|
if (SPIDER_FLY_TICK_RE.test(line)) {
|
|
530
583
|
return runTickAndRender({ planHolder, memoryDir, cache, toldFacts: [], gameConfig });
|
|
531
584
|
}
|
|
@@ -346,10 +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
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
.tuning { width: ${BOARD_PX}px; max-width: 100%; margin: 0 auto; box-sizing: border-box; }
|
|
349
|
+
/* Both the controls strip above the board and the tuning strip below it
|
|
350
|
+
match the board's own width (not the wider stage-left column they sit
|
|
351
|
+
in), so all three line up as one visual stack. */
|
|
352
|
+
.tuning, .controls-panel { width: ${BOARD_PX}px; max-width: 100%; margin: 0 auto; box-sizing: border-box; }
|
|
353
353
|
.hud h2, .chat h2, .tuning h2 {
|
|
354
354
|
font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 600;
|
|
355
355
|
margin: -.6rem -.75rem .5rem; padding: .42rem .75rem;
|
|
@@ -447,7 +447,7 @@ ${THEME_TOKENS_CSS}
|
|
|
447
447
|
.tuning-col.spider input[type="range"]::-moz-range-thumb { border-color: var(--taught); }
|
|
448
448
|
.tuning-col.fly input[type="range"]::-moz-range-thumb { border-color: var(--fly); }
|
|
449
449
|
.tuning-col input:disabled { opacity: .45; }
|
|
450
|
-
.controls-row { display: flex; align-items: center; gap: .6rem;
|
|
450
|
+
.controls-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
|
|
451
451
|
/* Chunky keycaps: raised by default, pressed-in on :active — the one place
|
|
452
452
|
this redesign uses interaction (not animation) to sell "physical
|
|
453
453
|
control panel", and it costs nothing under reduced-motion since neither
|
|
@@ -461,7 +461,7 @@ ${THEME_TOKENS_CSS}
|
|
|
461
461
|
page is remembered by. */
|
|
462
462
|
.controls-row .turn { margin-left: auto; font-family: ${MONO_STACK}; font-size: .82rem; letter-spacing: .05em; text-transform: uppercase; font-variant-numeric: tabular-nums; color: var(--chrome-well-ink); background: var(--chrome-well); border: 1px solid var(--chrome-brass); box-shadow: var(--chrome-shadow-inset); border-radius: 2px; padding: .3rem .7rem; }
|
|
463
463
|
.status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .5rem; padding-left: .5rem; border-left: 2px solid var(--chrome-brass); }
|
|
464
|
-
body.preview .side, body.preview .controls-
|
|
464
|
+
body.preview .side, body.preview .controls-panel, body.preview .tuning { display: none; }
|
|
465
465
|
body.preview main { padding: 0; max-width: none; }
|
|
466
466
|
body.preview .stage, body.preview .stage-left { display: block; }
|
|
467
467
|
body.preview .board-frame { margin: 0; }
|
|
@@ -474,6 +474,21 @@ ${THEME_TOKENS_CSS}
|
|
|
474
474
|
<h1>Multiple competing planning agents</h1>
|
|
475
475
|
<div class="stage">
|
|
476
476
|
<div class="stage-left">
|
|
477
|
+
<div class="controls-panel" id="controlsPanel" aria-label="game controls">
|
|
478
|
+
<div class="controls-row">
|
|
479
|
+
<button id="resetBtn" type="button" disabled>reset</button>
|
|
480
|
+
<button id="playBtn" type="button" disabled>▶ play</button>
|
|
481
|
+
<button id="stepBtn" type="button" disabled>step</button>
|
|
482
|
+
<span class="turn mono" id="turnLabel">turn: 0</span>
|
|
483
|
+
</div>
|
|
484
|
+
<div class="status" id="status">loading the engine…</div>
|
|
485
|
+
</div>
|
|
486
|
+
<div class="board-frame" id="boardFrame">
|
|
487
|
+
<canvas id="board" width="${BOARD_PX}" height="${BOARD_PX}" aria-label="the 10x10 board"></canvas>
|
|
488
|
+
<canvas id="pov" width="${BOARD_PX}" height="${BOARD_PX}" aria-hidden="true"></canvas>
|
|
489
|
+
<div class="sprite-layer" id="spriteLayer"></div>
|
|
490
|
+
<div class="thread-tip" id="threadTip"></div>
|
|
491
|
+
</div>
|
|
477
492
|
<div class="tuning" id="tuning">
|
|
478
493
|
<h2>live tuning — mass loss, spawn rate, vision, per class</h2>
|
|
479
494
|
<div class="tuning-grid">
|
|
@@ -497,12 +512,6 @@ ${THEME_TOKENS_CSS}
|
|
|
497
512
|
</div>
|
|
498
513
|
</div>
|
|
499
514
|
</div>
|
|
500
|
-
<div class="board-frame" id="boardFrame">
|
|
501
|
-
<canvas id="board" width="${BOARD_PX}" height="${BOARD_PX}" aria-label="the 10x10 board"></canvas>
|
|
502
|
-
<canvas id="pov" width="${BOARD_PX}" height="${BOARD_PX}" aria-hidden="true"></canvas>
|
|
503
|
-
<div class="sprite-layer" id="spriteLayer"></div>
|
|
504
|
-
<div class="thread-tip" id="threadTip"></div>
|
|
505
|
-
</div>
|
|
506
515
|
</div>
|
|
507
516
|
<aside class="side" aria-label="Chat and agents">
|
|
508
517
|
<div class="chat">
|
|
@@ -528,13 +537,6 @@ ${THEME_TOKENS_CSS}
|
|
|
528
537
|
</div>
|
|
529
538
|
</aside>
|
|
530
539
|
</div>
|
|
531
|
-
<div class="controls-row">
|
|
532
|
-
<button id="resetBtn" type="button" disabled>reset</button>
|
|
533
|
-
<button id="playBtn" type="button" disabled>▶ play</button>
|
|
534
|
-
<button id="stepBtn" type="button" disabled>step</button>
|
|
535
|
-
<span class="turn mono" id="turnLabel">turn: 0</span>
|
|
536
|
-
</div>
|
|
537
|
-
<div class="status" id="status">loading the engine…</div>
|
|
538
540
|
</main>
|
|
539
541
|
<script>
|
|
540
542
|
const SPIDERFLY = ${gridData};
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
|
|
20
20
|
import { createInMemoryStore, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
|
|
21
21
|
import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
|
|
22
|
+
import { splitSentencesPreservingPaths } from "../../services/sentences.mjs";
|
|
22
23
|
import { provenanceTagToSource } from "../../domain/memory/trust.mjs";
|
|
23
24
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
24
25
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
@@ -68,15 +69,18 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
|
|
|
68
69
|
let focus = null;
|
|
69
70
|
let last = null;
|
|
70
71
|
let planState = null;
|
|
71
|
-
|
|
72
|
+
// Tri-state, like the TUI: false (off), true (rescue on a miss), or
|
|
73
|
+
// "supplement" (also append a cited read-out under every grounded answer).
|
|
74
|
+
const normLive = (v) => (v === "supplement" ? "supplement" : Boolean(v));
|
|
75
|
+
let liveReferenceOn = normLive(liveReference);
|
|
72
76
|
|
|
73
77
|
return {
|
|
74
78
|
memoryDir,
|
|
75
79
|
sessionId,
|
|
76
80
|
get liveReference() { return liveReferenceOn; },
|
|
77
|
-
/** The page's toggle seam:
|
|
78
|
-
*
|
|
79
|
-
setLiveReference(v) { liveReferenceOn =
|
|
81
|
+
/** The page's toggle seam: set the live Wikipedia mode for every later turn
|
|
82
|
+
* (the `/wiki on|off|supplement` command sets the same state). */
|
|
83
|
+
setLiveReference(v) { liveReferenceOn = normLive(v); },
|
|
80
84
|
|
|
81
85
|
/** One dispatched turn. A throwing runTurn must never kill the session —
|
|
82
86
|
* the page has no other chance to show this turn's answer. */
|
|
@@ -95,7 +99,7 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
|
|
|
95
99
|
focus = result.focus;
|
|
96
100
|
last = result.last;
|
|
97
101
|
if ("planState" in result) planState = result.planState;
|
|
98
|
-
if (typeof result.liveReference === "boolean") liveReferenceOn = result.liveReference;
|
|
102
|
+
if (typeof result.liveReference === "boolean" || result.liveReference === "supplement") liveReferenceOn = result.liveReference;
|
|
99
103
|
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null, plan: result.plan ?? null };
|
|
100
104
|
},
|
|
101
105
|
};
|
|
@@ -158,4 +162,4 @@ export async function exportFactsJsonl(memoryDir) {
|
|
|
158
162
|
return serializeFactsJsonl(await loadMemory(memoryDir));
|
|
159
163
|
}
|
|
160
164
|
|
|
161
|
-
globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, normFactTerm, vocabExampleHint, memoryStats, openPersistedStore, exportFactsJsonl };
|
|
165
|
+
globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, normFactTerm, vocabExampleHint, memoryStats, openPersistedStore, exportFactsJsonl, splitSentences: splitSentencesPreservingPaths };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ingest-browser-entry.mjs — the esbuild entry for the ingest page's engine
|
|
2
|
+
// (public/ingest-browser.bundle.js, built by scripts/build-ingest-bundle.mjs),
|
|
3
|
+
// mirroring ledger-browser-entry.mjs's own createLedgerSession shape.
|
|
4
|
+
//
|
|
5
|
+
// The ingest page turns plain pasted/dropped text into stored facts by
|
|
6
|
+
// running each sentence through the SAME deterministic recognizer the chat
|
|
7
|
+
// teach lane already has (runTurn, src/services/chat.mjs) — no new NLU, no
|
|
8
|
+
// LLM, no guessing, exactly as the `tmct extract` CLI does it
|
|
9
|
+
// (src/services/extract-facts.mjs). A sentence the recognizer turns into a
|
|
10
|
+
// stored assertion is kept; every other sentence is honestly skipped.
|
|
11
|
+
//
|
|
12
|
+
// The recognizer is reached through ONE seam — `groundTextToFacts` — so a
|
|
13
|
+
// wider ingest tier (an optimistic fuzzy-match pass, a canonical/graph-linked
|
|
14
|
+
// output) slots in behind this single function without the page changing.
|
|
15
|
+
// Today the seam is the strict recognizer alone.
|
|
16
|
+
//
|
|
17
|
+
// Gitignored, Pages-demo-site-only output (scripts/build-demo-site.mjs builds
|
|
18
|
+
// it fresh on every deploy, never committed) — the same posture
|
|
19
|
+
// ledger-browser-entry.mjs documents for its own bundle. It carries the full
|
|
20
|
+
// runTurn engine, the same weight class as the chat/ledger bundles, and is
|
|
21
|
+
// never published.
|
|
22
|
+
import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
|
|
23
|
+
import { createInMemoryStore, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
|
|
24
|
+
import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
|
|
25
|
+
import { splitSentencesPreservingPaths } from "../../services/sentences.mjs";
|
|
26
|
+
import { touchedFactRows } from "../../domain/memory/touched-facts.mjs";
|
|
27
|
+
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
28
|
+
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
29
|
+
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The single recognizer seam. Splits `text` into sentences (wink's own
|
|
33
|
+
* boundary detection, never a regex), runs each through runTurn against
|
|
34
|
+
* `memoryDir`, and keeps a sentence only when runTurn's own record calls it a
|
|
35
|
+
* stored assertion (`record.via === "assert"`, `record.miss` false) that
|
|
36
|
+
* actually touched a Fact row — the identical keep/skip rule
|
|
37
|
+
* src/services/extract-facts.mjs's own runSentence holds.
|
|
38
|
+
*
|
|
39
|
+
* `onFact(fact)` (optional) is awaited after each grounded row so the page can
|
|
40
|
+
* render the canonical facts LIVE as they land, one at a time.
|
|
41
|
+
*
|
|
42
|
+
* Returns { sentences, recognized, skipped, facts } — `facts` an array of
|
|
43
|
+
* { subject, predicate, object, provenance, quantifier, sentence } in the same
|
|
44
|
+
* canonical shape `tmct extract` and the JSONL exporter emit.
|
|
45
|
+
*
|
|
46
|
+
* A wider ingest tier plugs in HERE, behind this one function: the page calls
|
|
47
|
+
* it and nothing else.
|
|
48
|
+
*/
|
|
49
|
+
export async function groundTextToFacts(text, { memoryDir, sessionId, graph, lexicon, vocabHint, onFact = null } = {}) {
|
|
50
|
+
const sentences = splitSentencesPreservingPaths(text);
|
|
51
|
+
const facts = [];
|
|
52
|
+
let focus = null;
|
|
53
|
+
let last = null;
|
|
54
|
+
let planState = null;
|
|
55
|
+
let recognized = 0;
|
|
56
|
+
|
|
57
|
+
for (const sentence of sentences) {
|
|
58
|
+
const before = readFactRows(await loadMemory(memoryDir));
|
|
59
|
+
let record;
|
|
60
|
+
try {
|
|
61
|
+
const result = await runTurn(sentence, {
|
|
62
|
+
config: null, source: null, graph, focus, last, memoryDir, sessionId,
|
|
63
|
+
env: {}, lexicon, vocabHint, planState,
|
|
64
|
+
});
|
|
65
|
+
focus = result.focus;
|
|
66
|
+
last = result.last;
|
|
67
|
+
if ("planState" in result) planState = result.planState;
|
|
68
|
+
record = result.record;
|
|
69
|
+
} catch {
|
|
70
|
+
continue; // a throwing sentence is a skip, never a page-killer
|
|
71
|
+
}
|
|
72
|
+
if (record?.via !== "assert" || record?.miss) continue;
|
|
73
|
+
const rows = touchedFactRows(before, readFactRows(await loadMemory(memoryDir)));
|
|
74
|
+
if (!rows.length) continue; // a Rule teach touches no Fact row — honest skip
|
|
75
|
+
recognized += 1;
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
const fact = {
|
|
78
|
+
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
79
|
+
provenance: row.provenance || "", quantifier: row.quantifier || "", sentence,
|
|
80
|
+
};
|
|
81
|
+
facts.push(fact);
|
|
82
|
+
if (onFact) await onFact(fact);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { sentences: sentences.length, recognized, skipped: sentences.length - recognized, facts };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A browser ingest session over the real turn engine — a fresh in-memory
|
|
91
|
+
* Backend-B store, so facts grounded through the page extend one real graph
|
|
92
|
+
* the page can then export. `seedPayload` (optional) pre-loads a graph the
|
|
93
|
+
* recognizer can recall and link against.
|
|
94
|
+
*
|
|
95
|
+
* Returns { memoryDir, sessionId, ingest }. `ingest(text, { onFact })` is the
|
|
96
|
+
* one call the page makes; it drives groundTextToFacts against this session's
|
|
97
|
+
* store and returns its { sentences, recognized, skipped, facts } summary.
|
|
98
|
+
*/
|
|
99
|
+
export function createIngestSession({ seedPayload = null, vocabSeeded = false } = {}) {
|
|
100
|
+
const memoryDir = createInMemoryStore();
|
|
101
|
+
if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
|
|
102
|
+
|
|
103
|
+
const graph = parseEntities({ individuals: [], objectProperties: [] });
|
|
104
|
+
const lexicon = loadLexicon();
|
|
105
|
+
const vocabHint = vocabExampleHint(vocabSeeded);
|
|
106
|
+
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
memoryDir,
|
|
110
|
+
sessionId,
|
|
111
|
+
ingest(text, { onFact = null } = {}) {
|
|
112
|
+
return groundTextToFacts(text, { memoryDir, sessionId, graph, lexicon, vocabHint, onFact });
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The session's whole triple store as JSONL — the same
|
|
119
|
+
* { subject, predicate, object, provenance } shape `tmct extract` and
|
|
120
|
+
* `tmct memory --export` emit, offered to the page as the canonical download.
|
|
121
|
+
*/
|
|
122
|
+
export async function exportFactsJsonl(memoryDir) {
|
|
123
|
+
return serializeFactsJsonl(await loadMemory(memoryDir));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
globalThis.tmctIngest = { createIngestSession, groundTextToFacts, exportFactsJsonl, registerWinkModel, normFactTerm };
|
|
@@ -17,7 +17,9 @@
|
|
|
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 { runTurn, vocabExampleHint } from "../../services/chat.mjs";
|
|
20
|
-
import { createInMemoryStore, normFactTerm } from "../../adapters/memory/core.mjs";
|
|
20
|
+
import { createInMemoryStore, normFactTerm, loadMemory } from "../../adapters/memory/core.mjs";
|
|
21
|
+
import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
|
|
22
|
+
import { splitSentencesPreservingPaths } from "../../services/sentences.mjs";
|
|
21
23
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
22
24
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
23
25
|
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
@@ -78,8 +80,19 @@ export function createLedgerSession({ seedPayload = null, vocabSeeded = false }
|
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
/** The session's whole triple store as JSONL — the same
|
|
84
|
+
* { subject, predicate, object, provenance } shape `tmct extract` and
|
|
85
|
+
* `tmct memory --export` emit. The ledger dock's "export facts" control reads
|
|
86
|
+
* this and offers it as a download. */
|
|
87
|
+
export async function exportFactsJsonl(memoryDir) {
|
|
88
|
+
return serializeFactsJsonl(await loadMemory(memoryDir));
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
// Re-exported so ledger-viz.mjs's own inline script never has to duplicate
|
|
82
92
|
// the derivation logic that builds rows/terms/edges/contradictions/
|
|
83
93
|
// worthALook/stats from a payload — the same posture chat-browser-entry.mjs
|
|
84
94
|
// takes re-exporting registerWinkModel for its own page's CDN wink load.
|
|
85
|
-
|
|
95
|
+
// splitSentences + exportFactsJsonl carry the dock's paste-and-drop ingest and
|
|
96
|
+
// its JSONL export across the bundle boundary, the same one-serializer posture
|
|
97
|
+
// chat-browser-entry.mjs holds for its own page.
|
|
98
|
+
globalThis.tmctLedger = { createLedgerSession, computeLedgerDataFromPayload, normFactTerm, registerWinkModel, splitSentences: splitSentencesPreservingPaths, exportFactsJsonl };
|