@polycode-projects/the-mechanical-code-talker 4.1.0 → 4.1.1
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/domain/sprite-request.mjs +1 -1
- package/src/services/adventure-viz.mjs +38 -38
- package/src/services/chat-page-viz.mjs +32 -32
- package/src/services/chat.mjs +1 -1
- package/src/services/code-explorer-viz.mjs +1 -1
- package/src/services/ingest-viz.mjs +19 -19
- package/src/services/ledger-viz.mjs +40 -43
- package/src/services/mud-viz.mjs +33 -28
- package/src/services/plan-viz.mjs +9 -9
- package/src/services/research-viz.mjs +17 -17
- package/src/services/spider-fly-viz.mjs +22 -22
- package/src/services/sprite-catalog-viz.mjs +4 -4
- package/src/surfaces/web/adventure-browser-entry.mjs +21 -22
- package/src/surfaces/web/chat-browser-entry.mjs +21 -2
- package/src/surfaces/web/code-explorer-browser-entry.mjs +28 -15
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +13 -4
- package/src/surfaces/web/ledger-browser-entry.mjs +18 -10
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +54 -54
- package/src/surfaces/web/mud-browser-entry.mjs +42 -21
- package/src/surfaces/web/plan-browser-entry.mjs +18 -11
- package/src/surfaces/web/research-browser-entry.mjs +22 -5
- package/src/surfaces/web/spider-fly-browser-entry.mjs +32 -13
- package/src/surfaces/web/sprites-browser-entry.mjs +15 -6
- package/src/surfaces/web/tmct-surface.mjs +147 -0
- package/src/surfaces/web/turn-session.mjs +6 -2
- package/src/tools/server.mjs +13 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
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; indexes a repo on request (tmct index) or reads any producer's graph.",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// spider-fly-viz.mjs's threadCellsForSpiderPlan takes its grid geometry as an
|
|
11
11
|
// argument: a function with no module-scope references survives `.toString()`
|
|
12
12
|
// splicing into a page script, and the browser already holds its own copies of
|
|
13
|
-
// those primitives on window.
|
|
13
|
+
// those primitives on window.tmct.
|
|
14
14
|
//
|
|
15
15
|
// The resolution CHAIN is derived by OBSERVATION, never by re-implementing
|
|
16
16
|
// sprite-templates.mjs's specificity order. At each term of the ancestor chain
|
|
@@ -42,17 +42,17 @@
|
|
|
42
42
|
//
|
|
43
43
|
// Four further pure helpers are exported for testing but NOT spliced,
|
|
44
44
|
// because each calls another module's export — the in-page script instead
|
|
45
|
-
// reaches through the browser bundle's own `
|
|
46
|
-
// how the inline script calls
|
|
45
|
+
// reaches through the browser bundle's own `tmct` surface (mirroring
|
|
46
|
+
// how the spider-fly page's inline script calls it rather than re-importing
|
|
47
47
|
// spider-fly-world.mjs): `roomCaptionText` (calls `worldDigestRows`; the
|
|
48
|
-
// in-page `captionFor` mirrors it against `
|
|
48
|
+
// in-page `captionFor` mirrors it against `tmct.page.worldDigestRows`),
|
|
49
49
|
// `pillsForRoom` (a thin wrapper over adventure.mjs's own exported
|
|
50
50
|
// `roomAffordances`, whose header explains why its list can never promise an
|
|
51
51
|
// action one of take/open/talk/examine would then refuse; the in-page
|
|
52
|
-
// `pillsFor` mirrors it against `
|
|
52
|
+
// `pillsFor` mirrors it against `tmct.page.roomAffordances`),
|
|
53
53
|
// `goalStatusLines` (calls `foldWorldState` and adventure-autoplay.mjs's own
|
|
54
54
|
// `exposedFacts`; the in-page `goalStatusLinesFor` mirrors both against the
|
|
55
|
-
// `
|
|
55
|
+
// `tmct` surface too), and `visitedRoomGraph` (a directions-only
|
|
56
56
|
// layout of the rooms a session has actually visited, now a thin wrapper over
|
|
57
57
|
// viz-room-graph.mjs's shared `directedGridLayout` — mud-viz.mjs's own
|
|
58
58
|
// burrowGraph wrote the same BFS-grid layout a second time under its own
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
// header for the algorithm and what `hints`/disconnected components mean).
|
|
61
61
|
// The in-page script never splices `directedGridLayout`/`roomGraphSvg`
|
|
62
62
|
// either, for the same not-self-contained reason: it calls
|
|
63
|
-
// `
|
|
63
|
+
// `tmct.page.directedGridLayout`/`tmct.page.roomGraphSvg` straight
|
|
64
64
|
// through the bundle instead of re-implementing the room map a second time.
|
|
65
65
|
//
|
|
66
66
|
// The chat dock (chatlog/chatform/chatq/pills, below) mirrors
|
|
@@ -479,8 +479,8 @@ export function groundedPlaceholder(actions, fallback) {
|
|
|
479
479
|
* row. `[]` on an honest miss (neither lookup returns anything) — never a
|
|
480
480
|
* fabricated suggestion. NOT .toString()-splice-safe (it calls two other
|
|
481
481
|
* modules' exports) — the in-page script mirrors this same combination
|
|
482
|
-
* against the browser bundle's own `
|
|
483
|
-
* `
|
|
482
|
+
* against the browser bundle's own `tmct.page.relatedForTerm`/
|
|
483
|
+
* `tmct.page.classAncestorChain`, the same reach-through-the-global
|
|
484
484
|
* pattern `pillsFor`/`captionFor` already use for their own adventure.mjs
|
|
485
485
|
* calls (see this module's header). */
|
|
486
486
|
export function suggestionsForTerm(rows, term) {
|
|
@@ -1199,7 +1199,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1199
1199
|
let allStoreRows = [];
|
|
1200
1200
|
|
|
1201
1201
|
// ---- persistence — the manor remembers your visit, on this device -------
|
|
1202
|
-
// Best-effort IndexedDB (
|
|
1202
|
+
// Best-effort IndexedDB (tmct.page.openPersistedStore): after any
|
|
1203
1203
|
// state-changing redraw, the whole Backend-B payload plus the visited-room
|
|
1204
1204
|
// exposure set snapshots under the "adventure" key, debounced. Restored on
|
|
1205
1205
|
// boot; reset clears it and starts the world over. The home page's preview
|
|
@@ -1297,22 +1297,22 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1297
1297
|
|
|
1298
1298
|
function captionFor(rows, state, here) {
|
|
1299
1299
|
const hereCased = here.charAt(0).toUpperCase() + here.slice(1);
|
|
1300
|
-
const lines =
|
|
1300
|
+
const lines = tmct.page.worldDigestRows(rows, state)
|
|
1301
1301
|
.filter((row) => row.subject !== "Player" && (row.object === here || row.subject === hereCased))
|
|
1302
1302
|
.map((row) => row.subject + " " + row.predicate + " " + row.object + ".");
|
|
1303
1303
|
return lines.length ? lines.join(" ") : "Nothing more about the " + here + " is written down yet.";
|
|
1304
1304
|
}
|
|
1305
1305
|
|
|
1306
1306
|
// ---- goal status — mirrors adventure-viz.mjs's own goalStatusLines
|
|
1307
|
-
// against
|
|
1307
|
+
// against tmct.page.foldWorldState/tmct.page.exposedFacts (the
|
|
1308
1308
|
// same mirroring captionFor/pillsFor already do against their own
|
|
1309
1309
|
// adventure.mjs exports), so a location is never claimed before the room
|
|
1310
1310
|
// holding it has actually been visited this session.
|
|
1311
1311
|
function goalStatusLinesFor(rows, state, visitedRoomIds) {
|
|
1312
1312
|
const ids = Array.from(new Set(rows.filter((r) => r.predicate === "mgx:is-objective" && r.object === "true").map((r) => r.subject)));
|
|
1313
1313
|
const carriedIds = new Set(carriedItems(rows, state, ACTING_SUBJECT).map((o) => o.subject));
|
|
1314
|
-
const exposedRows =
|
|
1315
|
-
const exposedState =
|
|
1314
|
+
const exposedRows = tmct.page.exposedFacts(rows, visitedRoomIds);
|
|
1315
|
+
const exposedState = tmct.page.foldWorldState(exposedRows);
|
|
1316
1316
|
return ids.map((id) => {
|
|
1317
1317
|
if (carriedIds.has(id)) return { subject: id, status: "carried", text: "carrying the " + id + " \\u2014 the adventure is won." };
|
|
1318
1318
|
const room = visibleRoomOf(exposedRows, exposedState, id, ACTING_SUBJECT);
|
|
@@ -1340,21 +1340,21 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1340
1340
|
// cursor per node; play mode's own map stays purely informational.
|
|
1341
1341
|
// roomGraphSvgFor mirrors the OLD inline roomMapSvg's own board-game sizing
|
|
1342
1342
|
// (a 64px square cell, 56x26px room footprints) through the shared
|
|
1343
|
-
// viz-room-graph.mjs renderer, reached via the
|
|
1343
|
+
// viz-room-graph.mjs renderer, reached via the tmct surface the
|
|
1344
1344
|
// same way captionFor/pillsFor already reach their own adventure.mjs
|
|
1345
1345
|
// calls: roomGraphSvg needs escapeHtml and a module-level exit-delta table
|
|
1346
1346
|
// this splice-safe script carries neither of, so it runs through the
|
|
1347
1347
|
// bundle rather than being spliced as text (see this page's own module
|
|
1348
1348
|
// header). visitedRoomGraphFor is the same posture for the layout half.
|
|
1349
1349
|
function roomGraphSvgFor(graph, clickable) {
|
|
1350
|
-
return
|
|
1350
|
+
return tmct.page.roomGraphSvg(graph, {
|
|
1351
1351
|
cellX: 64, cellY: 64, roomW: 56, roomH: 26,
|
|
1352
1352
|
clickable, selectedRoomId,
|
|
1353
1353
|
label: clickable ? "the whole manor \\u2014 click a room to inspect it" : "the rooms visited so far",
|
|
1354
1354
|
});
|
|
1355
1355
|
}
|
|
1356
1356
|
function visitedRoomGraphFor(state, visitedIds) {
|
|
1357
|
-
return
|
|
1357
|
+
return tmct.page.directedGridLayout(state, visitedIds, { actingSubject: ACTING_SUBJECT });
|
|
1358
1358
|
}
|
|
1359
1359
|
function renderRoomMap(rows, state, visitedRoomIds) {
|
|
1360
1360
|
mapWrapEl.innerHTML = roomGraphSvgFor(visitedRoomGraphFor(state, visitedRoomIds), false) || '<span class="empty-note">nowhere yet</span>';
|
|
@@ -1417,7 +1417,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1417
1417
|
addChatLine("u", esc(line));
|
|
1418
1418
|
addObjDockLine("u", esc(line));
|
|
1419
1419
|
return withLock(async () => {
|
|
1420
|
-
const result = await
|
|
1420
|
+
const result = await tmct.turn(line);
|
|
1421
1421
|
addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
1422
1422
|
addObjDockLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
1423
1423
|
const snap = await session.snapshot();
|
|
@@ -1442,7 +1442,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1442
1442
|
// nothing, so it needs no main-transcript echo — it IS the board's own
|
|
1443
1443
|
// content); pill/typed turns below do echo, since they can change state.
|
|
1444
1444
|
await withLock(async () => {
|
|
1445
|
-
const result = await
|
|
1445
|
+
const result = await tmct.turn("look " + subject);
|
|
1446
1446
|
objLookEl.textContent = result.answer;
|
|
1447
1447
|
});
|
|
1448
1448
|
}
|
|
@@ -1505,8 +1505,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1505
1505
|
|
|
1506
1506
|
// ---- contextual pills — refreshed every redraw from the CURRENT room's
|
|
1507
1507
|
// own roomAffordances-derived list (mirroring pillsForRoom against
|
|
1508
|
-
//
|
|
1509
|
-
// roomCaptionText against
|
|
1508
|
+
// tmct.page.roomAffordances, the same way captionFor above mirrors
|
|
1509
|
+
// roomCaptionText against tmct.page.worldDigestRows), so a pill for a
|
|
1510
1510
|
// room the player has left, or an object already taken, never lingers.
|
|
1511
1511
|
// Clicking one inserts its exact command text into the input and focuses
|
|
1512
1512
|
// it; it never auto-submits, so free typing still works and a clicked
|
|
@@ -1517,7 +1517,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1517
1517
|
// ever wants to edit before running, and drawing it in both places would
|
|
1518
1518
|
// make one control a suggestion and its twin an action.
|
|
1519
1519
|
function pillsFor(rows, state, here) {
|
|
1520
|
-
return
|
|
1520
|
+
return tmct.page.roomAffordances(rows, state, here);
|
|
1521
1521
|
}
|
|
1522
1522
|
function renderPills(rows, state, here) {
|
|
1523
1523
|
const actions = pillsFor(rows, state, here).filter((a) => a.indexOf("go ") !== 0);
|
|
@@ -1560,10 +1560,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1560
1560
|
const ROOT_FALLBACK_BY_CLASS = { adventurer: "person", person: "person", room: "room" };
|
|
1561
1561
|
const rootFallbackFor = (cls) => ROOT_FALLBACK_BY_CLASS[cls] || "portable";
|
|
1562
1562
|
function resolveObjectSprite(rows, s) {
|
|
1563
|
-
if (s.subject === "you") return
|
|
1564
|
-
return
|
|
1563
|
+
if (s.subject === "you") return tmct.page.resolveSpriteAsset("adventurer", [], [], activeSpriteTemplates, tmct.page.SPRITE_REGISTRY, { rootFallback: "person", instanceKey: "you" });
|
|
1564
|
+
return tmct.page.resolveSpriteAsset(
|
|
1565
1565
|
s.subject, spriteAncestryRows(rows, s.subject), factsForSubject(rows, s.subject),
|
|
1566
|
-
activeSpriteTemplates,
|
|
1566
|
+
activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
|
|
1567
1567
|
{ rootFallback: rootFallbackFor(s.spriteClass), instanceKey: s.subject },
|
|
1568
1568
|
);
|
|
1569
1569
|
}
|
|
@@ -1574,9 +1574,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1574
1574
|
// everything else falls back through room's own ancestor chain to the
|
|
1575
1575
|
// generic room icon.
|
|
1576
1576
|
function roomKindIconSvg(rows, here) {
|
|
1577
|
-
return
|
|
1577
|
+
return tmct.page.resolveSpriteAsset(
|
|
1578
1578
|
here, spriteAncestryRows(rows, here), factsForSubject(rows, here),
|
|
1579
|
-
activeSpriteTemplates,
|
|
1579
|
+
activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
|
|
1580
1580
|
{ rootFallback: "room", instanceKey: "roomkind-" + here },
|
|
1581
1581
|
);
|
|
1582
1582
|
}
|
|
@@ -1641,7 +1641,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1641
1641
|
ticker.pause();
|
|
1642
1642
|
addChatLine("u", esc(line));
|
|
1643
1643
|
return withLock(async () => {
|
|
1644
|
-
const result = await
|
|
1644
|
+
const result = await tmct.turn(line);
|
|
1645
1645
|
addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
1646
1646
|
const snap = await session.snapshot();
|
|
1647
1647
|
redraw(snap);
|
|
@@ -1703,9 +1703,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1703
1703
|
const classes = new Set(["adventurer"]);
|
|
1704
1704
|
subjects.forEach((s) => { if (s !== ACTING_SUBJECT) classes.add(spriteClassForObject(rows, s)); });
|
|
1705
1705
|
legendListEl.innerHTML = Array.from(classes).sort().map((cls) => {
|
|
1706
|
-
const svg =
|
|
1706
|
+
const svg = tmct.page.resolveSpriteAsset(
|
|
1707
1707
|
cls, spriteAncestryRows(rows, cls), factsForSubject(rows, cls),
|
|
1708
|
-
activeSpriteTemplates,
|
|
1708
|
+
activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
|
|
1709
1709
|
{ rootFallback: rootFallbackFor(cls), instanceKey: "legend-" + cls },
|
|
1710
1710
|
);
|
|
1711
1711
|
return spriteCardHtml(cls, cls, svg);
|
|
@@ -1724,8 +1724,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1724
1724
|
}
|
|
1725
1725
|
|
|
1726
1726
|
// ---- cursor-driven suggestion pills — the lateral SKOS neighbourhood
|
|
1727
|
-
// (
|
|
1728
|
-
// (
|
|
1727
|
+
// (tmct.page.relatedForTerm) plus the vertical is-a ancestor chain
|
|
1728
|
+
// (tmct.page.classAncestorChain), mirroring adventure-viz.mjs's own
|
|
1729
1729
|
// suggestionsForTerm against the browser bundle's global (the same
|
|
1730
1730
|
// reach-through-the-global pattern captionFor/pillsFor already use).
|
|
1731
1731
|
// Reads the FULL store (allStoreRows), not worldOnlyRows — a term's
|
|
@@ -1734,8 +1734,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1734
1734
|
function renderSuggestionPills() {
|
|
1735
1735
|
const term = wordBeforeCursor(editorTextEl.value, editorTextEl.selectionStart);
|
|
1736
1736
|
if (!term) { editorPillsEl.innerHTML = ""; return; }
|
|
1737
|
-
const related =
|
|
1738
|
-
const chain =
|
|
1737
|
+
const related = tmct.page.relatedForTerm(allStoreRows, term);
|
|
1738
|
+
const chain = tmct.page.classAncestorChain(term, allStoreRows);
|
|
1739
1739
|
const seen = new Set([term]);
|
|
1740
1740
|
const out = [];
|
|
1741
1741
|
const push = (label) => { if (label && !seen.has(label)) { seen.add(label); out.push(label); } };
|
|
@@ -1766,7 +1766,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1766
1766
|
const snap = await session.snapshot();
|
|
1767
1767
|
allStoreRows = snap.rows;
|
|
1768
1768
|
editRows = worldOnlyRows(snap.rows);
|
|
1769
|
-
editState =
|
|
1769
|
+
editState = tmct.page.foldWorldState(editRows);
|
|
1770
1770
|
if (result.unrecognized.length) {
|
|
1771
1771
|
const lines = result.unrecognized.map((u) => u.line).join(", ");
|
|
1772
1772
|
editorStatusEl.className = "edit-status pending";
|
|
@@ -1797,7 +1797,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1797
1797
|
const snap = await session.snapshot();
|
|
1798
1798
|
allStoreRows = snap.rows;
|
|
1799
1799
|
editRows = worldOnlyRows(snap.rows);
|
|
1800
|
-
editState =
|
|
1800
|
+
editState = tmct.page.foldWorldState(editRows);
|
|
1801
1801
|
editorTextEl.value = renderWorldEditorText(editRows, editState);
|
|
1802
1802
|
editorStatusEl.className = "edit-status";
|
|
1803
1803
|
editorStatusEl.textContent = "";
|
|
@@ -1818,7 +1818,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1818
1818
|
|
|
1819
1819
|
async function boot({ fresh = false } = {}) {
|
|
1820
1820
|
const saved = !fresh && persist ? await persist.load() : null;
|
|
1821
|
-
session = await
|
|
1821
|
+
session = await tmct.open(
|
|
1822
1822
|
world(),
|
|
1823
1823
|
saved && saved.payload && saved.payload.memoryPayload
|
|
1824
1824
|
? { restoredPayload: saved.payload.memoryPayload, restoredVisitedRoomIds: saved.payload.visitedRoomIds }
|
|
@@ -1877,9 +1877,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1877
1877
|
// to. Without that, one world's snapshot would restore into another's graph.
|
|
1878
1878
|
let siteVersion = "";
|
|
1879
1879
|
async function openPersistFor(worldName) {
|
|
1880
|
-
if (preview || !
|
|
1880
|
+
if (preview || !tmct.page.openPersistedStore) return;
|
|
1881
1881
|
if (!siteVersion) siteVersion = await fetchSiteVersion();
|
|
1882
|
-
persist =
|
|
1882
|
+
persist = tmct.page.openPersistedStore({ storeKey: "adventure", stamp: siteVersion + ":" + worldName });
|
|
1883
1883
|
}
|
|
1884
1884
|
|
|
1885
1885
|
if (scenarioSelectEl) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// document shaped exactly like spider-fly-viz.mjs/adventure-viz.mjs's own
|
|
3
3
|
// page-builders — one inlined <style> importing viz-theme.mjs's shared
|
|
4
4
|
// tokens, behaviour as an inlined IIFE — running the full chat engine
|
|
5
|
-
// (chat-browser.bundle.js's globalThis.
|
|
5
|
+
// (chat-browser.bundle.js's globalThis.tmct, chat-seed.json,
|
|
6
6
|
// public/reference-pack/) by same-origin relative paths. The same
|
|
7
7
|
// relationship spider-fly-viz.mjs's own inlined chat dock has with
|
|
8
8
|
// createSpiderFlySession: both call the shared session.turn(line), neither
|
|
@@ -539,7 +539,7 @@ ${THEME_TOKENS_CSS}
|
|
|
539
539
|
|
|
540
540
|
/* the provenance stats panel: what this session's memory holds, docked to
|
|
541
541
|
the right of the chat column (a real layout column, not an overlay) —
|
|
542
|
-
re-rendered after boot and after every turn from window.
|
|
542
|
+
re-rendered after boot and after every turn from window.tmct's own
|
|
543
543
|
memoryStats(), never a second provenance computation. */
|
|
544
544
|
.statsPanel { flex: 0 0 300px; max-width: 300px; overflow-y: auto; border-left: 1px solid var(--line); padding: 1.1rem 1.2rem 1.6rem; font-family: ${MONO_STACK}; font-size: .74rem; line-height: 1.55; }
|
|
545
545
|
.statsPanel h2 { font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin: 1.3rem 0 .5rem; }
|
|
@@ -554,7 +554,7 @@ ${THEME_TOKENS_CSS}
|
|
|
554
554
|
.statsPanel .persist-note { color: var(--muted); font-size: .64rem; margin: .4rem 0 0; }
|
|
555
555
|
|
|
556
556
|
/* the "researched this session" panel: its own section under the memory
|
|
557
|
-
stats, filled from window.
|
|
557
|
+
stats, filled from window.tmct.page.researchedFactRows() plus each
|
|
558
558
|
settled research turn's own answer text — the passage tmct actually
|
|
559
559
|
read, the article it read it from, and the facts that passage grounded.
|
|
560
560
|
A sibling section, not folded into #statsPanelStats — that div's own
|
|
@@ -1124,7 +1124,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1124
1124
|
})(),
|
|
1125
1125
|
stallGuard(),
|
|
1126
1126
|
]);
|
|
1127
|
-
window.
|
|
1127
|
+
window.tmct.page.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
1128
1128
|
winkStatus = "loaded";
|
|
1129
1129
|
} catch (err) {
|
|
1130
1130
|
winkStatus = "unavailable";
|
|
@@ -1170,8 +1170,8 @@ ${THEME_TOKENS_CSS}
|
|
|
1170
1170
|
if (!seedPayload) return null;
|
|
1171
1171
|
try { return structuredClone(seedPayload); } catch { return JSON.parse(JSON.stringify(seedPayload)); }
|
|
1172
1172
|
};
|
|
1173
|
-
function newSession() {
|
|
1174
|
-
return window.
|
|
1173
|
+
async function newSession() {
|
|
1174
|
+
return window.tmct.open({
|
|
1175
1175
|
seedPayload: cloneSeed(),
|
|
1176
1176
|
vocabSeeded: Boolean(seedPayload),
|
|
1177
1177
|
liveReference: liveReferenceForMode(checkedWikiMode()),
|
|
@@ -1205,7 +1205,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1205
1205
|
};
|
|
1206
1206
|
|
|
1207
1207
|
// ---- persistence: what you taught it survives a reload, on this device -
|
|
1208
|
-
// Best-effort IndexedDB (window.
|
|
1208
|
+
// Best-effort IndexedDB (window.tmct.page.openPersistedStore): the whole
|
|
1209
1209
|
// Backend-B payload snapshots after each teach turn, debounced so a burst
|
|
1210
1210
|
// of teaching costs one multi-MB write, not one per fact. The stamp ties a
|
|
1211
1211
|
// snapshot to this deploy (site version) AND this seed (its fact count and
|
|
@@ -1246,14 +1246,14 @@ ${THEME_TOKENS_CSS}
|
|
|
1246
1246
|
// it would keep merging peers' facts into a store nothing reads any more.
|
|
1247
1247
|
// Rejoining is a fresh invite, which is what a dropped node needs anyway.
|
|
1248
1248
|
dropRoom();
|
|
1249
|
-
window.tmctChatSession = newSession();
|
|
1250
|
-
const stats = await window.
|
|
1249
|
+
window.tmctChatSession = await newSession();
|
|
1250
|
+
const stats = await window.tmct.page.memoryStats(window.tmctChatSession.memoryDir);
|
|
1251
1251
|
addSystemLine("forgot everything taught on this device \\u2014 back to the fresh seed (" + statsSummaryLine(stats, bandLabelFor) + ").");
|
|
1252
1252
|
await renderStatsPanel(stats);
|
|
1253
1253
|
}
|
|
1254
1254
|
|
|
1255
1255
|
// ---- memory stats: the boot message's own numbers, and the docked panel -
|
|
1256
|
-
// Both read window.
|
|
1256
|
+
// Both read window.tmct.page.memoryStats(memoryDir) (chat-browser-entry.mjs)
|
|
1257
1257
|
// — one computation, reused, so the boot line and the panel can never
|
|
1258
1258
|
// disagree with each other about what this session's memory holds.
|
|
1259
1259
|
// bandLabelFor/statsSummaryLine/renderStatsPanelInto are the shared
|
|
@@ -1266,8 +1266,8 @@ ${THEME_TOKENS_CSS}
|
|
|
1266
1266
|
* rather than blanking it. */
|
|
1267
1267
|
async function renderStatsPanel(stats) {
|
|
1268
1268
|
if (!stats) {
|
|
1269
|
-
if (!window.tmctChatSession || !window.
|
|
1270
|
-
try { stats = await window.
|
|
1269
|
+
if (!window.tmctChatSession || !window.tmct.page.memoryStats) return;
|
|
1270
|
+
try { stats = await window.tmct.page.memoryStats(window.tmctChatSession.memoryDir); }
|
|
1271
1271
|
catch { return; }
|
|
1272
1272
|
}
|
|
1273
1273
|
factPillValueEl.textContent = Number(stats.total || 0).toLocaleString();
|
|
@@ -1282,7 +1282,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1282
1282
|
// and grounded so far — each entry pairs the passage a settled research
|
|
1283
1283
|
// turn's own answer cites (parseResearchAnswer, off the SAME "(source:
|
|
1284
1284
|
// research article ...)" text the chat bubble already shows) with the real
|
|
1285
|
-
// facts that turn stored, read back through window.
|
|
1285
|
+
// facts that turn stored, read back through window.tmct.
|
|
1286
1286
|
// researchedFactRows(memoryDir) rather than re-deriving them from the
|
|
1287
1287
|
// answer text — the citation names WHERE tmct read, the fact rows name
|
|
1288
1288
|
// WHAT it kept, and this panel never invents either from the other.
|
|
@@ -1348,9 +1348,9 @@ ${THEME_TOKENS_CSS}
|
|
|
1348
1348
|
* own entry — the passage was still read, even where nothing new stuck. */
|
|
1349
1349
|
async function noteResearchLearned(result) {
|
|
1350
1350
|
if (result.research === undefined || !result.record || result.record.miss) return;
|
|
1351
|
-
if (!window.
|
|
1351
|
+
if (!window.tmct.page.researchedFactRows || !window.tmctChatSession) return;
|
|
1352
1352
|
let rows;
|
|
1353
|
-
try { rows = await window.
|
|
1353
|
+
try { rows = await window.tmct.page.researchedFactRows(window.tmctChatSession.memoryDir); }
|
|
1354
1354
|
catch { return; }
|
|
1355
1355
|
const newFacts = [];
|
|
1356
1356
|
for (const row of rows) {
|
|
@@ -1446,7 +1446,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1446
1446
|
setBusy(true);
|
|
1447
1447
|
let result = null;
|
|
1448
1448
|
try {
|
|
1449
|
-
result = await window.
|
|
1449
|
+
result = await window.tmct.turn(q);
|
|
1450
1450
|
settleAssistantBubble(pendingRow, result.answer, result.record);
|
|
1451
1451
|
// Persist on ANY store write, not just a teach turn: a learn-on-miss
|
|
1452
1452
|
// load (a child pack, a reference or live-Wikipedia article, a
|
|
@@ -1587,10 +1587,10 @@ ${THEME_TOKENS_CSS}
|
|
|
1587
1587
|
// CLI paths emit), so what you taught leaves in the standard shape.
|
|
1588
1588
|
el("exportFacts").addEventListener("click", async () => {
|
|
1589
1589
|
const session = window.tmctChatSession;
|
|
1590
|
-
if (!session || !window.
|
|
1590
|
+
if (!session || !window.tmct.page.exportFactsJsonl) return;
|
|
1591
1591
|
let jsonl;
|
|
1592
1592
|
try {
|
|
1593
|
-
jsonl = await window.
|
|
1593
|
+
jsonl = await window.tmct.page.exportFactsJsonl(session.memoryDir);
|
|
1594
1594
|
} catch (err) {
|
|
1595
1595
|
statusEl.textContent = "couldn't export the facts (" + (err && err.message ? err.message : err) + ")";
|
|
1596
1596
|
return;
|
|
@@ -1607,7 +1607,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1607
1607
|
});
|
|
1608
1608
|
|
|
1609
1609
|
// "ingest file" feeds a whole .txt/.md through the SAME session, one
|
|
1610
|
-
// sentence at a time (window.
|
|
1610
|
+
// sentence at a time (window.tmct.page.splitSentences, then session.turn),
|
|
1611
1611
|
// teaching every sentence the recognizer grounds and skipping the rest
|
|
1612
1612
|
// honestly — the same pipeline the ingest page runs, reaching the chat's own
|
|
1613
1613
|
// memory so the taught facts answer questions straight away.
|
|
@@ -1616,7 +1616,7 @@ ${THEME_TOKENS_CSS}
|
|
|
1616
1616
|
const file = e.target.files && e.target.files[0];
|
|
1617
1617
|
e.target.value = "";
|
|
1618
1618
|
const session = window.tmctChatSession;
|
|
1619
|
-
if (!file || busy || !session || !window.
|
|
1619
|
+
if (!file || busy || !session || !window.tmct.page.splitSentences) return;
|
|
1620
1620
|
let text;
|
|
1621
1621
|
try {
|
|
1622
1622
|
text = await file.text();
|
|
@@ -1624,14 +1624,14 @@ ${THEME_TOKENS_CSS}
|
|
|
1624
1624
|
addSystemLine("couldn't read that file (" + (err && err.message ? err.message : err) + ").");
|
|
1625
1625
|
return;
|
|
1626
1626
|
}
|
|
1627
|
-
const sentences = window.
|
|
1627
|
+
const sentences = window.tmct.page.splitSentences(text);
|
|
1628
1628
|
if (!sentences.length) { addSystemLine("nothing to ingest in " + file.name + "."); return; }
|
|
1629
1629
|
setBusy(true);
|
|
1630
1630
|
statusEl.textContent = "ingesting " + file.name + "\\u2026";
|
|
1631
1631
|
let grounded = 0;
|
|
1632
1632
|
try {
|
|
1633
1633
|
for (const sentence of sentences) {
|
|
1634
|
-
const result = await
|
|
1634
|
+
const result = await tmct.turn(sentence);
|
|
1635
1635
|
if (result.record && result.record.via === "assert" && !result.record.miss) grounded += 1;
|
|
1636
1636
|
}
|
|
1637
1637
|
} catch (err) {
|
|
@@ -2221,16 +2221,16 @@ ${THEME_TOKENS_CSS}
|
|
|
2221
2221
|
}
|
|
2222
2222
|
|
|
2223
2223
|
async function boot() {
|
|
2224
|
-
if (!window.
|
|
2224
|
+
if (!window.tmct) {
|
|
2225
2225
|
statusEl.textContent = "the chat engine didn't load \\u2014 this page needs its build step (npm run demo:build)";
|
|
2226
2226
|
inputEl.placeholder = "chat engine unavailable";
|
|
2227
2227
|
return;
|
|
2228
2228
|
}
|
|
2229
2229
|
await Promise.all([fetchSeed(), tryLoadWink(), fetchSiteVersion().then((v) => { siteVersion = v; })]);
|
|
2230
2230
|
progressActive = false;
|
|
2231
|
-
window.
|
|
2232
|
-
if (window.
|
|
2233
|
-
persist = window.
|
|
2231
|
+
window.tmct.page.registerReferencePackProvider(fetchPackProvider);
|
|
2232
|
+
if (window.tmct.page.openPersistedStore) {
|
|
2233
|
+
persist = window.tmct.page.openPersistedStore({ storeKey: "chat", stamp: siteVersion + ":" + seedFacts + ":" + SEED_STAMP });
|
|
2234
2234
|
}
|
|
2235
2235
|
const savedRecord = persist ? await persist.load() : null;
|
|
2236
2236
|
const initialMode = readWikiMode();
|
|
@@ -2239,7 +2239,7 @@ ${THEME_TOKENS_CSS}
|
|
|
2239
2239
|
synthSliderEl.value = String(readSynthBudget());
|
|
2240
2240
|
synthValueEl.textContent = synthSliderEl.value;
|
|
2241
2241
|
if (savedRecord && savedRecord.payload) {
|
|
2242
|
-
window.tmctChatSession = window.
|
|
2242
|
+
window.tmctChatSession = await window.tmct.open({
|
|
2243
2243
|
seedPayload: savedRecord.payload,
|
|
2244
2244
|
vocabSeeded: true,
|
|
2245
2245
|
liveReference: liveReferenceForMode(initialMode),
|
|
@@ -2248,9 +2248,9 @@ ${THEME_TOKENS_CSS}
|
|
|
2248
2248
|
digestStructures: DIGEST_STRUCTURES,
|
|
2249
2249
|
});
|
|
2250
2250
|
} else {
|
|
2251
|
-
window.tmctChatSession = newSession();
|
|
2251
|
+
window.tmctChatSession = await newSession();
|
|
2252
2252
|
}
|
|
2253
|
-
const stats = await window.
|
|
2253
|
+
const stats = await window.tmct.page.memoryStats(window.tmctChatSession.memoryDir);
|
|
2254
2254
|
if (savedRecord) restoredCount = stats.taught.length;
|
|
2255
2255
|
const restoredNote = savedRecord
|
|
2256
2256
|
? " Restored " + restoredCount + " taught fact" + (restoredCount === 1 ? "" : "s")
|
|
@@ -2264,14 +2264,14 @@ ${THEME_TOKENS_CSS}
|
|
|
2264
2264
|
// seen-set from them so a later research turn only reports what's
|
|
2265
2265
|
// actually new, without fabricating passages for a visit this page
|
|
2266
2266
|
// was never open to read.
|
|
2267
|
-
if (window.
|
|
2267
|
+
if (window.tmct.page.researchedFactRows) {
|
|
2268
2268
|
try {
|
|
2269
|
-
const existingResearch = await window.
|
|
2269
|
+
const existingResearch = await window.tmct.page.researchedFactRows(window.tmctChatSession.memoryDir);
|
|
2270
2270
|
for (const row of existingResearch) researchedFactKeysSeen.add(row.subject + "|" + row.predicate + "|" + row.object);
|
|
2271
2271
|
} catch { /* best-effort seeding only — a fresh session has none to seed */ }
|
|
2272
2272
|
}
|
|
2273
2273
|
renderResearchedPanel();
|
|
2274
|
-
inputEl.placeholder = seedPayload ? 'try "what is a dog" or "list facts"' : window.
|
|
2274
|
+
inputEl.placeholder = seedPayload ? 'try "what is a dog" or "list facts"' : window.tmct.page.vocabExampleHint(false);
|
|
2275
2275
|
renderStatus();
|
|
2276
2276
|
setBusy(false);
|
|
2277
2277
|
inputEl.focus();
|
package/src/services/chat.mjs
CHANGED
|
@@ -2127,7 +2127,7 @@ export { moduleCountOf };
|
|
|
2127
2127
|
/** A KNOWN-empty code graph: a loaded graph object with 0 modules. A null graph
|
|
2128
2128
|
* (a bare runTurn that wasn't handed one) is "unknown", NOT empty — the empty
|
|
2129
2129
|
* orientation/greeting only fires when we actually hold an empty graph. */
|
|
2130
|
-
const noCodeGraph = (graph) => !!graph && moduleCountOf(graph) === 0;
|
|
2130
|
+
export const noCodeGraph = (graph) => !!graph && moduleCountOf(graph) === 0;
|
|
2131
2131
|
|
|
2132
2132
|
/** LIVE orientation examples: the example queries on the orientation card name
|
|
2133
2133
|
* entities from the LOADED graph — the sorted-first Module label and the
|
|
@@ -110,7 +110,7 @@ export function computeCodeExplorerData(payload, opts = {}) {
|
|
|
110
110
|
const CLIENT_JS = String.raw`
|
|
111
111
|
(function () {
|
|
112
112
|
var DATA = window.__CODE_EXPLORER__;
|
|
113
|
-
var api = window.
|
|
113
|
+
var api = window.tmct ? window.tmct.page : null;
|
|
114
114
|
var els = {
|
|
115
115
|
focus: document.getElementById("focus-name"),
|
|
116
116
|
ledger: document.getElementById("ledger"),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// self-contained document shaped exactly like chat-page-viz.mjs's own
|
|
3
3
|
// page-builder — one inlined <style> importing viz-theme.mjs's shared tokens,
|
|
4
4
|
// behaviour as an inlined IIFE — running the ingest engine
|
|
5
|
-
// (ingest-browser.bundle.js's globalThis.
|
|
5
|
+
// (ingest-browser.bundle.js's globalThis.tmct) by same-origin relative
|
|
6
6
|
// paths.
|
|
7
7
|
//
|
|
8
8
|
// The page's own chrome is a two-pane translate-tool layout: mode pills
|
|
@@ -133,7 +133,7 @@ ${THEME_TOKENS_CSS}
|
|
|
133
133
|
the right of the ingest column (a real layout column, not an overlay) —
|
|
134
134
|
the same class names and breakpoint chat-page-viz.mjs's own docked panel
|
|
135
135
|
uses, re-rendered after boot and after every ingest from
|
|
136
|
-
window.
|
|
136
|
+
window.tmct's own memoryStats(). */
|
|
137
137
|
.statsPanel { flex: 0 0 300px; max-width: 300px; overflow-y: auto; border-left: 1px solid var(--line); padding: 1.1rem 1.2rem 1.6rem; font-family: ${MONO_STACK}; font-size: .74rem; line-height: 1.55; display: flex; flex-direction: column; }
|
|
138
138
|
.statsPanel h2 { font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin: 1.3rem 0 .5rem; }
|
|
139
139
|
.statsPanel h2:first-child { margin-top: 0; }
|
|
@@ -359,8 +359,8 @@ ${THEME_TOKENS_CSS}
|
|
|
359
359
|
// ---- memory stats: the docked panel, same convention as chat.html --------
|
|
360
360
|
async function renderStatsPanel(stats) {
|
|
361
361
|
if (!stats) {
|
|
362
|
-
if (!session || !window.
|
|
363
|
-
try { stats = await window.
|
|
362
|
+
if (!session || !window.tmct.page.memoryStats) return;
|
|
363
|
+
try { stats = await window.tmct.page.memoryStats(session.memoryDir); }
|
|
364
364
|
catch { return; }
|
|
365
365
|
}
|
|
366
366
|
factPillValueEl.textContent = Number(stats.total || 0).toLocaleString();
|
|
@@ -415,13 +415,13 @@ ${THEME_TOKENS_CSS}
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
const cloneMemoryPayload = ${cloneMemoryPayload.toString()};
|
|
418
|
-
function newSession() {
|
|
419
|
-
return window.
|
|
418
|
+
async function newSession() {
|
|
419
|
+
return window.tmct.open({ seedPayload: cloneMemoryPayload(seedPayload), vocabSeeded: Boolean(seedPayload) });
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
// ---- engine boot ---------------------------------------------------------
|
|
423
423
|
const loadWinkVendor = ${loadWinkVendor.toString()};
|
|
424
|
-
const tryLoadWink = loadWinkVendor({ register: (factory) => window.
|
|
424
|
+
const tryLoadWink = loadWinkVendor({ register: (factory) => window.tmct.page.registerWinkModel(factory) });
|
|
425
425
|
|
|
426
426
|
// The deploy's own version, read off the service worker file the build
|
|
427
427
|
// already stamps — the only same-origin place the number exists at runtime
|
|
@@ -467,10 +467,10 @@ ${THEME_TOKENS_CSS}
|
|
|
467
467
|
clearTimeout(saveTimer);
|
|
468
468
|
saveTimer = null;
|
|
469
469
|
if (persist) await persist.clear();
|
|
470
|
-
session = newSession();
|
|
470
|
+
session = await newSession();
|
|
471
471
|
clearFactsPane();
|
|
472
472
|
updateIngestEnabled();
|
|
473
|
-
const stats = await window.
|
|
473
|
+
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
474
474
|
statusEl.textContent = "forgot everything taught on this device. Back to the fresh seed (" + statsSummaryLine(stats, bandLabelFor) + ").";
|
|
475
475
|
await renderStatsPanel(stats);
|
|
476
476
|
}
|
|
@@ -486,9 +486,9 @@ ${THEME_TOKENS_CSS}
|
|
|
486
486
|
await fetchSeedIfWanted();
|
|
487
487
|
clearTimeout(saveTimer);
|
|
488
488
|
saveTimer = null;
|
|
489
|
-
session = newSession();
|
|
489
|
+
session = await newSession();
|
|
490
490
|
clearFactsPane();
|
|
491
|
-
const stats = await window.
|
|
491
|
+
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
492
492
|
statusEl.textContent = statsSummaryLine(stats, bandLabelFor) + ". Ready.";
|
|
493
493
|
await renderStatsPanel(stats);
|
|
494
494
|
updateIngestEnabled();
|
|
@@ -496,7 +496,7 @@ ${THEME_TOKENS_CSS}
|
|
|
496
496
|
});
|
|
497
497
|
|
|
498
498
|
async function boot() {
|
|
499
|
-
if (!window.
|
|
499
|
+
if (!window.tmct) {
|
|
500
500
|
statusEl.textContent = "the ingest engine didn't load. This page needs its build step (npm run demo:build)";
|
|
501
501
|
return;
|
|
502
502
|
}
|
|
@@ -507,16 +507,16 @@ ${THEME_TOKENS_CSS}
|
|
|
507
507
|
fetchSiteVersion().then((v) => { siteVersion = v; }),
|
|
508
508
|
]);
|
|
509
509
|
progressActive = false;
|
|
510
|
-
if (window.
|
|
511
|
-
persist = window.
|
|
510
|
+
if (window.tmct.page.openPersistedStore) {
|
|
511
|
+
persist = window.tmct.page.openPersistedStore({ storeKey: "ingest", stamp: siteVersion + ":" + seedFacts + ":" + SEED_STAMP });
|
|
512
512
|
}
|
|
513
513
|
const savedRecord = persist ? await persist.load() : null;
|
|
514
514
|
session = savedRecord && savedRecord.payload
|
|
515
|
-
? window.
|
|
516
|
-
: newSession();
|
|
515
|
+
? await window.tmct.open({ seedPayload: savedRecord.payload, vocabSeeded: true })
|
|
516
|
+
: await newSession();
|
|
517
517
|
setMode(false);
|
|
518
518
|
updateIngestEnabled();
|
|
519
|
-
const stats = await window.
|
|
519
|
+
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
520
520
|
const winkPart = winkStatus === "loaded"
|
|
521
521
|
? "wink-nlp: loaded"
|
|
522
522
|
: "wink-nlp unavailable. The recognizer can't split sentences without it";
|
|
@@ -569,10 +569,10 @@ ${THEME_TOKENS_CSS}
|
|
|
569
569
|
|
|
570
570
|
// ---- download the canonical facts as JSONL -------------------------------
|
|
571
571
|
downloadBtn.addEventListener("click", async () => {
|
|
572
|
-
if (!session || !window.
|
|
572
|
+
if (!session || !window.tmct.page.exportFactsJsonl) return;
|
|
573
573
|
let jsonl;
|
|
574
574
|
try {
|
|
575
|
-
jsonl = await window.
|
|
575
|
+
jsonl = await window.tmct.page.exportFactsJsonl(session.memoryDir);
|
|
576
576
|
} catch (err) {
|
|
577
577
|
statusEl.textContent = "couldn't build the download (" + (err && err.message ? err.message : err) + ")";
|
|
578
578
|
return;
|