@polycode-projects/the-mechanical-code-talker 2.10.0 → 2.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/bin/tmct.mjs +16 -1
- package/data/templates/responses.jsonl +1 -0
- package/package.json +6 -1
- package/src/adapters/memory/export-jsonl.mjs +38 -0
- package/src/domain/ask.mjs +27 -2
- package/src/domain/cli-verbs.mjs +2 -1
- package/src/domain/code-explorer-hints.mjs +176 -0
- package/src/services/adventure-viz.mjs +29 -21
- package/src/services/chat-page-viz.mjs +39 -1
- package/src/services/chat.mjs +436 -26
- package/src/services/code-explorer-viz.mjs +343 -0
- package/src/services/import-file.mjs +69 -7
- package/src/services/spider-fly-viz.mjs +59 -3
- package/src/services/spider-fly.mjs +5 -2
- package/src/surfaces/web/chat-browser-entry.mjs +12 -1
- package/src/surfaces/web/code-explorer-browser-entry.mjs +79 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +119 -112
- package/src/tools/definitions.mjs +7 -0
- package/src/tools/handlers/index.mjs +2 -0
- package/src/tools/handlers/tmct-export.mjs +26 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// code-explorer-viz.mjs — the code-graph "ledger" the desktop shell renders:
|
|
2
|
+
// each import/call/contains edge read back as a plain sentence, a hint rail of
|
|
3
|
+
// suggested next queries, and a live chat dock over the same graph. The two
|
|
4
|
+
// derivations are pure so the shell, the packaging script, and the unit tests
|
|
5
|
+
// all share one code path; renderCodeExplorerHtml builds one self-contained
|
|
6
|
+
// document with no external requests.
|
|
7
|
+
//
|
|
8
|
+
// The channel is deliberately thin: this is the SAME ledger-pattern UI the
|
|
9
|
+
// browser ledger page uses, refocused on a code graph, and it stays servable
|
|
10
|
+
// as a plain page — only the Electron shell around it (electron/) is desktop.
|
|
11
|
+
|
|
12
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
|
|
13
|
+
import { generateCodeHints } from "../domain/code-explorer-hints.mjs";
|
|
14
|
+
|
|
15
|
+
// Third-person verb for each stored relation kind, symbol grain folded onto its
|
|
16
|
+
// coarse sibling. A kind with no row here reads back as itself, never breaking
|
|
17
|
+
// the sentence.
|
|
18
|
+
const EDGE_PHRASE = new Map([
|
|
19
|
+
["imports", "imports"],
|
|
20
|
+
["calls", "calls"], ["callsSymbol", "calls"],
|
|
21
|
+
["contains", "contains"],
|
|
22
|
+
["defines", "defines"],
|
|
23
|
+
["inherits", "inherits from"],
|
|
24
|
+
["tests", "tests"],
|
|
25
|
+
["touches", "touches"], ["touchesSymbol", "touches"],
|
|
26
|
+
["cochange", "co-changes with"],
|
|
27
|
+
["reexports", "re-exports"],
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function edgePhrase(kind) {
|
|
31
|
+
return EDGE_PHRASE.get(String(kind || "")) || String(kind || "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const LEDGER_ROW_LIMIT_DEFAULT = 4000;
|
|
35
|
+
|
|
36
|
+
/** Pure derivation over an entities payload (individuals + objectProperties).
|
|
37
|
+
* Returns { rows, terms, focus, stats, meta } — rows are one readable
|
|
38
|
+
* sentence per example edge, terms is the degree-ranked label index, and the
|
|
39
|
+
* neighbourhood of `focus` survives a row cap first so a huge graph degrades
|
|
40
|
+
* to "local + rest" rather than truncating the centre. */
|
|
41
|
+
export function computeCodeLedger(payload, { focus = null, rowLimit = LEDGER_ROW_LIMIT_DEFAULT } = {}) {
|
|
42
|
+
const individuals = Array.isArray(payload?.individuals) ? payload.individuals : [];
|
|
43
|
+
const classOf = new Map();
|
|
44
|
+
for (const ind of individuals) if (ind?.label && !classOf.has(ind.label)) classOf.set(ind.label, ind.class || "");
|
|
45
|
+
|
|
46
|
+
const groups = Array.isArray(payload?.objectProperties) ? payload.objectProperties : [];
|
|
47
|
+
const rows = [];
|
|
48
|
+
const degree = new Map();
|
|
49
|
+
const kindCounts = new Map();
|
|
50
|
+
const bumpTerm = (t) => { if (t) degree.set(t, (degree.get(t) || 0) + 1); };
|
|
51
|
+
for (const g of groups) {
|
|
52
|
+
if (!g?.predicate) continue;
|
|
53
|
+
const kind = String(g.predicate);
|
|
54
|
+
kindCounts.set(kind, (kindCounts.get(kind) || 0) + (Number(g.count) || 0));
|
|
55
|
+
for (const e of Array.isArray(g.examples) ? g.examples : []) {
|
|
56
|
+
const s = e?.subjectLabel || e?.subject;
|
|
57
|
+
const o = e?.objectLabel || e?.object;
|
|
58
|
+
if (!s || !o) continue;
|
|
59
|
+
rows.push({ s, kind, phrase: edgePhrase(kind), o, sClass: classOf.get(s) || "", oClass: classOf.get(o) || "" });
|
|
60
|
+
bumpTerm(s); bumpTerm(o);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const terms = [...degree.entries()]
|
|
65
|
+
.map(([term, deg]) => ({ term, degree: deg, class: classOf.get(term) || "" }))
|
|
66
|
+
.sort((a, b) => b.degree - a.degree || a.term.localeCompare(b.term));
|
|
67
|
+
|
|
68
|
+
let focusTerm = focus && degree.has(focus) ? focus : null;
|
|
69
|
+
if (!focusTerm && terms.length) focusTerm = terms[0].term;
|
|
70
|
+
|
|
71
|
+
const classCounts = new Map();
|
|
72
|
+
for (const ind of individuals) if (ind?.class) classCounts.set(ind.class, (classCounts.get(ind.class) || 0) + 1);
|
|
73
|
+
const stats = {
|
|
74
|
+
individuals: individuals.length,
|
|
75
|
+
edges: rows.length,
|
|
76
|
+
classes: [...classCounts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])),
|
|
77
|
+
kinds: [...kindCounts.entries()].filter(([, c]) => c > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const total = rows.length;
|
|
81
|
+
let shown = rows;
|
|
82
|
+
if (total > rowLimit) {
|
|
83
|
+
const near = new Set([focusTerm]);
|
|
84
|
+
for (const r of rows) { if (r.s === focusTerm) near.add(r.o); if (r.o === focusTerm) near.add(r.s); }
|
|
85
|
+
const inHood = (r) => near.has(r.s) || near.has(r.o);
|
|
86
|
+
shown = [...rows.filter(inHood), ...rows.filter((r) => !inHood(r))].slice(0, rowLimit);
|
|
87
|
+
}
|
|
88
|
+
return { rows: shown, terms, focus: focusTerm, stats, meta: { shown: shown.length, total, truncated: shown.length < total } };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Everything the page embeds, derived once from a payload: the ledger, the
|
|
92
|
+
* degree-ranked terms, the suggested queries, and the focus symbol. */
|
|
93
|
+
export function computeCodeExplorerData(payload, opts = {}) {
|
|
94
|
+
const ledger = computeCodeLedger(payload, opts);
|
|
95
|
+
const { focus, hints } = generateCodeHints(payload, { focus: ledger.focus });
|
|
96
|
+
return { payload, ledger, hints, focus: ledger.focus || focus, meta: { title: opts.title || "code graph" } };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const CLIENT_JS = String.raw`
|
|
100
|
+
(function () {
|
|
101
|
+
var DATA = window.__CODE_EXPLORER__;
|
|
102
|
+
var api = window.tmctCodeExplorer || null;
|
|
103
|
+
var els = {
|
|
104
|
+
focus: document.getElementById("focus-name"),
|
|
105
|
+
ledger: document.getElementById("ledger"),
|
|
106
|
+
hints: document.getElementById("hints"),
|
|
107
|
+
stats: document.getElementById("stats"),
|
|
108
|
+
log: document.getElementById("chat-log"),
|
|
109
|
+
form: document.getElementById("chat-form"),
|
|
110
|
+
input: document.getElementById("chat-input"),
|
|
111
|
+
dockNote: document.getElementById("dock-note"),
|
|
112
|
+
source: document.getElementById("source-name"),
|
|
113
|
+
};
|
|
114
|
+
var session = null;
|
|
115
|
+
|
|
116
|
+
function esc(s) {
|
|
117
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
|
118
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderStats(data) {
|
|
123
|
+
var s = data.ledger.stats;
|
|
124
|
+
var parts = [s.individuals + " individuals", s.edges + " edges"];
|
|
125
|
+
var cls = s.classes.slice(0, 4).map(function (c) { return c[1] + " " + c[0]; });
|
|
126
|
+
els.stats.textContent = parts.concat(cls).join(" · ");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderFocusRows(data) {
|
|
130
|
+
var focus = data.focus;
|
|
131
|
+
var rows = data.ledger.rows;
|
|
132
|
+
var near = rows.filter(function (r) { return r.s === focus || r.o === focus; });
|
|
133
|
+
var rest = rows.filter(function (r) { return r.s !== focus && r.o !== focus; });
|
|
134
|
+
var ordered = near.concat(rest);
|
|
135
|
+
els.ledger.innerHTML = ordered.map(function (r) {
|
|
136
|
+
var hot = (r.s === focus || r.o === focus) ? " row-focus" : "";
|
|
137
|
+
return '<li class="row' + hot + '">'
|
|
138
|
+
+ '<button class="term" data-term="' + esc(r.s) + '">' + esc(r.s) + '</button> '
|
|
139
|
+
+ '<span class="verb">' + esc(r.phrase) + '</span> '
|
|
140
|
+
+ '<button class="term" data-term="' + esc(r.o) + '">' + esc(r.o) + '</button>'
|
|
141
|
+
+ '</li>';
|
|
142
|
+
}).join("") || '<li class="row muted">no edges in this graph.</li>';
|
|
143
|
+
els.focus.textContent = focus || "—";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderHints(data) {
|
|
147
|
+
els.hints.innerHTML = data.hints.map(function (h) {
|
|
148
|
+
return '<button class="hint" data-q="' + esc(h.text) + '" title="' + esc(h.rationale) + '">'
|
|
149
|
+
+ esc(h.text) + '</button>';
|
|
150
|
+
}).join("") || '<span class="muted">nothing to suggest for this graph.</span>';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function focusOn(term) {
|
|
154
|
+
if (!api || !api.computeCodeExplorerData) return;
|
|
155
|
+
DATA = api.computeCodeExplorerData(DATA.payload, { focus: term, title: DATA.meta.title });
|
|
156
|
+
window.__CODE_EXPLORER__ = DATA;
|
|
157
|
+
mountView(DATA);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function mountView(data) {
|
|
161
|
+
renderStats(data);
|
|
162
|
+
renderFocusRows(data);
|
|
163
|
+
renderHints(data);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function appendLog(role, text) {
|
|
167
|
+
var div = document.createElement("div");
|
|
168
|
+
div.className = "turn turn-" + role;
|
|
169
|
+
div.innerHTML = '<span class="who">' + (role === "you" ? "you" : "tmct") + '</span><span class="said">' + esc(text) + '</span>';
|
|
170
|
+
els.log.appendChild(div);
|
|
171
|
+
els.log.scrollTop = els.log.scrollHeight;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function ensureSession() {
|
|
175
|
+
if (session || !api || !api.createCodeExplorerSession) return session;
|
|
176
|
+
var winkLoaded = true;
|
|
177
|
+
if (api.registerWinkModel && window.__WINK_LOADER__) {
|
|
178
|
+
try { var mod = await window.__WINK_LOADER__(); api.registerWinkModel(function () { return mod; }); }
|
|
179
|
+
catch (e) { winkLoaded = false; }
|
|
180
|
+
}
|
|
181
|
+
session = api.createCodeExplorerSession({ graphPayload: DATA.payload });
|
|
182
|
+
return session;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function ask(q) {
|
|
186
|
+
appendLog("you", q);
|
|
187
|
+
var s = await ensureSession();
|
|
188
|
+
if (!s) { appendLog("tmct", "the live dock is not loaded on this page."); return; }
|
|
189
|
+
var res = await s.turn(q);
|
|
190
|
+
appendLog("tmct", res.answer);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Delegate term + hint clicks.
|
|
194
|
+
document.addEventListener("click", function (ev) {
|
|
195
|
+
var t = ev.target.closest ? ev.target.closest("[data-term]") : null;
|
|
196
|
+
if (t) { focusOn(t.getAttribute("data-term")); return; }
|
|
197
|
+
var h = ev.target.closest ? ev.target.closest("[data-q]") : null;
|
|
198
|
+
if (h) { els.input.value = h.getAttribute("data-q"); els.input.focus(); if (api) ask(h.getAttribute("data-q")); return; }
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (els.form) {
|
|
202
|
+
els.form.addEventListener("submit", function (ev) {
|
|
203
|
+
ev.preventDefault();
|
|
204
|
+
var q = els.input.value.trim();
|
|
205
|
+
if (!q) return;
|
|
206
|
+
els.input.value = "";
|
|
207
|
+
ask(q);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Desktop pickers, present only under the Electron shell.
|
|
212
|
+
function wirePicker(id, method, updateSource) {
|
|
213
|
+
var btn = document.getElementById(id);
|
|
214
|
+
if (!btn) return;
|
|
215
|
+
if (!window.tmctDesktop || typeof window.tmctDesktop[method] !== "function") { btn.disabled = true; return; }
|
|
216
|
+
btn.addEventListener("click", async function () {
|
|
217
|
+
btn.disabled = true;
|
|
218
|
+
try {
|
|
219
|
+
var picked = await window.tmctDesktop[method]();
|
|
220
|
+
if (picked && picked.payload) {
|
|
221
|
+
session = null;
|
|
222
|
+
DATA = (api && api.computeCodeExplorerData)
|
|
223
|
+
? api.computeCodeExplorerData(picked.payload, { title: picked.name || "code graph" })
|
|
224
|
+
: DATA;
|
|
225
|
+
window.__CODE_EXPLORER__ = DATA;
|
|
226
|
+
if (updateSource && els.source) els.source.textContent = picked.name || "(loaded graph)";
|
|
227
|
+
els.log.innerHTML = "";
|
|
228
|
+
mountView(DATA);
|
|
229
|
+
}
|
|
230
|
+
} finally { btn.disabled = false; }
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
wirePicker("open-graph", "openGraph", true);
|
|
234
|
+
wirePicker("open-repo", "openRepo", true);
|
|
235
|
+
|
|
236
|
+
if (!api) {
|
|
237
|
+
if (els.dockNote) els.dockNote.textContent = "static view — the live chat dock is unavailable on this page.";
|
|
238
|
+
if (els.input) els.input.disabled = true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
mountView(DATA);
|
|
242
|
+
})();
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* One self-contained HTML document for the code explorer. `data` is
|
|
247
|
+
* computeCodeExplorerData's output. `bundleInline` inlines the dock engine
|
|
248
|
+
* (for a single-file page / a data: URL); otherwise `bundleAvailable` links
|
|
249
|
+
* `./code-explorer.bundle.js`. `winkLoaderInline` optionally inlines a wink
|
|
250
|
+
* model loader as `window.__WINK_LOADER__`.
|
|
251
|
+
*/
|
|
252
|
+
export function renderCodeExplorerHtml(data, { bundleInline = "", bundleAvailable = false, winkLoaderInline = "", sourceName = "demo code graph" } = {}) {
|
|
253
|
+
const payloadJson = embedJson(data.payload);
|
|
254
|
+
const dataJson = embedJson({ ledger: data.ledger, hints: data.hints, focus: data.focus, meta: data.meta });
|
|
255
|
+
const title = escapeHtml(data.meta?.title || "code explorer");
|
|
256
|
+
|
|
257
|
+
return `<!doctype html>
|
|
258
|
+
<html lang="en">
|
|
259
|
+
<head>
|
|
260
|
+
<meta charset="utf-8">
|
|
261
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
262
|
+
<title>tmct code explorer</title>
|
|
263
|
+
<style>
|
|
264
|
+
${THEME_TOKENS_CSS}
|
|
265
|
+
* { box-sizing: border-box; }
|
|
266
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; }
|
|
267
|
+
header { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; padding: 0.8rem 1.1rem; border-bottom: 1px solid var(--line); }
|
|
268
|
+
header h1 { font-size: 1.05rem; margin: 0; font-weight: 600; }
|
|
269
|
+
header .sub { color: var(--muted); font-size: 0.85rem; }
|
|
270
|
+
header .pickers { margin-left: auto; display: flex; gap: 0.5rem; }
|
|
271
|
+
button { font: inherit; cursor: pointer; }
|
|
272
|
+
button:disabled { cursor: default; opacity: 0.5; }
|
|
273
|
+
.pickers button { background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: 0.35rem 0.7rem; font-size: 0.85rem; }
|
|
274
|
+
#stats { padding: 0.4rem 1.1rem; color: var(--muted); font-size: 0.8rem; font-family: ${MONO_STACK}; border-bottom: 1px solid var(--line); }
|
|
275
|
+
main { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(260px, 1fr); gap: 0; align-items: stretch; }
|
|
276
|
+
@media (max-width: 720px) { main { grid-template-columns: 1fr; } }
|
|
277
|
+
.ledger-pane { padding: 0.6rem 1.1rem 2rem; min-height: 60vh; }
|
|
278
|
+
.rail { border-left: 1px solid var(--line); padding: 0.6rem 1rem 2rem; display: flex; flex-direction: column; gap: 1rem; }
|
|
279
|
+
h2 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin: 0 0 0.4rem; }
|
|
280
|
+
ul.rows { list-style: none; margin: 0; padding: 0; }
|
|
281
|
+
.row { padding: 0.28rem 0.4rem; border-radius: 5px; font-size: 0.95rem; line-height: 1.5; }
|
|
282
|
+
.row-focus { background: var(--corpus-soft); }
|
|
283
|
+
.row.muted, .muted { color: var(--muted); }
|
|
284
|
+
.term { background: none; border: none; padding: 0; color: var(--corpus); font-family: ${MONO_STACK}; font-size: 0.85rem; text-decoration: underline; text-decoration-color: var(--line); }
|
|
285
|
+
.term:hover { text-decoration-color: var(--corpus); }
|
|
286
|
+
.verb { color: var(--muted); }
|
|
287
|
+
.hints { display: flex; flex-direction: column; gap: 0.35rem; }
|
|
288
|
+
.hint { text-align: left; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 0.35rem 0.55rem; font-size: 0.85rem; color: var(--ink); }
|
|
289
|
+
.hint:hover { border-color: var(--corpus); }
|
|
290
|
+
.dock { display: flex; flex-direction: column; gap: 0.4rem; }
|
|
291
|
+
#chat-log { display: flex; flex-direction: column; gap: 0.4rem; max-height: 40vh; overflow-y: auto; }
|
|
292
|
+
.turn { font-size: 0.9rem; line-height: 1.45; }
|
|
293
|
+
.turn .who { display: block; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
|
|
294
|
+
.turn-you .said { color: var(--ink); }
|
|
295
|
+
.turn-tmct .said { color: var(--taught); white-space: pre-wrap; }
|
|
296
|
+
#chat-form { display: flex; gap: 0.4rem; }
|
|
297
|
+
#chat-input { flex: 1; font: inherit; padding: 0.4rem 0.5rem; border: 1px solid var(--line); border-radius: 6px; background: var(--card); color: var(--ink); }
|
|
298
|
+
#chat-form button { background: var(--corpus); color: #fff; border: none; border-radius: 6px; padding: 0.4rem 0.8rem; }
|
|
299
|
+
#dock-note { color: var(--muted); font-size: 0.78rem; }
|
|
300
|
+
.focus-line { font-family: ${MONO_STACK}; font-size: 0.85rem; }
|
|
301
|
+
</style>
|
|
302
|
+
</head>
|
|
303
|
+
<body>
|
|
304
|
+
<header>
|
|
305
|
+
<h1>tmct code explorer</h1>
|
|
306
|
+
<span class="sub">source: <span id="source-name">${escapeHtml(sourceName)}</span></span>
|
|
307
|
+
<div class="pickers">
|
|
308
|
+
<button id="open-graph">Open graph…</button>
|
|
309
|
+
<button id="open-repo">Open repo…</button>
|
|
310
|
+
</div>
|
|
311
|
+
</header>
|
|
312
|
+
<div id="stats"></div>
|
|
313
|
+
<main>
|
|
314
|
+
<section class="ledger-pane">
|
|
315
|
+
<h2>Facts around <span class="focus-line" id="focus-name">—</span></h2>
|
|
316
|
+
<ul class="rows" id="ledger"></ul>
|
|
317
|
+
</section>
|
|
318
|
+
<aside class="rail">
|
|
319
|
+
<div>
|
|
320
|
+
<h2>Try asking</h2>
|
|
321
|
+
<div class="hints" id="hints"></div>
|
|
322
|
+
</div>
|
|
323
|
+
<div class="dock">
|
|
324
|
+
<h2>Chat</h2>
|
|
325
|
+
<div id="chat-log"></div>
|
|
326
|
+
<form id="chat-form">
|
|
327
|
+
<input id="chat-input" type="text" autocomplete="off" placeholder="ask about this graph…">
|
|
328
|
+
<button type="submit">Ask</button>
|
|
329
|
+
</form>
|
|
330
|
+
<div id="dock-note"></div>
|
|
331
|
+
</div>
|
|
332
|
+
</aside>
|
|
333
|
+
</main>
|
|
334
|
+
<script>window.__CODE_EXPLORER__ = Object.assign({ payload: ${payloadJson} }, ${dataJson});</script>
|
|
335
|
+
${winkLoaderInline ? `<script>\n${embedScriptText(winkLoaderInline)}\n</script>` : ""}
|
|
336
|
+
${bundleInline ? `<script>\n${embedScriptText(bundleInline)}\n</script>` : ""}
|
|
337
|
+
${bundleAvailable && !bundleInline ? `<script src="./code-explorer.bundle.js"></script>` : ""}
|
|
338
|
+
<script>
|
|
339
|
+
${embedScriptText(CLIENT_JS)}
|
|
340
|
+
</script>
|
|
341
|
+
</body>
|
|
342
|
+
</html>`;
|
|
343
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
// import-file.mjs — `tmct import --file <definition.txt>`: teach a
|
|
2
|
-
// definition file
|
|
3
|
-
// live chat uses (runTurn)
|
|
1
|
+
// import-file.mjs — `tmct import --file <definition.txt|facts.jsonl>`: teach a
|
|
2
|
+
// definition file into a repo's memory. A plain-text file is taught one
|
|
3
|
+
// sentence at a time through the SAME recognizers the live chat uses (runTurn)
|
|
4
|
+
// — no separate parser, no guessing. A JSONL file (the shape `tmct extract` and
|
|
5
|
+
// `tmct memory --export` emit — one {subject, predicate, object, provenance}
|
|
6
|
+
// object per line) loads each fact straight into the store, so an exported
|
|
7
|
+
// triple-store round-trips back in with its provenance intact.
|
|
4
8
|
//
|
|
5
9
|
// The report is loud on purpose: a definition file that half-teaches produces
|
|
6
10
|
// a planner that finds wrong plans or no plans with no visible cause, so every
|
|
7
|
-
//
|
|
11
|
+
// line's outcome is printed and any decline makes the caller exit non-zero.
|
|
8
12
|
//
|
|
9
13
|
// `#` lines are comments (skipped, counted, never "declined") — a definition
|
|
10
14
|
// file carries its own example prompts this way.
|
|
@@ -13,10 +17,34 @@ import { readFile } from "node:fs/promises";
|
|
|
13
17
|
import { basename, resolve } from "node:path";
|
|
14
18
|
|
|
15
19
|
import { runTurn, uuidv7 } from "./chat.mjs";
|
|
16
|
-
import { loadMemory, readFactRows, appendFact, openConfiguredMemoryBackend } from "../adapters/memory/core.mjs";
|
|
20
|
+
import { loadMemory, readFactRows, appendFact, appendFacts, openConfiguredMemoryBackend } from "../adapters/memory/core.mjs";
|
|
17
21
|
import { loadConfig } from "../adapters/config.mjs";
|
|
18
22
|
import { splitSentencesPreservingPaths } from "./sentences.mjs";
|
|
19
23
|
|
|
24
|
+
/** A body line as a stored fact, or null when it is not a JSONL fact object.
|
|
25
|
+
* A fact line JSON-parses to an object carrying string subject/predicate/
|
|
26
|
+
* object; anything else (a plain sentence, malformed JSON, a JSON array) is
|
|
27
|
+
* not one. */
|
|
28
|
+
function parseFactLine(line) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed.startsWith("{")) return null;
|
|
31
|
+
let record;
|
|
32
|
+
try {
|
|
33
|
+
record = JSON.parse(trimmed);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (!record || typeof record !== "object") return null;
|
|
38
|
+
const { subject, predicate, object } = record;
|
|
39
|
+
if (typeof subject !== "string" || typeof predicate !== "string" || typeof object !== "string") return null;
|
|
40
|
+
if (!subject || !predicate || !object) return null;
|
|
41
|
+
return {
|
|
42
|
+
subject, predicate, object,
|
|
43
|
+
provenance: typeof record.provenance === "string" ? record.provenance : "",
|
|
44
|
+
quantifier: typeof record.quantifier === "string" ? record.quantifier : "",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
/**
|
|
21
49
|
* Teach every sentence of `filePath` into `repoRoot`'s memory store.
|
|
22
50
|
*
|
|
@@ -33,14 +61,48 @@ export async function importDefinitionFile(repoRoot, filePath, { env = process.e
|
|
|
33
61
|
|
|
34
62
|
const lines = text.split("\n");
|
|
35
63
|
const commentLines = lines.filter((l) => l.trim().startsWith("#"));
|
|
36
|
-
const
|
|
37
|
-
const
|
|
64
|
+
const bodyLines = lines.filter((l) => !l.trim().startsWith("#"));
|
|
65
|
+
const body = bodyLines.join("\n");
|
|
66
|
+
|
|
67
|
+
// A file whose every non-blank body line is a JSONL fact object is a
|
|
68
|
+
// triple-store dump, not prose — import each fact directly, keeping its own
|
|
69
|
+
// provenance, rather than pushing "{...}" through the sentence recognizer
|
|
70
|
+
// (which would decline it). This is the return leg for `tmct memory --export`
|
|
71
|
+
// and the browser pages' fact download.
|
|
72
|
+
const nonBlankBody = bodyLines.filter((l) => l.trim());
|
|
73
|
+
const factLines = nonBlankBody.map(parseFactLine);
|
|
74
|
+
const isFactFile = nonBlankBody.length > 0 && factLines.every(Boolean);
|
|
38
75
|
|
|
39
76
|
// An injected handle (a caller mid-session, or a build script pinned to the
|
|
40
77
|
// in-memory backend) is used as-is and left open — the caller owns it.
|
|
41
78
|
const opened = injectedMemoryDir ? null : await openConfiguredMemoryBackend(root, env);
|
|
42
79
|
const memoryDir = injectedMemoryDir ?? opened.dir;
|
|
43
80
|
const close = opened ? opened.close : async () => {};
|
|
81
|
+
|
|
82
|
+
if (isFactFile) {
|
|
83
|
+
const importReport = [`${basename(abs)} — ${factLines.length} fact(s), ${commentLines.length} comment line(s) skipped`, ""];
|
|
84
|
+
// One batched write for the whole dump — a triple export can carry every
|
|
85
|
+
// seed fact, so a per-fact loop (load+write each) would be quadratic.
|
|
86
|
+
try {
|
|
87
|
+
await appendFacts(memoryDir, factLines.map((fact) => ({
|
|
88
|
+
subject: fact.subject, predicate: fact.predicate, object: fact.object,
|
|
89
|
+
provenance: fact.provenance || sourceTag, quantifier: fact.quantifier,
|
|
90
|
+
})));
|
|
91
|
+
} finally {
|
|
92
|
+
await close();
|
|
93
|
+
}
|
|
94
|
+
const imported = factLines.map((fact) => `${fact.subject} ${fact.predicate} ${fact.object}`);
|
|
95
|
+
// A triple dump can carry tens of thousands of facts, so the per-line echo
|
|
96
|
+
// is capped — enough to show the shape, not a wall that overflows a caller's
|
|
97
|
+
// output buffer.
|
|
98
|
+
const LIST_CAP = 20;
|
|
99
|
+
for (const rendered of imported.slice(0, LIST_CAP)) importReport.push(` imported — ${rendered}`);
|
|
100
|
+
if (imported.length > LIST_CAP) importReport.push(` … and ${imported.length - LIST_CAP} more fact(s)`);
|
|
101
|
+
importReport.push("", `${imported.length} fact(s) imported, 0 declined, ${commentLines.length} comment line(s) skipped`);
|
|
102
|
+
return { sentences: factLines.length, taught: imported, declined: [], comments: commentLines.length, report: importReport.join("\n") };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const sentences = splitSentencesPreservingPaths(body);
|
|
44
106
|
const config = loadConfig(env, root);
|
|
45
107
|
|
|
46
108
|
const taught = [];
|
|
@@ -346,6 +346,10 @@ ${THEME_TOKENS_CSS}
|
|
|
346
346
|
negative margin equal to this padding, so it reads as one welded unit,
|
|
347
347
|
not a label floating inside a box. */
|
|
348
348
|
.hud, .chat, .tuning { background: var(--chrome-face); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-raised); border-radius: 2px; padding: .6rem .75rem; }
|
|
349
|
+
/* the tuning strip matches the board's own width (not the wider stage-left
|
|
350
|
+
column it sits in) so the two line up as one visual stack, the same way
|
|
351
|
+
the board-frame below it is already centered at a fixed width. */
|
|
352
|
+
.tuning { width: ${BOARD_PX}px; max-width: 100%; margin: 0 auto; box-sizing: border-box; }
|
|
349
353
|
.hud h2, .chat h2, .tuning h2 {
|
|
350
354
|
font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 600;
|
|
351
355
|
margin: -.6rem -.75rem .5rem; padding: .42rem .75rem;
|
|
@@ -357,12 +361,22 @@ ${THEME_TOKENS_CSS}
|
|
|
357
361
|
at once now, so the agent count (and a naive card list's own height)
|
|
358
362
|
can jump sharply; the panel must never grow the page underneath it. */
|
|
359
363
|
.hud-list { max-height: 420px; overflow-y: auto; }
|
|
360
|
-
.hud-row { display: flex;
|
|
364
|
+
.hud-row { display: flex; align-items: flex-start; gap: .6rem; padding: .4rem 0; border-top: 1px solid var(--chrome-edge-lo); box-shadow: inset 0 1px 0 var(--chrome-edge-hi); }
|
|
361
365
|
.hud-row:first-of-type { border-top: none; box-shadow: none; }
|
|
366
|
+
.hud-row.clickable { cursor: pointer; }
|
|
367
|
+
.hud-row.clickable:hover, .hud-row.clickable:focus-visible { background: var(--chrome-brass-soft); }
|
|
368
|
+
.hud-main { display: flex; flex-direction: column; gap: .1rem; flex: 1 1 auto; min-width: 0; }
|
|
362
369
|
.hud-id { font-family: ${MONO_STACK}; font-size: .74rem; font-weight: 600; }
|
|
363
370
|
.hud-id.spider { color: var(--taught); } .hud-id.fly { color: var(--fly); } .hud-id.egg { color: var(--muted); }
|
|
364
371
|
.hud-goal { font-size: .85rem; }
|
|
365
372
|
.hud-plan, .hud-belief { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); margin-top: .25rem; line-height: 1.4; padding-left: .5rem; border-left: 2px solid var(--chrome-brass); }
|
|
373
|
+
/* the click-expand facts panel (§28): beside the clicked spider/fly's own
|
|
374
|
+
row, never a separate popover or a second panel elsewhere on the page —
|
|
375
|
+
the same believedCellOf/beliefSnapshotFor read path spider-fly.mjs
|
|
376
|
+
already computes every tick for planning, rendered here as full
|
|
377
|
+
sentences instead of the compact believes:-line above. */
|
|
378
|
+
.hud-detail { flex: 1 1 auto; min-width: 0; font-family: ${MONO_STACK}; font-size: .64rem; line-height: 1.5; color: var(--chrome-well-ink); background: var(--chrome-well); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-inset); border-radius: 2px; padding: .35rem .5rem; }
|
|
379
|
+
.hud-detail-title { text-transform: uppercase; letter-spacing: .06em; opacity: .75; margin-bottom: .2rem; }
|
|
366
380
|
/* A stat-readout track: an inset "LCD" well, filled with a segmented pip
|
|
367
381
|
texture (repeating-linear-gradient) instead of a smooth gradient bar —
|
|
368
382
|
discrete resource units, the same reading-at-a-glance language a 90s
|
|
@@ -457,7 +471,7 @@ ${THEME_TOKENS_CSS}
|
|
|
457
471
|
<body>
|
|
458
472
|
<main>
|
|
459
473
|
<div class="eyebrow">tmct · spider and fly</div>
|
|
460
|
-
<h1>
|
|
474
|
+
<h1>Multiple competing planning agents</h1>
|
|
461
475
|
<div class="stage">
|
|
462
476
|
<div class="stage-left">
|
|
463
477
|
<div class="tuning" id="tuning">
|
|
@@ -543,6 +557,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
543
557
|
const spriteLayer = el("spriteLayer");
|
|
544
558
|
const threadTip = el("threadTip");
|
|
545
559
|
const hudEl = el("hud");
|
|
560
|
+
// Which agent ids currently show their expanded observed-facts panel —
|
|
561
|
+
// survives renderHud() rebuilding the list's innerHTML every tick, since
|
|
562
|
+
// that's the only way this state can persist across a full re-render.
|
|
563
|
+
const expandedAgents = new Set();
|
|
546
564
|
const chatlogEl = el("chatlog");
|
|
547
565
|
const chatformEl = el("chatform");
|
|
548
566
|
const chatqEl = el("chatq");
|
|
@@ -783,21 +801,59 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
783
801
|
return '<div class="hud-belief">believes: ' + text + "</div>";
|
|
784
802
|
}
|
|
785
803
|
|
|
804
|
+
// The full text rendering behind the click-expand panel: one sentence per
|
|
805
|
+
// other candidate the observer currently has a belief about, or "has not
|
|
806
|
+
// been observed" for a candidate it doesn't — the same belief map
|
|
807
|
+
// beliefLineHtml already compresses into a single line, spelled out in
|
|
808
|
+
// full beside the clicked agent instead of abbreviated above it.
|
|
809
|
+
function observedFactsHtml(observerId, belief) {
|
|
810
|
+
const entries = Object.entries(belief || {});
|
|
811
|
+
const lines = entries.length
|
|
812
|
+
? entries.map(([id, cell]) => '<div class="hud-detail-line">'
|
|
813
|
+
+ (cell ? esc(id) + " is at " + esc(cell) + "." : esc(id) + " has not been observed.")
|
|
814
|
+
+ "</div>").join("")
|
|
815
|
+
: '<div class="hud-detail-line">nothing else is on the board yet.</div>';
|
|
816
|
+
return '<div class="hud-detail"><div class="hud-detail-title">' + esc(observerId) + " observes</div>" + lines + "</div>";
|
|
817
|
+
}
|
|
818
|
+
|
|
786
819
|
function renderHud() {
|
|
787
820
|
const ids = Object.keys(lastAgents).sort();
|
|
788
821
|
if (!ids.length) { hudEl.innerHTML = '<div class="hud-empty">no agents on the board.</div>'; return; }
|
|
789
822
|
hudEl.innerHTML = ids.map((id) => {
|
|
790
823
|
const cls = classOfAgentId(id);
|
|
791
824
|
const a = lastAgents[id];
|
|
792
|
-
|
|
825
|
+
const clickable = cls === "spider" || cls === "fly";
|
|
826
|
+
const expanded = clickable && expandedAgents.has(id);
|
|
827
|
+
const main = '<div class="hud-main"><span class="hud-id ' + esc(cls) + '">' + esc(id) + '</span>'
|
|
793
828
|
+ '<span class="hud-goal">' + esc(goalById[id] || "watching\\u2026") + "</span>"
|
|
794
829
|
+ massBarHtml(cls, a.mass)
|
|
795
830
|
+ planLineHtml(a.plan)
|
|
796
831
|
+ beliefLineHtml(a.belief)
|
|
797
832
|
+ "</div>";
|
|
833
|
+
const attrs = clickable ? ' role="button" tabindex="0" aria-expanded="' + (expanded ? "true" : "false") + '"' : "";
|
|
834
|
+
return '<div class="hud-row' + (clickable ? " clickable" : "") + '" data-agent-id="' + esc(id) + '"' + attrs + '>'
|
|
835
|
+
+ main + (expanded ? observedFactsHtml(id, a.belief) : "")
|
|
836
|
+
+ "</div>";
|
|
798
837
|
}).join("");
|
|
799
838
|
}
|
|
800
839
|
|
|
840
|
+
function toggleAgentExpansion(id) {
|
|
841
|
+
if (!id) return;
|
|
842
|
+
if (expandedAgents.has(id)) expandedAgents.delete(id); else expandedAgents.add(id);
|
|
843
|
+
renderHud();
|
|
844
|
+
}
|
|
845
|
+
hudEl.addEventListener("click", (event) => {
|
|
846
|
+
const row = event.target.closest(".hud-row.clickable");
|
|
847
|
+
if (row) toggleAgentExpansion(row.dataset.agentId);
|
|
848
|
+
});
|
|
849
|
+
hudEl.addEventListener("keydown", (event) => {
|
|
850
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
851
|
+
const row = event.target.closest(".hud-row.clickable");
|
|
852
|
+
if (!row) return;
|
|
853
|
+
event.preventDefault();
|
|
854
|
+
toggleAgentExpansion(row.dataset.agentId);
|
|
855
|
+
});
|
|
856
|
+
|
|
801
857
|
const threadGeometry = {
|
|
802
858
|
parseCellId: (id) => tmctSpiderFly.parseCellId(id),
|
|
803
859
|
cellId: (x, y) => tmctSpiderFly.cellId(x, y),
|
|
@@ -636,8 +636,11 @@ function stepPlan(fromCell, toCell) {
|
|
|
636
636
|
* never ground truth: showing the observer's own honest gap
|
|
637
637
|
* between belief and reality (visibly widened by a deceiving pill or a fed
|
|
638
638
|
* false fact) is the whole point of that panel. Returns a plain
|
|
639
|
-
* `{ [candidateId]: cellId | null }` map.
|
|
640
|
-
|
|
639
|
+
* `{ [candidateId]: cellId | null }` map. Exported as the read path for
|
|
640
|
+
* what one agent can currently observe — every caller (this file's own
|
|
641
|
+
* tick loop, a viz panel, a future chat lane) reads the same computation,
|
|
642
|
+
* never a re-derived copy of it. */
|
|
643
|
+
export function beliefSnapshotFor(observerSubject, observerCell, candidateIds, state, opts) {
|
|
641
644
|
const belief = {};
|
|
642
645
|
for (const candidateId of candidateIds) {
|
|
643
646
|
if (candidateId === observerSubject) continue;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// without it.
|
|
19
19
|
import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
|
|
20
20
|
import { createInMemoryStore, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
|
|
21
|
+
import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
|
|
21
22
|
import { provenanceTagToSource } from "../../domain/memory/trust.mjs";
|
|
22
23
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
23
24
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
@@ -147,4 +148,14 @@ export async function memoryStats(memoryDir) {
|
|
|
147
148
|
return { total: rows.length, bandCounts, taught };
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
|
|
151
|
+
/**
|
|
152
|
+
* The session's whole triple store as JSONL — one
|
|
153
|
+
* { subject, predicate, object, provenance } object per line, the same shape
|
|
154
|
+
* `tmct extract` and `tmct memory --export` emit. Reads the live memory the
|
|
155
|
+
* same way memoryStats does; the page offers it as a download.
|
|
156
|
+
*/
|
|
157
|
+
export async function exportFactsJsonl(memoryDir) {
|
|
158
|
+
return serializeFactsJsonl(await loadMemory(memoryDir));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, normFactTerm, vocabExampleHint, memoryStats, openPersistedStore, exportFactsJsonl };
|