@polycode-projects/the-mechanical-code-talker 2.8.11 → 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 +1 -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 +23 -0
- package/src/services/plan-viz.mjs +10 -10
- package/src/surfaces/web/chat-browser-entry.mjs +52 -4
- 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.",
|
|
@@ -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);
|
|
@@ -535,6 +535,29 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
|
|
|
535
535
|
<meta charset="utf-8">
|
|
536
536
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
537
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>
|
|
538
561
|
<style>
|
|
539
562
|
${THEME_TOKENS_CSS}
|
|
540
563
|
html { background: var(--bg); }
|
|
@@ -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 };
|
|
@@ -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
|
});
|