@polycode-projects/the-mechanical-code-talker 1.10.14 → 1.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -114
- package/ROADMAP.md +14 -4
- package/bin/tmct.mjs +167 -77
- package/data/games/crates.txt +24 -0
- package/data/games/hanoi-3.txt +30 -0
- package/data/games/river.txt +33 -0
- package/package.json +1 -1
- package/src/ask.mjs +119 -6
- package/src/chat.mjs +1515 -52
- package/src/codegraph.mjs +24 -11
- package/src/domain.mjs +350 -0
- package/src/import-file.mjs +86 -0
- package/src/init.mjs +46 -1
- package/src/interpret/normalize.mjs +46 -0
- package/src/interpret/strategies/keywords.mjs +13 -9
- package/src/ledger-viz.mjs +635 -0
- package/src/memory/core.mjs +100 -17
- package/src/memory/shacl.mjs +20 -7
- package/src/memory-ask-browser-entry.mjs +9 -7
- package/src/memory-ask-browser.bundle.js +5648 -1177
- package/src/plan-viz.mjs +409 -0
- package/src/router/drive.mjs +122 -13
- package/src/router/guardrail.mjs +5 -0
- package/src/router/registry.mjs +55 -11
- package/src/router/resolver.mjs +13 -0
- package/src/router/taught.mjs +84 -0
- package/src/sentences.mjs +19 -0
- package/src/syllogise.mjs +154 -38
- package/src/viz-theme.mjs +66 -0
- package/src/wink-model.mjs +12 -6
- package/src/ask-browser-entry.mjs +0 -19
- package/src/ask-browser.bundle.js +0 -5411
- package/src/viz.mjs +0 -959
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
// ledger-viz.mjs — `tmct viz`: the memory graph as a readable ledger of
|
|
2
|
+
// fact-sentences around one focus term, with the in-page chat dock
|
|
3
|
+
// (PLAN_VIZ_LEDGER.md).
|
|
4
|
+
//
|
|
5
|
+
// Three pure/impure-separated pieces:
|
|
6
|
+
// - computeLedgerData(repoDir, opts) — I/O (loadMemory) + derivation
|
|
7
|
+
// - computeLedgerDataFromPayload(payload) — the pure derivation half
|
|
8
|
+
// - renderLedgerHtml(data) — pure string builder, one
|
|
9
|
+
// self-contained document, no external requests.
|
|
10
|
+
// readMemoryAskBundle() is the one extra bit of I/O renderLedgerHtml itself
|
|
11
|
+
// doesn't do: it reads the checked-in chat-dock engine bundle.
|
|
12
|
+
|
|
13
|
+
import { loadMemory, readFactRows, findContradictions, normFactTerm } from "./memory/core.mjs";
|
|
14
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
|
|
19
|
+
export const LEDGER_ROW_LIMIT_DEFAULT = 20000;
|
|
20
|
+
|
|
21
|
+
/** Read the checked-in browser memory-ask-engine bundle
|
|
22
|
+
* (`src/memory-ask-browser.bundle.js`) — the real memory-graph answer engine
|
|
23
|
+
* (chat.mjs's factAnswer/factReadBack) the chat dock runs on. Returns `""`,
|
|
24
|
+
* never throws, if the bundle hasn't been built — the page then renders with
|
|
25
|
+
* an honest "chat unavailable" note instead of a dock. */
|
|
26
|
+
export async function readMemoryAskBundle() {
|
|
27
|
+
try {
|
|
28
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
return await readFile(join(here, "memory-ask-browser.bundle.js"), "utf8");
|
|
30
|
+
} catch {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Predicate rendering + family grouping. A closed table with a verbatim
|
|
36
|
+
// fallback: an unknown predicate still reads as itself, never breaks the page.
|
|
37
|
+
const PHRASES = new Map([
|
|
38
|
+
["rdfs:subClassOf", "is a kind of"],
|
|
39
|
+
["rdf:type", "is a"],
|
|
40
|
+
["mgx:hasA", "has"],
|
|
41
|
+
["mgx:partOf", "is part of"],
|
|
42
|
+
["mgx:madeOf", "is made of"],
|
|
43
|
+
["mgx:capableOf", "can"],
|
|
44
|
+
["mgx:receivesAction", "can be"],
|
|
45
|
+
["mgx:usedFor", "is used for"],
|
|
46
|
+
["mgx:mannerOf", "is a way of"],
|
|
47
|
+
["mgx:ownedBy", "is owned by"],
|
|
48
|
+
["mgx:createdBy", "is created by"],
|
|
49
|
+
["mgx:hasProperty", "is"],
|
|
50
|
+
["mgx:atLocation", "is at"],
|
|
51
|
+
]);
|
|
52
|
+
const PREP_FOLD_RE = /^mgx:([a-z]+)-(on|in|at|onto|upon|under|over|beside|near|behind|above|below|inside|outside)$/;
|
|
53
|
+
const COMPARATIVE_RE = /^mgx:([a-z][a-z-]*)-than$/;
|
|
54
|
+
|
|
55
|
+
export function phraseFor(predicate) {
|
|
56
|
+
const p = String(predicate || "");
|
|
57
|
+
const curated = PHRASES.get(p);
|
|
58
|
+
if (curated) return curated;
|
|
59
|
+
const prep = PREP_FOLD_RE.exec(p);
|
|
60
|
+
if (prep) return `${prep[1]}s ${prep[2]}`;
|
|
61
|
+
const comp = COMPARATIVE_RE.exec(p);
|
|
62
|
+
if (comp) return `is ${comp[1].replace(/-/g, " ")} than`;
|
|
63
|
+
return p.replace(/^mgx:/, "").replace(/[-_]/g, " ");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const FAMILY_OF = new Map([
|
|
67
|
+
["rdfs:subClassOf", "is-a"], ["rdf:type", "is-a"],
|
|
68
|
+
["mgx:hasA", "has"], ["mgx:partOf", "has"], ["mgx:madeOf", "has"],
|
|
69
|
+
["mgx:capableOf", "can"], ["mgx:receivesAction", "can"],
|
|
70
|
+
["mgx:usedFor", "used-for"], ["mgx:mannerOf", "used-for"],
|
|
71
|
+
["mgx:ownedBy", "role"], ["mgx:createdBy", "role"], ["mgx:hasProperty", "role"],
|
|
72
|
+
]);
|
|
73
|
+
export const FAMILIES = Object.freeze(["is-a", "has", "can", "used-for", "rests-on", "role", "other"]);
|
|
74
|
+
|
|
75
|
+
export function familyFor(predicate) {
|
|
76
|
+
const p = String(predicate || "");
|
|
77
|
+
const fixed = FAMILY_OF.get(p);
|
|
78
|
+
if (fixed) return fixed;
|
|
79
|
+
if (PREP_FOLD_RE.test(p)) return "rests-on";
|
|
80
|
+
return "other";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Provenance bucket for one fact row: who says so. Taught wins over
|
|
84
|
+
* entailed (the stronger claim when a fact carries both), source types win
|
|
85
|
+
* over the legacy provenance string. */
|
|
86
|
+
export function provBucketFor(sourceTypes = [], provenance = "") {
|
|
87
|
+
const t = new Set(sourceTypes);
|
|
88
|
+
if (t.has("teach") || t.has("operator")) return "taught";
|
|
89
|
+
if (t.has("entailed")) return "entailed";
|
|
90
|
+
if (t.size) return "corpus";
|
|
91
|
+
const p = String(provenance);
|
|
92
|
+
if (/(^|\|\s*)(teach|ace|operator)\b/i.test(p)) return "taught";
|
|
93
|
+
if (/entailed/i.test(p)) return "entailed";
|
|
94
|
+
return "corpus";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const trustTierFor = (trust) => (trust >= 0.85 ? 3 : trust >= 0.5 ? 2 : 1);
|
|
98
|
+
|
|
99
|
+
/** Pure derivation over a loaded memory payload (the half scripts/
|
|
100
|
+
* build-demo-site.mjs consumes directly). Returns
|
|
101
|
+
* { rows, terms, edges, focus, contradictions, worthALook, payload, meta }. */
|
|
102
|
+
export function computeLedgerDataFromPayload(payload, { focus, term, rowLimit = LEDGER_ROW_LIMIT_DEFAULT } = {}) {
|
|
103
|
+
const individuals = payload?.individuals || [];
|
|
104
|
+
const indById = new Map(individuals.map((i) => [i?.id, i]));
|
|
105
|
+
const factRows = readFactRows(payload);
|
|
106
|
+
|
|
107
|
+
const rows = factRows.map((r) => {
|
|
108
|
+
const ind = indById.get(r.id);
|
|
109
|
+
const createdAt = (ind?.attributes || []).find((a) => a?.key === "createdAt")?.value || "";
|
|
110
|
+
return {
|
|
111
|
+
id: r.id, s: r.subject, p: r.predicate, o: r.object,
|
|
112
|
+
phrase: phraseFor(r.predicate),
|
|
113
|
+
prov: provBucketFor(r.sourceTypes, r.provenance),
|
|
114
|
+
trustTier: trustTierFor(r.trust),
|
|
115
|
+
family: familyFor(r.predicate),
|
|
116
|
+
createdAt,
|
|
117
|
+
src: r.provenance || (r.sourceTypes || []).join(" ") || "unrecorded",
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
rows.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || "") || a.id.localeCompare(b.id));
|
|
121
|
+
|
|
122
|
+
// Term index + adjacency.
|
|
123
|
+
const termMap = new Map(); // term -> { degree, best: {trust, prov}, newest }
|
|
124
|
+
const bump = (t, row, trust) => {
|
|
125
|
+
if (!t) return;
|
|
126
|
+
const cur = termMap.get(t) || { term: t, degree: 0, prov: row.prov, best: -1, newest: "" };
|
|
127
|
+
cur.degree += 1;
|
|
128
|
+
if (trust > cur.best) { cur.best = trust; cur.prov = row.prov; }
|
|
129
|
+
if ((row.createdAt || "") > cur.newest) cur.newest = row.createdAt || "";
|
|
130
|
+
termMap.set(t, cur);
|
|
131
|
+
};
|
|
132
|
+
const trustById = new Map(factRows.map((r) => [r.id, r.trust]));
|
|
133
|
+
for (const row of rows) {
|
|
134
|
+
bump(row.s, row, trustById.get(row.id) || 0);
|
|
135
|
+
bump(row.o, row, trustById.get(row.id) || 0);
|
|
136
|
+
}
|
|
137
|
+
const terms = [...termMap.values()]
|
|
138
|
+
.map(({ term: t, degree, prov, newest }) => ({ term: t, degree, prov, newest }))
|
|
139
|
+
.sort((a, b) => b.degree - a.degree || a.term.localeCompare(b.term));
|
|
140
|
+
const edges = rows.map((r) => ({ s: r.s, o: r.o, id: r.id }));
|
|
141
|
+
|
|
142
|
+
// Focus: the asked term when it resolves; otherwise the newest taught
|
|
143
|
+
// row's subject, then the highest-degree term. A miss never seeds a
|
|
144
|
+
// phantom term.
|
|
145
|
+
let focusTerm = null;
|
|
146
|
+
const asked = focus || term;
|
|
147
|
+
if (asked) {
|
|
148
|
+
const nf = normFactTerm(asked);
|
|
149
|
+
if (termMap.has(nf)) focusTerm = nf;
|
|
150
|
+
}
|
|
151
|
+
if (!focusTerm) {
|
|
152
|
+
const newestTaught = rows.find((r) => r.prov === "taught");
|
|
153
|
+
if (newestTaught) focusTerm = newestTaught.s;
|
|
154
|
+
else if (terms.length) focusTerm = terms[0].term;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const rowIds = new Set(rows.map((r) => r.id));
|
|
158
|
+
const contradictions = findContradictions(payload)
|
|
159
|
+
.map((group) => group.map((r) => r.id).filter((id) => rowIds.has(id)))
|
|
160
|
+
.filter((ids) => ids.length > 1);
|
|
161
|
+
|
|
162
|
+
const newestTaughtRow = rows.find((r) => r.prov === "taught") || null;
|
|
163
|
+
const firstContra = contradictions.length ? rows.find((r) => r.id === contradictions[0][0]) : null;
|
|
164
|
+
const worthALook = {
|
|
165
|
+
newestTaught: newestTaughtRow ? { rowId: newestTaughtRow.id, term: newestTaughtRow.s } : null,
|
|
166
|
+
contradictions: { count: contradictions.length, firstFocusTerm: firstContra ? firstContra.s : null },
|
|
167
|
+
biggestHub: terms.length ? { term: terms[0].term, degree: terms[0].degree } : null,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Row cap: the focus 2-hop neighborhood survives first (the minimap's own
|
|
171
|
+
// radius, so facet counts and the map stay mutually correct), the rest by
|
|
172
|
+
// recency, so a huge store degrades to "recent + local" honestly.
|
|
173
|
+
const total = rows.length;
|
|
174
|
+
let shownRows = rows;
|
|
175
|
+
if (total > rowLimit) {
|
|
176
|
+
const hop1 = new Set();
|
|
177
|
+
for (const e of edges) {
|
|
178
|
+
if (e.s === focusTerm) hop1.add(e.o);
|
|
179
|
+
if (e.o === focusTerm) hop1.add(e.s);
|
|
180
|
+
}
|
|
181
|
+
const near = new Set([focusTerm, ...hop1]);
|
|
182
|
+
const inHood = (r) => near.has(r.s) || near.has(r.o);
|
|
183
|
+
const hood = rows.filter(inHood);
|
|
184
|
+
const rest = rows.filter((r) => !inHood(r));
|
|
185
|
+
shownRows = [...hood, ...rest].slice(0, rowLimit);
|
|
186
|
+
}
|
|
187
|
+
const meta = { shown: shownRows.length, total, truncated: shownRows.length < total };
|
|
188
|
+
|
|
189
|
+
return { rows: shownRows, terms, edges, focus: focusTerm, contradictions, worthALook, payload, meta };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Load the memory graph under `repoDir` and derive the ledger data. Never
|
|
193
|
+
* throws on a missing/empty memory dir. */
|
|
194
|
+
export async function computeLedgerData(repoDir, opts = {}) {
|
|
195
|
+
const payload = await loadMemory(repoDir);
|
|
196
|
+
return computeLedgerDataFromPayload(payload, opts);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The chat dock's answer-to-focus resolver. Pass 1: earliest term label (≥3
|
|
200
|
+
* chars, space-boundary) appearing in the ANSWER text. Pass 2: strip the
|
|
201
|
+
* QUESTION's crust and try the remainder as one normalized term. Returns a
|
|
202
|
+
* term string or null. Self-contained on purpose: its source is injected
|
|
203
|
+
* into the rendered page verbatim, so it must close over nothing. */
|
|
204
|
+
export function resolveAnsweredTerm(answerText, questionText, terms, normFn) {
|
|
205
|
+
const hay = " " + String(answerText || "").toLowerCase() + " ";
|
|
206
|
+
let best = null;
|
|
207
|
+
for (const t of terms || []) {
|
|
208
|
+
const label = String((t && t.term) || "").toLowerCase();
|
|
209
|
+
if (label.length < 3) continue;
|
|
210
|
+
const a = hay.indexOf(" " + label);
|
|
211
|
+
const b = hay.indexOf(label + " ");
|
|
212
|
+
const idx = a !== -1 ? a : b;
|
|
213
|
+
if (idx === -1) continue;
|
|
214
|
+
if (!best || idx < best.idx) best = { term: t.term, idx };
|
|
215
|
+
}
|
|
216
|
+
if (best) return best.term;
|
|
217
|
+
const stripped = String(questionText || "").toLowerCase()
|
|
218
|
+
.replace(/^(what|where|who|which|does|do|is|are)\b/, "")
|
|
219
|
+
.replace(/\b(is|are|used for|do|does|mean|means|a|an|the)\b/g, " ")
|
|
220
|
+
.replace(/[?.!]+$/, "")
|
|
221
|
+
.replace(/\s+/g, " ")
|
|
222
|
+
.trim();
|
|
223
|
+
if (!stripped || typeof normFn !== "function") return null;
|
|
224
|
+
const norm = normFn(stripped);
|
|
225
|
+
return (terms || []).some((t) => t && t.term === norm) ? norm : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** One complete, self-contained document: the ledger, segment rail,
|
|
229
|
+
* worth-a-look panel, breadcrumb/search, two-hop minimap, and (when the
|
|
230
|
+
* memory-ask bundle is present) the ask-the-graph chat dock, all over the
|
|
231
|
+
* embedded LEDGER/PAYLOAD data. */
|
|
232
|
+
export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, worthALook, payload, meta, memoryAskBundle } = {}) {
|
|
233
|
+
const ledgerJson = embedJson({ rows: rows || [], terms: terms || [], edges: edges || [], focus: focus || null, contradictions: contradictions || [], worthALook: worthALook || null, meta: meta || { shown: 0, total: 0, truncated: false } });
|
|
234
|
+
const payloadJson = embedJson(payload || { individuals: [], objectProperties: [] });
|
|
235
|
+
const shown = meta?.shown ?? (rows || []).length;
|
|
236
|
+
const title = `tmct ledger — ${shown} fact${shown === 1 ? "" : "s"}${focus ? ` (focus: ${escapeHtml(focus)})` : ""}`;
|
|
237
|
+
const bundleStr = typeof memoryAskBundle === "string" ? memoryAskBundle : "";
|
|
238
|
+
const hasMemChat = bundleStr.length > 0;
|
|
239
|
+
// The placeholder is honest: the canonical exchange only when its terms are
|
|
240
|
+
// really in this payload, otherwise a real term from this graph.
|
|
241
|
+
const termSet = new Set((terms || []).map((t) => t.term));
|
|
242
|
+
const placeholder = termSet.has("ishmael")
|
|
243
|
+
? "who is the grandfather of ishmael"
|
|
244
|
+
: (terms && terms.length ? `ask the graph… e.g. what is ${terms[0].term}` : "ask the graph…");
|
|
245
|
+
const dockHtml = hasMemChat
|
|
246
|
+
? `<div class="chat">
|
|
247
|
+
<div class="chatlog" id="chatlog" aria-live="polite"></div>
|
|
248
|
+
<form class="chatask" id="chatform">
|
|
249
|
+
<span class="prompt mono">tmct></span>
|
|
250
|
+
<input id="chatq" type="text" placeholder="${escapeHtml(placeholder)}" aria-label="Ask the graph">
|
|
251
|
+
</form>
|
|
252
|
+
</div>`
|
|
253
|
+
: `<div class="chat chat-off"><p class="chatnote">chat unavailable — run <span class="mono">npm run build:ask-bundle</span> to enable the in-page ask engine.</p></div>`;
|
|
254
|
+
|
|
255
|
+
return `<!doctype html>
|
|
256
|
+
<html lang="en">
|
|
257
|
+
<head>
|
|
258
|
+
<meta charset="utf-8">
|
|
259
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
260
|
+
<title>${escapeHtml(title)}</title>
|
|
261
|
+
<style>
|
|
262
|
+
${THEME_TOKENS_CSS}
|
|
263
|
+
html { background: var(--bg); }
|
|
264
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
265
|
+
.mono { font-family: ${MONO_STACK}; }
|
|
266
|
+
main { max-width: 1080px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
|
|
267
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); display: flex; flex-wrap: wrap; gap: .4em 1.2em; }
|
|
268
|
+
h1 { font-size: 1.4rem; margin: .3rem 0 .9rem; text-wrap: balance; }
|
|
269
|
+
button { font: inherit; color: inherit; background: none; border: none; padding: 0; cursor: pointer; }
|
|
270
|
+
button:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: 4px; }
|
|
271
|
+
.topbar { display: flex; flex-wrap: wrap; align-items: center; gap: .8rem; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: .5rem 0; margin-bottom: 1.1rem; }
|
|
272
|
+
.crumbs { display: flex; align-items: center; flex-wrap: wrap; gap: .35rem; font-size: .78rem; }
|
|
273
|
+
.crumbs .sep { color: var(--muted); }
|
|
274
|
+
.crumb { font-family: ${MONO_STACK}; font-size: .74rem; padding: .12rem .5rem; border: 1px solid var(--line); border-radius: 99px; background: var(--card); }
|
|
275
|
+
.crumb[aria-current="true"] { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
276
|
+
.search { margin-left: auto; display: flex; align-items: center; gap: .5rem; }
|
|
277
|
+
.search input { font-family: ${MONO_STACK}; font-size: .78rem; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .3rem .6rem; width: 170px; }
|
|
278
|
+
.search .miss { font-size: .72rem; color: var(--alert); font-family: ${MONO_STACK}; }
|
|
279
|
+
.app { display: grid; grid-template-columns: 190px minmax(0,1fr) 230px; gap: 1.3rem; grid-template-areas: "rail ledger aside"; }
|
|
280
|
+
.rail { grid-area: rail; } .ledger { grid-area: ledger; } .aside { grid-area: aside; }
|
|
281
|
+
@media (max-width: 880px) { .app { grid-template-columns: 1fr; grid-template-areas: "ledger" "aside" "rail"; } .search { margin-left: 0; } }
|
|
282
|
+
.rail h2, .aside h2 { font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 1rem 0 .4rem; }
|
|
283
|
+
.rail h2:first-child { margin-top: 0; }
|
|
284
|
+
.segs { display: flex; flex-direction: column; gap: .28rem; }
|
|
285
|
+
.seg { display: flex; align-items: center; gap: .45rem; font-family: ${MONO_STACK}; font-size: .73rem; padding: .2rem .55rem; border: 1px solid var(--line); border-radius: 99px; background: var(--card); text-align: left; }
|
|
286
|
+
.seg .n { margin-left: auto; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
287
|
+
.seg .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
|
|
288
|
+
.seg.off { opacity: .35; }
|
|
289
|
+
.seg.on { border-color: transparent; color: #fff; }
|
|
290
|
+
.seg.on .n { color: #fff; opacity: .85; }
|
|
291
|
+
.seg.on.neutral { background: var(--ink); color: var(--bg); } .seg.on.neutral .n { color: var(--bg); }
|
|
292
|
+
.seg.on.c-taught { background: var(--taught); } .seg.on.c-corpus { background: var(--corpus); } .seg.on.c-entail { background: var(--entail); }
|
|
293
|
+
.d-taught { background: var(--taught); } .d-corpus { background: var(--corpus); } .d-entail { background: var(--entail); }
|
|
294
|
+
.focuscard { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: .8rem 1rem; margin-bottom: 1rem; }
|
|
295
|
+
.focuscard .term { font-size: 1.35rem; font-weight: 700; }
|
|
296
|
+
.focuscard .klass, .focuscard .stats { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); margin-top: .3rem; font-variant-numeric: tabular-nums; }
|
|
297
|
+
.group { margin: 1.1rem 0; }
|
|
298
|
+
.group h3 { font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 0 0 .35rem; display: flex; align-items: baseline; gap: .5rem; }
|
|
299
|
+
.group h3::after { content: ""; flex: 1; border-top: 1px solid var(--line); transform: translateY(-.2em); }
|
|
300
|
+
.rows { display: flex; flex-direction: column; gap: .35rem; }
|
|
301
|
+
.row { background: var(--card); border: 1px solid var(--line); border-left: 3px solid var(--line); border-radius: 0 8px 8px 0; padding: .45rem .75rem; display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .5rem; font-size: .95rem; }
|
|
302
|
+
.row.p-taught.t3 { border-left-color: var(--taught-t3); } .row.p-taught.t2 { border-left-color: var(--taught-t2); } .row.p-taught.t1 { border-left-color: var(--taught-t1); }
|
|
303
|
+
.row.p-corpus.t3 { border-left-color: var(--corpus-t3); } .row.p-corpus.t2 { border-left-color: var(--corpus-t2); } .row.p-corpus.t1 { border-left-color: var(--corpus-t1); }
|
|
304
|
+
.row.p-entail.t3 { border-left-color: var(--entail-t3); } .row.p-entail.t2 { border-left-color: var(--entail-t2); } .row.p-entail.t1 { border-left-color: var(--entail-t1); }
|
|
305
|
+
.chip { font-family: ${MONO_STACK}; font-size: .76rem; padding: .05rem .45rem; border: 1px solid var(--line); border-radius: 99px; background: var(--bg); }
|
|
306
|
+
button.chip:hover { border-color: var(--ink); }
|
|
307
|
+
.chip.here { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
308
|
+
.prov { margin-left: auto; font-family: ${MONO_STACK}; font-size: .64rem; padding: .08rem .5rem; border-radius: 99px; white-space: nowrap; }
|
|
309
|
+
.prov.p-taught { color: var(--taught); background: var(--taught-soft); }
|
|
310
|
+
.prov.p-corpus { color: var(--corpus); background: var(--corpus-soft); }
|
|
311
|
+
.prov.p-entail { color: var(--entail); background: var(--entail-soft); }
|
|
312
|
+
.contra { border: 1px solid var(--alert); border-radius: 10px; padding: .5rem; margin-top: .35rem; }
|
|
313
|
+
.contra .label { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--alert); margin: 0 0 .35rem .2rem; }
|
|
314
|
+
.empty { color: var(--muted); font-size: .9rem; border: 1px dashed var(--line); border-radius: 8px; padding: .8rem 1rem; }
|
|
315
|
+
.looks { display: flex; flex-direction: column; gap: .45rem; }
|
|
316
|
+
.look { display: block; text-align: left; background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: .5rem .65rem; font-size: .8rem; width: 100%; }
|
|
317
|
+
.look:hover { border-color: var(--ink); }
|
|
318
|
+
.look .tag { display: block; font-family: ${MONO_STACK}; font-size: .62rem; letter-spacing: .06em; text-transform: uppercase; margin-bottom: .18rem; }
|
|
319
|
+
.look.k-taught .tag { color: var(--taught); } .look.k-alert .tag { color: var(--alert); } .look.k-hub .tag { color: var(--corpus); }
|
|
320
|
+
.mapwrap { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: .45rem; margin-top: .45rem; }
|
|
321
|
+
.mapwrap canvas { display: block; width: 100%; height: 160px; cursor: pointer; }
|
|
322
|
+
.mapnote { font-family: ${MONO_STACK}; font-size: .62rem; color: var(--muted); margin: .3rem .2rem 0; }
|
|
323
|
+
.chat { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: .65rem .85rem; margin-bottom: 1rem; }
|
|
324
|
+
.chatlog { display: flex; flex-direction: column; gap: .45rem; max-height: 220px; overflow-y: auto; }
|
|
325
|
+
.chatlog:empty { display: none; }
|
|
326
|
+
.chatlog .u { font-family: ${MONO_STACK}; font-size: .76rem; color: var(--muted); }
|
|
327
|
+
.chatlog .u::before { content: "tmct> "; color: var(--taught); }
|
|
328
|
+
.chatlog .a { font-size: .9rem; line-height: 1.45; }
|
|
329
|
+
.chatlog .a.miss { color: var(--muted); }
|
|
330
|
+
.chatlog .a.goal { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); }
|
|
331
|
+
.chatask { display: flex; align-items: center; gap: .5rem; }
|
|
332
|
+
.chatlog:not(:empty) + .chatask { border-top: 1px solid var(--line); margin-top: .55rem; padding-top: .55rem; }
|
|
333
|
+
.chatask .prompt { color: var(--taught); font-size: .78rem; }
|
|
334
|
+
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .32rem .6rem; min-width: 0; }
|
|
335
|
+
.chatnote { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); margin: 0; }
|
|
336
|
+
@media (prefers-reduced-motion: no-preference) { .seg, .chip, .look { transition: border-color .12s ease, background-color .12s ease; } }
|
|
337
|
+
</style>
|
|
338
|
+
</head>
|
|
339
|
+
<body>
|
|
340
|
+
<main>
|
|
341
|
+
<div class="eyebrow"><span>tmct · memory ledger</span><span id="counts"></span></div>
|
|
342
|
+
<h1>A graph you can read</h1>
|
|
343
|
+
<div class="topbar">
|
|
344
|
+
<nav class="crumbs" id="crumbs" aria-label="Focus trail"></nav>
|
|
345
|
+
<div class="search">
|
|
346
|
+
<input id="q" type="text" placeholder="go to term… (enter)" aria-label="Go to term">
|
|
347
|
+
<span class="miss" id="qmiss" aria-live="polite"></span>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="app">
|
|
351
|
+
<aside class="rail" aria-label="Segments">
|
|
352
|
+
<h2>who says so</h2><div class="segs" id="segProv"></div>
|
|
353
|
+
<h2>kind of fact</h2><div class="segs" id="segFam"></div>
|
|
354
|
+
<h2>when learned</h2><div class="segs" id="segRec"></div>
|
|
355
|
+
</aside>
|
|
356
|
+
<section class="ledger">
|
|
357
|
+
${dockHtml}
|
|
358
|
+
<div id="ledger" aria-live="polite"></div>
|
|
359
|
+
</section>
|
|
360
|
+
<aside class="aside" aria-label="Highlights and minimap">
|
|
361
|
+
<h2>worth a look</h2><div class="looks" id="looks"></div>
|
|
362
|
+
<h2>two hops out</h2>
|
|
363
|
+
<div class="mapwrap">
|
|
364
|
+
<canvas id="map" width="230" height="160" aria-label="Two-hop neighborhood minimap"></canvas>
|
|
365
|
+
<p class="mapnote">dots = terms · click to refocus · dim = filtered out</p>
|
|
366
|
+
</div>
|
|
367
|
+
</aside>
|
|
368
|
+
</div>
|
|
369
|
+
</main>
|
|
370
|
+
<script>
|
|
371
|
+
const LEDGER = ${ledgerJson};
|
|
372
|
+
const PAYLOAD = ${payloadJson};
|
|
373
|
+
</script>
|
|
374
|
+
${hasMemChat ? `<script>\n${bundleStr}\n</script>` : ""}
|
|
375
|
+
<script>
|
|
376
|
+
(function () {
|
|
377
|
+
"use strict";
|
|
378
|
+
const DAY = 86400000;
|
|
379
|
+
const el = (id) => document.getElementById(id);
|
|
380
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
381
|
+
const FAMS = ["is-a", "has", "can", "used-for", "rests-on", "role", "other"];
|
|
382
|
+
const FAM_LABEL = { "is-a": "is a kind of", has: "has", can: "can", "used-for": "used for", "rests-on": "rests on", role: "role / property", other: "other" };
|
|
383
|
+
const PROVS = [["taught", "you taught"], ["corpus", "corpus"], ["entail", "entailed"]];
|
|
384
|
+
const RECS = ["today", "this week", "older"];
|
|
385
|
+
const provKey = (p) => (p === "entailed" ? "entail" : p); // css class key
|
|
386
|
+
const termIndex = new Map(LEDGER.terms.map((t) => [t.term, t]));
|
|
387
|
+
const rowById = new Map(LEDGER.rows.map((r) => [r.id, r]));
|
|
388
|
+
const contraById = new Map();
|
|
389
|
+
LEDGER.contradictions.forEach((ids, gi) => ids.forEach((id) => contraById.set(id, gi)));
|
|
390
|
+
|
|
391
|
+
let focus = LEDGER.focus;
|
|
392
|
+
let trail = focus ? [{ term: focus, label: null }] : [];
|
|
393
|
+
const sel = { prov: new Set(), fam: new Set(), rec: new Set() };
|
|
394
|
+
|
|
395
|
+
const recOf = (r) => {
|
|
396
|
+
const t = Date.parse(r.createdAt);
|
|
397
|
+
if (!Number.isFinite(t)) return "older";
|
|
398
|
+
const age = Date.now() - t;
|
|
399
|
+
return age < DAY ? "today" : age < 7 * DAY ? "this week" : "older";
|
|
400
|
+
};
|
|
401
|
+
const touches = (r, term) => r.s === term || r.o === term;
|
|
402
|
+
function passes(r, skip) {
|
|
403
|
+
if (skip !== "prov" && sel.prov.size && !sel.prov.has(r.prov)) return false;
|
|
404
|
+
if (skip !== "fam" && sel.fam.size && !sel.fam.has(r.family)) return false;
|
|
405
|
+
if (skip !== "rec" && sel.rec.size && !sel.rec.has(recOf(r))) return false;
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function segButton(group, value, label, count, cls) {
|
|
410
|
+
const b = document.createElement("button");
|
|
411
|
+
const on = sel[group].has(value);
|
|
412
|
+
b.className = "seg" + (on ? " on " + (cls || "neutral") : "") + (count === 0 ? " off" : "");
|
|
413
|
+
b.setAttribute("aria-pressed", String(on));
|
|
414
|
+
if (cls && !on) { const d = document.createElement("span"); d.className = "dot " + cls.replace("c-", "d-"); b.appendChild(d); }
|
|
415
|
+
const lab = document.createElement("span"); lab.textContent = label; b.appendChild(lab);
|
|
416
|
+
const n = document.createElement("span"); n.className = "n"; n.textContent = String(count); b.appendChild(n);
|
|
417
|
+
b.addEventListener("click", () => { if (on) sel[group].delete(value); else sel[group].add(value); render(); });
|
|
418
|
+
return b;
|
|
419
|
+
}
|
|
420
|
+
function renderSegs() {
|
|
421
|
+
const mine = LEDGER.rows.filter((r) => focus && touches(r, focus));
|
|
422
|
+
const put = (id, group, values, labelOf, clsOf) => {
|
|
423
|
+
const box = el(id); box.innerHTML = "";
|
|
424
|
+
for (const v of values) {
|
|
425
|
+
const count = mine.filter((r) => passes(r, group) &&
|
|
426
|
+
(group === "prov" ? r.prov === v : group === "fam" ? r.family === v : recOf(r) === v)).length;
|
|
427
|
+
box.appendChild(segButton(group, v, labelOf(v), count, clsOf ? clsOf(v) : null));
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
put("segProv", "prov", PROVS.map((p) => p[0]), (v) => PROVS.find((p) => p[0] === v)[1], (v) => "c-" + provKey(v));
|
|
431
|
+
put("segFam", "fam", FAMS, (v) => FAM_LABEL[v], null);
|
|
432
|
+
put("segRec", "rec", RECS, (v) => v, null);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const chipHtml = (term) => term === focus
|
|
436
|
+
? '<span class="chip here">' + esc(term) + "</span>"
|
|
437
|
+
: '<button class="chip" data-go="' + esc(term) + '">' + esc(term) + "</button>";
|
|
438
|
+
function rowHtml(r) {
|
|
439
|
+
const cls = "row p-" + provKey(r.prov) + " t" + r.trustTier;
|
|
440
|
+
const date = r.createdAt ? " · " + esc(String(r.createdAt).slice(0, 10)) : "";
|
|
441
|
+
return '<div class="' + cls + '">' + chipHtml(r.s) + " <span>" + esc(r.phrase) + "</span> " + chipHtml(r.o) +
|
|
442
|
+
'<span class="prov p-' + provKey(r.prov) + '">' + esc(r.src) + date + "</span></div>";
|
|
443
|
+
}
|
|
444
|
+
function renderLedger() {
|
|
445
|
+
const box = el("ledger");
|
|
446
|
+
if (!focus) { box.innerHTML = '<div class="empty">Nothing in memory yet. Teach a fact in chat, then re-run tmct viz.</div>'; return; }
|
|
447
|
+
const all = LEDGER.rows.filter((r) => touches(r, focus));
|
|
448
|
+
const mine = all.filter((r) => passes(r, null));
|
|
449
|
+
const srcs = new Set(all.map((r) => r.src.split(" | ")[0]));
|
|
450
|
+
const dates = all.map((r) => r.createdAt).filter(Boolean).sort();
|
|
451
|
+
const klass = all.find((r) => r.s === focus && r.family === "is-a");
|
|
452
|
+
let html = '<div class="focuscard"><div class="term">' + esc(focus) + "</div>" +
|
|
453
|
+
'<div class="klass">' + (klass ? esc(klass.phrase + " " + klass.o) : "no class recorded") + "</div>" +
|
|
454
|
+
'<div class="stats">' + all.length + " facts · " + srcs.size + " source" + (srcs.size === 1 ? "" : "s") +
|
|
455
|
+
(dates.length ? " · first " + esc(dates[0].slice(0, 10)) + " · last " + esc(dates[dates.length - 1].slice(0, 10)) : "") + "</div></div>";
|
|
456
|
+
const bracketed = new Set();
|
|
457
|
+
for (const fam of FAMS) {
|
|
458
|
+
const rows = mine.filter((r) => r.family === fam && !bracketed.has(r.id));
|
|
459
|
+
if (!rows.length) continue;
|
|
460
|
+
let inner = "";
|
|
461
|
+
const groupsHere = new Map();
|
|
462
|
+
for (const r of rows) {
|
|
463
|
+
const gi = contraById.get(r.id);
|
|
464
|
+
if (gi !== undefined) { if (!groupsHere.has(gi)) groupsHere.set(gi, []); groupsHere.get(gi).push(r); }
|
|
465
|
+
}
|
|
466
|
+
const inBracket = new Set();
|
|
467
|
+
for (const [, grp] of groupsHere) if (grp.length > 1) grp.forEach((r) => inBracket.add(r.id));
|
|
468
|
+
inner += rows.filter((r) => !inBracket.has(r.id)).map(rowHtml).join("");
|
|
469
|
+
for (const [, grp] of groupsHere) {
|
|
470
|
+
if (grp.length < 2) continue;
|
|
471
|
+
grp.forEach((r) => bracketed.add(r.id));
|
|
472
|
+
inner += '<div class="contra"><p class="label">more than one answer on record — shown, never merged</p><div class="rows">' + grp.map(rowHtml).join("") + "</div></div>";
|
|
473
|
+
}
|
|
474
|
+
html += '<div class="group"><h3>' + esc(FAM_LABEL[fam]) + '</h3><div class="rows">' + inner + "</div></div>";
|
|
475
|
+
}
|
|
476
|
+
if (!mine.length) html += '<div class="empty">No facts about <span class="mono">' + esc(focus) + "</span> match the current segments. Clear a segment on the left, or refocus from the minimap.</div>";
|
|
477
|
+
box.innerHTML = html;
|
|
478
|
+
box.querySelectorAll("[data-go]").forEach((b) => b.addEventListener("click", () => refocus(b.getAttribute("data-go"))));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function renderLooks() {
|
|
482
|
+
const box = el("looks"); box.innerHTML = "";
|
|
483
|
+
const w = LEDGER.worthALook || {};
|
|
484
|
+
const looks = [];
|
|
485
|
+
if (w.newestTaught) {
|
|
486
|
+
const r = rowById.get(w.newestTaught.rowId);
|
|
487
|
+
looks.push({ cls: "k-taught", tag: "newest teach", target: w.newestTaught.term, body: r ? esc(r.s + " " + r.phrase + " " + r.o) : esc(w.newestTaught.term) });
|
|
488
|
+
}
|
|
489
|
+
if (w.contradictions && w.contradictions.count) {
|
|
490
|
+
looks.push({ cls: "k-alert", tag: w.contradictions.count + " with more than one answer", target: w.contradictions.firstFocusTerm, body: '<span class="mono">' + esc(w.contradictions.firstFocusTerm) + "</span> has answers that differ — read both" });
|
|
491
|
+
}
|
|
492
|
+
if (w.biggestHub) {
|
|
493
|
+
looks.push({ cls: "k-hub", tag: "biggest hub", target: w.biggestHub.term, body: '<span class="mono">' + esc(w.biggestHub.term) + "</span> touches " + w.biggestHub.degree + " facts" });
|
|
494
|
+
}
|
|
495
|
+
for (const l of looks) {
|
|
496
|
+
const b = document.createElement("button");
|
|
497
|
+
b.className = "look " + l.cls;
|
|
498
|
+
b.innerHTML = '<span class="tag">' + esc(l.tag) + '</span><span class="body">' + l.body + "</span>";
|
|
499
|
+
if (l.target) b.addEventListener("click", () => refocus(l.target));
|
|
500
|
+
box.appendChild(b);
|
|
501
|
+
}
|
|
502
|
+
if (!looks.length) box.innerHTML = '<div class="empty">Nothing to flag yet.</div>';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let hits = [];
|
|
506
|
+
const cssVar = (n) => getComputedStyle(document.documentElement).getPropertyValue(n).trim();
|
|
507
|
+
function renderMap() {
|
|
508
|
+
const canvas = el("map");
|
|
509
|
+
const dpr = window.devicePixelRatio || 1;
|
|
510
|
+
const w = canvas.clientWidth || 230, h = 160;
|
|
511
|
+
canvas.width = w * dpr; canvas.height = h * dpr;
|
|
512
|
+
const ctx = canvas.getContext("2d"); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, w, h);
|
|
513
|
+
hits = [];
|
|
514
|
+
if (!focus) return;
|
|
515
|
+
const color = { taught: cssVar("--taught"), corpus: cssVar("--corpus"), entailed: cssVar("--entail"), entail: cssVar("--entail") };
|
|
516
|
+
const lineC = cssVar("--line"), inkC = cssVar("--ink");
|
|
517
|
+
const hop1 = new Set(), hop2 = new Set();
|
|
518
|
+
for (const e of LEDGER.edges) { if (e.s === focus) hop1.add(e.o); if (e.o === focus) hop1.add(e.s); }
|
|
519
|
+
hop1.delete(focus);
|
|
520
|
+
for (const e of LEDGER.edges) {
|
|
521
|
+
if (hop1.has(e.s) && e.o !== focus && !hop1.has(e.o)) hop2.add(e.o);
|
|
522
|
+
if (hop1.has(e.o) && e.s !== focus && !hop1.has(e.s)) hop2.add(e.s);
|
|
523
|
+
}
|
|
524
|
+
const cx = w / 2, cy = h / 2, r1 = 42, r2 = 68;
|
|
525
|
+
const pos = { [focus]: { x: cx, y: cy } };
|
|
526
|
+
const place = (set, r) => { const a = [...set].sort(); a.forEach((t, i) => { const ang = (2 * Math.PI * i) / a.length - Math.PI / 2; pos[t] = { x: cx + r * Math.cos(ang), y: cy + r * Math.sin(ang) }; }); };
|
|
527
|
+
place(hop1, r1); place(hop2, r2);
|
|
528
|
+
ctx.strokeStyle = lineC; ctx.lineWidth = 1;
|
|
529
|
+
for (const e of LEDGER.edges) { const a = pos[e.s], b = pos[e.o]; if (a && b) { ctx.globalAlpha = .5; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } }
|
|
530
|
+
ctx.globalAlpha = 1;
|
|
531
|
+
const visible = (t) => t === focus || hop2.has(t) ||
|
|
532
|
+
LEDGER.rows.some((r) => (r.s === t || r.o === t) && touches(r, focus) && passes(r, null));
|
|
533
|
+
for (const t of Object.keys(pos)) {
|
|
534
|
+
const p = pos[t], isF = t === focus, r = isF ? 6 : 4;
|
|
535
|
+
ctx.globalAlpha = visible(t) ? 1 : .25;
|
|
536
|
+
ctx.fillStyle = isF ? inkC : (color[(termIndex.get(t) || {}).prov] || inkC);
|
|
537
|
+
ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, 2 * Math.PI); ctx.fill();
|
|
538
|
+
hits.push({ x: p.x, y: p.y, r: r + 5, term: t });
|
|
539
|
+
}
|
|
540
|
+
ctx.globalAlpha = 1; ctx.fillStyle = inkC;
|
|
541
|
+
ctx.font = "9px ui-monospace, Menlo, monospace";
|
|
542
|
+
ctx.textAlign = "center";
|
|
543
|
+
ctx.fillText(focus, cx, cy - 10);
|
|
544
|
+
}
|
|
545
|
+
el("map").addEventListener("click", (e) => {
|
|
546
|
+
const rect = e.target.getBoundingClientRect();
|
|
547
|
+
const x = e.clientX - rect.left, y = e.clientY - rect.top;
|
|
548
|
+
for (const hit of hits) if ((x - hit.x) ** 2 + (y - hit.y) ** 2 <= hit.r ** 2) { refocus(hit.term); return; }
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
function renderCrumbs() {
|
|
552
|
+
const box = el("crumbs"); box.innerHTML = "";
|
|
553
|
+
trail.forEach((c, i) => {
|
|
554
|
+
if (i) { const s = document.createElement("span"); s.className = "sep"; s.textContent = "\\u203a"; box.appendChild(s); }
|
|
555
|
+
const b = document.createElement("button");
|
|
556
|
+
b.className = "crumb"; b.textContent = c.label || c.term;
|
|
557
|
+
b.title = c.term;
|
|
558
|
+
if (i === trail.length - 1) b.setAttribute("aria-current", "true");
|
|
559
|
+
b.addEventListener("click", () => { trail = trail.slice(0, i + 1); focus = c.term; render(); });
|
|
560
|
+
box.appendChild(b);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
function refocusWithLabel(term, label) {
|
|
564
|
+
if (!term || !termIndex.has(term)) return;
|
|
565
|
+
if (term !== focus) { focus = term; trail.push({ term, label: label || null }); }
|
|
566
|
+
render();
|
|
567
|
+
}
|
|
568
|
+
function refocus(term) { refocusWithLabel(term, null); }
|
|
569
|
+
el("q").addEventListener("keydown", (e) => {
|
|
570
|
+
if (e.key !== "Enter") return;
|
|
571
|
+
const want = e.target.value.trim().toLowerCase();
|
|
572
|
+
const hit = [...termIndex.keys()].find((t) => t.toLowerCase() === want);
|
|
573
|
+
if (hit) { el("qmiss").textContent = ""; e.target.value = ""; refocus(hit); }
|
|
574
|
+
else el("qmiss").textContent = "no such term";
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// ---- the chat dock: the SAME engine + payload the CLI answers from ------
|
|
578
|
+
const resolveAnsweredTerm = ${resolveAnsweredTerm.toString()};
|
|
579
|
+
const chatForm = el("chatform");
|
|
580
|
+
if (chatForm && typeof tmctMemoryAsk !== "undefined") {
|
|
581
|
+
const memHandle = tmctMemoryAsk.createInMemoryStore();
|
|
582
|
+
memHandle.payload = PAYLOAD;
|
|
583
|
+
const log = el("chatlog");
|
|
584
|
+
const addLine = (cls, html) => {
|
|
585
|
+
const d = document.createElement("div");
|
|
586
|
+
d.className = cls; d.innerHTML = html;
|
|
587
|
+
log.appendChild(d); log.scrollTop = log.scrollHeight;
|
|
588
|
+
};
|
|
589
|
+
chatForm.addEventListener("submit", (e) => {
|
|
590
|
+
e.preventDefault();
|
|
591
|
+
const input = el("chatq");
|
|
592
|
+
const q = input.value.trim();
|
|
593
|
+
if (!q) return;
|
|
594
|
+
input.value = "";
|
|
595
|
+
addLine("u", esc(q));
|
|
596
|
+
(async () => {
|
|
597
|
+
let fact = null;
|
|
598
|
+
try { fact = await tmctMemoryAsk.factAnswer(memHandle, q, null, true, {}); } catch { fact = null; }
|
|
599
|
+
// runAsk's own cascade is factAnswer ?? factReadBack; chain the same
|
|
600
|
+
// way when the bundle exposes the second reader (relation chases —
|
|
601
|
+
// "who is the grandfather of ishmael" — live there, not in factAnswer).
|
|
602
|
+
if (!(fact && fact.text) && typeof tmctMemoryAsk.factReadBack === "function") {
|
|
603
|
+
try { fact = await tmctMemoryAsk.factReadBack(memHandle, q, null, true, null); } catch { fact = null; }
|
|
604
|
+
}
|
|
605
|
+
if (fact && fact.text) {
|
|
606
|
+
addLine("a", esc(fact.text).replace(/\\n/g, "<br>"));
|
|
607
|
+
// Same formatting contract as chat's withGoalLine: capitalized,
|
|
608
|
+
// full-stop-terminated, rendered only when the engine deduced one.
|
|
609
|
+
if (fact.goal) addLine("a goal", "Goal (inferred): " + esc(fact.goal.charAt(0).toUpperCase() + fact.goal.slice(1)) + ".");
|
|
610
|
+
const hit = resolveAnsweredTerm(fact.text, q, LEDGER.terms, tmctMemoryAsk.normFactTerm);
|
|
611
|
+
if (hit) refocusWithLabel(hit, q);
|
|
612
|
+
} else {
|
|
613
|
+
const tips = LEDGER.terms.filter((t) => t.term.length >= 3).slice(0, 2)
|
|
614
|
+
.map((t) => '"what is ' + esc(t.term) + '"').join(" \\u00b7 ");
|
|
615
|
+
addLine("a miss", "I can't ground that in this graph" + (tips ? " \\u2014 try: " + tips : "") + ".");
|
|
616
|
+
}
|
|
617
|
+
})();
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function renderCounts() {
|
|
622
|
+
const m = LEDGER.meta;
|
|
623
|
+
el("counts").textContent = m.truncated ? "showing " + m.shown + " of " + m.total + " facts" : m.shown + " facts";
|
|
624
|
+
}
|
|
625
|
+
function render() { renderCounts(); renderCrumbs(); renderSegs(); renderLedger(); renderLooks(); renderMap(); }
|
|
626
|
+
window.addEventListener("resize", renderMap);
|
|
627
|
+
new MutationObserver(renderMap).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
|
628
|
+
if (window.matchMedia) window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", renderMap);
|
|
629
|
+
render();
|
|
630
|
+
})();
|
|
631
|
+
</script>
|
|
632
|
+
</body>
|
|
633
|
+
</html>
|
|
634
|
+
`;
|
|
635
|
+
}
|