@polycode-projects/the-mechanical-code-talker 4.1.1 → 4.1.2
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 +31 -18
- package/bin/tmct.mjs +3 -0
- package/data/templates/responses.jsonl +3 -0
- package/package.json +2 -1
- package/src/adapters/memory/core.mjs +1358 -196
- package/src/adapters/memory/inspect.mjs +11 -0
- package/src/adapters/memory/shacl.mjs +38 -0
- package/src/adapters/p2p/webrtc-transport.mjs +28 -5
- package/src/domain/ask-vocab.mjs +39 -0
- package/src/domain/ask.mjs +183 -34
- package/src/domain/grammar/assert.mjs +8 -2
- package/src/domain/hanoi-board.mjs +232 -0
- package/src/domain/ingest-facts.mjs +120 -0
- package/src/domain/interpret/normalize.mjs +49 -0
- package/src/domain/memory/compaction.mjs +284 -0
- package/src/domain/memory/resolution.mjs +171 -0
- package/src/domain/memory/trust.mjs +175 -5
- package/src/domain/memory-facts.mjs +139 -0
- package/src/domain/p2p/facts.mjs +21 -0
- package/src/domain/p2p/peer-id.mjs +15 -0
- package/src/domain/p2p/provenance-relabel.mjs +13 -2
- package/src/domain/p2p/sync-filter.mjs +5 -1
- package/src/domain/p2p/wire.mjs +7 -4
- package/src/domain/scene-compose.mjs +2 -2
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/services/adventure-viz.mjs +5 -1
- package/src/services/adventure.mjs +70 -44
- package/src/services/chat-page-viz.mjs +381 -310
- package/src/services/chat.mjs +273 -155
- package/src/services/code-explorer-viz.mjs +141 -54
- package/src/services/index.mjs +1 -1
- package/src/services/ingest-viz.mjs +134 -9
- package/src/services/ledger-viz.mjs +7 -4
- package/src/services/memory-panel-viz.mjs +8 -3
- package/src/services/mud-turn.mjs +11 -8
- package/src/services/mud-viz.mjs +441 -206
- package/src/services/p2p-room.mjs +110 -23
- package/src/services/plan-viz.mjs +63 -4
- package/src/services/research-viz.mjs +18 -7
- package/src/services/share-overlay-viz.mjs +623 -0
- package/src/services/spider-fly-viz.mjs +2 -2
- package/src/services/sprite-catalog-viz.mjs +303 -78
- package/src/surfaces/web/adventure-browser-entry.mjs +27 -5
- package/src/surfaces/web/chat-browser-entry.mjs +37 -10
- package/src/surfaces/web/code-explorer-browser-entry.mjs +4 -3
- package/src/surfaces/web/ingest-browser-entry.mjs +73 -12
- package/src/surfaces/web/ledger-browser-entry.mjs +32 -7
- package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
- package/src/surfaces/web/mud-browser-entry.mjs +38 -7
- package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
- package/src/surfaces/web/plan-browser-entry.mjs +33 -2
- package/src/surfaces/web/research-browser-entry.mjs +11 -19
- package/src/surfaces/web/sprites-browser-entry.mjs +39 -8
- package/src/surfaces/web/tmct-surface.mjs +12 -0
- package/src/surfaces/web/turn-session.mjs +10 -3
|
@@ -107,6 +107,107 @@ export function computeCodeExplorerData(payload, opts = {}) {
|
|
|
107
107
|
return { payload, ledger, hints, focus: ledger.focus || focus, meta: { title: opts.title || "code graph" } };
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// ---- client script pieces, spliced into CLIENT_JS below -------------------
|
|
111
|
+
// Named module functions so the page's own source reads as a list of calls
|
|
112
|
+
// into real, unit-tested code rather than an inlined block. Each is written
|
|
113
|
+
// to survive a `.toString()` round trip into a browser `<script>` tag: no
|
|
114
|
+
// closure over this module's own scope, only its parameters and whichever
|
|
115
|
+
// sibling below it is also spliced into the same page script.
|
|
116
|
+
|
|
117
|
+
/** One line naming the loaded graph's own size: individual/edge counts, then
|
|
118
|
+
* up to four of its most common classes. */
|
|
119
|
+
export function statsSummaryLine(ledgerStats) {
|
|
120
|
+
var parts = [ledgerStats.individuals + " individuals", ledgerStats.edges + " edges"];
|
|
121
|
+
var cls = ledgerStats.classes.slice(0, 4).map(function (c) { return c[1] + " " + c[0]; });
|
|
122
|
+
return parts.concat(cls).join(" · ");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Every fact the chat can draw on: the graph's own edges plus whatever the
|
|
126
|
+
* general-knowledge seed has counted so far. */
|
|
127
|
+
export function factTotalText(edges, seedFacts) {
|
|
128
|
+
return (edges + seedFacts).toLocaleString();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function rowKey(row) { return JSON.stringify([row.s, row.kind, row.o]); }
|
|
132
|
+
|
|
133
|
+
/** The sidebar's rows: the engine's own answer about the focus when it
|
|
134
|
+
* grounded one, the row list's plain filter otherwise, then whatever
|
|
135
|
+
* neither named — a fold over the rest of the graph. */
|
|
136
|
+
export function focusRowsHtml(data, related) {
|
|
137
|
+
var focus = data.focus;
|
|
138
|
+
var rows = data.ledger.rows;
|
|
139
|
+
var grounded = Boolean(related && related.grounded);
|
|
140
|
+
var near = grounded ? related.rows : rows.filter(function (r) { return r.s === focus || r.o === focus; });
|
|
141
|
+
var nearKeys = {};
|
|
142
|
+
near.forEach(function (r) { nearKeys[rowKey(r)] = true; });
|
|
143
|
+
var rest = rows.filter(function (r) { return !nearKeys[rowKey(r)]; });
|
|
144
|
+
var ordered = near.concat(rest);
|
|
145
|
+
return ordered.map(function (r) {
|
|
146
|
+
var hot = (r.s === focus || r.o === focus) ? " row-focus" : "";
|
|
147
|
+
return '<li class="row' + hot + '">'
|
|
148
|
+
+ '<button class="term" data-term="' + escapeHtml(r.s) + '">' + escapeHtml(r.s) + '</button> '
|
|
149
|
+
+ '<span class="verb">' + escapeHtml(r.phrase) + '</span> '
|
|
150
|
+
+ '<button class="term" data-term="' + escapeHtml(r.o) + '">' + escapeHtml(r.o) + '</button>'
|
|
151
|
+
+ '</li>';
|
|
152
|
+
}).join("") || (grounded
|
|
153
|
+
? '<li class="row muted">nothing in this graph relates to ' + escapeHtml(focus) + '.</li>'
|
|
154
|
+
: '<li class="row muted">no edges in this graph.</li>');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function hintsHtml(hints) {
|
|
158
|
+
return hints.map(function (h) {
|
|
159
|
+
return '<button class="hint" data-q="' + escapeHtml(h.text) + '" title="' + escapeHtml(h.rationale) + '">'
|
|
160
|
+
+ escapeHtml(h.text) + '</button>';
|
|
161
|
+
}).join("") || '<span class="muted">nothing to suggest for this graph.</span>';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function turnHtml(role, text) {
|
|
165
|
+
return '<span class="who">' + (role === "you" ? "you" : "tmct") + '</span><span class="said">' + escapeHtml(text) + '</span>';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function chatEmptyStateHtml(hasEngine) {
|
|
169
|
+
return hasEngine
|
|
170
|
+
? '<p class="chat-empty-head">Ask the graph, or ask it anything</p>'
|
|
171
|
+
+ '<p>Ask about the code graph on the left, or about anything its general knowledge covers, like “what is a queue”.</p>'
|
|
172
|
+
+ '<p class="chat-empty-hint">Try one of the questions below, or type your own.</p>'
|
|
173
|
+
: '<p class="chat-empty-head">Static view</p>'
|
|
174
|
+
+ '<p>This page shows a fixed snapshot of the graph. The live chat is not available here.</p>';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function mbText(n) { return (n / 1048576).toFixed(1); }
|
|
178
|
+
|
|
179
|
+
/** How many of the seed's own individuals are facts — the general-knowledge
|
|
180
|
+
* half of the fact-total pill. */
|
|
181
|
+
export function seedFactsFromPayload(payload) {
|
|
182
|
+
return (payload.individuals || []).filter(function (i) { return i.class === "Fact"; }).length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function seedLoadingNote(loadedBytes, totalBytes) {
|
|
186
|
+
return "loading general knowledge… " + mbText(loadedBytes) + (totalBytes ? " of " + mbText(totalBytes) : "") + " MB";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** One retry with a cache-busting query param, fetch side only: a CDN edge
|
|
190
|
+
* can serve a corrupted or truncated precompressed response (a transient
|
|
191
|
+
* bad cache entry, not a code defect — real bytes decompress fine, and the
|
|
192
|
+
* same URL fetched moments later is clean), and JSON.parse throwing is the
|
|
193
|
+
* only signal of that. `fetcher` is `fetchWithProgress` (or a stand-in for
|
|
194
|
+
* a test); `baseUrl` already carries the build's own content-hash query
|
|
195
|
+
* string, if any, so the bust param joins with `&` or `?` accordingly. */
|
|
196
|
+
export async function fetchSeedPayloadWithRetry(fetcher, baseUrl, seedQuery, onProgress) {
|
|
197
|
+
var lastErr = null;
|
|
198
|
+
for (var attempt = 1; attempt <= 2; attempt++) {
|
|
199
|
+
try {
|
|
200
|
+
var bust = attempt === 1 ? "" : (seedQuery ? "&" : "?") + "retry=1";
|
|
201
|
+
var seedBlob = await fetcher(baseUrl + seedQuery + bust, onProgress);
|
|
202
|
+
var text = await seedBlob.text();
|
|
203
|
+
return JSON.parse(text); // throwing here is the corrupted/truncated-response retry signal
|
|
204
|
+
} catch (e) {
|
|
205
|
+
lastErr = e;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw lastErr;
|
|
209
|
+
}
|
|
210
|
+
|
|
110
211
|
const CLIENT_JS = String.raw`
|
|
111
212
|
(function () {
|
|
112
213
|
var DATA = window.__CODE_EXPLORER__;
|
|
@@ -126,13 +227,17 @@ const CLIENT_JS = String.raw`
|
|
|
126
227
|
};
|
|
127
228
|
var session = null;
|
|
128
229
|
|
|
129
|
-
var
|
|
230
|
+
var escapeHtml = ${escapeHtml.toString()};
|
|
231
|
+
var rowKey = ${rowKey.toString()};
|
|
232
|
+
var statsSummaryLine = ${statsSummaryLine.toString()};
|
|
233
|
+
var factTotalText = ${factTotalText.toString()};
|
|
234
|
+
var focusRowsHtml = ${focusRowsHtml.toString()};
|
|
235
|
+
var hintsHtml = ${hintsHtml.toString()};
|
|
236
|
+
var turnHtml = ${turnHtml.toString()};
|
|
237
|
+
var chatEmptyStateHtml = ${chatEmptyStateHtml.toString()};
|
|
130
238
|
|
|
131
239
|
function renderStats(data) {
|
|
132
|
-
|
|
133
|
-
var parts = [s.individuals + " individuals", s.edges + " edges"];
|
|
134
|
-
var cls = s.classes.slice(0, 4).map(function (c) { return c[1] + " " + c[0]; });
|
|
135
|
-
els.stats.textContent = parts.concat(cls).join(" · ");
|
|
240
|
+
els.stats.textContent = statsSummaryLine(data.ledger.stats);
|
|
136
241
|
renderFactTotal(data);
|
|
137
242
|
}
|
|
138
243
|
|
|
@@ -141,7 +246,7 @@ const CLIENT_JS = String.raw`
|
|
|
141
246
|
function renderFactTotal(data) {
|
|
142
247
|
if (!els.factTotal) return;
|
|
143
248
|
var edges = (data && data.ledger && data.ledger.stats && data.ledger.stats.edges) || 0;
|
|
144
|
-
els.factTotal.textContent = (edges
|
|
249
|
+
els.factTotal.textContent = factTotalText(edges, seedState.facts);
|
|
145
250
|
}
|
|
146
251
|
|
|
147
252
|
// "What relates to the focus", put to the engine. askRelatedFacts asks
|
|
@@ -159,41 +264,19 @@ const CLIENT_JS = String.raw`
|
|
|
159
264
|
}
|
|
160
265
|
}
|
|
161
266
|
|
|
162
|
-
|
|
163
|
-
|
|
267
|
+
// The engine's answer whenever it grounded one. The row list's own split is
|
|
268
|
+
// what is left when there is no engine (the static page) or when every
|
|
269
|
+
// question came back parsed as something else — an identifier that is
|
|
270
|
+
// itself a relation verb reads as a question about the verb. Whatever the
|
|
271
|
+
// neighbourhood did not already name follows, in degree order: a bulk view
|
|
272
|
+
// of the rest of the graph, which is a fold and not a question.
|
|
164
273
|
function renderFocusRows(data, related) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// The engine's answer whenever it grounded one. The row list's own split is
|
|
168
|
-
// what is left when there is no engine (the static page) or when every
|
|
169
|
-
// question came back parsed as something else — an identifier that is
|
|
170
|
-
// itself a relation verb reads as a question about the verb.
|
|
171
|
-
var grounded = Boolean(related && related.grounded);
|
|
172
|
-
var near = grounded ? related.rows : rows.filter(function (r) { return r.s === focus || r.o === focus; });
|
|
173
|
-
var nearKeys = {};
|
|
174
|
-
near.forEach(function (r) { nearKeys[rowKey(r)] = true; });
|
|
175
|
-
// Whatever the neighbourhood did not already name, in degree order: a bulk
|
|
176
|
-
// view of the rest of the graph, which is a fold and not a question.
|
|
177
|
-
var rest = rows.filter(function (r) { return !nearKeys[rowKey(r)]; });
|
|
178
|
-
var ordered = near.concat(rest);
|
|
179
|
-
els.ledger.innerHTML = ordered.map(function (r) {
|
|
180
|
-
var hot = (r.s === focus || r.o === focus) ? " row-focus" : "";
|
|
181
|
-
return '<li class="row' + hot + '">'
|
|
182
|
-
+ '<button class="term" data-term="' + esc(r.s) + '">' + esc(r.s) + '</button> '
|
|
183
|
-
+ '<span class="verb">' + esc(r.phrase) + '</span> '
|
|
184
|
-
+ '<button class="term" data-term="' + esc(r.o) + '">' + esc(r.o) + '</button>'
|
|
185
|
-
+ '</li>';
|
|
186
|
-
}).join("") || (grounded
|
|
187
|
-
? '<li class="row muted">nothing in this graph relates to ' + esc(focus) + '.</li>'
|
|
188
|
-
: '<li class="row muted">no edges in this graph.</li>');
|
|
189
|
-
els.focus.textContent = focus || "—";
|
|
274
|
+
els.ledger.innerHTML = focusRowsHtml(data, related);
|
|
275
|
+
els.focus.textContent = data.focus || "—";
|
|
190
276
|
}
|
|
191
277
|
|
|
192
278
|
function renderHints(data) {
|
|
193
|
-
els.hints.innerHTML = data.hints
|
|
194
|
-
return '<button class="hint" data-q="' + esc(h.text) + '" title="' + esc(h.rationale) + '">'
|
|
195
|
-
+ esc(h.text) + '</button>';
|
|
196
|
-
}).join("") || '<span class="muted">nothing to suggest for this graph.</span>';
|
|
279
|
+
els.hints.innerHTML = hintsHtml(data.hints);
|
|
197
280
|
}
|
|
198
281
|
|
|
199
282
|
function focusOn(term) {
|
|
@@ -218,7 +301,7 @@ const CLIENT_JS = String.raw`
|
|
|
218
301
|
clearEmptyState();
|
|
219
302
|
var div = document.createElement("div");
|
|
220
303
|
div.className = "turn turn-" + role;
|
|
221
|
-
div.innerHTML =
|
|
304
|
+
div.innerHTML = turnHtml(role, text);
|
|
222
305
|
els.log.appendChild(div);
|
|
223
306
|
els.log.scrollTop = els.log.scrollHeight;
|
|
224
307
|
}
|
|
@@ -227,12 +310,7 @@ const CLIENT_JS = String.raw`
|
|
|
227
310
|
var div = document.createElement("div");
|
|
228
311
|
div.className = "chat-empty";
|
|
229
312
|
div.id = "chat-empty";
|
|
230
|
-
div.innerHTML = api
|
|
231
|
-
? '<p class="chat-empty-head">Ask the graph, or ask it anything</p>'
|
|
232
|
-
+ '<p>Ask about the code graph on the left, or about anything its general knowledge covers, like “what is a queue”.</p>'
|
|
233
|
-
+ '<p class="chat-empty-hint">Try one of the questions below, or type your own.</p>'
|
|
234
|
-
: '<p class="chat-empty-head">Static view</p>'
|
|
235
|
-
+ '<p>This page shows a fixed snapshot of the graph. The live chat is not available here.</p>';
|
|
313
|
+
div.innerHTML = chatEmptyStateHtml(Boolean(api));
|
|
236
314
|
els.log.appendChild(div);
|
|
237
315
|
}
|
|
238
316
|
|
|
@@ -248,25 +326,34 @@ const CLIENT_JS = String.raw`
|
|
|
248
326
|
var SEED_QUERY = DATA.seedStamp ? "?b=" + DATA.seedStamp : "";
|
|
249
327
|
var seedState = { status: api ? "loading" : "absent", payload: null, facts: 0 };
|
|
250
328
|
function seedNote(text) { if (els.seedStatus) els.seedStatus.textContent = text; }
|
|
251
|
-
function mbText(n) { return (n / 1048576).toFixed(1); }
|
|
252
329
|
|
|
330
|
+
var mbText = ${mbText.toString()};
|
|
253
331
|
var fetchWithProgress = ${fetchWithProgress.toString()};
|
|
332
|
+
var seedFactsFromPayload = ${seedFactsFromPayload.toString()};
|
|
333
|
+
var seedLoadingNote = ${seedLoadingNote.toString()};
|
|
334
|
+
var fetchSeedPayloadWithRetry = ${fetchSeedPayloadWithRetry.toString()};
|
|
335
|
+
|
|
336
|
+
// The desktop shell's readSeed() reads off local disk, not a CDN, so it
|
|
337
|
+
// keeps its single attempt in loadSeed() below; only this fetch path retries.
|
|
338
|
+
function fetchSeedPayload() {
|
|
339
|
+
return fetchSeedPayloadWithRetry(fetchWithProgress, "./chat-seed.json", SEED_QUERY, function (loaded, total) {
|
|
340
|
+
seedNote(seedLoadingNote(loaded, total));
|
|
341
|
+
});
|
|
342
|
+
}
|
|
254
343
|
|
|
255
344
|
async function loadSeed() {
|
|
256
345
|
try {
|
|
257
|
-
var
|
|
346
|
+
var payload = null;
|
|
258
347
|
if (window.tmctDesktop && typeof window.tmctDesktop.readSeed === "function") {
|
|
259
348
|
seedNote("loading general knowledge…");
|
|
260
|
-
text = await window.tmctDesktop.readSeed();
|
|
349
|
+
var text = await window.tmctDesktop.readSeed();
|
|
350
|
+
if (text) payload = JSON.parse(text);
|
|
261
351
|
} else {
|
|
262
|
-
|
|
263
|
-
seedNote("loading general knowledge… " + mbText(loaded) + (total ? " of " + mbText(total) : "") + " MB");
|
|
264
|
-
});
|
|
265
|
-
text = await seedBlob.text();
|
|
352
|
+
payload = await fetchSeedPayload();
|
|
266
353
|
}
|
|
267
|
-
if (
|
|
268
|
-
seedState.payload =
|
|
269
|
-
seedState.facts = (seedState.payload
|
|
354
|
+
if (payload) {
|
|
355
|
+
seedState.payload = payload;
|
|
356
|
+
seedState.facts = seedFactsFromPayload(seedState.payload);
|
|
270
357
|
seedState.status = "ready";
|
|
271
358
|
seedNote("general knowledge: " + seedState.facts + " facts");
|
|
272
359
|
renderFactTotal(DATA);
|
package/src/services/index.mjs
CHANGED
|
@@ -39,7 +39,7 @@ export { relationKind, impactClosure } from "../domain/codegraph.mjs";
|
|
|
39
39
|
export { createGraphService } from "../adapters/providers/graph-service.mjs";
|
|
40
40
|
|
|
41
41
|
// Tool dispatch (slash-commands and CLI tool calls route through here).
|
|
42
|
-
export { dispatchTool } from "../tools/server.mjs";
|
|
42
|
+
export { dispatchTool, dispatchToolStructured } from "../tools/server.mjs";
|
|
43
43
|
|
|
44
44
|
// Conversational memory — tmct's OWN OWL-labelled graph under
|
|
45
45
|
// .tmct/memory/, distinct from any provider-supplied code graph.
|
|
@@ -113,7 +113,8 @@ ${THEME_TOKENS_CSS}
|
|
|
113
113
|
.browse { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: .16rem .55rem; background: var(--card); }
|
|
114
114
|
.browse:hover { color: var(--ink); }
|
|
115
115
|
|
|
116
|
-
/* right: the canonical facts, on the soft panel background
|
|
116
|
+
/* right: the canonical facts, on the soft panel background, with the ask
|
|
117
|
+
dock pinned under them — you read what landed, then question it in place. */
|
|
117
118
|
.outPane { background: var(--card); }
|
|
118
119
|
#facts { flex: 1 1 auto; overflow-y: auto; padding: .6rem .9rem 1rem; font-family: ${MONO_STACK}; font-size: .8rem; }
|
|
119
120
|
#facts .fact { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: baseline; padding: .3rem 0; border-bottom: 1px solid var(--line); }
|
|
@@ -123,6 +124,24 @@ ${THEME_TOKENS_CSS}
|
|
|
123
124
|
#facts .fact .prov { grid-column: 1 / -1; color: var(--muted); font-size: .66rem; }
|
|
124
125
|
#facts .empty { color: var(--muted); text-align: center; max-width: 24rem; line-height: 1.6; margin: 3rem auto 0; }
|
|
125
126
|
|
|
127
|
+
/* the ask dock: one line in, one answer out, against the graph this session
|
|
128
|
+
projects from what it has ingested. */
|
|
129
|
+
.askDock { flex: 0 0 auto; border-top: 1px solid var(--line); display: flex; flex-direction: column; }
|
|
130
|
+
#askLog { max-height: 11rem; overflow-y: auto; padding: .5rem .9rem 0; font-family: ${MONO_STACK}; font-size: .78rem; }
|
|
131
|
+
#askLog:empty { display: none; }
|
|
132
|
+
.askLine { margin: 0 0 .35rem; white-space: pre-wrap; word-break: break-word; }
|
|
133
|
+
.askLine.q { color: var(--muted); }
|
|
134
|
+
.askLine.q::before { content: "> "; }
|
|
135
|
+
.askLine.a { color: var(--ink); }
|
|
136
|
+
.askLine.detail { color: var(--muted); font-size: .68rem; }
|
|
137
|
+
.askLine.miss { color: var(--muted); font-style: italic; }
|
|
138
|
+
.askRow { display: flex; align-items: center; gap: .5rem; padding: .5rem .9rem .6rem; }
|
|
139
|
+
#askq { flex: 1 1 auto; min-width: 0; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); font-family: ${MONO_STACK}; font-size: .78rem; padding: .35rem .6rem; }
|
|
140
|
+
#askq::placeholder { color: var(--muted); }
|
|
141
|
+
#askq:disabled { opacity: .55; }
|
|
142
|
+
.askGo { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .32rem .85rem; background: var(--card); }
|
|
143
|
+
.askGo:disabled { opacity: .45; cursor: default; }
|
|
144
|
+
|
|
126
145
|
.actions { flex: 0 0 auto; display: flex; align-items: center; gap: .6rem; padding: .6rem 1.1rem; border-top: 1px solid var(--line); flex-wrap: wrap; }
|
|
127
146
|
.actions .btn { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .35rem .9rem; background: var(--card); }
|
|
128
147
|
.actions .btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
@@ -203,6 +222,14 @@ ${THEME_TOKENS_CSS}
|
|
|
203
222
|
<span id="factCount" class="mono"></span>
|
|
204
223
|
</div>
|
|
205
224
|
<div id="facts"><p class="empty">Nothing ingested yet. The facts it grounds will appear here as it reads.</p></div>
|
|
225
|
+
<form class="askDock" id="askForm" autocomplete="off">
|
|
226
|
+
<div id="askLog" aria-live="polite"></div>
|
|
227
|
+
<div class="askRow">
|
|
228
|
+
<input type="text" id="askq" aria-label="Ask about what you have ingested"
|
|
229
|
+
placeholder="ask about what you’ve ingested…" disabled>
|
|
230
|
+
<button type="submit" class="askGo" id="askGo" disabled>ask</button>
|
|
231
|
+
</div>
|
|
232
|
+
</form>
|
|
206
233
|
</section>
|
|
207
234
|
</main>
|
|
208
235
|
<div class="actions">
|
|
@@ -248,6 +275,10 @@ ${THEME_TOKENS_CSS}
|
|
|
248
275
|
const fuzzyToggleEl = el("fuzzyToggle");
|
|
249
276
|
const statsPanelEl = el("statsPanel");
|
|
250
277
|
const factPillValueEl = el("factPillValue");
|
|
278
|
+
const askFormEl = el("askForm");
|
|
279
|
+
const askInputEl = el("askq");
|
|
280
|
+
const askGoBtn = el("askGo");
|
|
281
|
+
const askLogEl = el("askLog");
|
|
251
282
|
|
|
252
283
|
let session = null;
|
|
253
284
|
let grounded = 0; // facts on show in the right pane, from the CURRENT ingest only
|
|
@@ -356,6 +387,81 @@ ${THEME_TOKENS_CSS}
|
|
|
356
387
|
factsEl.scrollTop = factsEl.scrollHeight;
|
|
357
388
|
}
|
|
358
389
|
|
|
390
|
+
// ---- the ask dock: one question, put to what this session has ingested ---
|
|
391
|
+
// Two real routes, tried in order, and neither one guesses. window.tmct.ask
|
|
392
|
+
// puts the question to the graph this session projects from its OWN grounded
|
|
393
|
+
// rows (ingest-facts.mjs), which is what answers a listing — "list dogs"
|
|
394
|
+
// reads the beagles actually on record. On a miss, window.tmct.turn runs the
|
|
395
|
+
// full chat turn engine over the same store, which reads a term's own facts
|
|
396
|
+
// back ("what is a beagle") and takes a taught line ("remember: …") the
|
|
397
|
+
// graph shape has no lane for. Both missing is the honest wall, and the note
|
|
398
|
+
// there names the kinds this memory really holds instead of inventing an
|
|
399
|
+
// example question.
|
|
400
|
+
let asking = false;
|
|
401
|
+
|
|
402
|
+
function updateAskEnabled() {
|
|
403
|
+
const ready = Boolean(session) && !asking;
|
|
404
|
+
askInputEl.disabled = !ready;
|
|
405
|
+
askGoBtn.disabled = !ready || !askInputEl.value.trim();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function addAskLine(cls, text) {
|
|
409
|
+
const line = document.createElement("div");
|
|
410
|
+
line.className = "askLine " + cls;
|
|
411
|
+
line.textContent = text;
|
|
412
|
+
askLogEl.appendChild(line);
|
|
413
|
+
askLogEl.scrollTop = askLogEl.scrollHeight;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// The engine writes its answer first and its reasoning trailers ("Goal
|
|
417
|
+
// (inferred): …", "Canonical: …") after a blank line. The answer leads; the
|
|
418
|
+
// trailers stay, quieter, because they are how a reader audits it.
|
|
419
|
+
function addAskAnswer(answer) {
|
|
420
|
+
const blocks = String(answer).split(/\\n{2,}/).map(function (b) { return b.trim(); }).filter(Boolean);
|
|
421
|
+
if (!blocks.length) return;
|
|
422
|
+
addAskLine("a", blocks[0]);
|
|
423
|
+
for (const block of blocks.slice(1)) addAskLine("detail", block);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function askMissNote() {
|
|
427
|
+
const kinds = session && session.askableClasses ? session.askableClasses() : [];
|
|
428
|
+
if (!kinds.length) return "Nothing of your own is in this session's memory yet. Ingest some text first.";
|
|
429
|
+
return "I can't ground that in what you've ingested. The kinds it holds: " + kinds.slice(0, 8).join(", ") + ".";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
askInputEl.addEventListener("input", updateAskEnabled);
|
|
433
|
+
askFormEl.addEventListener("submit", async (e) => {
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
const q = askInputEl.value.trim();
|
|
436
|
+
if (!q || asking || !session) return;
|
|
437
|
+
asking = true;
|
|
438
|
+
askInputEl.value = "";
|
|
439
|
+
updateAskEnabled();
|
|
440
|
+
addAskLine("q", q);
|
|
441
|
+
try {
|
|
442
|
+
let asked = null;
|
|
443
|
+
try { asked = await window.tmct.ask(q); } catch { asked = null; }
|
|
444
|
+
if (asked && asked.answer && !asked.miss) { addAskAnswer(asked.answer); return; }
|
|
445
|
+
let turned = null;
|
|
446
|
+
try { turned = await window.tmct.turn(q); } catch { turned = null; }
|
|
447
|
+
if (turned && turned.answer && !(turned.record && turned.record.miss)) {
|
|
448
|
+
addAskAnswer(turned.answer);
|
|
449
|
+
// A taught line writes to the same store an ingest does, so it earns
|
|
450
|
+
// the same debounced save and the same panel refresh.
|
|
451
|
+
if (turned.record && turned.record.via === "assert") {
|
|
452
|
+
scheduleSave();
|
|
453
|
+
await renderStatsPanel();
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
addAskLine("miss", askMissNote());
|
|
458
|
+
} finally {
|
|
459
|
+
asking = false;
|
|
460
|
+
updateAskEnabled();
|
|
461
|
+
askInputEl.focus();
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
359
465
|
// ---- memory stats: the docked panel, same convention as chat.html --------
|
|
360
466
|
async function renderStatsPanel(stats) {
|
|
361
467
|
if (!stats) {
|
|
@@ -402,16 +508,28 @@ ${THEME_TOKENS_CSS}
|
|
|
402
508
|
// The one branch this page's seed choice makes: checked, fetch and parse
|
|
403
509
|
// the same chat-seed.json chat.html embeds; unchecked, skip the request
|
|
404
510
|
// outright and stay on the previous empty-store fast path.
|
|
511
|
+
//
|
|
512
|
+
// One retry with a cache-busting query param: a CDN edge can serve a
|
|
513
|
+
// corrupted or truncated precompressed response (a transient bad cache
|
|
514
|
+
// entry, not a code defect — real bytes decompress fine, and the same URL
|
|
515
|
+
// fetched moments later is clean), and JSON.parse throwing is the only
|
|
516
|
+
// signal of that. The bust param forces a fresh fetch past that one entry.
|
|
405
517
|
async function fetchSeedIfWanted() {
|
|
406
518
|
if (!seedToggleEl.checked) { seedPayload = null; seedFacts = 0; return; }
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
519
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
520
|
+
try {
|
|
521
|
+
const bust = attempt === 1 ? "" : (SEED_QUERY ? "&" : "?") + "retry=1";
|
|
522
|
+
const blob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY + bust, (loaded, total) => noteProgress("seed", loaded, total));
|
|
523
|
+
seedPayload = JSON.parse(await blob.text());
|
|
524
|
+
seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
|
|
525
|
+
return;
|
|
526
|
+
} catch (err) {
|
|
527
|
+
if (attempt === 2) {
|
|
528
|
+
seedPayload = null;
|
|
529
|
+
seedFacts = 0;
|
|
530
|
+
console.warn("tmct ingest: chat-seed.json unavailable — starting unseeded", err);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
415
533
|
}
|
|
416
534
|
}
|
|
417
535
|
const cloneMemoryPayload = ${cloneMemoryPayload.toString()};
|
|
@@ -469,7 +587,9 @@ ${THEME_TOKENS_CSS}
|
|
|
469
587
|
if (persist) await persist.clear();
|
|
470
588
|
session = await newSession();
|
|
471
589
|
clearFactsPane();
|
|
590
|
+
askLogEl.textContent = "";
|
|
472
591
|
updateIngestEnabled();
|
|
592
|
+
updateAskEnabled();
|
|
473
593
|
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
474
594
|
statusEl.textContent = "forgot everything taught on this device. Back to the fresh seed (" + statsSummaryLine(stats, bandLabelFor) + ").";
|
|
475
595
|
await renderStatsPanel(stats);
|
|
@@ -488,10 +608,12 @@ ${THEME_TOKENS_CSS}
|
|
|
488
608
|
saveTimer = null;
|
|
489
609
|
session = await newSession();
|
|
490
610
|
clearFactsPane();
|
|
611
|
+
askLogEl.textContent = "";
|
|
491
612
|
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
492
613
|
statusEl.textContent = statsSummaryLine(stats, bandLabelFor) + ". Ready.";
|
|
493
614
|
await renderStatsPanel(stats);
|
|
494
615
|
updateIngestEnabled();
|
|
616
|
+
updateAskEnabled();
|
|
495
617
|
sourceEl.focus();
|
|
496
618
|
});
|
|
497
619
|
|
|
@@ -516,6 +638,7 @@ ${THEME_TOKENS_CSS}
|
|
|
516
638
|
: await newSession();
|
|
517
639
|
setMode(false);
|
|
518
640
|
updateIngestEnabled();
|
|
641
|
+
updateAskEnabled();
|
|
519
642
|
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
520
643
|
const winkPart = winkStatus === "loaded"
|
|
521
644
|
? "wink-nlp: loaded"
|
|
@@ -609,6 +732,7 @@ ${THEME_TOKENS_CSS}
|
|
|
609
732
|
sourceTag = "pasted text";
|
|
610
733
|
srcLabel.textContent = modeDocBtn.getAttribute("aria-pressed") === "true" ? "drop or browse for a file" : "pasted text";
|
|
611
734
|
clearFactsPane();
|
|
735
|
+
askLogEl.textContent = "";
|
|
612
736
|
statusEl.textContent = "cleared";
|
|
613
737
|
updateIngestEnabled();
|
|
614
738
|
sourceEl.focus();
|
|
@@ -618,6 +742,7 @@ ${THEME_TOKENS_CSS}
|
|
|
618
742
|
console.error("tmct ingest failed to boot", err);
|
|
619
743
|
statusEl.textContent = "the ingest page failed to start (" + (err && err.message ? err.message : err) + ")";
|
|
620
744
|
});
|
|
745
|
+
window.tmct.ready = window.tmctIngestReady;
|
|
621
746
|
})();
|
|
622
747
|
</script>
|
|
623
748
|
</body>
|
|
@@ -145,13 +145,16 @@ const trustTierFor = (trust) => (trust >= 0.85 ? 3 : trust >= 0.5 ? 2 : 1);
|
|
|
145
145
|
* build-demo-site.mjs consumes directly). Returns
|
|
146
146
|
* { rows, terms, edges, focus, contradictions, worthALook, payload, meta }. */
|
|
147
147
|
export function computeLedgerDataFromPayload(payload, { focus, term, rowLimit = LEDGER_ROW_LIMIT_DEFAULT } = {}) {
|
|
148
|
-
const individuals = payload?.individuals || [];
|
|
149
|
-
const indById = new Map(individuals.map((i) => [i?.id, i]));
|
|
150
148
|
const factRows = readFactRows(payload);
|
|
151
149
|
|
|
152
150
|
const rows = factRows.map((r) => {
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
// A row is now a GROUP of one-or-more per-source assertion records
|
|
152
|
+
// (PLAN_FACT.md's re-key), so there is no single individual to join
|
|
153
|
+
// against by r.id any more — r.id is the group's own id, distinct from
|
|
154
|
+
// every member record's id. The newest assertion's own createdAt is what
|
|
155
|
+
// "when was this learned" means for a row the ledger already sorts
|
|
156
|
+
// newest-first.
|
|
157
|
+
const createdAt = (r.assertions || []).reduce((newest, a) => (a.createdAt > newest ? a.createdAt : newest), "");
|
|
155
158
|
return {
|
|
156
159
|
id: r.id, s: r.subject, p: r.predicate, o: r.object,
|
|
157
160
|
phrase: phraseFor(r.predicate),
|
|
@@ -75,12 +75,17 @@ export async function clearSiteAssetCaches() {
|
|
|
75
75
|
|
|
76
76
|
/** Fetch `url` reading the body as a stream, reporting (loadedBytes,
|
|
77
77
|
* totalBytes) after every chunk — total is 0 when the response carries no
|
|
78
|
-
* Content-Length
|
|
79
|
-
*
|
|
78
|
+
* Content-Length, OR when Content-Encoding is set: the stream this function
|
|
79
|
+
* reads is always the DECOMPRESSED body (the browser decompresses before
|
|
80
|
+
* handing it to a reader), but Content-Length names the compressed wire
|
|
81
|
+
* size — reporting that as "total" against decompressed "loaded" bytes
|
|
82
|
+
* makes progress read as over 100% almost immediately. Resolves to a Blob
|
|
83
|
+
* of the whole body. Falls back to a single-shot blob() read when the
|
|
84
|
+
* runtime has no streaming body reader. */
|
|
80
85
|
export async function fetchWithProgress(url, onProgress) {
|
|
81
86
|
const res = await fetch(url);
|
|
82
87
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
83
|
-
const total = Number(res.headers.get("content-length")) || 0;
|
|
88
|
+
const total = res.headers.get("content-encoding") ? 0 : Number(res.headers.get("content-length")) || 0;
|
|
84
89
|
if (!res.body || !res.body.getReader) {
|
|
85
90
|
const blob = await res.blob();
|
|
86
91
|
onProgress(blob.size, total || blob.size);
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
foldWorldState, worldActionRows, runWorldCommand, recordTold, recordExamined,
|
|
56
56
|
recordMassDrain, personKnowledgeLines, objectClassChain, diggableDirections,
|
|
57
57
|
isOutOfPlay, outOfPlayReasonOf, outOfPlayPhrase, massDrainPerTurnOf,
|
|
58
|
-
parseSnapshotSubject,
|
|
58
|
+
parseSnapshotSubject, characterTestimonyTag,
|
|
59
59
|
} from "./adventure.mjs";
|
|
60
60
|
|
|
61
61
|
const FOOD_CLASS = "food";
|
|
@@ -267,6 +267,9 @@ export async function runMudTurn(character, {
|
|
|
267
267
|
const opened = await readWorld(memoryDir);
|
|
268
268
|
const room = opened.state.placements.get(character)?.object ?? null;
|
|
269
269
|
const turn = k ?? opened.state.turnCount + 1;
|
|
270
|
+
// The run this turn belongs to, stamped onto the testimony it writes so a
|
|
271
|
+
// recast's first turns outrank whatever the replaced run had to say.
|
|
272
|
+
const epoch = opened.state.epoch;
|
|
270
273
|
const actions = [];
|
|
271
274
|
const notes = [];
|
|
272
275
|
const learnedBefore = knownTopics(opened.rows, opened.state, character);
|
|
@@ -300,7 +303,7 @@ export async function runMudTurn(character, {
|
|
|
300
303
|
notes.push(`MUD — ${step}: ${reason}`);
|
|
301
304
|
};
|
|
302
305
|
|
|
303
|
-
await investigateRoom({ character, turn, room, memoryDir, cache, actions, notes, runCommand, recordSkip });
|
|
306
|
+
await investigateRoom({ character, turn, epoch, room, memoryDir, cache, actions, notes, runCommand, recordSkip });
|
|
304
307
|
|
|
305
308
|
const walked = await readWorld(memoryDir);
|
|
306
309
|
const walkedRoom = walked.state.placements.get(character)?.object ?? room;
|
|
@@ -397,7 +400,7 @@ async function exploreUnvisited({ character, memoryDir, runCommand, recordSkip }
|
|
|
397
400
|
* same empty greeting every turn for the rest of the run. The talk and the
|
|
398
401
|
* examine write testimony, which never folds into the playable state; only
|
|
399
402
|
* the manipulation touches the world. */
|
|
400
|
-
async function investigateRoom({ character, turn, room, memoryDir, cache, recordSkip, runCommand, actions, notes }) {
|
|
403
|
+
async function investigateRoom({ character, turn, epoch = 0, room, memoryDir, cache, recordSkip, runCommand, actions, notes }) {
|
|
401
404
|
const { rows, state } = await readWorld(memoryDir);
|
|
402
405
|
const roomMates = castIn(state, room, character);
|
|
403
406
|
const alreadyKnown = knownTopics(rows, state, character);
|
|
@@ -420,7 +423,7 @@ async function investigateRoom({ character, turn, room, memoryDir, cache, record
|
|
|
420
423
|
// drops out of worthSpeakingTo, and drops back in the moment either side
|
|
421
424
|
// learns a food the other has not heard of.
|
|
422
425
|
if (!alreadyKnown.has(teller)) {
|
|
423
|
-
await recordExamined(memoryDir, { observer: character, thing: teller, k: turn, cache });
|
|
426
|
+
await recordExamined(memoryDir, { observer: character, thing: teller, k: turn, epoch, cache });
|
|
424
427
|
alreadyKnown.add(teller);
|
|
425
428
|
}
|
|
426
429
|
if (!told) {
|
|
@@ -433,13 +436,13 @@ async function investigateRoom({ character, turn, room, memoryDir, cache, record
|
|
|
433
436
|
});
|
|
434
437
|
notes.push(`MUD — talk: ${character} greeted ${teller}; ${teller} knows of no food to share`);
|
|
435
438
|
} else {
|
|
436
|
-
await recordTold(memoryDir, { asker: character, teller, thing: told, k: turn, cache });
|
|
439
|
+
await recordTold(memoryDir, { asker: character, teller, thing: told, k: turn, epoch, cache });
|
|
437
440
|
alreadyKnown.add(told);
|
|
438
441
|
actions.push({
|
|
439
442
|
step: "investigate", kind: "ask", teller, thing: told, miss: false,
|
|
440
443
|
text: `the ${character} asks the ${teller} about food, and hears about the ${told}.`,
|
|
441
444
|
});
|
|
442
|
-
notes.push(`MUD — ask: ${teller} told ${character} about ${told}; written as
|
|
445
|
+
notes.push(`MUD — ask: ${teller} told ${character} about ${told}; written as ${characterTestimonyTag(teller, turn, { epoch })}`);
|
|
443
446
|
}
|
|
444
447
|
}
|
|
445
448
|
|
|
@@ -452,13 +455,13 @@ async function investigateRoom({ character, turn, room, memoryDir, cache, record
|
|
|
452
455
|
if (!examined) {
|
|
453
456
|
recordSkip("investigate", "nothing unexamined stands here", "");
|
|
454
457
|
} else {
|
|
455
|
-
await recordExamined(memoryDir, { observer: character, thing: examined, k: turn, cache });
|
|
458
|
+
await recordExamined(memoryDir, { observer: character, thing: examined, k: turn, epoch, cache });
|
|
456
459
|
alreadyKnown.add(examined);
|
|
457
460
|
actions.push({
|
|
458
461
|
step: "investigate", kind: "examine", thing: examined, miss: false,
|
|
459
462
|
text: `the ${character} examines the ${examined}.`,
|
|
460
463
|
});
|
|
461
|
-
notes.push(`MUD — examine: ${character} looked at ${examined}; written as
|
|
464
|
+
notes.push(`MUD — examine: ${character} looked at ${examined}; written as ${characterTestimonyTag(character, turn, { epoch })}`);
|
|
462
465
|
}
|
|
463
466
|
|
|
464
467
|
await manipulateSomething({ character, turn, room, memoryDir, runCommand, recordSkip });
|