@polycode-projects/the-mechanical-code-talker 2.8.3 → 2.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.8.3",
3
+ "version": "2.8.5",
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))} &middot; 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; white-space: nowrap; }
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 &middot; 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 &middot; click to refocus &middot; 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>