@polycode-projects/the-mechanical-code-talker 2.10.5 → 2.11.0
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 +2 -2
- package/corpus/sprites/src/sprite-facts.jsonl +18 -0
- package/corpus/worlds/manifest.json +5 -5
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +27 -0
- package/data/sprites/book-icon.toml +12 -0
- package/data/sprites/cellar-icon.toml +12 -0
- package/data/sprites/drawing-room-icon.toml +13 -0
- package/data/sprites/garden-icon.toml +12 -0
- package/data/sprites/kitchen-icon.toml +13 -0
- package/data/sprites/library-icon.toml +12 -0
- package/data/sprites/pan-icon.toml +11 -0
- package/data/sprites/study-icon.toml +12 -0
- package/package.json +5 -2
- package/src/adapters/corpus/wikipedia-live.mjs +182 -26
- package/src/adapters/corpus/worlds-pack.mjs +8 -2
- package/src/adapters/toml-config.mjs +6 -0
- package/src/domain/memory/trust.mjs +11 -0
- package/src/domain/worlds-pack.mjs +50 -0
- package/src/services/adventure-autoplay.mjs +5 -2
- package/src/services/adventure-viz.mjs +301 -33
- package/src/services/adventure.mjs +162 -14
- package/src/services/chat-page-viz.mjs +265 -189
- package/src/services/chat-session.mjs +15 -5
- package/src/services/chat.mjs +286 -43
- package/src/services/code-explorer-viz.mjs +183 -75
- package/src/services/extract-facts.mjs +118 -28
- package/src/services/ingest-viz.mjs +328 -79
- package/src/services/ledger-viz.mjs +99 -0
- package/src/services/memory-panel-viz.mjs +159 -0
- package/src/services/research.mjs +266 -0
- package/src/services/sentences.mjs +19 -0
- package/src/services/spider-fly-viz.mjs +21 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
- package/src/surfaces/web/chat-browser-entry.mjs +28 -11
- package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
- package/src/surfaces/web/ingest-browser-entry.mjs +123 -41
- package/src/surfaces/web/ledger-browser-entry.mjs +10 -4
- package/src/surfaces/web/memory-ask-browser.bundle.js +112 -112
- package/src/surfaces/web/memory-stats.mjs +53 -0
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
// code-explorer-viz.mjs — the code
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// code-explorer-viz.mjs — the code explorer as a full-viewport IDE shell:
|
|
2
|
+
// a title bar (graph source, Open graph…/Open repo…), an explorer sidebar
|
|
3
|
+
// reading each import/call/contains edge back as a plain sentence, a chat
|
|
4
|
+
// centre over the same graph with a rail of suggested questions, and a status
|
|
5
|
+
// bar carrying the graph's own counts. The chat session seeds BOTH the loaded
|
|
6
|
+
// code graph and chat.html's general-knowledge bands (./chat-seed.json,
|
|
7
|
+
// fetched lazily at runtime), so one conversation answers "what is a queue"
|
|
8
|
+
// and "what imports src/core/model.mjs" alike — and degrades to graph-only
|
|
9
|
+
// when the seed asset is unavailable.
|
|
7
10
|
//
|
|
8
|
-
// The
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
+
// The derivations are pure so the shell, the packaging scripts, and the unit
|
|
12
|
+
// tests all share one code path; renderCodeExplorerHtml builds one
|
|
13
|
+
// self-contained document that both packagers ship unchanged — the site build
|
|
14
|
+
// (scripts/build-demo-site.mjs) and the Electron desktop shell
|
|
15
|
+
// (scripts/build-electron-app.mjs). Panels are plain sections inside the
|
|
16
|
+
// sidebar/centre grid, so a later panel is one more <section class="panel">,
|
|
17
|
+
// not a re-architecture.
|
|
11
18
|
|
|
12
19
|
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
|
|
13
20
|
import { generateCodeHints } from "../domain/code-explorer-hints.mjs";
|
|
@@ -121,6 +128,7 @@ const CLIENT_JS = String.raw`
|
|
|
121
128
|
input: document.getElementById("chat-input"),
|
|
122
129
|
dockNote: document.getElementById("dock-note"),
|
|
123
130
|
source: document.getElementById("source-name"),
|
|
131
|
+
seedStatus: document.getElementById("seed-status"),
|
|
124
132
|
};
|
|
125
133
|
var session = null;
|
|
126
134
|
|
|
@@ -174,7 +182,7 @@ const CLIENT_JS = String.raw`
|
|
|
174
182
|
renderHints(data);
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
function
|
|
185
|
+
function appendTurn(role, text) {
|
|
178
186
|
var div = document.createElement("div");
|
|
179
187
|
div.className = "turn turn-" + role;
|
|
180
188
|
div.innerHTML = '<span class="who">' + (role === "you" ? "you" : "tmct") + '</span><span class="said">' + esc(text) + '</span>';
|
|
@@ -182,23 +190,93 @@ const CLIENT_JS = String.raw`
|
|
|
182
190
|
els.log.scrollTop = els.log.scrollHeight;
|
|
183
191
|
}
|
|
184
192
|
|
|
193
|
+
function appendNote(text) {
|
|
194
|
+
var div = document.createElement("div");
|
|
195
|
+
div.className = "turn turn-note";
|
|
196
|
+
div.textContent = text;
|
|
197
|
+
els.log.appendChild(div);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---- the general-knowledge seed --------------------------------------
|
|
201
|
+
// The same chat-seed.json chat.html boots from, loaded lazily so the shell
|
|
202
|
+
// paints instantly: same-origin fetch on the site, the preload bridge's
|
|
203
|
+
// readSeed under the desktop shell (a file:// page cannot fetch). The
|
|
204
|
+
// status bar narrates the load; a missing or failing seed leaves the chat
|
|
205
|
+
// graph-only and says so, never broken.
|
|
206
|
+
var seedState = { status: api ? "loading" : "absent", payload: null, facts: 0 };
|
|
207
|
+
function seedNote(text) { if (els.seedStatus) els.seedStatus.textContent = text; }
|
|
208
|
+
function mbText(n) { return (n / 1048576).toFixed(1); }
|
|
209
|
+
|
|
210
|
+
async function fetchTextWithProgress(url, onProgress) {
|
|
211
|
+
var res = await fetch(url);
|
|
212
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
213
|
+
var total = Number(res.headers.get("content-length")) || 0;
|
|
214
|
+
if (!res.body || !res.body.getReader) return res.text();
|
|
215
|
+
var reader = res.body.getReader();
|
|
216
|
+
var chunks = [];
|
|
217
|
+
var loaded = 0;
|
|
218
|
+
for (;;) {
|
|
219
|
+
var step = await reader.read();
|
|
220
|
+
if (step.done) break;
|
|
221
|
+
chunks.push(step.value);
|
|
222
|
+
loaded += step.value.byteLength;
|
|
223
|
+
onProgress(loaded, total);
|
|
224
|
+
}
|
|
225
|
+
return new Blob(chunks).text();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function loadSeed() {
|
|
229
|
+
try {
|
|
230
|
+
var text = null;
|
|
231
|
+
if (window.tmctDesktop && typeof window.tmctDesktop.readSeed === "function") {
|
|
232
|
+
seedNote("loading general knowledge…");
|
|
233
|
+
text = await window.tmctDesktop.readSeed();
|
|
234
|
+
} else {
|
|
235
|
+
text = await fetchTextWithProgress("./chat-seed.json", function (loaded, total) {
|
|
236
|
+
seedNote("loading general knowledge… " + mbText(loaded) + (total ? " of " + mbText(total) : "") + " MB");
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (text) {
|
|
240
|
+
seedState.payload = JSON.parse(text);
|
|
241
|
+
seedState.facts = (seedState.payload.individuals || []).filter(function (i) { return i.class === "Fact"; }).length;
|
|
242
|
+
seedState.status = "ready";
|
|
243
|
+
seedNote("general knowledge: " + seedState.facts + " facts");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
} catch (e) {
|
|
247
|
+
console.warn("tmct code explorer: chat-seed unavailable, continuing graph-only", e);
|
|
248
|
+
}
|
|
249
|
+
seedState.status = "absent";
|
|
250
|
+
seedNote("graph-only — general knowledge unavailable");
|
|
251
|
+
}
|
|
252
|
+
var seedPromise = api ? loadSeed() : Promise.resolve();
|
|
253
|
+
|
|
254
|
+
function cloneSeed() {
|
|
255
|
+
if (!seedState.payload) return null;
|
|
256
|
+
try { return structuredClone(seedState.payload); } catch (e) { return JSON.parse(JSON.stringify(seedState.payload)); }
|
|
257
|
+
}
|
|
258
|
+
|
|
185
259
|
async function ensureSession() {
|
|
186
260
|
if (session || !api || !api.createCodeExplorerSession) return session;
|
|
187
|
-
var winkLoaded = true;
|
|
188
261
|
if (api.registerWinkModel && window.__WINK_LOADER__) {
|
|
189
262
|
try { var mod = await window.__WINK_LOADER__(); api.registerWinkModel(function () { return mod; }); }
|
|
190
|
-
catch (e) {
|
|
263
|
+
catch (e) { /* the lemma/POS tier is optional */ }
|
|
191
264
|
}
|
|
192
|
-
|
|
265
|
+
await seedPromise;
|
|
266
|
+
session = api.createCodeExplorerSession({
|
|
267
|
+
graphPayload: DATA.payload,
|
|
268
|
+
seedPayload: cloneSeed(),
|
|
269
|
+
vocabSeeded: seedState.status === "ready",
|
|
270
|
+
});
|
|
193
271
|
return session;
|
|
194
272
|
}
|
|
195
273
|
|
|
196
274
|
async function ask(q) {
|
|
197
|
-
|
|
275
|
+
appendTurn("you", q);
|
|
198
276
|
var s = await ensureSession();
|
|
199
|
-
if (!s) {
|
|
277
|
+
if (!s) { appendTurn("tmct", "the live chat is not loaded on this page."); return; }
|
|
200
278
|
var res = await s.turn(q);
|
|
201
|
-
|
|
279
|
+
appendTurn("tmct", res.answer);
|
|
202
280
|
}
|
|
203
281
|
|
|
204
282
|
// Delegate term + hint clicks.
|
|
@@ -219,7 +297,8 @@ const CLIENT_JS = String.raw`
|
|
|
219
297
|
});
|
|
220
298
|
}
|
|
221
299
|
|
|
222
|
-
// Desktop pickers, present only under the Electron shell.
|
|
300
|
+
// Desktop pickers, present only under the Electron shell. A swapped graph
|
|
301
|
+
// starts a fresh session; the general-knowledge seed carries over.
|
|
223
302
|
function wirePicker(id, method, updateSource) {
|
|
224
303
|
var btn = document.getElementById(id);
|
|
225
304
|
if (!btn) return;
|
|
@@ -245,8 +324,11 @@ const CLIENT_JS = String.raw`
|
|
|
245
324
|
wirePicker("open-repo", "openRepo", true);
|
|
246
325
|
|
|
247
326
|
if (!api) {
|
|
248
|
-
if (els.dockNote) els.dockNote.textContent = "static view — the live chat
|
|
327
|
+
if (els.dockNote) els.dockNote.textContent = "static view — the live chat is unavailable on this page.";
|
|
249
328
|
if (els.input) els.input.disabled = true;
|
|
329
|
+
seedNote("static view");
|
|
330
|
+
} else {
|
|
331
|
+
appendNote("Ask about this code graph — or anything its general knowledge covers, like “what is a queue”.");
|
|
250
332
|
}
|
|
251
333
|
|
|
252
334
|
mountView(DATA);
|
|
@@ -254,8 +336,8 @@ const CLIENT_JS = String.raw`
|
|
|
254
336
|
`;
|
|
255
337
|
|
|
256
338
|
/**
|
|
257
|
-
* One self-contained HTML document
|
|
258
|
-
* computeCodeExplorerData's output. `bundleInline` inlines the
|
|
339
|
+
* One self-contained HTML document: the full-viewport IDE shell over
|
|
340
|
+
* computeCodeExplorerData's output. `bundleInline` inlines the chat engine
|
|
259
341
|
* (for a single-file page / a data: URL); otherwise `bundleAvailable` links
|
|
260
342
|
* `./code-explorer.bundle.js`. `winkLoaderInline` optionally inlines a wink
|
|
261
343
|
* model loader as `window.__WINK_LOADER__`. `showDesktopLink` adds a line
|
|
@@ -265,7 +347,6 @@ const CLIENT_JS = String.raw`
|
|
|
265
347
|
export function renderCodeExplorerHtml(data, { bundleInline = "", bundleAvailable = false, winkLoaderInline = "", sourceName = "demo code graph", showDesktopLink = false } = {}) {
|
|
266
348
|
const payloadJson = embedJson(data.payload);
|
|
267
349
|
const dataJson = embedJson({ ledger: data.ledger, hints: data.hints, focus: data.focus, meta: data.meta });
|
|
268
|
-
const title = escapeHtml(data.meta?.title || "code explorer");
|
|
269
350
|
|
|
270
351
|
return `<!doctype html>
|
|
271
352
|
<html lang="en">
|
|
@@ -276,75 +357,102 @@ export function renderCodeExplorerHtml(data, { bundleInline = "", bundleAvailabl
|
|
|
276
357
|
<style>
|
|
277
358
|
${THEME_TOKENS_CSS}
|
|
278
359
|
* { box-sizing: border-box; }
|
|
279
|
-
body {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
360
|
+
html, body { height: 100%; }
|
|
361
|
+
body { margin: 0; overflow: hidden; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; }
|
|
362
|
+
.shell { height: 100%; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; }
|
|
363
|
+
|
|
364
|
+
.titlebar { display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; padding: 0.5rem 0.9rem; background: var(--card); border-bottom: 1px solid var(--line); box-shadow: inset 0 -2px 0 var(--entail-soft); }
|
|
365
|
+
.titlebar .mark { width: 15px; height: 15px; color: var(--entail); flex: none; }
|
|
366
|
+
.titlebar h1 { font-size: 0.95rem; margin: 0; font-weight: 600; letter-spacing: 0.02em; }
|
|
367
|
+
.titlebar .sub { color: var(--muted); font-size: 0.76rem; font-family: ${MONO_STACK}; }
|
|
368
|
+
.titlebar .sub a { color: var(--corpus); }
|
|
369
|
+
.titlebar .pickers { margin-left: auto; display: flex; gap: 0.45rem; }
|
|
284
370
|
button { font: inherit; cursor: pointer; }
|
|
285
371
|
button:disabled { cursor: default; opacity: 0.5; }
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
.
|
|
291
|
-
.
|
|
292
|
-
|
|
372
|
+
button:focus-visible, input:focus-visible { outline: 2px solid var(--entail); outline-offset: 1px; }
|
|
373
|
+
.pickers button { background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 4px; padding: 0.3rem 0.65rem; font-size: 0.8rem; }
|
|
374
|
+
.pickers button:hover { border-color: var(--entail); }
|
|
375
|
+
|
|
376
|
+
.workbench { display: grid; grid-template-columns: minmax(230px, 320px) minmax(0, 1fr); min-height: 0; }
|
|
377
|
+
.sidebar { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--line); }
|
|
378
|
+
.panel { display: flex; flex-direction: column; min-height: 0; flex: 1; }
|
|
379
|
+
.panel-head { margin: 0; padding: 0.45rem 0.9rem; flex: none; display: flex; align-items: baseline; gap: 0.45rem; font-family: ${MONO_STACK}; font-size: 0.66rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.09em; color: var(--muted); border-bottom: 1px solid var(--line); }
|
|
380
|
+
.panel-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0.5rem 0.9rem 1rem; }
|
|
381
|
+
.focus-line { font-family: ${MONO_STACK}; font-size: 0.72rem; color: var(--corpus); text-transform: none; letter-spacing: 0; overflow-wrap: anywhere; }
|
|
293
382
|
ul.rows { list-style: none; margin: 0; padding: 0; }
|
|
294
|
-
.row { padding: 0.
|
|
383
|
+
.row { padding: 0.26rem 0.35rem; border-radius: 4px; font-size: 0.88rem; line-height: 1.45; }
|
|
295
384
|
.row-focus { background: var(--corpus-soft); }
|
|
296
385
|
.row.muted, .muted { color: var(--muted); }
|
|
297
|
-
.term { background: none; border: none; padding: 0; color: var(--corpus); font-family: ${MONO_STACK}; font-size: 0.
|
|
386
|
+
.term { background: none; border: none; padding: 0; color: var(--corpus); font-family: ${MONO_STACK}; font-size: 0.8rem; text-align: left; text-decoration: underline; text-decoration-color: var(--line); overflow-wrap: anywhere; }
|
|
298
387
|
.term:hover { text-decoration-color: var(--corpus); }
|
|
299
388
|
.verb { color: var(--muted); }
|
|
300
|
-
|
|
301
|
-
.
|
|
302
|
-
.
|
|
303
|
-
.
|
|
304
|
-
|
|
305
|
-
.turn { font-
|
|
306
|
-
.turn
|
|
307
|
-
.turn-
|
|
308
|
-
.turn-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
.
|
|
389
|
+
|
|
390
|
+
.editor { display: flex; flex-direction: column; min-height: 0; background: var(--card); }
|
|
391
|
+
#chat-log { flex: 1; min-height: 0; overflow-y: auto; padding: 0.9rem 1.1rem; display: flex; flex-direction: column; gap: 0.7rem; }
|
|
392
|
+
.turn { max-width: 46rem; font-size: 0.95rem; line-height: 1.5; }
|
|
393
|
+
.turn .who { display: block; font-family: ${MONO_STACK}; font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); margin-bottom: 0.15rem; }
|
|
394
|
+
.turn-you .said { font-family: ${MONO_STACK}; font-size: 0.84rem; }
|
|
395
|
+
.turn-tmct { border-left: 2px solid var(--entail); padding-left: 0.7rem; }
|
|
396
|
+
.turn-tmct .said { white-space: pre-wrap; }
|
|
397
|
+
.turn-note { color: var(--muted); font-style: italic; font-size: 0.85rem; }
|
|
398
|
+
.suggest { flex: none; border-top: 1px solid var(--line); padding: 0.45rem 1.1rem 0.1rem; }
|
|
399
|
+
.suggest-label { font-family: ${MONO_STACK}; font-size: 0.66rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.09em; color: var(--muted); }
|
|
400
|
+
.hints { display: flex; flex-wrap: wrap; gap: 0.35rem; max-height: 5.6rem; overflow-y: auto; padding: 0.35rem 0 0.45rem; }
|
|
401
|
+
.hint { background: var(--bg); border: 1px solid var(--line); border-radius: 999px; padding: 0.24rem 0.7rem; font-size: 0.8rem; color: var(--ink); text-align: left; }
|
|
402
|
+
.hint:hover { border-color: var(--entail); }
|
|
403
|
+
#chat-form { flex: none; display: flex; gap: 0.5rem; padding: 0.55rem 1.1rem 0.8rem; border-top: 1px solid var(--line); }
|
|
404
|
+
#chat-input { flex: 1; min-width: 0; font: inherit; font-size: 0.92rem; padding: 0.5rem 0.65rem; border: 1px solid var(--line); border-radius: 4px; background: var(--bg); color: var(--ink); }
|
|
405
|
+
#chat-form button { background: var(--entail); color: var(--card); font-weight: 600; border: none; border-radius: 4px; padding: 0.5rem 1rem; font-size: 0.9rem; }
|
|
406
|
+
|
|
407
|
+
.statusbar { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; padding: 0.32rem 0.9rem; font-family: ${MONO_STACK}; font-size: 0.7rem; color: var(--muted); background: var(--entail-soft); border-top: 1px solid var(--entail); }
|
|
408
|
+
#seed-status { margin-left: auto; text-align: right; }
|
|
409
|
+
|
|
410
|
+
@media (max-width: 760px) {
|
|
411
|
+
.workbench { grid-template-columns: 1fr; grid-template-rows: minmax(0, 34%) minmax(0, 1fr); }
|
|
412
|
+
.sidebar { border-right: none; border-bottom: 1px solid var(--line); }
|
|
413
|
+
}
|
|
314
414
|
</style>
|
|
315
415
|
</head>
|
|
316
416
|
<body>
|
|
317
|
-
<
|
|
318
|
-
<
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
<
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
<div id="stats"></div>
|
|
327
|
-
<main>
|
|
328
|
-
<section class="ledger-pane">
|
|
329
|
-
<h2>Facts around <span class="focus-line" id="focus-name">—</span></h2>
|
|
330
|
-
<ul class="rows" id="ledger"></ul>
|
|
331
|
-
</section>
|
|
332
|
-
<aside class="rail">
|
|
333
|
-
<div>
|
|
334
|
-
<h2>Try asking</h2>
|
|
335
|
-
<div class="hints" id="hints"></div>
|
|
417
|
+
<div class="shell">
|
|
418
|
+
<header class="titlebar">
|
|
419
|
+
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 0l1 2.3a5.8 5.8 0 0 1 1.9.8L13.3 2l.7.7-1.1 2.4c.4.6.6 1.2.8 1.9L16 8l-2.3 1a5.8 5.8 0 0 1-.8 1.9l1.1 2.4-.7.7-2.4-1.1a5.8 5.8 0 0 1-1.9.8L8 16l-1-2.3a5.8 5.8 0 0 1-1.9-.8L2.7 14l-.7-.7 1.1-2.4a5.8 5.8 0 0 1-.8-1.9L0 8l2.3-1c.2-.7.4-1.3.8-1.9L2 2.7l.7-.7 2.4 1.1A5.8 5.8 0 0 1 7 2.3L8 0zm0 5.2A2.8 2.8 0 1 0 8 10.8 2.8 2.8 0 0 0 8 5.2z"/></svg>
|
|
420
|
+
<h1>tmct code explorer</h1>
|
|
421
|
+
<span class="sub">source: <span id="source-name">${escapeHtml(sourceName)}</span></span>
|
|
422
|
+
${showDesktopLink ? `<span class="sub">Also available as a <a href="${DESKTOP_APP_URL}">desktop app</a>.</span>` : ""}
|
|
423
|
+
<div class="pickers">
|
|
424
|
+
<button id="open-graph">Open graph…</button>
|
|
425
|
+
<button id="open-repo">Open repo…</button>
|
|
336
426
|
</div>
|
|
337
|
-
|
|
338
|
-
|
|
427
|
+
</header>
|
|
428
|
+
<div class="workbench">
|
|
429
|
+
<aside class="sidebar">
|
|
430
|
+
<section class="panel" data-panel="explorer">
|
|
431
|
+
<h2 class="panel-head">Facts around <span class="focus-line" id="focus-name">—</span></h2>
|
|
432
|
+
<div class="panel-body">
|
|
433
|
+
<ul class="rows" id="ledger"></ul>
|
|
434
|
+
</div>
|
|
435
|
+
</section>
|
|
436
|
+
</aside>
|
|
437
|
+
<section class="editor" data-panel="conversation">
|
|
438
|
+
<h2 class="panel-head">Conversation</h2>
|
|
339
439
|
<div id="chat-log"></div>
|
|
440
|
+
<div class="suggest">
|
|
441
|
+
<span class="suggest-label">Try asking</span>
|
|
442
|
+
<div class="hints" id="hints"></div>
|
|
443
|
+
</div>
|
|
340
444
|
<form id="chat-form">
|
|
341
|
-
<input id="chat-input" type="text" autocomplete="off" placeholder="ask about this graph…">
|
|
445
|
+
<input id="chat-input" type="text" autocomplete="off" placeholder="ask about this graph, or anything it knows…">
|
|
342
446
|
<button type="submit">Ask</button>
|
|
343
447
|
</form>
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
448
|
+
</section>
|
|
449
|
+
</div>
|
|
450
|
+
<footer class="statusbar">
|
|
451
|
+
<span id="stats"></span>
|
|
452
|
+
<span id="dock-note"></span>
|
|
453
|
+
<span id="seed-status"></span>
|
|
454
|
+
</footer>
|
|
455
|
+
</div>
|
|
348
456
|
<script>window.__CODE_EXPLORER__ = Object.assign({ payload: ${payloadJson} }, ${dataJson});</script>
|
|
349
457
|
${winkLoaderInline ? `<script>\n${embedScriptText(winkLoaderInline)}\n</script>` : ""}
|
|
350
458
|
${bundleInline ? `<script>\n${embedScriptText(bundleInline)}\n</script>` : ""}
|
|
@@ -54,7 +54,7 @@ import { tmpdir } from "node:os";
|
|
|
54
54
|
import { basename, join, resolve } from "node:path";
|
|
55
55
|
|
|
56
56
|
import { runTurn, uuidv7 } from "./chat.mjs";
|
|
57
|
-
import { splitSentencesPreservingPaths } from "./sentences.mjs";
|
|
57
|
+
import { splitSentencesPreservingPaths, stripCitationResidue } from "./sentences.mjs";
|
|
58
58
|
import { loadMemory, readFactRows, appendFact } from "../adapters/memory/core.mjs";
|
|
59
59
|
import { loadConfig } from "../adapters/config.mjs";
|
|
60
60
|
import { touchedFactRows } from "../domain/memory/touched-facts.mjs";
|
|
@@ -202,6 +202,63 @@ export function optimisticTriples(sentence, { lexicon = loadLexicon(), nlp } = {
|
|
|
202
202
|
return engine ? optimisticTriplesPos(sentence, lexicon, engine) : optimisticTriplesLexical(sentence, lexicon);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// A closed set of clause markers a compound sentence hinges on. A candidate
|
|
206
|
+
// fragment is only kept when it stands alone as a clause; the marker set is
|
|
207
|
+
// deliberately small so a run-on never shatters into noise.
|
|
208
|
+
const CLAUSE_MARKER_RE = /,?\s+(?:because|since|although|though|whereas|while|so|and|but)\s+/i;
|
|
209
|
+
// The copula/auxiliary/light-verb words that mark a fragment as a would-be
|
|
210
|
+
// clause when no wink POS tagger is on hand — the same closed-list discipline
|
|
211
|
+
// the strict teach frames use in place of a probabilistic tag.
|
|
212
|
+
const CLAUSE_VERBISH = new Set([
|
|
213
|
+
"is", "are", "was", "were", "be", "been", "being", "am",
|
|
214
|
+
"has", "have", "had", "can", "could", "will", "would", "should",
|
|
215
|
+
"do", "does", "did", "want", "wants", "need", "needs", "make", "makes", "made",
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
/** Does the fragment carry a verb-ish token — a real VERB/AUX when wink tags
|
|
219
|
+
* it, else one of the closed copula/aux words above? A clause with no verb is
|
|
220
|
+
* a noun pile, never a sentence to ground. */
|
|
221
|
+
function fragmentHasVerb(fragment, nlp) {
|
|
222
|
+
const engine = nlp === undefined ? winkInstance() : nlp;
|
|
223
|
+
if (engine) {
|
|
224
|
+
try {
|
|
225
|
+
const pos = engine.readDoc(String(fragment)).tokens().out(engine.its.pos);
|
|
226
|
+
if (pos.some((p) => p === "VERB" || p === "AUX")) return true;
|
|
227
|
+
} catch { /* fall through to the closed-list check */ }
|
|
228
|
+
}
|
|
229
|
+
return String(fragment).toLowerCase().split(/\s+/).some((w) => CLAUSE_VERBISH.has(w));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The grounding candidates a sentence offers, WHOLE SENTENCE FIRST: the strict
|
|
234
|
+
* recognizer should always get the full sentence before any fragment, so a
|
|
235
|
+
* clause split only ever adds fallbacks, never pre-empts a clean whole-sentence
|
|
236
|
+
* read. When the sentence hinges on a closed clause marker
|
|
237
|
+
* (because/since/although/though/whereas/while/so/and/but), each side is added
|
|
238
|
+
* as a fallback — but only a fragment of at least 3 tokens that carries a
|
|
239
|
+
* verb-ish token, so a stray connective can't split a sentence into noise.
|
|
240
|
+
* Returns [sentence, ...fragments] with no duplicates.
|
|
241
|
+
*/
|
|
242
|
+
export function clauseCandidates(sentence, { nlp } = {}) {
|
|
243
|
+
const whole = String(sentence ?? "").trim();
|
|
244
|
+
const out = [whole];
|
|
245
|
+
if (!whole) return out;
|
|
246
|
+
const parts = whole.split(CLAUSE_MARKER_RE).map((s) => s.trim()).filter(Boolean);
|
|
247
|
+
if (parts.length <= 1) return out;
|
|
248
|
+
for (const part of parts) {
|
|
249
|
+
if (part === whole || out.includes(part)) continue;
|
|
250
|
+
if (part.split(/\s+/).length < 3) continue;
|
|
251
|
+
if (!fragmentHasVerb(part, nlp)) continue;
|
|
252
|
+
out.push(part);
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// The pronoun subjects a bounded carry substitutes with the paragraph's last
|
|
258
|
+
// grounded subject. Ingest only — a chat turn resolves "it"/"they" against the
|
|
259
|
+
// live focus, never a stale paragraph carry.
|
|
260
|
+
const PRONOUN_LEAD_RE = /^(?:they|it|these|those|this)\b\s*/i;
|
|
261
|
+
|
|
205
262
|
/** A readable predicate for canonical output: the local part of an rdfs:/ace:
|
|
206
263
|
* CURIE, otherwise the predicate verbatim. */
|
|
207
264
|
const readablePredicate = (predicate) => String(predicate).replace(/^[a-z]+:/i, "");
|
|
@@ -256,7 +313,11 @@ export async function ingestText(text, {
|
|
|
256
313
|
memoryDir = null, sourceTag = "text", optimistic = false,
|
|
257
314
|
canonical = false, config = null, lexicon = null,
|
|
258
315
|
} = {}) {
|
|
259
|
-
|
|
316
|
+
// Paragraphs first (blank-line separated), so the pronoun carry never bridges
|
|
317
|
+
// a topic break: a fresh paragraph clears the last-subject it would resolve
|
|
318
|
+
// "they"/"it" against. Each paragraph then splits into sentences the shared
|
|
319
|
+
// path-preserving way.
|
|
320
|
+
const paragraphs = String(text ?? "").split(/\n[ \t]*\n/);
|
|
260
321
|
const ephemeral = !memoryDir;
|
|
261
322
|
const dir = memoryDir || await mkdtemp(join(tmpdir(), "tmct-ingest-"));
|
|
262
323
|
// `dir` may be a backend handle (a sqlite store), not a path — only a real
|
|
@@ -268,46 +329,75 @@ export async function ingestText(text, {
|
|
|
268
329
|
|
|
269
330
|
const extracted = [];
|
|
270
331
|
const optimisticFacts = [];
|
|
332
|
+
let sentenceCount = 0;
|
|
271
333
|
let recognizedSentences = 0;
|
|
272
334
|
let optimisticSentences = 0;
|
|
273
335
|
|
|
336
|
+
// One recognized read of some text form: null when the strict recognizer
|
|
337
|
+
// grounds nothing, else the Fact rows it touched.
|
|
338
|
+
const strictRows = async (form) => {
|
|
339
|
+
const { recognized, rows } = await runSentence(form, { config: cfg, memoryDir: dir });
|
|
340
|
+
return recognized && rows.length ? rows : null;
|
|
341
|
+
};
|
|
342
|
+
|
|
274
343
|
try {
|
|
275
|
-
for (const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
344
|
+
for (const paragraph of paragraphs) {
|
|
345
|
+
// The last unique grounded subject in THIS paragraph, carried onto a
|
|
346
|
+
// later pronoun-led sentence the strict recognizer couldn't ground on
|
|
347
|
+
// its own. Cleared at the paragraph boundary.
|
|
348
|
+
let carrySubject = null;
|
|
349
|
+
for (const sentence of splitSentencesPreservingPaths(paragraph)) {
|
|
350
|
+
sentenceCount += 1;
|
|
351
|
+
const cleaned = stripCitationResidue(sentence);
|
|
352
|
+
// Whole sentence first, then each closed-marker clause as a fallback.
|
|
353
|
+
let rows = null;
|
|
354
|
+
for (const candidate of clauseCandidates(cleaned, { nlp })) {
|
|
355
|
+
rows = await strictRows(candidate);
|
|
356
|
+
if (rows) break;
|
|
357
|
+
}
|
|
358
|
+
// Bounded pronoun carry: a "they/it/these/those/this …" sentence the
|
|
359
|
+
// recognizer skipped is retried once with the paragraph's last grounded
|
|
360
|
+
// subject in the pronoun's place. Never a chat turn — ingest only.
|
|
361
|
+
if (!rows && carrySubject && PRONOUN_LEAD_RE.test(cleaned)) {
|
|
362
|
+
rows = await strictRows(cleaned.replace(PRONOUN_LEAD_RE, `${carrySubject} `));
|
|
363
|
+
}
|
|
364
|
+
if (rows) {
|
|
365
|
+
recognizedSentences += 1;
|
|
366
|
+
const subjects = new Set(rows.map((r) => r.subject));
|
|
367
|
+
if (subjects.size === 1) carrySubject = [...subjects][0];
|
|
368
|
+
const tag = `extracted:${sourceTag}`;
|
|
369
|
+
for (const row of rows) {
|
|
370
|
+
await appendFact(dir, {
|
|
371
|
+
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
372
|
+
provenance: tag, quantifier: row.quantifier || "",
|
|
373
|
+
});
|
|
374
|
+
extracted.push({
|
|
375
|
+
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
376
|
+
provenance: tag, quantifier: row.quantifier || "", sentence,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (!optimistic) continue;
|
|
382
|
+
const candidates = optimisticTriples(cleaned, { lexicon: lex, nlp });
|
|
383
|
+
if (!candidates.length) continue;
|
|
384
|
+
optimisticSentences += 1;
|
|
385
|
+
const tag = `optimistic-extract:${sourceTag}`;
|
|
386
|
+
for (const t of candidates) {
|
|
281
387
|
await appendFact(dir, {
|
|
282
|
-
subject:
|
|
283
|
-
provenance: tag, quantifier: row.quantifier || "",
|
|
284
|
-
});
|
|
285
|
-
extracted.push({
|
|
286
|
-
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
287
|
-
provenance: tag, quantifier: row.quantifier || "", sentence,
|
|
388
|
+
subject: t.subject, predicate: t.predicate, object: t.object, provenance: tag,
|
|
288
389
|
});
|
|
390
|
+
optimisticFacts.push({ ...t, provenance: tag, sentence });
|
|
289
391
|
}
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (!optimistic) continue;
|
|
293
|
-
const candidates = optimisticTriples(sentence, { lexicon: lex, nlp });
|
|
294
|
-
if (!candidates.length) continue;
|
|
295
|
-
optimisticSentences += 1;
|
|
296
|
-
const tag = `optimistic-extract:${sourceTag}`;
|
|
297
|
-
for (const t of candidates) {
|
|
298
|
-
await appendFact(dir, {
|
|
299
|
-
subject: t.subject, predicate: t.predicate, object: t.object, provenance: tag,
|
|
300
|
-
});
|
|
301
|
-
optimisticFacts.push({ ...t, provenance: tag, sentence });
|
|
302
392
|
}
|
|
303
393
|
}
|
|
304
394
|
|
|
305
395
|
const result = {
|
|
306
|
-
sentences:
|
|
396
|
+
sentences: sentenceCount,
|
|
307
397
|
recognized: recognizedSentences,
|
|
308
398
|
extracted,
|
|
309
399
|
optimistic: optimisticFacts,
|
|
310
|
-
skipped:
|
|
400
|
+
skipped: sentenceCount - recognizedSentences - optimisticSentences,
|
|
311
401
|
};
|
|
312
402
|
if (canonical) {
|
|
313
403
|
result.canonical = canonicalLines([...extracted, ...optimisticFacts], readFactRows(await loadMemory(dir)));
|