@polycode-projects/the-mechanical-code-talker 2.8.9 → 2.8.11
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
|
+
"version": "2.8.11",
|
|
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.",
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
"build:chat-bundle": "node scripts/build-chat-bundle.mjs",
|
|
139
139
|
"build:spider-fly-bundle": "node scripts/build-spider-fly-bundle.mjs",
|
|
140
140
|
"build:plan-bundle": "node scripts/build-plan-bundle.mjs",
|
|
141
|
+
"build:ledger-bundle": "node scripts/build-ledger-bundle.mjs",
|
|
141
142
|
"build:chat-seed": "node scripts/build-chat-seed.mjs",
|
|
142
143
|
"build:demo-graph": "node scripts/build-demo-graph.mjs",
|
|
143
144
|
"build:demo-pack": "node scripts/build-demo-pack.mjs",
|
|
@@ -393,12 +393,20 @@ function microbarsHtml(items) {
|
|
|
393
393
|
/** The dashboard strip: fact/term totals, the tier bar, graph density, a data-
|
|
394
394
|
* quality tile keyed off the SAME contradiction count worthALook surfaces,
|
|
395
395
|
* and the corpus-bundle/predicate leaderboards. Every tile reads straight
|
|
396
|
-
* off `stats` (computeLedgerStats) — no client-side recomputation.
|
|
397
|
-
|
|
396
|
+
* off `stats` (computeLedgerStats) — no client-side recomputation.
|
|
397
|
+
*
|
|
398
|
+
* Also the live dock's own re-render target: `id="dash"` gives a post-teach
|
|
399
|
+
* refresh a single element to replace (`el("dash").outerHTML = dashboardHtml(...)`
|
|
400
|
+
* — see renderLedgerHtml's inline script), and `fresh: true` marks that
|
|
401
|
+
* replacement with a quiet settle-fade (`.dash.fresh`, below) rather than a
|
|
402
|
+
* silent swap — the SAME function serves the server-rendered initial paint
|
|
403
|
+
* (fresh omitted) and every live update after it, so the two can never drift. */
|
|
404
|
+
function dashboardHtml(stats, { fresh = false } = {}) {
|
|
398
405
|
const s = stats || {};
|
|
399
406
|
const total = s.totalFacts || 0;
|
|
407
|
+
const freshCls = fresh ? " fresh" : "";
|
|
400
408
|
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>`;
|
|
409
|
+
return `<section class="dash${freshCls}" id="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
410
|
}
|
|
403
411
|
const terms = s.totalTerms || 0;
|
|
404
412
|
const qualityCls = s.contradictionCount > 0 ? " tile-alert" : "";
|
|
@@ -406,7 +414,7 @@ function dashboardHtml(stats) {
|
|
|
406
414
|
const predicatesHtml = s.predicates?.length
|
|
407
415
|
? microbarsHtml(s.predicates.map((p) => ({ label: p.phrase || p.predicate, count: p.count })))
|
|
408
416
|
: `<span class="tile-sub">none yet</span>`;
|
|
409
|
-
return `<section class="dash" aria-label="Ledger metrics">
|
|
417
|
+
return `<section class="dash${freshCls}" id="dash" aria-label="Ledger metrics">
|
|
410
418
|
<div class="tile">
|
|
411
419
|
<span class="tile-label">facts.total</span>
|
|
412
420
|
<span class="tile-value">${total}</span>
|
|
@@ -462,11 +470,40 @@ function sparklineSvg(stats) {
|
|
|
462
470
|
</svg>`;
|
|
463
471
|
}
|
|
464
472
|
|
|
473
|
+
/** The sparkline's own caption — "learned <date>" for a single-day graph,
|
|
474
|
+
* "first … last …" once it spans more than one, or the generic fallback
|
|
475
|
+
* before any dated fact exists. Its own named function (not inlined at the
|
|
476
|
+
* one server call site) so the live dock's post-teach refresh can call the
|
|
477
|
+
* identical logic client-side — see renderLedgerHtml's `sparkCaptionHtml`
|
|
478
|
+
* toString-embed, below. */
|
|
479
|
+
function sparkCaptionHtml(stats) {
|
|
480
|
+
return stats?.firstLearned && stats?.lastLearned
|
|
481
|
+
? (stats.firstLearned.slice(0, 10) === stats.lastLearned.slice(0, 10)
|
|
482
|
+
? `learned ${escapeHtml(stats.lastLearned.slice(0, 10))}`
|
|
483
|
+
: `first ${escapeHtml(stats.firstLearned.slice(0, 10))} · last ${escapeHtml(stats.lastLearned.slice(0, 10))}`)
|
|
484
|
+
: "cumulative facts, teach order";
|
|
485
|
+
}
|
|
486
|
+
|
|
465
487
|
/** One complete, self-contained document: the ledger, segment rail,
|
|
466
488
|
* worth-a-look panel, breadcrumb/search, two-hop minimap, and (when the
|
|
467
489
|
* memory-ask bundle is present) the ask-the-graph chat dock, all over the
|
|
468
|
-
* embedded LEDGER/PAYLOAD data.
|
|
469
|
-
|
|
490
|
+
* embedded LEDGER/PAYLOAD data.
|
|
491
|
+
*
|
|
492
|
+
* `ledgerBundleAvailable` (default false — every existing caller, including
|
|
493
|
+
* bin/tmct.mjs's `tmct viz` and every test that doesn't pass it) governs
|
|
494
|
+
* ONE thing: whether an external `<script src="./ledger-browser.bundle.js">`
|
|
495
|
+
* reference is emitted at all. That bundle carries the full runTurn engine
|
|
496
|
+
* (teach AND ask) and is Pages-demo-site-only — never built or shipped
|
|
497
|
+
* alongside the CLI's own output — so the CLI's `renderLedgerHtml` call
|
|
498
|
+
* never sets this and the page stays exactly as documented above, one
|
|
499
|
+
* self-contained document with no external requests. Only
|
|
500
|
+
* scripts/build-demo-site.mjs, which builds the sibling bundle itself
|
|
501
|
+
* first, passes `true`. The dock's own runtime code below ALSO gates on
|
|
502
|
+
* `typeof tmctLedger !== "undefined"` regardless — the two checks answer
|
|
503
|
+
* different questions (did this render even offer the reference; did the
|
|
504
|
+
* browser actually manage to load it), and both must hold for the live
|
|
505
|
+
* path to run. */
|
|
506
|
+
export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, worthALook, payload, meta, memoryAskBundle, stats, ledgerBundleAvailable = false } = {}) {
|
|
470
507
|
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 } });
|
|
471
508
|
const payloadJson = embedJson(payload || { individuals: [], objectProperties: [] });
|
|
472
509
|
const shown = meta?.shown ?? (rows || []).length;
|
|
@@ -474,7 +511,10 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
|
|
|
474
511
|
const bundleStr = typeof memoryAskBundle === "string" ? memoryAskBundle : "";
|
|
475
512
|
const hasMemChat = bundleStr.length > 0;
|
|
476
513
|
// The placeholder is honest: the canonical exchange only when its terms are
|
|
477
|
-
// really in this payload, otherwise a real term from this graph.
|
|
514
|
+
// really in this payload, otherwise a real term from this graph. Left as
|
|
515
|
+
// the query-only wording even when the live bundle is offered — the dock's
|
|
516
|
+
// own script swaps it for a teach-aware placeholder the moment it confirms
|
|
517
|
+
// tmctLedger actually loaded (never claimed ahead of that confirmation).
|
|
478
518
|
const termSet = new Set((terms || []).map((t) => t.term));
|
|
479
519
|
const placeholder = termSet.has("ishmael")
|
|
480
520
|
? "who is the grandfather of ishmael"
|
|
@@ -488,11 +528,6 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
|
|
|
488
528
|
</form>
|
|
489
529
|
</div>`
|
|
490
530
|
: `<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";
|
|
496
531
|
|
|
497
532
|
return `<!doctype html>
|
|
498
533
|
<html lang="en">
|
|
@@ -594,12 +629,26 @@ ${THEME_TOKENS_CSS}
|
|
|
594
629
|
.chatlog .a { font-size: .9rem; line-height: 1.45; }
|
|
595
630
|
.chatlog .a.miss { color: var(--muted); }
|
|
596
631
|
.chatlog .a.goal { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); }
|
|
632
|
+
.chatlog .a.pending { color: var(--muted); font-style: italic; }
|
|
633
|
+
.chatlog .a.taught { border-left: 2px solid var(--taught); padding-left: .55rem; }
|
|
634
|
+
.chatlog .a.taught .tag { display: block; font-family: ${MONO_STACK}; font-size: .62rem; letter-spacing: .06em; text-transform: uppercase; color: var(--taught); margin-bottom: .18rem; }
|
|
597
635
|
.chatask { display: flex; align-items: center; gap: .5rem; }
|
|
598
636
|
.chatlog:not(:empty) + .chatask { border-top: 1px solid var(--line); margin-top: .55rem; padding-top: .55rem; }
|
|
599
637
|
.chatask .prompt { color: var(--taught); font-size: .78rem; }
|
|
600
638
|
.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; }
|
|
639
|
+
.chatask input:disabled { opacity: .6; }
|
|
601
640
|
.chatnote { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); margin: 0; }
|
|
602
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
641
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
642
|
+
.seg, .chip, .look { transition: border-color .12s ease, background-color .12s ease; }
|
|
643
|
+
/* A live teach re-render swaps the dash section wholesale (dashboardHtml
|
|
644
|
+
is called again in full — see the inline script below), so the "this
|
|
645
|
+
just changed" signal has to be an animation on the fresh markup itself,
|
|
646
|
+
never a transition (which needs an old and new state on the SAME node
|
|
647
|
+
to interpolate between). Off entirely under reduced motion — the
|
|
648
|
+
numbers still update, just without the settle-fade. */
|
|
649
|
+
.dash.fresh .tile { animation: freshsettle 1.6s ease-out; }
|
|
650
|
+
@keyframes freshsettle { from { background: var(--taught-soft); } to { background: var(--card); } }
|
|
651
|
+
}
|
|
603
652
|
</style>
|
|
604
653
|
</head>
|
|
605
654
|
<body>
|
|
@@ -632,18 +681,22 @@ ${THEME_TOKENS_CSS}
|
|
|
632
681
|
<p class="mapnote">dots = terms · click to refocus · dim = filtered out</p>
|
|
633
682
|
</div>
|
|
634
683
|
<h2>ingestion</h2>
|
|
635
|
-
<div class="mapwrap sparkwrap">
|
|
684
|
+
<div class="mapwrap sparkwrap" id="sparkWrap">
|
|
636
685
|
${sparklineSvg(stats)}
|
|
637
|
-
<p class="mapnote">${
|
|
686
|
+
<p class="mapnote">${sparkCaptionHtml(stats)}</p>
|
|
638
687
|
</div>
|
|
639
688
|
</aside>
|
|
640
689
|
</div>
|
|
641
690
|
</main>
|
|
642
691
|
<script>
|
|
643
|
-
const
|
|
692
|
+
// let, not const: a successful live teach reassigns this wholesale
|
|
693
|
+
// (applyLedgerData, in the script below) so the page re-renders the graph
|
|
694
|
+
// it actually holds, not the snapshot from the moment the page loaded.
|
|
695
|
+
let LEDGER = ${ledgerJson};
|
|
644
696
|
const PAYLOAD = ${payloadJson};
|
|
645
697
|
</script>
|
|
646
698
|
${hasMemChat ? `<script>\n${bundleStr}\n</script>` : ""}
|
|
699
|
+
${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` : ""}
|
|
647
700
|
<script>
|
|
648
701
|
(function () {
|
|
649
702
|
"use strict";
|
|
@@ -651,15 +704,39 @@ ${hasMemChat ? `<script>\n${bundleStr}\n</script>` : ""}
|
|
|
651
704
|
const facetCounts = ${facetCounts.toString()};
|
|
652
705
|
const el = (id) => document.getElementById(id);
|
|
653
706
|
const esc = ${escapeHtml.toString()};
|
|
707
|
+
// Aliased so the dashboard/sparkline builders below (toString-embedded
|
|
708
|
+
// verbatim from ledger-viz.mjs's own Node-side source, which calls
|
|
709
|
+
// escapeHtml/pct by their real names) resolve without a second copy.
|
|
710
|
+
const escapeHtml = esc;
|
|
711
|
+
const pct = ${pct.toString()};
|
|
712
|
+
const tierTileHtml = ${tierTileHtml.toString()};
|
|
713
|
+
const microbarsHtml = ${microbarsHtml.toString()};
|
|
714
|
+
const dashboardHtml = ${dashboardHtml.toString()};
|
|
715
|
+
const sparklineSvg = ${sparklineSvg.toString()};
|
|
716
|
+
const sparkCaptionHtml = ${sparkCaptionHtml.toString()};
|
|
654
717
|
const FAMS = ["is-a", "has", "can", "used-for", "rests-on", "role", "other"];
|
|
655
718
|
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" };
|
|
656
719
|
const PROVS = [["taught", "you taught"], ["corpus", "corpus"], ["entail", "entailed"]];
|
|
657
720
|
const RECS = ["today", "this week", "older"];
|
|
658
721
|
const provKey = (p) => (p === "entailed" ? "entail" : p); // css class key
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
722
|
+
|
|
723
|
+
// LEDGER (declared "let" in the embed script above this one — a classic,
|
|
724
|
+
// non-module script's top-level bindings are reachable as bare identifiers
|
|
725
|
+
// by every later script tag in the document, never as window.LEDGER; see
|
|
726
|
+
// e2e/pages-ledger.test.mjs's own note on this) and its indexes start as
|
|
727
|
+
// the server-rendered snapshot but are REASSIGNED wholesale after a live
|
|
728
|
+
// teach (applyLedgerData, below) — a successful teach through the dock
|
|
729
|
+
// changes the underlying graph, and the page has to show that, not keep
|
|
730
|
+
// rendering the page-load snapshot next to a chat log that merely SAYS
|
|
731
|
+
// something new was learned.
|
|
732
|
+
let termIndex, rowById, contraById;
|
|
733
|
+
function rebuildIndexes() {
|
|
734
|
+
termIndex = new Map(LEDGER.terms.map((t) => [t.term, t]));
|
|
735
|
+
rowById = new Map(LEDGER.rows.map((r) => [r.id, r]));
|
|
736
|
+
contraById = new Map();
|
|
737
|
+
LEDGER.contradictions.forEach((ids, gi) => ids.forEach((id) => contraById.set(id, gi)));
|
|
738
|
+
}
|
|
739
|
+
rebuildIndexes();
|
|
663
740
|
|
|
664
741
|
let focus = LEDGER.focus;
|
|
665
742
|
let trail = focus ? [{ term: focus, label: null }] : [];
|
|
@@ -845,10 +922,133 @@ ${hasMemChat ? `<script>\n${bundleStr}\n</script>` : ""}
|
|
|
845
922
|
else el("qmiss").textContent = "no such term";
|
|
846
923
|
});
|
|
847
924
|
|
|
848
|
-
//
|
|
925
|
+
// A successful live teach re-derives the WHOLE ledger (computeLedgerDataFromPayload,
|
|
926
|
+
// the sibling bundle's own re-export of the exact function this page's build
|
|
927
|
+
// step called) and re-mounts it here — the same view a fresh page load
|
|
928
|
+
// would render, never a stale snapshot next to a chat log that only SAYS
|
|
929
|
+
// something new was learned. The dashboard/sparkline sections are replaced
|
|
930
|
+
// wholesale (dashboardHtml/sparklineSvg, toString-embedded above, are the
|
|
931
|
+
// SAME functions the server used for the very first paint) rather than
|
|
932
|
+
// patched, so they can never drift from what a fresh render would produce.
|
|
933
|
+
function applyLedgerData(freshData) {
|
|
934
|
+
LEDGER = {
|
|
935
|
+
rows: freshData.rows, terms: freshData.terms, edges: freshData.edges,
|
|
936
|
+
focus: freshData.focus, contradictions: freshData.contradictions,
|
|
937
|
+
worthALook: freshData.worthALook, meta: freshData.meta,
|
|
938
|
+
};
|
|
939
|
+
rebuildIndexes();
|
|
940
|
+
el("dash").outerHTML = dashboardHtml(freshData.stats, { fresh: true });
|
|
941
|
+
el("sparkWrap").innerHTML = sparklineSvg(freshData.stats) + '<p class="mapnote">' + sparkCaptionHtml(freshData.stats) + "</p>";
|
|
942
|
+
// computeLedgerDataFromPayload resolves an unset focus to the newest
|
|
943
|
+
// taught row's own subject — passing no explicit term (below) means every
|
|
944
|
+
// successful teach naturally jumps the view to what was just learned.
|
|
945
|
+
if (freshData.focus) refocusWithLabel(freshData.focus, null);
|
|
946
|
+
else render();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// ---- the chat dock: LIVE teach+ask over the sibling ledger-browser.bundle.js
|
|
950
|
+
// (window.tmctLedger) when the demo build offered it AND it actually
|
|
951
|
+
// loaded; falls back to the read-only tmctMemoryAsk engine below —
|
|
952
|
+
// unchanged from before this page could teach at all — otherwise.
|
|
953
|
+
// bin/tmct.mjs's own \`tmct viz\` output never offers the live bundle
|
|
954
|
+
// (renderLedgerHtml's own ledgerBundleAvailable defaults false there), so
|
|
955
|
+
// this branch is simply never reachable on a CLI-generated page.
|
|
849
956
|
const resolveAnsweredTerm = ${resolveAnsweredTerm.toString()};
|
|
850
957
|
const chatForm = el("chatform");
|
|
851
|
-
if (chatForm && typeof
|
|
958
|
+
if (chatForm && typeof tmctLedger !== "undefined" && typeof tmctLedger.createLedgerSession === "function") {
|
|
959
|
+
const log = el("chatlog");
|
|
960
|
+
const chatqEl = el("chatq");
|
|
961
|
+
chatqEl.placeholder = 'ask or teach the graph\\u2026 e.g. "blue is a peg"';
|
|
962
|
+
const addLine = (cls, html) => {
|
|
963
|
+
const d = document.createElement("div");
|
|
964
|
+
d.className = cls; d.innerHTML = html;
|
|
965
|
+
log.appendChild(d); log.scrollTop = log.scrollHeight;
|
|
966
|
+
return d;
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
let session = null;
|
|
970
|
+
// Serializes every engine-touching call through this one dock — the same
|
|
971
|
+
// posture plan-viz.mjs's own chat-assert dock takes with its withLock,
|
|
972
|
+
// so a fast double-submit can never race two turns over one session.
|
|
973
|
+
let lock = Promise.resolve();
|
|
974
|
+
const withLock = (fn) => { const run = lock.then(fn, fn); lock = run.catch(() => {}); return run; };
|
|
975
|
+
|
|
976
|
+
// The SAME bounded-race wink-nlp CDN load plan-viz.mjs's own chat-assert
|
|
977
|
+
// dock uses: a cross-origin dynamic import() can neither resolve nor
|
|
978
|
+
// reject on some failures, so an unbounded await would leave a session
|
|
979
|
+
// stuck forever. Best-effort — a teach sentence that needs the lemma tier
|
|
980
|
+
// just declines honestly without it, same as a checkout missing the
|
|
981
|
+
// optional deps.
|
|
982
|
+
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
983
|
+
const winkTimeout = (ms, reason) => new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
|
|
984
|
+
let winkReady = null;
|
|
985
|
+
function tryLoadWink() {
|
|
986
|
+
if (winkReady) return winkReady;
|
|
987
|
+
winkReady = (async () => {
|
|
988
|
+
try {
|
|
989
|
+
const mods = await Promise.race([
|
|
990
|
+
Promise.all([import("wink-nlp"), import("wink-eng-lite-web-model")]),
|
|
991
|
+
winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink-nlp CDN load timed out"),
|
|
992
|
+
]);
|
|
993
|
+
const winkNLP = mods[0].default;
|
|
994
|
+
const model = mods[1].default;
|
|
995
|
+
tmctLedger.registerWinkModel(() => ({ winkNLP: winkNLP, model: model }));
|
|
996
|
+
} catch (err) {
|
|
997
|
+
// eslint-disable-next-line no-console
|
|
998
|
+
console.warn("tmct ledger: wink-nlp CDN load failed, continuing without the lemma/POS tier", err);
|
|
999
|
+
}
|
|
1000
|
+
})();
|
|
1001
|
+
return winkReady;
|
|
1002
|
+
}
|
|
1003
|
+
tryLoadWink(); // fire eagerly at load, so it is likely settled by the first interaction
|
|
1004
|
+
|
|
1005
|
+
async function ensureSession() {
|
|
1006
|
+
if (session) return session;
|
|
1007
|
+
await tryLoadWink();
|
|
1008
|
+
session = await tmctLedger.createLedgerSession({ seedPayload: PAYLOAD });
|
|
1009
|
+
return session;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
chatForm.addEventListener("submit", (e) => {
|
|
1013
|
+
e.preventDefault();
|
|
1014
|
+
const q = chatqEl.value.trim();
|
|
1015
|
+
if (!q) return;
|
|
1016
|
+
chatqEl.value = "";
|
|
1017
|
+
addLine("u", esc(q));
|
|
1018
|
+
const pending = addLine("a pending", "computing\\u2026");
|
|
1019
|
+
chatqEl.disabled = true;
|
|
1020
|
+
withLock(async () => {
|
|
1021
|
+
try {
|
|
1022
|
+
const s = await ensureSession();
|
|
1023
|
+
const result = await s.turn(q);
|
|
1024
|
+
const record = result.record;
|
|
1025
|
+
const taught = !!record && record.miss === false && (record.via === "assert" || record.via === "retract");
|
|
1026
|
+
const body = esc(result.answer).replace(/\\n/g, "<br>");
|
|
1027
|
+
pending.className = "a" + (taught ? " taught" : (record && record.miss ? " miss" : ""));
|
|
1028
|
+
pending.innerHTML = taught ? '<span class="tag">taught</span>' + body : body;
|
|
1029
|
+
if (taught) {
|
|
1030
|
+
const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
|
|
1031
|
+
applyLedgerData(fresh);
|
|
1032
|
+
} else if (!(record && record.miss)) {
|
|
1033
|
+
// Only a genuine answer (never a miss) tries to resolve a
|
|
1034
|
+
// refocus target — the honest-miss cascade's own boilerplate
|
|
1035
|
+
// ("Run \`tmct init\`…") can contain a real term as an
|
|
1036
|
+
// ordinary English word (e.g. "run", if the graph happens to
|
|
1037
|
+
// hold it), and resolveAnsweredTerm has no way to tell that
|
|
1038
|
+
// apart from the term genuinely being discussed.
|
|
1039
|
+
const hit = resolveAnsweredTerm(result.answer, q, LEDGER.terms, tmctLedger.normFactTerm);
|
|
1040
|
+
if (hit) refocusWithLabel(hit, q);
|
|
1041
|
+
}
|
|
1042
|
+
} catch {
|
|
1043
|
+
pending.className = "a miss";
|
|
1044
|
+
pending.textContent = "Something went wrong answering that. Try rephrasing, or reload the page.";
|
|
1045
|
+
} finally {
|
|
1046
|
+
chatqEl.disabled = false;
|
|
1047
|
+
chatqEl.focus();
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
} else if (chatForm && typeof tmctMemoryAsk !== "undefined") {
|
|
852
1052
|
const memHandle = tmctMemoryAsk.createInMemoryStore();
|
|
853
1053
|
memHandle.payload = PAYLOAD;
|
|
854
1054
|
const log = el("chatlog");
|
|
@@ -283,6 +283,78 @@ export function buildSpriteCatalogEntries({ iconTemplates = [], largeTemplates =
|
|
|
283
283
|
});
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
// ---- scene composer (pure) ----
|
|
287
|
+
//
|
|
288
|
+
// The free-text "there is a..." box (PLAN_GAMES_UPLIFT_V3.md's own precedent:
|
|
289
|
+
// adventure-viz.mjs's roomSceneObjects/room-frame) over THIS page's own
|
|
290
|
+
// already-real classes — never a general NLU pass. extractSceneItems is the
|
|
291
|
+
// one pure, unit-testable piece; the DOM index it's matched against
|
|
292
|
+
// (className -> real swatch labels, read straight off this page's own
|
|
293
|
+
// already-rendered `.card`/`.swatch-label` markup at load time — see this
|
|
294
|
+
// module's own header for why that beats re-embedding the same SVG data a
|
|
295
|
+
// second time) is built client-side in the inline script below, since
|
|
296
|
+
// walking rendered DOM has no meaning in this pure module.
|
|
297
|
+
|
|
298
|
+
/** `text`'s lowercase word runs with their token index, the unit
|
|
299
|
+
* extractSceneItems matches class names against — punctuation never fuses
|
|
300
|
+
* two real words into one token nor splits one real word into two. */
|
|
301
|
+
function tokenizeSceneText(text) {
|
|
302
|
+
const tokens = [];
|
|
303
|
+
const re = /[A-Za-z]+/g;
|
|
304
|
+
let m;
|
|
305
|
+
while ((m = re.exec(String(text ?? "")))) tokens.push({ word: m[0].toLowerCase() });
|
|
306
|
+
return tokens;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Every real catalog class the free-typed `text` names, in the order each
|
|
310
|
+
* first appears, paired with the real material label (one of that SAME
|
|
311
|
+
* class's own swatch labels — never another class's, never a fabricated
|
|
312
|
+
* one) immediately preceding it, or `null`. `classIndex` is
|
|
313
|
+
* `{className: {materials}}` with `materials` keyed by lowercase label
|
|
314
|
+
* (sprite-catalog-viz's own client-side `buildClassIndexFromDom` output, or
|
|
315
|
+
* an equivalent test fixture) — a class name absent from `classIndex` can
|
|
316
|
+
* never match, and a modifier word that isn't one of ITS matched class's
|
|
317
|
+
* own material labels is silently dropped rather than guessed at, the same
|
|
318
|
+
* honest-miss posture an unrecognized class name gets (an unmatched word,
|
|
319
|
+
* e.g. "red" before a lamp with no red material, is never an error, just
|
|
320
|
+
* silently not drawn). A multi-word class name (e.g. "body of water") is
|
|
321
|
+
* checked, at every token position, before any shorter class that would
|
|
322
|
+
* otherwise claim part of it — candidates are tried longest-word-count
|
|
323
|
+
* first, so the longer name always wins the position it starts at. Pure. */
|
|
324
|
+
export function extractSceneItems(text, classIndex) {
|
|
325
|
+
const index = classIndex || {};
|
|
326
|
+
const candidates = Object.keys(index)
|
|
327
|
+
.map((name) => ({ name, words: name.toLowerCase().split(/\s+/).filter(Boolean) }))
|
|
328
|
+
.filter((c) => c.words.length)
|
|
329
|
+
.sort((a, b) => b.words.length - a.words.length || b.name.length - a.name.length);
|
|
330
|
+
const tokens = tokenizeSceneText(text);
|
|
331
|
+
const used = new Array(tokens.length).fill(false);
|
|
332
|
+
const items = [];
|
|
333
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
334
|
+
if (used[i]) continue;
|
|
335
|
+
const hit = candidates.find(({ words }) => {
|
|
336
|
+
if (i + words.length > tokens.length) return false;
|
|
337
|
+
for (let k = 0; k < words.length; k += 1) {
|
|
338
|
+
if (used[i + k] || tokens[i + k].word !== words[k]) return false;
|
|
339
|
+
}
|
|
340
|
+
return true;
|
|
341
|
+
});
|
|
342
|
+
if (!hit) continue;
|
|
343
|
+
let materialLabel = null;
|
|
344
|
+
if (i > 0 && !used[i - 1]) {
|
|
345
|
+
const materials = index[hit.name]?.materials || {};
|
|
346
|
+
const prevWord = tokens[i - 1].word;
|
|
347
|
+
if (Object.prototype.hasOwnProperty.call(materials, prevWord)) {
|
|
348
|
+
materialLabel = prevWord;
|
|
349
|
+
used[i - 1] = true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (let k = 0; k < hit.words.length; k += 1) used[i + k] = true;
|
|
353
|
+
items.push({ className: hit.name, materialLabel });
|
|
354
|
+
}
|
|
355
|
+
return items;
|
|
356
|
+
}
|
|
357
|
+
|
|
286
358
|
// ---- rendering ----
|
|
287
359
|
|
|
288
360
|
function chainHtml(chain) {
|
|
@@ -361,7 +433,26 @@ ${THEME_TOKENS_CSS}
|
|
|
361
433
|
main { max-width: 1180px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
|
|
362
434
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
363
435
|
h1 { font-size: 1.4rem; margin: .3rem 0 .6rem; text-wrap: balance; }
|
|
364
|
-
|
|
436
|
+
|
|
437
|
+
.composer { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: .6rem 0 1.3rem; }
|
|
438
|
+
@media (max-width: 700px) { .composer { grid-template-columns: 1fr; } }
|
|
439
|
+
.composer .panel { background: var(--card); border: 1px solid var(--line); border-top: 2px solid var(--taught); padding: .75rem .85rem; min-width: 0; }
|
|
440
|
+
.composer h2 { font-family: ${SERIF_STACK}; font-variant: small-caps; font-size: .82rem; letter-spacing: .04em; color: var(--muted); font-weight: 600; margin: 0 0 .55rem; }
|
|
441
|
+
.composeform { display: flex; align-items: center; gap: .5rem; }
|
|
442
|
+
.composeform .prompt { color: var(--taught); font-size: .8rem; white-space: nowrap; }
|
|
443
|
+
.composeform input { flex: 1; font-family: ${MONO_STACK}; font-size: .82rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 4px; padding: .38rem .6rem; min-width: 0; }
|
|
444
|
+
.composeform input:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
445
|
+
.pills { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .6rem; }
|
|
446
|
+
.pill { font-family: ${MONO_STACK}; font-size: .7rem; padding: .22rem .55rem; border: 1px solid var(--line); border-radius: 999px; background: var(--bg); color: var(--ink); cursor: pointer; }
|
|
447
|
+
.pill:hover { border-color: var(--taught); }
|
|
448
|
+
.pill:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
449
|
+
.scene-frame { min-height: 5.6rem; display: flex; align-items: center; }
|
|
450
|
+
.scene-row { display: flex; flex-wrap: wrap; gap: .8rem; align-items: flex-start; width: 100%; }
|
|
451
|
+
.scene-card { display: flex; flex-direction: column; align-items: center; width: 74px; }
|
|
452
|
+
.scene-sprite { width: 60px; height: 60px; border-radius: 8px; background: var(--bg); border: 1px solid var(--line); display: flex; align-items: center; justify-content: center; padding: 8px; box-sizing: border-box; }
|
|
453
|
+
.scene-sprite svg { width: 100%; height: 100%; display: block; }
|
|
454
|
+
.scene-label { font-size: .7rem; text-align: center; color: var(--ink); margin-top: .3rem; line-height: 1.2; }
|
|
455
|
+
.empty-note { font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); }
|
|
365
456
|
.topbar { position: sticky; top: 0; z-index: 2; background: var(--bg); display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .9rem; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: .6rem 0; margin: 1rem 0 1.2rem; }
|
|
366
457
|
.jump { font-family: ${MONO_STACK}; font-size: .72rem; padding: .18rem .55rem; border: 1px solid var(--line); border-radius: 99px; background: var(--card); color: var(--ink); text-decoration: none; }
|
|
367
458
|
.jump:hover { border-color: var(--taught); }
|
|
@@ -395,21 +486,39 @@ ${THEME_TOKENS_CSS}
|
|
|
395
486
|
.swatch-caption { font-family: ${MONO_STACK}; font-size: .58rem; color: var(--muted); line-height: 1.25; margin-top: .15rem; word-break: break-word; }
|
|
396
487
|
.swatch-treat { display: block; opacity: .8; }
|
|
397
488
|
footer.page { max-width: 74ch; margin: 2.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); }
|
|
398
|
-
@media (prefers-reduced-motion: no-preference) { .jump, .swatch { transition: border-color .12s ease, opacity .12s ease; } }
|
|
489
|
+
@media (prefers-reduced-motion: no-preference) { .jump, .swatch, .pill { transition: border-color .12s ease, opacity .12s ease; } }
|
|
399
490
|
</style>
|
|
400
491
|
</head>
|
|
401
492
|
<body>
|
|
402
493
|
<main>
|
|
403
494
|
<div class="eyebrow">tmct · the sprite library</div>
|
|
404
|
-
<h1>
|
|
405
|
-
<
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
495
|
+
<h1>Sprites</h1>
|
|
496
|
+
<div class="composer">
|
|
497
|
+
<section class="panel compose-panel" aria-label="Describe a scene">
|
|
498
|
+
<h2>describe a scene</h2>
|
|
499
|
+
<form class="composeform" id="composeForm">
|
|
500
|
+
<span class="prompt mono">there is a</span>
|
|
501
|
+
<input id="composeq" type="text" autocomplete="off"
|
|
502
|
+
placeholder="red lamp, a doctor with a hat, and a cabinet"
|
|
503
|
+
aria-label="Continue the sentence: there is a…">
|
|
504
|
+
</form>
|
|
505
|
+
<div class="pills" id="composePills" role="group" aria-label="quick words to add">
|
|
506
|
+
<button type="button" class="pill" data-fill="doctor">doctor</button>
|
|
507
|
+
<button type="button" class="pill" data-fill="hat">hat</button>
|
|
508
|
+
<button type="button" class="pill" data-fill="wood cabinet">wood cabinet</button>
|
|
509
|
+
<button type="button" class="pill" data-fill="glass lamp">glass lamp</button>
|
|
510
|
+
<button type="button" class="pill" data-fill="cat">cat</button>
|
|
511
|
+
<button type="button" class="pill" data-fill="garden">garden</button>
|
|
512
|
+
</div>
|
|
513
|
+
</section>
|
|
514
|
+
<section class="panel viewer-panel" aria-label="The composed scene">
|
|
515
|
+
<h2>the scene</h2>
|
|
516
|
+
<div class="scene-frame" id="sceneFrame">
|
|
517
|
+
<div class="scene-row" id="sceneRow" aria-live="polite"></div>
|
|
518
|
+
<span class="empty-note" id="sceneEmpty">nothing recognized yet — try “a doctor with a hat, and a cabinet”.</span>
|
|
519
|
+
</div>
|
|
520
|
+
</section>
|
|
521
|
+
</div>
|
|
413
522
|
<div class="topbar">
|
|
414
523
|
<nav aria-label="Jump to group">${navHtml}</nav>
|
|
415
524
|
<div class="filter">
|
|
@@ -445,6 +554,77 @@ const SPRITE_CATALOG = ${pageData};
|
|
|
445
554
|
}
|
|
446
555
|
q.addEventListener("input", apply);
|
|
447
556
|
apply();
|
|
557
|
+
|
|
558
|
+
// ---- the scene composer — reads the class/material index straight off
|
|
559
|
+
// THIS page's own already-rendered card and swatch-label markup (this
|
|
560
|
+
// module's own header explains why: never a second embedded copy of the
|
|
561
|
+
// same swatch data), so the composed scene below only ever shows a sprite
|
|
562
|
+
// this same page already proved the resolver draws.
|
|
563
|
+
const esc = ${escapeHtml.toString()};
|
|
564
|
+
const tokenizeSceneText = ${tokenizeSceneText.toString()};
|
|
565
|
+
const extractSceneItems = ${extractSceneItems.toString()};
|
|
566
|
+
|
|
567
|
+
function buildClassIndexFromDom() {
|
|
568
|
+
const index = {};
|
|
569
|
+
for (const card of cards) {
|
|
570
|
+
const largeRow = card.querySelector('.tier-row[data-tier="large"]');
|
|
571
|
+
if (!largeRow) continue;
|
|
572
|
+
let defaultSvg = null;
|
|
573
|
+
const materials = {};
|
|
574
|
+
for (const swatch of largeRow.querySelectorAll(".swatch")) {
|
|
575
|
+
const labelEl = swatch.querySelector(".swatch-label");
|
|
576
|
+
const svgEl = swatch.querySelector(".swatch-img");
|
|
577
|
+
if (!labelEl || !svgEl) continue;
|
|
578
|
+
const svg = svgEl.innerHTML;
|
|
579
|
+
if (swatch.classList.contains("plain")) defaultSvg = svg;
|
|
580
|
+
else if (swatch.classList.contains("fallback")) { if (!defaultSvg) defaultSvg = svg; }
|
|
581
|
+
else materials[labelEl.textContent.trim().toLowerCase()] = svg;
|
|
582
|
+
}
|
|
583
|
+
if (!defaultSvg) {
|
|
584
|
+
const firstSvg = largeRow.querySelector(".swatch-img");
|
|
585
|
+
if (firstSvg) defaultSvg = firstSvg.innerHTML;
|
|
586
|
+
}
|
|
587
|
+
index[card.dataset.cls] = { defaultSvg, materials };
|
|
588
|
+
}
|
|
589
|
+
return index;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const classIndex = buildClassIndexFromDom();
|
|
593
|
+
const composeqEl = document.getElementById("composeq");
|
|
594
|
+
const composeFormEl = document.getElementById("composeForm");
|
|
595
|
+
const composePillsEl = document.getElementById("composePills");
|
|
596
|
+
const sceneRowEl = document.getElementById("sceneRow");
|
|
597
|
+
const sceneEmptyEl = document.getElementById("sceneEmpty");
|
|
598
|
+
|
|
599
|
+
function renderScene(text) {
|
|
600
|
+
const items = extractSceneItems(text, classIndex);
|
|
601
|
+
if (!items.length) {
|
|
602
|
+
sceneRowEl.innerHTML = "";
|
|
603
|
+
sceneEmptyEl.hidden = false;
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
sceneEmptyEl.hidden = true;
|
|
607
|
+
sceneRowEl.innerHTML = items.map((item) => {
|
|
608
|
+
const entry = classIndex[item.className];
|
|
609
|
+
if (!entry) return "";
|
|
610
|
+
const svg = (item.materialLabel && entry.materials[item.materialLabel]) || entry.defaultSvg || "";
|
|
611
|
+
const label = item.materialLabel ? item.materialLabel + " " + item.className : item.className;
|
|
612
|
+
return '<div class="scene-card"><div class="scene-sprite">' + svg + '</div><div class="scene-label">' + esc(label) + "</div></div>";
|
|
613
|
+
}).join("");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
composeqEl.addEventListener("input", () => renderScene(composeqEl.value));
|
|
617
|
+
composeFormEl.addEventListener("submit", (e) => { e.preventDefault(); renderScene(composeqEl.value); });
|
|
618
|
+
composePillsEl.addEventListener("click", (e) => {
|
|
619
|
+
const btn = e.target.closest(".pill");
|
|
620
|
+
if (!btn) return;
|
|
621
|
+
const phrase = btn.dataset.fill || "";
|
|
622
|
+
const current = composeqEl.value.trim();
|
|
623
|
+
composeqEl.value = current ? current + ", a " + phrase : phrase;
|
|
624
|
+
composeqEl.focus();
|
|
625
|
+
renderScene(composeqEl.value);
|
|
626
|
+
});
|
|
627
|
+
renderScene("");
|
|
448
628
|
})();
|
|
449
629
|
</script>
|
|
450
630
|
</body>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// ledger-browser-entry.mjs — the esbuild entry for the memory-ledger page's
|
|
2
|
+
// LIVE chat dock (public/ledger-browser.bundle.js, built by
|
|
3
|
+
// scripts/build-ledger-bundle.mjs), mirroring chat-browser-entry.mjs's own
|
|
4
|
+
// createChatSession shape exactly. The ledger dock is general free-text
|
|
5
|
+
// teach+ask, the same shape chat.html's own dock is — not board-game
|
|
6
|
+
// specific like plan-browser-entry.mjs's createPlanSession, which has to
|
|
7
|
+
// teach and solve a puzzle up front.
|
|
8
|
+
//
|
|
9
|
+
// Gitignored, Pages-demo-site-only output (scripts/build-demo-site.mjs
|
|
10
|
+
// builds it fresh on every deploy, never committed) — see
|
|
11
|
+
// memory-ask-browser-entry.mjs's own header for the contrast: THAT bundle
|
|
12
|
+
// (factAnswer/factReadBack only, ~1.0MB) is the one COMMITTED under src/ and
|
|
13
|
+
// packed by `npm publish`, because `tmct viz` is a real CLI command run
|
|
14
|
+
// against a user's own local .tmct store and must ship with the package.
|
|
15
|
+
// This bundle carries the FULL runTurn engine (~1.5MB, the same weight
|
|
16
|
+
// class as chat/spider-fly/adventure/plan's own browser bundles) and is
|
|
17
|
+
// never published — only the hosted demo site's public/ledger.html links to
|
|
18
|
+
// it, as an optional sibling script the page degrades honestly without.
|
|
19
|
+
import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
|
|
20
|
+
import { createInMemoryStore, normFactTerm } from "../../adapters/memory/core.mjs";
|
|
21
|
+
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
22
|
+
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
23
|
+
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
24
|
+
import { computeLedgerDataFromPayload } from "../../services/ledger-viz.mjs";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A browser ledger-dock session over the real turn engine — createChatSession's
|
|
28
|
+
* exact shape, seeded from the ledger page's own embedded PAYLOAD (the same
|
|
29
|
+
* loadMemory()-shaped object renderLedgerHtml embeds as `const PAYLOAD`), so
|
|
30
|
+
* a fact taught through the dock extends the SAME graph the page renders
|
|
31
|
+
* from, never a disconnected one.
|
|
32
|
+
*
|
|
33
|
+
* Returns { memoryDir, sessionId, turn }, identical to createChatSession.
|
|
34
|
+
* ledger-viz.mjs's own inline script calls computeLedgerDataFromPayload
|
|
35
|
+
* (re-exported below) on `memoryDir.payload` after a turn whose record shows
|
|
36
|
+
* a successful write (`via: "assert"` or `via: "retract"`, `miss: false`) to
|
|
37
|
+
* re-derive the page's rows/terms/edges/stats and re-mount the same view a
|
|
38
|
+
* fresh page load would have rendered — a plain query never re-derives.
|
|
39
|
+
*/
|
|
40
|
+
export function createLedgerSession({ seedPayload = null, vocabSeeded = false } = {}) {
|
|
41
|
+
const memoryDir = createInMemoryStore();
|
|
42
|
+
// Spread onto the store's own empty payload so a partial seed still
|
|
43
|
+
// carries the classes/prefixes scaffolding the write path recounts —
|
|
44
|
+
// teach turns must work regardless of what the seed payload carries.
|
|
45
|
+
if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
|
|
46
|
+
|
|
47
|
+
const graph = parseEntities({ individuals: [], objectProperties: [] });
|
|
48
|
+
const lexicon = loadLexicon();
|
|
49
|
+
const vocabHint = vocabExampleHint(vocabSeeded);
|
|
50
|
+
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
51
|
+
|
|
52
|
+
let focus = null;
|
|
53
|
+
let last = null;
|
|
54
|
+
let planState = null;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
memoryDir,
|
|
58
|
+
sessionId,
|
|
59
|
+
|
|
60
|
+
/** One dispatched turn. A throwing runTurn must never kill the session —
|
|
61
|
+
* the page has no other chance to show this turn's answer. */
|
|
62
|
+
async turn(line) {
|
|
63
|
+
let result;
|
|
64
|
+
try {
|
|
65
|
+
result = await runTurn(line, {
|
|
66
|
+
config: null, source: null, graph, focus, last, memoryDir, sessionId,
|
|
67
|
+
env: {}, lexicon, vocabHint, planState,
|
|
68
|
+
});
|
|
69
|
+
} catch (e) {
|
|
70
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
71
|
+
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null };
|
|
72
|
+
}
|
|
73
|
+
focus = result.focus;
|
|
74
|
+
last = result.last;
|
|
75
|
+
if ("planState" in result) planState = result.planState;
|
|
76
|
+
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null };
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Re-exported so ledger-viz.mjs's own inline script never has to duplicate
|
|
82
|
+
// the derivation logic that builds rows/terms/edges/contradictions/
|
|
83
|
+
// worthALook/stats from a payload — the same posture chat-browser-entry.mjs
|
|
84
|
+
// takes re-exporting registerWinkModel for its own page's CDN wink load.
|
|
85
|
+
globalThis.tmctLedger = { createLedgerSession, computeLedgerDataFromPayload, normFactTerm, registerWinkModel };
|