@polycode-projects/the-mechanical-code-talker 2.8.10 → 2.8.13
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/package.json +2 -1
- package/src/adapters/memory/blocks.mjs +12 -1
- package/src/adapters/memory/core.mjs +7 -1
- package/src/services/chat-page-viz.mjs +169 -42
- package/src/services/ledger-viz.mjs +245 -22
- package/src/services/plan-viz.mjs +10 -10
- package/src/surfaces/web/chat-browser-entry.mjs +52 -4
- package/src/surfaces/web/ledger-browser-entry.mjs +85 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
"build:chat-bundle": "node scripts/build-chat-bundle.mjs",
|
|
139
139
|
"build:spider-fly-bundle": "node scripts/build-spider-fly-bundle.mjs",
|
|
140
140
|
"build:plan-bundle": "node scripts/build-plan-bundle.mjs",
|
|
141
|
+
"build:ledger-bundle": "node scripts/build-ledger-bundle.mjs",
|
|
141
142
|
"build:chat-seed": "node scripts/build-chat-seed.mjs",
|
|
142
143
|
"build:demo-graph": "node scripts/build-demo-graph.mjs",
|
|
143
144
|
"build:demo-pack": "node scripts/build-demo-pack.mjs",
|
|
@@ -9,6 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import { splitIdentifierWords, tokenizeProse } from "../../domain/prose.mjs";
|
|
10
10
|
import { idfWeight } from "../../domain/text-stats.mjs";
|
|
11
11
|
import { SOURCE_PRIOR } from "../../domain/memory/trust.mjs";
|
|
12
|
+
import { isMemoryOrSqliteHandle } from "./core.mjs";
|
|
12
13
|
|
|
13
14
|
// A block inherits its Source's trust (operator 1.0, corpus 0.7); retrieval
|
|
14
15
|
// weights relevance × trust via a bounded factor (0.5 + trust, ~[0.5, 1.5]).
|
|
@@ -50,8 +51,14 @@ async function atomicWrite(file, text) {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/** Load the block index ({ blocks: { id: { file, tokens[], rank } } }); a
|
|
53
|
-
* missing index is the empty bootstrap.
|
|
54
|
+
* missing index is the empty bootstrap. Session-block folding writes to
|
|
55
|
+
* `.tmct/memory/blocks/` on disk (fold.mjs, session end) — a Backend B/C
|
|
56
|
+
* handle (createInMemoryStore/createSqliteMemoryStore) has no such directory
|
|
57
|
+
* to read, so it gets the SAME empty bootstrap a fresh Backend A repo gets,
|
|
58
|
+
* never a raw `path.join(dir, …)` TypeError from treating the handle object
|
|
59
|
+
* as a path string. */
|
|
54
60
|
export async function loadBlockIndex(dir) {
|
|
61
|
+
if (isMemoryOrSqliteHandle(dir)) return { blocks: {} };
|
|
55
62
|
try {
|
|
56
63
|
return JSON.parse(await readFile(join(blocksDir(dir), INDEX_NAME), "utf8"));
|
|
57
64
|
} catch (e) {
|
|
@@ -142,6 +149,9 @@ function rerank(index) {
|
|
|
142
149
|
*/
|
|
143
150
|
export async function saveBlock(dir, { id, text, sourceType = DEFAULT_BLOCK_SOURCE_TYPE, createdAt = "" }) {
|
|
144
151
|
if (!id) throw new Error("a block needs an id");
|
|
152
|
+
if (isMemoryOrSqliteHandle(dir)) {
|
|
153
|
+
throw new Error("saveBlock: dir is a memory/sqlite handle — session-block folding is Backend A only (no on-disk blocks/ directory to write)");
|
|
154
|
+
}
|
|
145
155
|
const bdir = blocksDir(dir);
|
|
146
156
|
await mkdir(bdir, { recursive: true });
|
|
147
157
|
const file = `${safeName(id)}.txt`;
|
|
@@ -161,6 +171,7 @@ export async function saveBlock(dir, { id, text, sourceType = DEFAULT_BLOCK_SOUR
|
|
|
161
171
|
|
|
162
172
|
/** Remove a block (id unknown → no-op) and re-rank the survivors. */
|
|
163
173
|
export async function removeBlock(dir, id) {
|
|
174
|
+
if (isMemoryOrSqliteHandle(dir)) return false; // nothing was ever written; removing is a no-op, not an error
|
|
164
175
|
const index = await loadBlockIndex(dir);
|
|
165
176
|
const entry = index.blocks[id];
|
|
166
177
|
if (!entry) return false;
|
|
@@ -153,7 +153,13 @@ function isMemoryHandle(dir) {
|
|
|
153
153
|
function isSqliteHandle(dir) {
|
|
154
154
|
return !!dir && typeof dir === "object" && dir.backend === BACKEND_SQLITE;
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
/** True for a Backend B (in-memory) or Backend C (sqlite) handle — anything
|
|
157
|
+
* that is NOT a plain repo-path string. Exported so every OTHER module that
|
|
158
|
+
* takes a `dir` and might reach a raw `node:path`/`node:fs` call (blocks.mjs's
|
|
159
|
+
* session-block index chief among them) can guard the same way this module
|
|
160
|
+
* already does, instead of a bare `join(dir, …)` throwing Node's generic
|
|
161
|
+
* "path argument must be of type string" at a caller with no idea why. */
|
|
162
|
+
export function isMemoryOrSqliteHandle(dir) {
|
|
157
163
|
return isMemoryHandle(dir) || isSqliteHandle(dir);
|
|
158
164
|
}
|
|
159
165
|
|
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
// chat-page-viz.mjs —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// inlined
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// engine/bundle, the same relationship spider-fly-viz.mjs's own inlined chat
|
|
11
|
-
// dock already has with createSpiderFlySession (both call the shared
|
|
12
|
-
// session.turn(line), neither reimplements it).
|
|
1
|
+
// chat-page-viz.mjs — chat.html, the full-screen chat page: a self-contained
|
|
2
|
+
// document shaped exactly like spider-fly-viz.mjs/adventure-viz.mjs's own
|
|
3
|
+
// page-builders — one inlined <style> importing viz-theme.mjs's shared
|
|
4
|
+
// tokens, behaviour as an inlined IIFE — running the full chat engine
|
|
5
|
+
// (chat-browser.bundle.js's globalThis.tmctChat, chat-seed.json,
|
|
6
|
+
// public/reference-pack/) by same-origin relative paths. The same
|
|
7
|
+
// relationship spider-fly-viz.mjs's own inlined chat dock has with
|
|
8
|
+
// createSpiderFlySession: both call the shared session.turn(line), neither
|
|
9
|
+
// reimplements it.
|
|
13
10
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
11
|
+
// This page's own chrome: full-screen post-ChatGPT-style layout (centered
|
|
12
|
+
// message column, bottom-fixed composer,
|
|
16
13
|
// message bubbles) and this page's own signature element — a quiet
|
|
17
14
|
// per-message PROVENANCE CHIP (taught / corpus / entailed) next to every
|
|
18
15
|
// grounded answer, tmct's actual differentiator versus an LLM chatbot. The
|
|
19
16
|
// chip is read straight off the SAME "(source: ...)" citation chat.mjs's own
|
|
20
17
|
// factPhrase/renderFactLine convention already appends to most answers (see
|
|
21
18
|
// e.g. `dog is a kind of animal (source: corpus:conceptnet ...)` — already
|
|
22
|
-
// asserted by e2e/pages-chat.test.mjs against
|
|
19
|
+
// asserted by e2e/pages-chat-fullscreen.test.mjs against this page), never a
|
|
23
20
|
// second provenance computation against memory internals: `provBucketFor`
|
|
24
21
|
// (ledger-viz.mjs) is spliced in unmodified and applied to whatever citation
|
|
25
22
|
// text the answer already carries, so this page's chip and the ledger's own
|
|
@@ -34,7 +31,7 @@
|
|
|
34
31
|
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
35
32
|
import { provBucketFor } from "./ledger-viz.mjs";
|
|
36
33
|
|
|
37
|
-
const DEFAULT_TITLE = "
|
|
34
|
+
const DEFAULT_TITLE = "the-mechanical-code-talker — talk to it";
|
|
38
35
|
|
|
39
36
|
// Reads left-to-right as the reader meets each tier: what you taught it
|
|
40
37
|
// directly, what its bundled corpus already knew, what it worked out itself.
|
|
@@ -107,10 +104,36 @@ export function renderChatHtml({ title = DEFAULT_TITLE } = {}) {
|
|
|
107
104
|
<meta charset="utf-8">
|
|
108
105
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
109
106
|
<title>${escapeHtml(title)}</title>
|
|
107
|
+
<!--
|
|
108
|
+
Import map: resolves the "wink-nlp"/"wink-eng-lite-web-model" bare specifiers the
|
|
109
|
+
sibling chat bundle's own dynamic import() needs (pinned to the exact versions
|
|
110
|
+
package.json depends on) to esm.sh CDN builds — the same seam plan.html and
|
|
111
|
+
ledger.html each wire up for their own live sessions, mirrored here because this
|
|
112
|
+
page can be opened standalone (no import map inherited from a host document). The
|
|
113
|
+
bundle itself never touches wink-nlp directly — wink-model.mjs's own header
|
|
114
|
+
explains why a static import would drag the ~1 MB model into every bundle; only
|
|
115
|
+
the page's own inline script performs this CDN import, the same bounded-race
|
|
116
|
+
tryLoadWink() pattern public/tmct-browser.mjs uses.
|
|
117
|
+
-->
|
|
118
|
+
<script type="importmap">
|
|
119
|
+
{
|
|
120
|
+
"imports": {
|
|
121
|
+
"wink-nlp": "https://esm.sh/wink-nlp@2.4.0",
|
|
122
|
+
"wink-eng-lite-web-model": "https://esm.sh/wink-eng-lite-web-model@1.8.1"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
</script>
|
|
110
126
|
<style>
|
|
111
127
|
${THEME_TOKENS_CSS}
|
|
112
128
|
html, body { height: 100%; }
|
|
113
|
-
body
|
|
129
|
+
/* body is the OUTER row: the chat column plus the stats panel docked to its
|
|
130
|
+
right, each a sibling flex item stretching to the full viewport height
|
|
131
|
+
(flex row's default cross-axis stretch) — .chatCol carries the column
|
|
132
|
+
layout the chat chrome itself needs (topbar/main/composer/statusline
|
|
133
|
+
stacked), so this page keeps working exactly as before, just inside one
|
|
134
|
+
more layer. */
|
|
135
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; display: flex; overflow: hidden; }
|
|
136
|
+
.chatCol { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
|
|
114
137
|
.mono { font-family: ${MONO_STACK}; }
|
|
115
138
|
button { font: inherit; color: inherit; background: none; cursor: pointer; border: none; }
|
|
116
139
|
button:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
@@ -126,7 +149,14 @@ ${THEME_TOKENS_CSS}
|
|
|
126
149
|
.dot-taught { background: var(--taught); } .dot-corpus { background: var(--corpus); } .dot-entail { background: var(--entail); }
|
|
127
150
|
|
|
128
151
|
main.chatMain { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain; }
|
|
129
|
-
|
|
152
|
+
/* box-sizing: border-box so min-height: 100% below counts the padding IN,
|
|
153
|
+
not on top of it — without this, .messages renders ~45px taller than
|
|
154
|
+
main.chatMain's own visible height (its 1.2rem/1.6rem vertical padding,
|
|
155
|
+
added past a content-box min-height), so the initial scrollToEnd() (the
|
|
156
|
+
very first thing boot() does, right after the boot system line lands)
|
|
157
|
+
scrolls that overflow out of view and clips the boot message half under
|
|
158
|
+
the topbar before anyone reads it. */
|
|
159
|
+
.messages { box-sizing: border-box; max-width: 720px; margin: 0 auto; padding: 1.2rem 1rem 1.6rem; display: flex; flex-direction: column; gap: .15rem; min-height: 100%; }
|
|
130
160
|
|
|
131
161
|
.msg-row { display: flex; flex-direction: column; margin: .35rem 0; max-width: 100%; }
|
|
132
162
|
.msg-row.user { align-items: flex-end; }
|
|
@@ -155,6 +185,22 @@ ${THEME_TOKENS_CSS}
|
|
|
155
185
|
.composer-inner button[type="submit"]:disabled { opacity: .4; cursor: default; }
|
|
156
186
|
.statusline { max-width: 720px; margin: 0 auto; padding: 0 1.1rem .6rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
157
187
|
|
|
188
|
+
/* the provenance stats panel: what this session's memory holds, docked to
|
|
189
|
+
the right of the chat column (a real layout column, not an overlay) —
|
|
190
|
+
re-rendered after boot and after every turn from window.tmctChat's own
|
|
191
|
+
memoryStats(), never a second provenance computation. */
|
|
192
|
+
.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; }
|
|
193
|
+
.statsPanel h2 { font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin: 1.3rem 0 .5rem; }
|
|
194
|
+
.statsPanel h2:first-child { margin-top: 0; }
|
|
195
|
+
.statsPanel .band-row { display: flex; justify-content: space-between; gap: .6rem; margin: 0; padding: .12rem 0; }
|
|
196
|
+
.statsPanel .band-count { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
197
|
+
.statsPanel .taught-item { margin: 0 0 .7rem; }
|
|
198
|
+
.statsPanel .taught-tag { display: block; color: var(--muted); font-size: .66rem; margin-top: .15rem; word-break: break-word; }
|
|
199
|
+
.statsPanel .empty { color: var(--muted); margin: 0; }
|
|
200
|
+
|
|
201
|
+
@media (max-width: 860px) {
|
|
202
|
+
.statsPanel { display: none; }
|
|
203
|
+
}
|
|
158
204
|
@media (max-width: 560px) {
|
|
159
205
|
.legend { display: none; }
|
|
160
206
|
.bubble { max-width: 92%; }
|
|
@@ -165,24 +211,29 @@ ${THEME_TOKENS_CSS}
|
|
|
165
211
|
</style>
|
|
166
212
|
</head>
|
|
167
213
|
<body>
|
|
168
|
-
<
|
|
169
|
-
<
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
214
|
+
<div class="chatCol">
|
|
215
|
+
<header class="topbar">
|
|
216
|
+
<div class="brand">
|
|
217
|
+
<span class="eyebrow">the-mechanical-code-talker</span>
|
|
218
|
+
<h1>Talk to it</h1>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="legend" aria-hidden="true">${legendHtml}</div>
|
|
221
|
+
</header>
|
|
222
|
+
<main class="chatMain">
|
|
223
|
+
<div class="messages" id="messages" role="log" aria-live="polite" aria-label="Conversation"></div>
|
|
224
|
+
</main>
|
|
225
|
+
<form class="composer" id="composer">
|
|
226
|
+
<div class="composer-inner">
|
|
227
|
+
<input id="composerInput" type="text" autocomplete="off" autocapitalize="off" spellcheck="false"
|
|
228
|
+
placeholder="loading the engine…" aria-label="Ask tmct something" disabled>
|
|
229
|
+
<button type="submit" id="composerSend" aria-label="Send" disabled>→</button>
|
|
230
|
+
</div>
|
|
231
|
+
</form>
|
|
232
|
+
<div class="statusline" id="status">loading the engine…</div>
|
|
233
|
+
</div>
|
|
234
|
+
<aside class="statsPanel" id="statsPanel" aria-label="This session's memory">
|
|
235
|
+
<p class="empty">loading memory stats…</p>
|
|
236
|
+
</aside>
|
|
186
237
|
<script src="./chat-browser.bundle.js"></script>
|
|
187
238
|
<script>
|
|
188
239
|
(function () {
|
|
@@ -196,6 +247,7 @@ ${THEME_TOKENS_CSS}
|
|
|
196
247
|
const inputEl = el("composerInput");
|
|
197
248
|
const sendBtn = el("composerSend");
|
|
198
249
|
const statusEl = el("status");
|
|
250
|
+
const statsPanelEl = el("statsPanel");
|
|
199
251
|
|
|
200
252
|
function scrollToEnd() {
|
|
201
253
|
messagesEl.parentElement.scrollTop = messagesEl.parentElement.scrollHeight;
|
|
@@ -265,8 +317,8 @@ ${THEME_TOKENS_CSS}
|
|
|
265
317
|
}
|
|
266
318
|
|
|
267
319
|
// ---- engine boot -------------------------------------------------------
|
|
268
|
-
//
|
|
269
|
-
//
|
|
320
|
+
// The same bounded-race wink load public/tmct-browser.mjs uses, against
|
|
321
|
+
// this page's own bundle/seed/pack.
|
|
270
322
|
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
271
323
|
const timeoutAfter = (ms, reason) => new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
|
|
272
324
|
|
|
@@ -329,6 +381,77 @@ ${THEME_TOKENS_CSS}
|
|
|
329
381
|
},
|
|
330
382
|
};
|
|
331
383
|
|
|
384
|
+
// ---- memory stats: the boot message's own numbers, and the docked panel -
|
|
385
|
+
// Both read window.tmctChat.memoryStats(memoryDir) (chat-browser-entry.mjs)
|
|
386
|
+
// — one computation, reused, so the boot line and the panel can never
|
|
387
|
+
// disagree with each other about what this session's memory holds.
|
|
388
|
+
const BAND_LABELS = { human: "human persona", seon: "seon ontology", conceptnet: "ConceptNet" };
|
|
389
|
+
const BAND_ORDER = ["human", "seon", "conceptnet", "taught this session", "other"];
|
|
390
|
+
const bandLabel = (key) => BAND_LABELS[key] || key;
|
|
391
|
+
|
|
392
|
+
/** The boot system line's own memory summary — every seed band this
|
|
393
|
+
* session actually loaded, named with its real count, left-to-right in
|
|
394
|
+
* BAND_ORDER; a session with nothing seeded says so plainly instead of
|
|
395
|
+
* naming zero facts. */
|
|
396
|
+
function statsSummaryLine(stats) {
|
|
397
|
+
if (!stats || !stats.total) return "no starter memory; starting empty";
|
|
398
|
+
const parts = BAND_ORDER.filter((k) => stats.bandCounts[k]).map((k) => stats.bandCounts[k] + " " + bandLabel(k));
|
|
399
|
+
return parts.length
|
|
400
|
+
? "starter memory: " + parts.join(" + ") + " (" + stats.total + " facts total)"
|
|
401
|
+
: stats.total + " starter facts loaded";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function bandRow(label, count) {
|
|
405
|
+
const row = document.createElement("p");
|
|
406
|
+
row.className = "band-row";
|
|
407
|
+
const l = document.createElement("span");
|
|
408
|
+
l.textContent = label;
|
|
409
|
+
const c = document.createElement("span");
|
|
410
|
+
c.className = "band-count";
|
|
411
|
+
c.textContent = String(count);
|
|
412
|
+
row.appendChild(l);
|
|
413
|
+
row.appendChild(c);
|
|
414
|
+
return row;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** (Re)render the docked panel from a memoryStats() result — stats may be
|
|
418
|
+
* passed in already-computed (boot reuses its own call rather than asking
|
|
419
|
+
* twice); omitted, it fetches fresh. Best-effort: a session not ready yet,
|
|
420
|
+
* or a read that throws, leaves the panel showing whatever it last showed
|
|
421
|
+
* rather than blanking it. */
|
|
422
|
+
async function renderStatsPanel(stats) {
|
|
423
|
+
if (!stats) {
|
|
424
|
+
if (!window.tmctChatSession || !window.tmctChat.memoryStats) return;
|
|
425
|
+
try { stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir); }
|
|
426
|
+
catch { return; }
|
|
427
|
+
}
|
|
428
|
+
statsPanelEl.textContent = "";
|
|
429
|
+
statsPanelEl.appendChild(Object.assign(document.createElement("h2"), { textContent: "this session's memory" }));
|
|
430
|
+
statsPanelEl.appendChild(bandRow("total facts", stats.total));
|
|
431
|
+
for (const key of BAND_ORDER) {
|
|
432
|
+
if (stats.bandCounts[key]) statsPanelEl.appendChild(bandRow(bandLabel(key), stats.bandCounts[key]));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
statsPanelEl.appendChild(Object.assign(document.createElement("h2"), { textContent: "taught this session" }));
|
|
436
|
+
if (!stats.taught.length) {
|
|
437
|
+
const empty = document.createElement("p");
|
|
438
|
+
empty.className = "empty";
|
|
439
|
+
empty.textContent = 'nothing yet \\u2014 teach it something ("a dog is a kind of animal") and it lands here, with its source.';
|
|
440
|
+
statsPanelEl.appendChild(empty);
|
|
441
|
+
} else {
|
|
442
|
+
for (const fact of stats.taught.slice(-8).reverse()) {
|
|
443
|
+
const item = document.createElement("p");
|
|
444
|
+
item.className = "taught-item";
|
|
445
|
+
item.appendChild(document.createTextNode(fact.subject + " " + fact.predicate + " " + fact.object));
|
|
446
|
+
const tag = document.createElement("span");
|
|
447
|
+
tag.className = "taught-tag";
|
|
448
|
+
tag.textContent = fact.tag;
|
|
449
|
+
item.appendChild(tag);
|
|
450
|
+
statsPanelEl.appendChild(item);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
332
455
|
function renderStatus() {
|
|
333
456
|
const seedPart = seedPayload
|
|
334
457
|
? "starter memory: " + seedFacts + " facts"
|
|
@@ -358,7 +481,10 @@ ${THEME_TOKENS_CSS}
|
|
|
358
481
|
const pendingRow = addPendingAssistantBubble();
|
|
359
482
|
setBusy(true);
|
|
360
483
|
window.tmctChatSession.turn(q)
|
|
361
|
-
.then((result) =>
|
|
484
|
+
.then((result) => {
|
|
485
|
+
settleAssistantBubble(pendingRow, result.answer, result.record);
|
|
486
|
+
return renderStatsPanel(); // a teach turn just grew this session's memory; a plain ask leaves it unchanged either way
|
|
487
|
+
})
|
|
362
488
|
.catch((err) => settleAssistantBubble(pendingRow,
|
|
363
489
|
"something went wrong answering that (" + (err && err.message ? err.message : err) + ") \\u2014 try rephrasing",
|
|
364
490
|
{ miss: true }))
|
|
@@ -377,9 +503,10 @@ ${THEME_TOKENS_CSS}
|
|
|
377
503
|
await Promise.all([fetchSeed(), tryLoadWink()]);
|
|
378
504
|
window.tmctChat.registerReferencePackProvider(fetchPackProvider);
|
|
379
505
|
window.tmctChatSession = newSession();
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
506
|
+
const stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir);
|
|
507
|
+
addSystemLine("tmct \\u2014 the real engine, running in this page \\u2014 " + statsSummaryLine(stats)
|
|
508
|
+
+ ". Ask it something, or teach it a fact of your own.");
|
|
509
|
+
await renderStatsPanel(stats);
|
|
383
510
|
inputEl.placeholder = seedPayload ? 'try "what is a dog"' : window.tmctChat.vocabExampleHint(false);
|
|
384
511
|
renderStatus();
|
|
385
512
|
setBusy(false);
|
|
@@ -393,12 +393,20 @@ function microbarsHtml(items) {
|
|
|
393
393
|
/** The dashboard strip: fact/term totals, the tier bar, graph density, a data-
|
|
394
394
|
* quality tile keyed off the SAME contradiction count worthALook surfaces,
|
|
395
395
|
* and the corpus-bundle/predicate leaderboards. Every tile reads straight
|
|
396
|
-
* off `stats` (computeLedgerStats) — no client-side recomputation.
|
|
397
|
-
|
|
396
|
+
* off `stats` (computeLedgerStats) — no client-side recomputation.
|
|
397
|
+
*
|
|
398
|
+
* Also the live dock's own re-render target: `id="dash"` gives a post-teach
|
|
399
|
+
* refresh a single element to replace (`el("dash").outerHTML = dashboardHtml(...)`
|
|
400
|
+
* — see renderLedgerHtml's inline script), and `fresh: true` marks that
|
|
401
|
+
* replacement with a quiet settle-fade (`.dash.fresh`, below) rather than a
|
|
402
|
+
* silent swap — the SAME function serves the server-rendered initial paint
|
|
403
|
+
* (fresh omitted) and every live update after it, so the two can never drift. */
|
|
404
|
+
function dashboardHtml(stats, { fresh = false } = {}) {
|
|
398
405
|
const s = stats || {};
|
|
399
406
|
const total = s.totalFacts || 0;
|
|
407
|
+
const freshCls = fresh ? " fresh" : "";
|
|
400
408
|
if (!total) {
|
|
401
|
-
return `<section class="dash" aria-label="Ledger metrics"><div class="tile"><span class="tile-label">facts.total</span><span class="tile-value">0</span><span class="tile-sub">nothing taught yet</span></div></section>`;
|
|
409
|
+
return `<section class="dash${freshCls}" id="dash" aria-label="Ledger metrics"><div class="tile"><span class="tile-label">facts.total</span><span class="tile-value">0</span><span class="tile-sub">nothing taught yet</span></div></section>`;
|
|
402
410
|
}
|
|
403
411
|
const terms = s.totalTerms || 0;
|
|
404
412
|
const qualityCls = s.contradictionCount > 0 ? " tile-alert" : "";
|
|
@@ -406,7 +414,7 @@ function dashboardHtml(stats) {
|
|
|
406
414
|
const predicatesHtml = s.predicates?.length
|
|
407
415
|
? microbarsHtml(s.predicates.map((p) => ({ label: p.phrase || p.predicate, count: p.count })))
|
|
408
416
|
: `<span class="tile-sub">none yet</span>`;
|
|
409
|
-
return `<section class="dash" aria-label="Ledger metrics">
|
|
417
|
+
return `<section class="dash${freshCls}" id="dash" aria-label="Ledger metrics">
|
|
410
418
|
<div class="tile">
|
|
411
419
|
<span class="tile-label">facts.total</span>
|
|
412
420
|
<span class="tile-value">${total}</span>
|
|
@@ -462,11 +470,40 @@ function sparklineSvg(stats) {
|
|
|
462
470
|
</svg>`;
|
|
463
471
|
}
|
|
464
472
|
|
|
473
|
+
/** The sparkline's own caption — "learned <date>" for a single-day graph,
|
|
474
|
+
* "first … last …" once it spans more than one, or the generic fallback
|
|
475
|
+
* before any dated fact exists. Its own named function (not inlined at the
|
|
476
|
+
* one server call site) so the live dock's post-teach refresh can call the
|
|
477
|
+
* identical logic client-side — see renderLedgerHtml's `sparkCaptionHtml`
|
|
478
|
+
* toString-embed, below. */
|
|
479
|
+
function sparkCaptionHtml(stats) {
|
|
480
|
+
return stats?.firstLearned && stats?.lastLearned
|
|
481
|
+
? (stats.firstLearned.slice(0, 10) === stats.lastLearned.slice(0, 10)
|
|
482
|
+
? `learned ${escapeHtml(stats.lastLearned.slice(0, 10))}`
|
|
483
|
+
: `first ${escapeHtml(stats.firstLearned.slice(0, 10))} · last ${escapeHtml(stats.lastLearned.slice(0, 10))}`)
|
|
484
|
+
: "cumulative facts, teach order";
|
|
485
|
+
}
|
|
486
|
+
|
|
465
487
|
/** One complete, self-contained document: the ledger, segment rail,
|
|
466
488
|
* worth-a-look panel, breadcrumb/search, two-hop minimap, and (when the
|
|
467
489
|
* memory-ask bundle is present) the ask-the-graph chat dock, all over the
|
|
468
|
-
* embedded LEDGER/PAYLOAD data.
|
|
469
|
-
|
|
490
|
+
* embedded LEDGER/PAYLOAD data.
|
|
491
|
+
*
|
|
492
|
+
* `ledgerBundleAvailable` (default false — every existing caller, including
|
|
493
|
+
* bin/tmct.mjs's `tmct viz` and every test that doesn't pass it) governs
|
|
494
|
+
* ONE thing: whether an external `<script src="./ledger-browser.bundle.js">`
|
|
495
|
+
* reference is emitted at all. That bundle carries the full runTurn engine
|
|
496
|
+
* (teach AND ask) and is Pages-demo-site-only — never built or shipped
|
|
497
|
+
* alongside the CLI's own output — so the CLI's `renderLedgerHtml` call
|
|
498
|
+
* never sets this and the page stays exactly as documented above, one
|
|
499
|
+
* self-contained document with no external requests. Only
|
|
500
|
+
* scripts/build-demo-site.mjs, which builds the sibling bundle itself
|
|
501
|
+
* first, passes `true`. The dock's own runtime code below ALSO gates on
|
|
502
|
+
* `typeof tmctLedger !== "undefined"` regardless — the two checks answer
|
|
503
|
+
* different questions (did this render even offer the reference; did the
|
|
504
|
+
* browser actually manage to load it), and both must hold for the live
|
|
505
|
+
* path to run. */
|
|
506
|
+
export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, worthALook, payload, meta, memoryAskBundle, stats, ledgerBundleAvailable = false } = {}) {
|
|
470
507
|
const ledgerJson = embedJson({ rows: rows || [], terms: terms || [], edges: edges || [], focus: focus || null, contradictions: contradictions || [], worthALook: worthALook || null, meta: meta || { shown: 0, total: 0, truncated: false } });
|
|
471
508
|
const payloadJson = embedJson(payload || { individuals: [], objectProperties: [] });
|
|
472
509
|
const shown = meta?.shown ?? (rows || []).length;
|
|
@@ -474,7 +511,10 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
|
|
|
474
511
|
const bundleStr = typeof memoryAskBundle === "string" ? memoryAskBundle : "";
|
|
475
512
|
const hasMemChat = bundleStr.length > 0;
|
|
476
513
|
// The placeholder is honest: the canonical exchange only when its terms are
|
|
477
|
-
// really in this payload, otherwise a real term from this graph.
|
|
514
|
+
// really in this payload, otherwise a real term from this graph. Left as
|
|
515
|
+
// the query-only wording even when the live bundle is offered — the dock's
|
|
516
|
+
// own script swaps it for a teach-aware placeholder the moment it confirms
|
|
517
|
+
// tmctLedger actually loaded (never claimed ahead of that confirmation).
|
|
478
518
|
const termSet = new Set((terms || []).map((t) => t.term));
|
|
479
519
|
const placeholder = termSet.has("ishmael")
|
|
480
520
|
? "who is the grandfather of ishmael"
|
|
@@ -488,11 +528,6 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
|
|
|
488
528
|
</form>
|
|
489
529
|
</div>`
|
|
490
530
|
: `<div class="chat chat-off"><p class="chatnote">chat unavailable — run <span class="mono">npm run build:ask-bundle</span> to enable the in-page ask engine.</p></div>`;
|
|
491
|
-
const sparkCaption = stats?.firstLearned && stats?.lastLearned
|
|
492
|
-
? (stats.firstLearned.slice(0, 10) === stats.lastLearned.slice(0, 10)
|
|
493
|
-
? `learned ${escapeHtml(stats.lastLearned.slice(0, 10))}`
|
|
494
|
-
: `first ${escapeHtml(stats.firstLearned.slice(0, 10))} · last ${escapeHtml(stats.lastLearned.slice(0, 10))}`)
|
|
495
|
-
: "cumulative facts, teach order";
|
|
496
531
|
|
|
497
532
|
return `<!doctype html>
|
|
498
533
|
<html lang="en">
|
|
@@ -500,6 +535,29 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
|
|
|
500
535
|
<meta charset="utf-8">
|
|
501
536
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
502
537
|
<title>${escapeHtml(title)}</title>
|
|
538
|
+
<!--
|
|
539
|
+
Import map: resolves the "wink-nlp"/"wink-eng-lite-web-model" bare specifiers the
|
|
540
|
+
live ask-and-teach dock's own dynamic import() needs (pinned to the exact versions
|
|
541
|
+
package.json depends on) to esm.sh CDN builds — the same seam chat.html and
|
|
542
|
+
plan.html each wire up for their own live sessions, mirrored here because this page
|
|
543
|
+
can be opened standalone (no import map inherited from a host document; this
|
|
544
|
+
includes the self-contained file tmct viz writes to disk — a plain cross-origin
|
|
545
|
+
dynamic import() of a remote https:// URL, not a file:// read, so it works the
|
|
546
|
+
same way offline this page's own try/catch already handles for the deployed site:
|
|
547
|
+
the fetch fails and the wink tier degrades gracefully). Nothing in this file
|
|
548
|
+
touches wink-nlp directly — wink-model.mjs's own header explains why a static
|
|
549
|
+
import would drag the ~1 MB model into every bundle; only the page's own inline
|
|
550
|
+
script performs this CDN import, the same bounded-race tryLoadWink() pattern
|
|
551
|
+
public/tmct-browser.mjs uses.
|
|
552
|
+
-->
|
|
553
|
+
<script type="importmap">
|
|
554
|
+
{
|
|
555
|
+
"imports": {
|
|
556
|
+
"wink-nlp": "https://esm.sh/wink-nlp@2.4.0",
|
|
557
|
+
"wink-eng-lite-web-model": "https://esm.sh/wink-eng-lite-web-model@1.8.1"
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
</script>
|
|
503
561
|
<style>
|
|
504
562
|
${THEME_TOKENS_CSS}
|
|
505
563
|
html { background: var(--bg); }
|
|
@@ -594,12 +652,26 @@ ${THEME_TOKENS_CSS}
|
|
|
594
652
|
.chatlog .a { font-size: .9rem; line-height: 1.45; }
|
|
595
653
|
.chatlog .a.miss { color: var(--muted); }
|
|
596
654
|
.chatlog .a.goal { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); }
|
|
655
|
+
.chatlog .a.pending { color: var(--muted); font-style: italic; }
|
|
656
|
+
.chatlog .a.taught { border-left: 2px solid var(--taught); padding-left: .55rem; }
|
|
657
|
+
.chatlog .a.taught .tag { display: block; font-family: ${MONO_STACK}; font-size: .62rem; letter-spacing: .06em; text-transform: uppercase; color: var(--taught); margin-bottom: .18rem; }
|
|
597
658
|
.chatask { display: flex; align-items: center; gap: .5rem; }
|
|
598
659
|
.chatlog:not(:empty) + .chatask { border-top: 1px solid var(--line); margin-top: .55rem; padding-top: .55rem; }
|
|
599
660
|
.chatask .prompt { color: var(--taught); font-size: .78rem; }
|
|
600
661
|
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .32rem .6rem; min-width: 0; }
|
|
662
|
+
.chatask input:disabled { opacity: .6; }
|
|
601
663
|
.chatnote { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); margin: 0; }
|
|
602
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
664
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
665
|
+
.seg, .chip, .look { transition: border-color .12s ease, background-color .12s ease; }
|
|
666
|
+
/* A live teach re-render swaps the dash section wholesale (dashboardHtml
|
|
667
|
+
is called again in full — see the inline script below), so the "this
|
|
668
|
+
just changed" signal has to be an animation on the fresh markup itself,
|
|
669
|
+
never a transition (which needs an old and new state on the SAME node
|
|
670
|
+
to interpolate between). Off entirely under reduced motion — the
|
|
671
|
+
numbers still update, just without the settle-fade. */
|
|
672
|
+
.dash.fresh .tile { animation: freshsettle 1.6s ease-out; }
|
|
673
|
+
@keyframes freshsettle { from { background: var(--taught-soft); } to { background: var(--card); } }
|
|
674
|
+
}
|
|
603
675
|
</style>
|
|
604
676
|
</head>
|
|
605
677
|
<body>
|
|
@@ -632,18 +704,22 @@ ${THEME_TOKENS_CSS}
|
|
|
632
704
|
<p class="mapnote">dots = terms · click to refocus · dim = filtered out</p>
|
|
633
705
|
</div>
|
|
634
706
|
<h2>ingestion</h2>
|
|
635
|
-
<div class="mapwrap sparkwrap">
|
|
707
|
+
<div class="mapwrap sparkwrap" id="sparkWrap">
|
|
636
708
|
${sparklineSvg(stats)}
|
|
637
|
-
<p class="mapnote">${
|
|
709
|
+
<p class="mapnote">${sparkCaptionHtml(stats)}</p>
|
|
638
710
|
</div>
|
|
639
711
|
</aside>
|
|
640
712
|
</div>
|
|
641
713
|
</main>
|
|
642
714
|
<script>
|
|
643
|
-
const
|
|
715
|
+
// let, not const: a successful live teach reassigns this wholesale
|
|
716
|
+
// (applyLedgerData, in the script below) so the page re-renders the graph
|
|
717
|
+
// it actually holds, not the snapshot from the moment the page loaded.
|
|
718
|
+
let LEDGER = ${ledgerJson};
|
|
644
719
|
const PAYLOAD = ${payloadJson};
|
|
645
720
|
</script>
|
|
646
721
|
${hasMemChat ? `<script>\n${bundleStr}\n</script>` : ""}
|
|
722
|
+
${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` : ""}
|
|
647
723
|
<script>
|
|
648
724
|
(function () {
|
|
649
725
|
"use strict";
|
|
@@ -651,15 +727,39 @@ ${hasMemChat ? `<script>\n${bundleStr}\n</script>` : ""}
|
|
|
651
727
|
const facetCounts = ${facetCounts.toString()};
|
|
652
728
|
const el = (id) => document.getElementById(id);
|
|
653
729
|
const esc = ${escapeHtml.toString()};
|
|
730
|
+
// Aliased so the dashboard/sparkline builders below (toString-embedded
|
|
731
|
+
// verbatim from ledger-viz.mjs's own Node-side source, which calls
|
|
732
|
+
// escapeHtml/pct by their real names) resolve without a second copy.
|
|
733
|
+
const escapeHtml = esc;
|
|
734
|
+
const pct = ${pct.toString()};
|
|
735
|
+
const tierTileHtml = ${tierTileHtml.toString()};
|
|
736
|
+
const microbarsHtml = ${microbarsHtml.toString()};
|
|
737
|
+
const dashboardHtml = ${dashboardHtml.toString()};
|
|
738
|
+
const sparklineSvg = ${sparklineSvg.toString()};
|
|
739
|
+
const sparkCaptionHtml = ${sparkCaptionHtml.toString()};
|
|
654
740
|
const FAMS = ["is-a", "has", "can", "used-for", "rests-on", "role", "other"];
|
|
655
741
|
const FAM_LABEL = { "is-a": "is a kind of", has: "has", can: "can", "used-for": "used for", "rests-on": "rests on", role: "role / property", other: "other" };
|
|
656
742
|
const PROVS = [["taught", "you taught"], ["corpus", "corpus"], ["entail", "entailed"]];
|
|
657
743
|
const RECS = ["today", "this week", "older"];
|
|
658
744
|
const provKey = (p) => (p === "entailed" ? "entail" : p); // css class key
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
745
|
+
|
|
746
|
+
// LEDGER (declared "let" in the embed script above this one — a classic,
|
|
747
|
+
// non-module script's top-level bindings are reachable as bare identifiers
|
|
748
|
+
// by every later script tag in the document, never as window.LEDGER; see
|
|
749
|
+
// e2e/pages-ledger.test.mjs's own note on this) and its indexes start as
|
|
750
|
+
// the server-rendered snapshot but are REASSIGNED wholesale after a live
|
|
751
|
+
// teach (applyLedgerData, below) — a successful teach through the dock
|
|
752
|
+
// changes the underlying graph, and the page has to show that, not keep
|
|
753
|
+
// rendering the page-load snapshot next to a chat log that merely SAYS
|
|
754
|
+
// something new was learned.
|
|
755
|
+
let termIndex, rowById, contraById;
|
|
756
|
+
function rebuildIndexes() {
|
|
757
|
+
termIndex = new Map(LEDGER.terms.map((t) => [t.term, t]));
|
|
758
|
+
rowById = new Map(LEDGER.rows.map((r) => [r.id, r]));
|
|
759
|
+
contraById = new Map();
|
|
760
|
+
LEDGER.contradictions.forEach((ids, gi) => ids.forEach((id) => contraById.set(id, gi)));
|
|
761
|
+
}
|
|
762
|
+
rebuildIndexes();
|
|
663
763
|
|
|
664
764
|
let focus = LEDGER.focus;
|
|
665
765
|
let trail = focus ? [{ term: focus, label: null }] : [];
|
|
@@ -845,10 +945,133 @@ ${hasMemChat ? `<script>\n${bundleStr}\n</script>` : ""}
|
|
|
845
945
|
else el("qmiss").textContent = "no such term";
|
|
846
946
|
});
|
|
847
947
|
|
|
848
|
-
//
|
|
948
|
+
// A successful live teach re-derives the WHOLE ledger (computeLedgerDataFromPayload,
|
|
949
|
+
// the sibling bundle's own re-export of the exact function this page's build
|
|
950
|
+
// step called) and re-mounts it here — the same view a fresh page load
|
|
951
|
+
// would render, never a stale snapshot next to a chat log that only SAYS
|
|
952
|
+
// something new was learned. The dashboard/sparkline sections are replaced
|
|
953
|
+
// wholesale (dashboardHtml/sparklineSvg, toString-embedded above, are the
|
|
954
|
+
// SAME functions the server used for the very first paint) rather than
|
|
955
|
+
// patched, so they can never drift from what a fresh render would produce.
|
|
956
|
+
function applyLedgerData(freshData) {
|
|
957
|
+
LEDGER = {
|
|
958
|
+
rows: freshData.rows, terms: freshData.terms, edges: freshData.edges,
|
|
959
|
+
focus: freshData.focus, contradictions: freshData.contradictions,
|
|
960
|
+
worthALook: freshData.worthALook, meta: freshData.meta,
|
|
961
|
+
};
|
|
962
|
+
rebuildIndexes();
|
|
963
|
+
el("dash").outerHTML = dashboardHtml(freshData.stats, { fresh: true });
|
|
964
|
+
el("sparkWrap").innerHTML = sparklineSvg(freshData.stats) + '<p class="mapnote">' + sparkCaptionHtml(freshData.stats) + "</p>";
|
|
965
|
+
// computeLedgerDataFromPayload resolves an unset focus to the newest
|
|
966
|
+
// taught row's own subject — passing no explicit term (below) means every
|
|
967
|
+
// successful teach naturally jumps the view to what was just learned.
|
|
968
|
+
if (freshData.focus) refocusWithLabel(freshData.focus, null);
|
|
969
|
+
else render();
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// ---- the chat dock: LIVE teach+ask over the sibling ledger-browser.bundle.js
|
|
973
|
+
// (window.tmctLedger) when the demo build offered it AND it actually
|
|
974
|
+
// loaded; falls back to the read-only tmctMemoryAsk engine below —
|
|
975
|
+
// unchanged from before this page could teach at all — otherwise.
|
|
976
|
+
// bin/tmct.mjs's own \`tmct viz\` output never offers the live bundle
|
|
977
|
+
// (renderLedgerHtml's own ledgerBundleAvailable defaults false there), so
|
|
978
|
+
// this branch is simply never reachable on a CLI-generated page.
|
|
849
979
|
const resolveAnsweredTerm = ${resolveAnsweredTerm.toString()};
|
|
850
980
|
const chatForm = el("chatform");
|
|
851
|
-
if (chatForm && typeof
|
|
981
|
+
if (chatForm && typeof tmctLedger !== "undefined" && typeof tmctLedger.createLedgerSession === "function") {
|
|
982
|
+
const log = el("chatlog");
|
|
983
|
+
const chatqEl = el("chatq");
|
|
984
|
+
chatqEl.placeholder = 'ask or teach the graph\\u2026 e.g. "blue is a peg"';
|
|
985
|
+
const addLine = (cls, html) => {
|
|
986
|
+
const d = document.createElement("div");
|
|
987
|
+
d.className = cls; d.innerHTML = html;
|
|
988
|
+
log.appendChild(d); log.scrollTop = log.scrollHeight;
|
|
989
|
+
return d;
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
let session = null;
|
|
993
|
+
// Serializes every engine-touching call through this one dock — the same
|
|
994
|
+
// posture plan-viz.mjs's own chat-assert dock takes with its withLock,
|
|
995
|
+
// so a fast double-submit can never race two turns over one session.
|
|
996
|
+
let lock = Promise.resolve();
|
|
997
|
+
const withLock = (fn) => { const run = lock.then(fn, fn); lock = run.catch(() => {}); return run; };
|
|
998
|
+
|
|
999
|
+
// The SAME bounded-race wink-nlp CDN load plan-viz.mjs's own chat-assert
|
|
1000
|
+
// dock uses: a cross-origin dynamic import() can neither resolve nor
|
|
1001
|
+
// reject on some failures, so an unbounded await would leave a session
|
|
1002
|
+
// stuck forever. Best-effort — a teach sentence that needs the lemma tier
|
|
1003
|
+
// just declines honestly without it, same as a checkout missing the
|
|
1004
|
+
// optional deps.
|
|
1005
|
+
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
1006
|
+
const winkTimeout = (ms, reason) => new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
|
|
1007
|
+
let winkReady = null;
|
|
1008
|
+
function tryLoadWink() {
|
|
1009
|
+
if (winkReady) return winkReady;
|
|
1010
|
+
winkReady = (async () => {
|
|
1011
|
+
try {
|
|
1012
|
+
const mods = await Promise.race([
|
|
1013
|
+
Promise.all([import("wink-nlp"), import("wink-eng-lite-web-model")]),
|
|
1014
|
+
winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink-nlp CDN load timed out"),
|
|
1015
|
+
]);
|
|
1016
|
+
const winkNLP = mods[0].default;
|
|
1017
|
+
const model = mods[1].default;
|
|
1018
|
+
tmctLedger.registerWinkModel(() => ({ winkNLP: winkNLP, model: model }));
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
// eslint-disable-next-line no-console
|
|
1021
|
+
console.warn("tmct ledger: wink-nlp CDN load failed, continuing without the lemma/POS tier", err);
|
|
1022
|
+
}
|
|
1023
|
+
})();
|
|
1024
|
+
return winkReady;
|
|
1025
|
+
}
|
|
1026
|
+
tryLoadWink(); // fire eagerly at load, so it is likely settled by the first interaction
|
|
1027
|
+
|
|
1028
|
+
async function ensureSession() {
|
|
1029
|
+
if (session) return session;
|
|
1030
|
+
await tryLoadWink();
|
|
1031
|
+
session = await tmctLedger.createLedgerSession({ seedPayload: PAYLOAD });
|
|
1032
|
+
return session;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
chatForm.addEventListener("submit", (e) => {
|
|
1036
|
+
e.preventDefault();
|
|
1037
|
+
const q = chatqEl.value.trim();
|
|
1038
|
+
if (!q) return;
|
|
1039
|
+
chatqEl.value = "";
|
|
1040
|
+
addLine("u", esc(q));
|
|
1041
|
+
const pending = addLine("a pending", "computing\\u2026");
|
|
1042
|
+
chatqEl.disabled = true;
|
|
1043
|
+
withLock(async () => {
|
|
1044
|
+
try {
|
|
1045
|
+
const s = await ensureSession();
|
|
1046
|
+
const result = await s.turn(q);
|
|
1047
|
+
const record = result.record;
|
|
1048
|
+
const taught = !!record && record.miss === false && (record.via === "assert" || record.via === "retract");
|
|
1049
|
+
const body = esc(result.answer).replace(/\\n/g, "<br>");
|
|
1050
|
+
pending.className = "a" + (taught ? " taught" : (record && record.miss ? " miss" : ""));
|
|
1051
|
+
pending.innerHTML = taught ? '<span class="tag">taught</span>' + body : body;
|
|
1052
|
+
if (taught) {
|
|
1053
|
+
const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
|
|
1054
|
+
applyLedgerData(fresh);
|
|
1055
|
+
} else if (!(record && record.miss)) {
|
|
1056
|
+
// Only a genuine answer (never a miss) tries to resolve a
|
|
1057
|
+
// refocus target — the honest-miss cascade's own boilerplate
|
|
1058
|
+
// ("Run \`tmct init\`…") can contain a real term as an
|
|
1059
|
+
// ordinary English word (e.g. "run", if the graph happens to
|
|
1060
|
+
// hold it), and resolveAnsweredTerm has no way to tell that
|
|
1061
|
+
// apart from the term genuinely being discussed.
|
|
1062
|
+
const hit = resolveAnsweredTerm(result.answer, q, LEDGER.terms, tmctLedger.normFactTerm);
|
|
1063
|
+
if (hit) refocusWithLabel(hit, q);
|
|
1064
|
+
}
|
|
1065
|
+
} catch {
|
|
1066
|
+
pending.className = "a miss";
|
|
1067
|
+
pending.textContent = "Something went wrong answering that. Try rephrasing, or reload the page.";
|
|
1068
|
+
} finally {
|
|
1069
|
+
chatqEl.disabled = false;
|
|
1070
|
+
chatqEl.focus();
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
} else if (chatForm && typeof tmctMemoryAsk !== "undefined") {
|
|
852
1075
|
const memHandle = tmctMemoryAsk.createInMemoryStore();
|
|
853
1076
|
memHandle.payload = PAYLOAD;
|
|
854
1077
|
const log = el("chatlog");
|
|
@@ -296,13 +296,13 @@ export function renderPlanHtml({ plan, rendersAs = {}, sizeOrder = [], title } =
|
|
|
296
296
|
<!--
|
|
297
297
|
Import map: resolves the "wink-nlp"/"wink-eng-lite-web-model" bare specifiers the
|
|
298
298
|
live re-solve session's own dynamic import() needs (pinned to the exact versions
|
|
299
|
-
package.json depends on) to esm.sh CDN builds — the same seam
|
|
300
|
-
|
|
301
|
-
opened standalone (no import map inherited from a host document). The
|
|
302
|
-
itself (./plan-browser.bundle.js) never touches wink-nlp directly —
|
|
303
|
-
own header explains why a static import would drag the ~1 MB
|
|
304
|
-
bundle; only the page's own inline script performs this CDN
|
|
305
|
-
public/
|
|
299
|
+
package.json depends on) to esm.sh CDN builds — the same seam chat.html and
|
|
300
|
+
ledger.html each wire up for their own live sessions, mirrored here because this
|
|
301
|
+
page can be opened standalone (no import map inherited from a host document). The
|
|
302
|
+
bundle itself (./plan-browser.bundle.js) never touches wink-nlp directly —
|
|
303
|
+
wink-model.mjs's own header explains why a static import would drag the ~1 MB
|
|
304
|
+
model into every bundle; only the page's own inline script performs this CDN
|
|
305
|
+
import, the same bounded-race tryLoadWink() pattern public/tmct-browser.mjs uses.
|
|
306
306
|
-->
|
|
307
307
|
<script type="importmap">
|
|
308
308
|
{
|
|
@@ -636,9 +636,9 @@ const PLAN = ${embedded};
|
|
|
636
636
|
// "moving" to "move" — without it that one teach sentence honestly
|
|
637
637
|
// declines and every position fact taught after it fails in turn. Load
|
|
638
638
|
// wink from the CDN and register it, the SAME bounded-race pattern
|
|
639
|
-
// public/
|
|
640
|
-
//
|
|
641
|
-
//
|
|
639
|
+
// public/tmct-browser.mjs uses: a cross-origin dynamic import() can
|
|
640
|
+
// neither resolve nor reject on some failures, so an unbounded await
|
|
641
|
+
// would leave a resolve stuck forever.
|
|
642
642
|
// Awaited before EVERY session creation below (idempotent — a second
|
|
643
643
|
// await after the first attempt already settled resolves immediately).
|
|
644
644
|
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// chat-browser-entry.mjs — the esbuild entry for
|
|
2
|
-
//
|
|
1
|
+
// chat-browser-entry.mjs — the esbuild entry for chat.html's embedded chat
|
|
2
|
+
// (built by scripts/build-chat-bundle.mjs).
|
|
3
3
|
//
|
|
4
4
|
// Unlike memory-ask-browser-entry.mjs (factAnswer/factReadBack only), this
|
|
5
5
|
// exposes the FULL turn engine: createChatSession wraps chat.mjs's runTurn
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
// Web Crypto API instead, with a Date.now fallback for contexts
|
|
18
18
|
// without it.
|
|
19
19
|
import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
|
|
20
|
-
import { createInMemoryStore, normFactTerm } from "../../adapters/memory/core.mjs";
|
|
20
|
+
import { createInMemoryStore, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
|
|
21
|
+
import { provenanceTagToSource } from "../../domain/memory/trust.mjs";
|
|
21
22
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
22
23
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
23
24
|
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
@@ -84,4 +85,51 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false } =
|
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
/**
|
|
89
|
+
* The memory a running session holds, broken down by where each fact came
|
|
90
|
+
* from: the seed corpus bands it booted with (keyed by the SAME band name
|
|
91
|
+
* build-chat-seed.mjs/extensions.mjs seed under — "human", "seon",
|
|
92
|
+
* "conceptnet" today, whichever bands a future seed adds tomorrow) plus
|
|
93
|
+
* whatever has been taught THIS session over chat. Reuses memory/trust.mjs's
|
|
94
|
+
* own `provenanceTagToSource` against each fact's already-stored provenance
|
|
95
|
+
* tag(s) (readFactRows' `provenance`, the ' | '-joined compat string) rather
|
|
96
|
+
* than inventing a second provenance parse — the same tag chat.mjs's own
|
|
97
|
+
* "(source: ...)" citation already carries, so this panel and a turn's
|
|
98
|
+
* citation always agree on where a fact came from.
|
|
99
|
+
*
|
|
100
|
+
* Returns { total, bandCounts, taught }: `bandCounts` maps a band label to
|
|
101
|
+
* its fact count ("taught this session" for a teach/operator-sourced fact
|
|
102
|
+
* with no corpus band, "other" for anything provenance can't place);
|
|
103
|
+
* `taught` lists every session-taught fact (subject/predicate/object + its
|
|
104
|
+
* own provenance tag), most-recently-taught last — a stats panel's
|
|
105
|
+
* provenance column reads straight off this, no further lookup needed.
|
|
106
|
+
*/
|
|
107
|
+
export async function memoryStats(memoryDir) {
|
|
108
|
+
const memory = await loadMemory(memoryDir);
|
|
109
|
+
const rows = readFactRows(memory);
|
|
110
|
+
const bandCounts = {};
|
|
111
|
+
const taught = [];
|
|
112
|
+
for (const row of rows) {
|
|
113
|
+
const tags = String(row.provenance || "").split(" | ").filter(Boolean);
|
|
114
|
+
let band = null;
|
|
115
|
+
let isTaught = false;
|
|
116
|
+
let taughtTag = "";
|
|
117
|
+
for (const tag of tags) {
|
|
118
|
+
const src = provenanceTagToSource(tag);
|
|
119
|
+
if (!src) continue;
|
|
120
|
+
// corpusWeak (a /r/RelatedTo-strength triple, e.g. ConceptNet's or
|
|
121
|
+
// SEON's own weaker associations) names the SAME band as corpus (a
|
|
122
|
+
// /r/IsA-strength one) — both carry `src.name`, and a band count that
|
|
123
|
+
// dropped the weak tier would undercount a corpus by exactly its
|
|
124
|
+
// weak-relation facts (SEON: 19 of them, all `corpus-weak:seon`).
|
|
125
|
+
if ((src.kind === "corpus" || src.kind === "corpusWeak") && src.name) band = src.name;
|
|
126
|
+
if (src.kind === "teach" || src.kind === "operator") { isTaught = true; taughtTag = tag; }
|
|
127
|
+
}
|
|
128
|
+
const label = band || (isTaught ? "taught this session" : "other");
|
|
129
|
+
bandCounts[label] = (bandCounts[label] || 0) + 1;
|
|
130
|
+
if (isTaught) taught.push({ subject: row.subject, predicate: row.predicate, object: row.object, tag: taughtTag });
|
|
131
|
+
}
|
|
132
|
+
return { total: rows.length, bandCounts, taught };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, normFactTerm, vocabExampleHint, memoryStats };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// ledger-browser-entry.mjs — the esbuild entry for the memory-ledger page's
|
|
2
|
+
// LIVE chat dock (public/ledger-browser.bundle.js, built by
|
|
3
|
+
// scripts/build-ledger-bundle.mjs), mirroring chat-browser-entry.mjs's own
|
|
4
|
+
// createChatSession shape exactly. The ledger dock is general free-text
|
|
5
|
+
// teach+ask, the same shape chat.html's own dock is — not board-game
|
|
6
|
+
// specific like plan-browser-entry.mjs's createPlanSession, which has to
|
|
7
|
+
// teach and solve a puzzle up front.
|
|
8
|
+
//
|
|
9
|
+
// Gitignored, Pages-demo-site-only output (scripts/build-demo-site.mjs
|
|
10
|
+
// builds it fresh on every deploy, never committed) — see
|
|
11
|
+
// memory-ask-browser-entry.mjs's own header for the contrast: THAT bundle
|
|
12
|
+
// (factAnswer/factReadBack only, ~1.0MB) is the one COMMITTED under src/ and
|
|
13
|
+
// packed by `npm publish`, because `tmct viz` is a real CLI command run
|
|
14
|
+
// against a user's own local .tmct store and must ship with the package.
|
|
15
|
+
// This bundle carries the FULL runTurn engine (~1.5MB, the same weight
|
|
16
|
+
// class as chat/spider-fly/adventure/plan's own browser bundles) and is
|
|
17
|
+
// never published — only the hosted demo site's public/ledger.html links to
|
|
18
|
+
// it, as an optional sibling script the page degrades honestly without.
|
|
19
|
+
import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
|
|
20
|
+
import { createInMemoryStore, normFactTerm } from "../../adapters/memory/core.mjs";
|
|
21
|
+
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
22
|
+
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
23
|
+
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
24
|
+
import { computeLedgerDataFromPayload } from "../../services/ledger-viz.mjs";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A browser ledger-dock session over the real turn engine — createChatSession's
|
|
28
|
+
* exact shape, seeded from the ledger page's own embedded PAYLOAD (the same
|
|
29
|
+
* loadMemory()-shaped object renderLedgerHtml embeds as `const PAYLOAD`), so
|
|
30
|
+
* a fact taught through the dock extends the SAME graph the page renders
|
|
31
|
+
* from, never a disconnected one.
|
|
32
|
+
*
|
|
33
|
+
* Returns { memoryDir, sessionId, turn }, identical to createChatSession.
|
|
34
|
+
* ledger-viz.mjs's own inline script calls computeLedgerDataFromPayload
|
|
35
|
+
* (re-exported below) on `memoryDir.payload` after a turn whose record shows
|
|
36
|
+
* a successful write (`via: "assert"` or `via: "retract"`, `miss: false`) to
|
|
37
|
+
* re-derive the page's rows/terms/edges/stats and re-mount the same view a
|
|
38
|
+
* fresh page load would have rendered — a plain query never re-derives.
|
|
39
|
+
*/
|
|
40
|
+
export function createLedgerSession({ seedPayload = null, vocabSeeded = false } = {}) {
|
|
41
|
+
const memoryDir = createInMemoryStore();
|
|
42
|
+
// Spread onto the store's own empty payload so a partial seed still
|
|
43
|
+
// carries the classes/prefixes scaffolding the write path recounts —
|
|
44
|
+
// teach turns must work regardless of what the seed payload carries.
|
|
45
|
+
if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
|
|
46
|
+
|
|
47
|
+
const graph = parseEntities({ individuals: [], objectProperties: [] });
|
|
48
|
+
const lexicon = loadLexicon();
|
|
49
|
+
const vocabHint = vocabExampleHint(vocabSeeded);
|
|
50
|
+
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
51
|
+
|
|
52
|
+
let focus = null;
|
|
53
|
+
let last = null;
|
|
54
|
+
let planState = null;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
memoryDir,
|
|
58
|
+
sessionId,
|
|
59
|
+
|
|
60
|
+
/** One dispatched turn. A throwing runTurn must never kill the session —
|
|
61
|
+
* the page has no other chance to show this turn's answer. */
|
|
62
|
+
async turn(line) {
|
|
63
|
+
let result;
|
|
64
|
+
try {
|
|
65
|
+
result = await runTurn(line, {
|
|
66
|
+
config: null, source: null, graph, focus, last, memoryDir, sessionId,
|
|
67
|
+
env: {}, lexicon, vocabHint, planState,
|
|
68
|
+
});
|
|
69
|
+
} catch (e) {
|
|
70
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
71
|
+
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null };
|
|
72
|
+
}
|
|
73
|
+
focus = result.focus;
|
|
74
|
+
last = result.last;
|
|
75
|
+
if ("planState" in result) planState = result.planState;
|
|
76
|
+
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null };
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Re-exported so ledger-viz.mjs's own inline script never has to duplicate
|
|
82
|
+
// the derivation logic that builds rows/terms/edges/contradictions/
|
|
83
|
+
// worthALook/stats from a payload — the same posture chat-browser-entry.mjs
|
|
84
|
+
// takes re-exporting registerWinkModel for its own page's CDN wink load.
|
|
85
|
+
globalThis.tmctLedger = { createLedgerSession, computeLedgerDataFromPayload, normFactTerm, registerWinkModel };
|
|
@@ -9268,6 +9268,7 @@ ${bodyText}` : graphText, tier });
|
|
|
9268
9268
|
findContradictions: () => findContradictions,
|
|
9269
9269
|
findRuleByName: () => findRuleByName,
|
|
9270
9270
|
findRulesByName: () => findRulesByName,
|
|
9271
|
+
isMemoryOrSqliteHandle: () => isMemoryOrSqliteHandle,
|
|
9271
9272
|
loadMemory: () => loadMemory,
|
|
9272
9273
|
loadSyllogiseState: () => loadSyllogiseState,
|
|
9273
9274
|
normFactPredicate: () => normFactPredicate,
|
|
@@ -22059,6 +22060,7 @@ ${codeblock}`, options);
|
|
|
22059
22060
|
init_prose();
|
|
22060
22061
|
init_text_stats();
|
|
22061
22062
|
init_trust();
|
|
22063
|
+
init_core();
|
|
22062
22064
|
BLOCKS_DIR_REL = join(".tmct", "memory", "blocks");
|
|
22063
22065
|
}
|
|
22064
22066
|
});
|