@polycode-projects/the-mechanical-code-talker 2.8.3 → 2.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/services/ledger-viz.mjs +239 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
|
@@ -170,6 +170,11 @@ export function computeLedgerDataFromPayload(payload, { focus, term, rowLimit =
|
|
|
170
170
|
contradictions: { count: contradictions.length, firstFocusTerm: firstContra ? firstContra.s : null },
|
|
171
171
|
biggestHub: terms.length ? { term: terms[0].term, degree: terms[0].degree } : null,
|
|
172
172
|
};
|
|
173
|
+
// Dashboard-strip stats: always over the FULL rows/terms/contradictions —
|
|
174
|
+
// computed here, before the row-cap section below, so a huge graph's
|
|
175
|
+
// tile numbers stay the whole-graph truth even when the ledger view itself
|
|
176
|
+
// degrades to "recent + local".
|
|
177
|
+
const stats = computeLedgerStats(rows, terms, contradictions);
|
|
173
178
|
|
|
174
179
|
// Row cap: the focus 2-hop neighborhood survives first (the minimap's own
|
|
175
180
|
// radius, so facet counts and the map stay mutually correct), the rest by
|
|
@@ -190,7 +195,7 @@ export function computeLedgerDataFromPayload(payload, { focus, term, rowLimit =
|
|
|
190
195
|
}
|
|
191
196
|
const meta = { shown: shownRows.length, total, truncated: shownRows.length < total };
|
|
192
197
|
|
|
193
|
-
return { rows: shownRows, terms, edges, focus: focusTerm, contradictions, worthALook, payload, meta };
|
|
198
|
+
return { rows: shownRows, terms, edges, focus: focusTerm, contradictions, worthALook, payload, meta, stats };
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
/** Load the memory graph under `repoDir` and derive the ledger data. Never
|
|
@@ -261,11 +266,207 @@ export function facetCounts(rows, focus, sel, now) {
|
|
|
261
266
|
return counts;
|
|
262
267
|
}
|
|
263
268
|
|
|
269
|
+
// ---- dashboard-strip stats: real counts over the FULL fact-row set --------
|
|
270
|
+
// (never the row-capped view — meta.total's own honesty contract) for the
|
|
271
|
+
// telemetry-style tiles atop the existing 3-column layout.
|
|
272
|
+
|
|
273
|
+
const BUNDLE_TOP_N = 6;
|
|
274
|
+
const PREDICATE_TOP_N = 6;
|
|
275
|
+
const SPARK_MAX_POINTS = 48;
|
|
276
|
+
|
|
277
|
+
/** Corpus-bundle grouping key for one fact's `src` string: the first
|
|
278
|
+
* pipe-segment, folded to its stable source-family prefix
|
|
279
|
+
* ("teach:chat:<session>@<ts>" -> "teach:chat", "corpus:human /r/IsA" ->
|
|
280
|
+
* "corpus:human", "import:hanoi-3.txt" stays whole — the filename IS the
|
|
281
|
+
* bundle). A closed table with a verbatim fallback, same posture as
|
|
282
|
+
* phraseFor: an unrecognized tag still reads as itself. */
|
|
283
|
+
export function bundleKeyFor(src) {
|
|
284
|
+
const first = String(src || "").split(" | ")[0].trim();
|
|
285
|
+
if (!first) return "unrecorded";
|
|
286
|
+
if (first.startsWith("teach:chat")) return "teach:chat";
|
|
287
|
+
if (first.startsWith("ace:chat")) return "ace:chat";
|
|
288
|
+
if (first === "operator" || first.startsWith("operator:")) return "operator";
|
|
289
|
+
if (first.startsWith("entailed")) {
|
|
290
|
+
const rule = first.slice("entailed".length).replace(/^[:\s]+/, "").split(/[\s:]/)[0];
|
|
291
|
+
return rule ? `entailed:${rule}` : "entailed";
|
|
292
|
+
}
|
|
293
|
+
if (first.startsWith("corpus:")) {
|
|
294
|
+
const name = first.slice("corpus:".length).split(" ")[0];
|
|
295
|
+
return name ? `corpus:${name}` : "corpus";
|
|
296
|
+
}
|
|
297
|
+
return first; // import:<file>, provider/reference/web sourceTypes, etc — verbatim
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Human label for a bundleKeyFor() key. */
|
|
301
|
+
export function bundleLabelFor(key) {
|
|
302
|
+
const k = String(key || "");
|
|
303
|
+
if (k === "teach:chat") return "taught via chat";
|
|
304
|
+
if (k === "ace:chat") return "taught (parsed)";
|
|
305
|
+
if (k === "operator") return "operator-asserted";
|
|
306
|
+
if (k === "unrecorded") return "unrecorded";
|
|
307
|
+
if (k.startsWith("corpus:")) return `corpus: ${k.slice(7)}`;
|
|
308
|
+
if (k.startsWith("import:")) return `imported: ${k.slice(7)}`;
|
|
309
|
+
if (k.startsWith("entailed:")) return `entailed: ${k.slice(9)}`;
|
|
310
|
+
if (k === "entailed") return "entailed";
|
|
311
|
+
return k;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Dashboard-strip stats over `rows` (the FULL set — pass it before any
|
|
315
|
+
* row-cap slicing) and `terms`/`contradictions` from the same derivation.
|
|
316
|
+
* Every number is a straight count or ratio over real rows; nothing here is
|
|
317
|
+
* estimated or placeholder. The ingestion sparkline plots cumulative fact
|
|
318
|
+
* count in TEACH order (oldest first), not wall-clock time — a store built
|
|
319
|
+
* in one script run has timestamps seconds apart, so order carries the
|
|
320
|
+
* signal the clock can't. */
|
|
321
|
+
export function computeLedgerStats(rows, terms, contradictions) {
|
|
322
|
+
const list = rows || [];
|
|
323
|
+
const byProv = { taught: 0, corpus: 0, entailed: 0 };
|
|
324
|
+
const byTier = { 1: 0, 2: 0, 3: 0 };
|
|
325
|
+
const bundleCounts = new Map();
|
|
326
|
+
const predicateCounts = new Map(); // predicate -> { phrase, count }
|
|
327
|
+
for (const r of list) {
|
|
328
|
+
byProv[r.prov] = (byProv[r.prov] || 0) + 1;
|
|
329
|
+
byTier[r.trustTier] = (byTier[r.trustTier] || 0) + 1;
|
|
330
|
+
const bk = bundleKeyFor(r.src);
|
|
331
|
+
bundleCounts.set(bk, (bundleCounts.get(bk) || 0) + 1);
|
|
332
|
+
const pc = predicateCounts.get(r.p) || { phrase: r.phrase, count: 0 };
|
|
333
|
+
pc.count += 1;
|
|
334
|
+
predicateCounts.set(r.p, pc);
|
|
335
|
+
}
|
|
336
|
+
const bundles = [...bundleCounts.entries()]
|
|
337
|
+
.map(([key, count]) => ({ key, label: bundleLabelFor(key), count }))
|
|
338
|
+
.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key))
|
|
339
|
+
.slice(0, BUNDLE_TOP_N);
|
|
340
|
+
const predicates = [...predicateCounts.entries()]
|
|
341
|
+
.map(([predicate, v]) => ({ predicate, phrase: v.phrase, count: v.count }))
|
|
342
|
+
.sort((a, b) => b.count - a.count || a.predicate.localeCompare(b.predicate))
|
|
343
|
+
.slice(0, PREDICATE_TOP_N);
|
|
344
|
+
|
|
345
|
+
const totalDegree = (terms || []).reduce((sum, t) => sum + (t.degree || 0), 0);
|
|
346
|
+
const density = terms && terms.length ? totalDegree / terms.length : 0;
|
|
347
|
+
|
|
348
|
+
// Cumulative fact count in teach order, sampled to SPARK_MAX_POINTS for a
|
|
349
|
+
// large graph — every plotted value is a real cumulative count at a real
|
|
350
|
+
// index, never interpolated.
|
|
351
|
+
const ordered = list.filter((r) => r.createdAt).slice().reverse(); // rows sort newest-first
|
|
352
|
+
const n = ordered.length;
|
|
353
|
+
const sparkline = [];
|
|
354
|
+
if (n > 0) {
|
|
355
|
+
const stride = Math.max(1, Math.ceil(n / SPARK_MAX_POINTS));
|
|
356
|
+
for (let i = stride - 1; i < n; i += stride) sparkline.push(i + 1);
|
|
357
|
+
if (sparkline[sparkline.length - 1] !== n) sparkline.push(n);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
totalFacts: list.length,
|
|
362
|
+
totalTerms: (terms || []).length,
|
|
363
|
+
byProv, byTier, bundles, predicates, density,
|
|
364
|
+
contradictionCount: (contradictions || []).length,
|
|
365
|
+
firstLearned: n ? ordered[0].createdAt : null,
|
|
366
|
+
lastLearned: n ? ordered[n - 1].createdAt : null,
|
|
367
|
+
sparkline,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const pct = (n, total) => (total > 0 ? (n / total) * 100 : 0);
|
|
372
|
+
|
|
373
|
+
/** The facts.by-tier tile: a proportional bar plus a counts legend, both over
|
|
374
|
+
* the SAME three taught/corpus/entailed tokens the rest of the page already
|
|
375
|
+
* colors provenance with. Server-rendered once; the tier split doesn't
|
|
376
|
+
* change with the interactive segment filters below it. */
|
|
377
|
+
function tierTileHtml(byProv, total) {
|
|
378
|
+
const segs = [["taught", byProv.taught || 0], ["corpus", byProv.corpus || 0], ["entail", byProv.entailed || 0]];
|
|
379
|
+
const bar = segs.map(([cls, n]) => `<span class="tseg t-${cls}" style="width:${pct(n, total).toFixed(2)}%"></span>`).join("");
|
|
380
|
+
const legend = segs.map(([cls, n]) => `<span class="tleg t-${cls}">${n} ${cls === "entail" ? "entailed" : cls}</span>`).join("");
|
|
381
|
+
return `<div class="tierbar" role="img" aria-label="${segs.map(([c, n]) => `${n} ${c === "entail" ? "entailed" : c}`).join(", ")} of ${total} facts">${bar}</div><div class="tierlegend">${legend}</div>`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** A small leaderboard bar list — bundles or predicates, each scaled to the
|
|
385
|
+
* list's own max (not the grand total), like a top-N panel on a dashboard. */
|
|
386
|
+
function microbarsHtml(items) {
|
|
387
|
+
const max = items.reduce((m, it) => Math.max(m, it.count), 0) || 1;
|
|
388
|
+
return items.map((it) =>
|
|
389
|
+
`<div class="bbar"><span class="bblabel">${escapeHtml(it.label)}</span><span class="bbtrack"><span class="bbfill" style="width:${pct(it.count, max).toFixed(1)}%"></span></span><span class="bbn">${it.count}</span></div>`,
|
|
390
|
+
).join("");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** The dashboard strip: fact/term totals, the tier bar, graph density, a data-
|
|
394
|
+
* quality tile keyed off the SAME contradiction count worthALook surfaces,
|
|
395
|
+
* and the corpus-bundle/predicate leaderboards. Every tile reads straight
|
|
396
|
+
* off `stats` (computeLedgerStats) — no client-side recomputation. */
|
|
397
|
+
function dashboardHtml(stats) {
|
|
398
|
+
const s = stats || {};
|
|
399
|
+
const total = s.totalFacts || 0;
|
|
400
|
+
if (!total) {
|
|
401
|
+
return `<section class="dash" aria-label="Ledger metrics"><div class="tile"><span class="tile-label">facts.total</span><span class="tile-value">0</span><span class="tile-sub">nothing taught yet</span></div></section>`;
|
|
402
|
+
}
|
|
403
|
+
const terms = s.totalTerms || 0;
|
|
404
|
+
const qualityCls = s.contradictionCount > 0 ? " tile-alert" : "";
|
|
405
|
+
const bundlesHtml = s.bundles?.length ? microbarsHtml(s.bundles) : `<span class="tile-sub">no sources recorded</span>`;
|
|
406
|
+
const predicatesHtml = s.predicates?.length
|
|
407
|
+
? microbarsHtml(s.predicates.map((p) => ({ label: p.phrase || p.predicate, count: p.count })))
|
|
408
|
+
: `<span class="tile-sub">none yet</span>`;
|
|
409
|
+
return `<section class="dash" aria-label="Ledger metrics">
|
|
410
|
+
<div class="tile">
|
|
411
|
+
<span class="tile-label">facts.total</span>
|
|
412
|
+
<span class="tile-value">${total}</span>
|
|
413
|
+
<span class="tile-sub">${terms} term${terms === 1 ? "" : "s"} tracked</span>
|
|
414
|
+
</div>
|
|
415
|
+
<div class="tile tile-wide">
|
|
416
|
+
<span class="tile-label">facts.by-tier</span>
|
|
417
|
+
${tierTileHtml(s.byProv || {}, total)}
|
|
418
|
+
</div>
|
|
419
|
+
<div class="tile">
|
|
420
|
+
<span class="tile-label">graph.avg-degree</span>
|
|
421
|
+
<span class="tile-value">${(s.density || 0).toFixed(1)}</span>
|
|
422
|
+
<span class="tile-sub">facts per term</span>
|
|
423
|
+
</div>
|
|
424
|
+
<div class="tile${qualityCls}">
|
|
425
|
+
<span class="tile-label">data.quality</span>
|
|
426
|
+
<span class="tile-value">${s.contradictionCount || 0}</span>
|
|
427
|
+
<span class="tile-sub">term${s.contradictionCount === 1 ? "" : "s"} with more than one answer</span>
|
|
428
|
+
</div>
|
|
429
|
+
<div class="tile tile-wide">
|
|
430
|
+
<span class="tile-label">corpus.bundles</span>
|
|
431
|
+
<div class="bundlebars">${bundlesHtml}</div>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="tile tile-wide">
|
|
434
|
+
<span class="tile-label">predicate.top</span>
|
|
435
|
+
<div class="bundlebars">${predicatesHtml}</div>
|
|
436
|
+
</div>
|
|
437
|
+
</section>`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** The aside's ingestion sparkline: a real cumulative-count polyline over
|
|
441
|
+
* `stats.sparkline` (see computeLedgerStats), server-rendered as inline SVG
|
|
442
|
+
* so it needs no client JS and repaints for free on a theme switch via the
|
|
443
|
+
* CSS custom properties its classes resolve against. */
|
|
444
|
+
function sparklineSvg(stats) {
|
|
445
|
+
const pts = stats?.sparkline || [];
|
|
446
|
+
const W = 200, H = 44, PAD = 3;
|
|
447
|
+
if (!pts.length) return `<p class="mapnote">not enough dated facts yet</p>`;
|
|
448
|
+
if (pts.length === 1) {
|
|
449
|
+
return `<svg class="spark" viewBox="0 0 ${W} ${H}" role="img" aria-label="1 fact taught"><circle class="dot" cx="${W / 2}" cy="${H / 2}" r="2.5"></circle></svg>`;
|
|
450
|
+
}
|
|
451
|
+
const max = pts[pts.length - 1]; // cumulative and monotonic — last is the max
|
|
452
|
+
const stepX = (W - PAD * 2) / (pts.length - 1);
|
|
453
|
+
const y = (v) => H - PAD - (v / max) * (H - PAD * 2);
|
|
454
|
+
const coords = pts.map((v, i) => [PAD + i * stepX, y(v)]);
|
|
455
|
+
const line = coords.map(([x, yy], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)},${yy.toFixed(1)}`).join(" ");
|
|
456
|
+
const area = `${line} L${coords[coords.length - 1][0].toFixed(1)},${H} L${coords[0][0].toFixed(1)},${H} Z`;
|
|
457
|
+
const last = coords[coords.length - 1];
|
|
458
|
+
return `<svg class="spark" viewBox="0 0 ${W} ${H}" role="img" aria-label="${pts.length} sampled points, ${max} facts total">
|
|
459
|
+
<path class="fill" d="${area}"></path>
|
|
460
|
+
<path class="line" d="${line}"></path>
|
|
461
|
+
<circle class="dot" cx="${last[0].toFixed(1)}" cy="${last[1].toFixed(1)}" r="2.5"></circle>
|
|
462
|
+
</svg>`;
|
|
463
|
+
}
|
|
464
|
+
|
|
264
465
|
/** One complete, self-contained document: the ledger, segment rail,
|
|
265
466
|
* worth-a-look panel, breadcrumb/search, two-hop minimap, and (when the
|
|
266
467
|
* memory-ask bundle is present) the ask-the-graph chat dock, all over the
|
|
267
468
|
* embedded LEDGER/PAYLOAD data. */
|
|
268
|
-
export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, worthALook, payload, meta, memoryAskBundle } = {}) {
|
|
469
|
+
export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, worthALook, payload, meta, memoryAskBundle, stats } = {}) {
|
|
269
470
|
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 } });
|
|
270
471
|
const payloadJson = embedJson(payload || { individuals: [], objectProperties: [] });
|
|
271
472
|
const shown = meta?.shown ?? (rows || []).length;
|
|
@@ -287,6 +488,11 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
|
|
|
287
488
|
</form>
|
|
288
489
|
</div>`
|
|
289
490
|
: `<div class="chat chat-off"><p class="chatnote">chat unavailable — run <span class="mono">npm run build:ask-bundle</span> to enable the in-page ask engine.</p></div>`;
|
|
491
|
+
const sparkCaption = stats?.firstLearned && stats?.lastLearned
|
|
492
|
+
? (stats.firstLearned.slice(0, 10) === stats.lastLearned.slice(0, 10)
|
|
493
|
+
? `learned ${escapeHtml(stats.lastLearned.slice(0, 10))}`
|
|
494
|
+
: `first ${escapeHtml(stats.firstLearned.slice(0, 10))} · last ${escapeHtml(stats.lastLearned.slice(0, 10))}`)
|
|
495
|
+
: "cumulative facts, teach order";
|
|
290
496
|
|
|
291
497
|
return `<!doctype html>
|
|
292
498
|
<html lang="en">
|
|
@@ -312,6 +518,30 @@ ${THEME_TOKENS_CSS}
|
|
|
312
518
|
.search { margin-left: auto; display: flex; align-items: center; gap: .5rem; }
|
|
313
519
|
.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; }
|
|
314
520
|
.search .miss { font-size: .72rem; color: var(--alert); font-family: ${MONO_STACK}; }
|
|
521
|
+
.dash { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: .6rem; margin: 0 0 1.1rem; }
|
|
522
|
+
.tile { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: .55rem .7rem .6rem; display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
|
|
523
|
+
.tile-wide { grid-column: span 2; }
|
|
524
|
+
.tile-alert { border-color: var(--alert); }
|
|
525
|
+
.tile-label { font-family: ${MONO_STACK}; font-size: .62rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
|
|
526
|
+
.tile-value { font-family: ${MONO_STACK}; font-size: 1.5rem; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.15; }
|
|
527
|
+
.tile-alert .tile-value { color: var(--alert); }
|
|
528
|
+
.tile-sub { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
529
|
+
.tierbar { display: flex; height: 8px; border-radius: 99px; overflow: hidden; background: var(--line); margin-top: .3rem; }
|
|
530
|
+
.tseg { height: 100%; }
|
|
531
|
+
.tseg.t-taught { background: var(--taught); } .tseg.t-corpus { background: var(--corpus); } .tseg.t-entail { background: var(--entail); }
|
|
532
|
+
.tierlegend { display: flex; flex-wrap: wrap; gap: .15rem .7rem; margin-top: .35rem; font-family: ${MONO_STACK}; font-size: .65rem; }
|
|
533
|
+
.tleg.t-taught { color: var(--taught); } .tleg.t-corpus { color: var(--corpus); } .tleg.t-entail { color: var(--entail); }
|
|
534
|
+
.bundlebars { display: flex; flex-direction: column; gap: .22rem; margin-top: .35rem; }
|
|
535
|
+
.bbar { display: grid; grid-template-columns: minmax(0,1fr) 3.4rem 1.6rem; align-items: center; gap: .4rem; font-family: ${MONO_STACK}; font-size: .68rem; }
|
|
536
|
+
.bblabel { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
537
|
+
.bbtrack { background: var(--line); border-radius: 99px; height: 5px; overflow: hidden; }
|
|
538
|
+
.bbfill { display: block; height: 100%; background: var(--ink); opacity: .55; border-radius: 99px; }
|
|
539
|
+
.bbn { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
|
|
540
|
+
.sparkwrap { margin-top: .5rem; }
|
|
541
|
+
.spark { display: block; width: 100%; height: 44px; }
|
|
542
|
+
.spark .fill { fill: var(--muted); opacity: .18; stroke: none; }
|
|
543
|
+
.spark .line { fill: none; stroke: var(--ink); stroke-width: 1.4; }
|
|
544
|
+
.spark .dot { fill: var(--ink); }
|
|
315
545
|
.app { display: grid; grid-template-columns: 190px minmax(0,1fr) 230px; gap: 1.3rem; grid-template-areas: "rail ledger aside"; }
|
|
316
546
|
.rail { grid-area: rail; } .ledger { grid-area: ledger; } .aside { grid-area: aside; }
|
|
317
547
|
@media (max-width: 880px) { .app { grid-template-columns: 1fr; grid-template-areas: "ledger" "aside" "rail"; } .search { margin-left: 0; } }
|
|
@@ -341,7 +571,7 @@ ${THEME_TOKENS_CSS}
|
|
|
341
571
|
.chip { font-family: ${MONO_STACK}; font-size: .76rem; padding: .05rem .45rem; border: 1px solid var(--line); border-radius: 99px; background: var(--bg); }
|
|
342
572
|
button.chip:hover { border-color: var(--ink); }
|
|
343
573
|
.chip.here { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
344
|
-
.prov { margin-left: auto; font-family: ${MONO_STACK}; font-size: .64rem; padding: .08rem .5rem; border-radius: 99px;
|
|
574
|
+
.prov { margin-left: auto; font-family: ${MONO_STACK}; font-size: .64rem; padding: .08rem .5rem; border-radius: 99px; max-width: 100%; overflow-wrap: anywhere; }
|
|
345
575
|
.prov.p-taught { color: var(--taught); background: var(--taught-soft); }
|
|
346
576
|
.prov.p-corpus { color: var(--corpus); background: var(--corpus-soft); }
|
|
347
577
|
.prov.p-entail { color: var(--entail); background: var(--entail-soft); }
|
|
@@ -376,6 +606,7 @@ ${THEME_TOKENS_CSS}
|
|
|
376
606
|
<main>
|
|
377
607
|
<div class="eyebrow"><span>tmct · memory ledger</span><span id="counts"></span></div>
|
|
378
608
|
<h1>A graph you can read</h1>
|
|
609
|
+
${dashboardHtml(stats)}
|
|
379
610
|
<div class="topbar">
|
|
380
611
|
<nav class="crumbs" id="crumbs" aria-label="Focus trail"></nav>
|
|
381
612
|
<div class="search">
|
|
@@ -400,6 +631,11 @@ ${THEME_TOKENS_CSS}
|
|
|
400
631
|
<canvas id="map" width="230" height="160" aria-label="Two-hop neighborhood minimap"></canvas>
|
|
401
632
|
<p class="mapnote">dots = terms · click to refocus · dim = filtered out</p>
|
|
402
633
|
</div>
|
|
634
|
+
<h2>ingestion</h2>
|
|
635
|
+
<div class="mapwrap sparkwrap">
|
|
636
|
+
${sparklineSvg(stats)}
|
|
637
|
+
<p class="mapnote">${sparkCaption}</p>
|
|
638
|
+
</div>
|
|
403
639
|
</aside>
|
|
404
640
|
</div>
|
|
405
641
|
</main>
|