@polycode-projects/the-mechanical-code-talker 4.0.1 → 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/README.md +2 -1
- package/corpus/sprites/src/sprite-facts.jsonl +375 -8
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +36 -0
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +161 -14
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +119 -150
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +64 -48
- package/src/services/chat.mjs +102 -34
- package/src/services/code-explorer-viz.mjs +52 -50
- package/src/services/ingest-viz.mjs +32 -74
- package/src/services/ledger-viz.mjs +87 -70
- package/src/services/memory-panel-viz.mjs +38 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +119 -225
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +13 -12
- package/src/services/research-viz.mjs +25 -67
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +97 -136
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +274 -224
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +75 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
- package/src/surfaces/web/chat-browser-entry.mjs +51 -107
- package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
- package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +70 -49
- package/src/surfaces/web/plan-browser-entry.mjs +39 -50
- package/src/surfaces/web/research-browser-entry.mjs +48 -46
- package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
- package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
- package/src/surfaces/web/tmct-surface.mjs +147 -0
- package/src/surfaces/web/turn-session.mjs +124 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +37 -6
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
// (built by scripts/build-chat-bundle.mjs).
|
|
3
3
|
//
|
|
4
4
|
// Unlike memory-ask-browser-entry.mjs (factAnswer/factReadBack only), this
|
|
5
|
-
// exposes the FULL turn engine: createChatSession wraps
|
|
6
|
-
//
|
|
7
|
-
// src/services/chat-session.mjs's
|
|
8
|
-
// filesystem side effect (no transcript
|
|
9
|
-
// Memory is an in-memory Backend-B handle,
|
|
10
|
-
// built seed payload (scripts/build-chat-seed.mjs),
|
|
11
|
-
// proof chains and the honest miss all run
|
|
5
|
+
// exposes the FULL turn engine: createChatSession wraps the shared
|
|
6
|
+
// createTurnSession (turn-session.mjs) around chat.mjs's runTurn — the
|
|
7
|
+
// focus/last/planState/researchState threading src/services/chat-session.mjs's
|
|
8
|
+
// createSession.turn does, minus every filesystem side effect (no transcript
|
|
9
|
+
// log, no sidecar, no graph upsert). Memory is an in-memory Backend-B handle,
|
|
10
|
+
// optionally pre-loaded with a built seed payload (scripts/build-chat-seed.mjs),
|
|
11
|
+
// so teach turns, recall, proof chains and the honest miss all run
|
|
12
|
+
// client-side with zero I/O.
|
|
12
13
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// Two browser traps this file still owns, beyond what createTurnSession
|
|
15
|
+
// already covers (passing `env: {}` explicitly, since a browser has no
|
|
16
|
+
// `process` global; the crash-resistant catch fallback):
|
|
16
17
|
// - the uuid adapter needs node:crypto — the session id comes from the
|
|
17
18
|
// Web Crypto API instead, with a Date.now fallback for contexts
|
|
18
19
|
// without it;
|
|
@@ -25,15 +26,15 @@
|
|
|
25
26
|
// adapter — its own setDigestStructures is a documented no-op there
|
|
26
27
|
// (see that module's header), so the call is harmless either way, never
|
|
27
28
|
// a load error and never a behavior change on the Node side.
|
|
28
|
-
import {
|
|
29
|
-
import { createInMemoryStore, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
|
|
30
|
-
import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
|
|
29
|
+
import { vocabExampleHint } from "../../services/chat.mjs";
|
|
30
|
+
import { createInMemoryStore, normFactTerm, loadMemory, readFactRows, applySeedPayload } from "../../adapters/memory/core.mjs";
|
|
31
31
|
import { splitSentencesPreservingPaths } from "../../services/sentences.mjs";
|
|
32
|
-
import { provenanceTagToSource } from "../../domain/memory/trust.mjs";
|
|
33
32
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
34
33
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
35
34
|
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
36
35
|
import { setDigestStructures } from "../../adapters/corpus/digest-bank.mjs";
|
|
36
|
+
import { createTurnSession } from "./turn-session.mjs";
|
|
37
|
+
import { memoryStats, exportFactsJsonl } from "./memory-stats.mjs";
|
|
37
38
|
// The reference-pack provider seam: the page registers a fetch-backed
|
|
38
39
|
// provider over public/reference-pack/ so the engine's pack lookups work
|
|
39
40
|
// where the gzipped fs layout cannot (the module's own fs loader degrades to
|
|
@@ -50,6 +51,8 @@ import { registerLiveReferenceProvider, registerResearchProvider } from "../../a
|
|
|
50
51
|
// decides when to save/load/clear; this entry only carries the wrapper
|
|
51
52
|
// across the bundle boundary.
|
|
52
53
|
import { openPersistedStore } from "./idb-persist.mjs";
|
|
54
|
+
import { publishTmctSurface } from "./tmct-surface.mjs";
|
|
55
|
+
import { graphAsk, enginePlan } from "./engine-surface.mjs";
|
|
53
56
|
|
|
54
57
|
/**
|
|
55
58
|
* A browser chat session over the real turn engine.
|
|
@@ -69,17 +72,14 @@ import { openPersistedStore } from "./idb-persist.mjs";
|
|
|
69
72
|
* is module-scope, last write wins, and every session created after this one
|
|
70
73
|
* shares it.
|
|
71
74
|
*
|
|
72
|
-
* Returns { memoryDir, sessionId, turn }. `turn(line)` resolves to
|
|
75
|
+
* Returns { memoryDir, sessionId, graph, turn }. `turn(line)` resolves to
|
|
73
76
|
* { answer, end, record, plan } and threads focus/last/planState between
|
|
74
77
|
* calls exactly as the CLI session does.
|
|
75
78
|
*/
|
|
76
79
|
export function createChatSession({ seedPayload = null, vocabSeeded = false, liveReference = false, onLiveLookup = null, synthesisBudget = 12, digestStructures = null } = {}) {
|
|
77
80
|
setDigestStructures(digestStructures || []);
|
|
78
81
|
const memoryDir = createInMemoryStore();
|
|
79
|
-
|
|
80
|
-
// and objectProperties only) still carries the classes/prefixes scaffolding
|
|
81
|
-
// the write path recounts — teach turns must work on any seed.
|
|
82
|
-
if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
|
|
82
|
+
applySeedPayload(memoryDir, seedPayload);
|
|
83
83
|
|
|
84
84
|
// A known-empty code graph: code-structure questions get the same honest
|
|
85
85
|
// no-code-graph answer an un-pointed CLI session gives, never a crash.
|
|
@@ -88,10 +88,6 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
|
|
|
88
88
|
const vocabHint = vocabExampleHint(vocabSeeded);
|
|
89
89
|
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
90
90
|
|
|
91
|
-
let focus = null;
|
|
92
|
-
let last = null;
|
|
93
|
-
let planState = null;
|
|
94
|
-
let researchState = null;
|
|
95
91
|
// Four-state, like the CLI: false (off), true (rescue on a miss),
|
|
96
92
|
// "supplement" (also append a cited read-out under every grounded vocabulary
|
|
97
93
|
// answer), or "always" (widen that to every grounded answer). The two string
|
|
@@ -102,9 +98,23 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
|
|
|
102
98
|
// page's slider sets it; 0 stores article facts without any entailed rows.
|
|
103
99
|
let synthesisBudgetOn = Number.isFinite(synthesisBudget) ? synthesisBudget : 12;
|
|
104
100
|
|
|
101
|
+
const session = createTurnSession({
|
|
102
|
+
memoryDir, graph, lexicon, sessionId, vocabHint,
|
|
103
|
+
buildExtraOptions: () => ({
|
|
104
|
+
liveReference: liveReferenceOn, onLiveLookup,
|
|
105
|
+
uiContext: "browser", synthesisBudget: synthesisBudgetOn,
|
|
106
|
+
}),
|
|
107
|
+
// `result.liveReference` mirrors a `/wiki on|off|supplement|always`
|
|
108
|
+
// command back into this session's own toggle state.
|
|
109
|
+
captureExtraState: (result) => {
|
|
110
|
+
if (typeof result.liveReference === "boolean" || result.liveReference === "supplement" || result.liveReference === "always") liveReferenceOn = result.liveReference;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
105
114
|
return {
|
|
106
115
|
memoryDir,
|
|
107
116
|
sessionId,
|
|
117
|
+
graph,
|
|
108
118
|
get liveReference() { return liveReferenceOn; },
|
|
109
119
|
/** The page's toggle seam: set the live Wikipedia mode for every later turn
|
|
110
120
|
* (the `/wiki on|off|supplement|always` command sets the same state). */
|
|
@@ -113,92 +123,10 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
|
|
|
113
123
|
/** The page's slider seam: set the auto-synthesis budget for every later
|
|
114
124
|
* learn-on-miss load. Clamped to a non-negative integer; 0 disables it. */
|
|
115
125
|
setSynthesisBudget(n) { synthesisBudgetOn = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0; },
|
|
116
|
-
|
|
117
|
-
/** One dispatched turn. A throwing runTurn must never kill the session —
|
|
118
|
-
* the page has no other chance to show this turn's answer. */
|
|
119
|
-
async turn(line) {
|
|
120
|
-
let result;
|
|
121
|
-
try {
|
|
122
|
-
result = await runTurn(line, {
|
|
123
|
-
config: null, source: null, graph, focus, last, memoryDir, sessionId,
|
|
124
|
-
env: {}, lexicon, vocabHint, planState, researchState,
|
|
125
|
-
liveReference: liveReferenceOn, onLiveLookup,
|
|
126
|
-
uiContext: "browser", synthesisBudget: synthesisBudgetOn,
|
|
127
|
-
});
|
|
128
|
-
} catch (e) {
|
|
129
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
130
|
-
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null, plan: null, research: null };
|
|
131
|
-
}
|
|
132
|
-
focus = result.focus;
|
|
133
|
-
last = result.last;
|
|
134
|
-
if ("planState" in result) planState = result.planState;
|
|
135
|
-
if ("researchState" in result) researchState = result.researchState;
|
|
136
|
-
if (typeof result.liveReference === "boolean" || result.liveReference === "supplement" || result.liveReference === "always") liveReferenceOn = result.liveReference;
|
|
137
|
-
// `research` distinguishes three cases on purpose: a queue snapshot
|
|
138
|
-
// (research turn), null (a research turn that ended the run), and
|
|
139
|
-
// undefined (not a research turn — the page leaves its controls alone).
|
|
140
|
-
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null, plan: result.plan ?? null, research: result.research };
|
|
141
|
-
},
|
|
126
|
+
turn: session.turn,
|
|
142
127
|
};
|
|
143
128
|
}
|
|
144
129
|
|
|
145
|
-
/**
|
|
146
|
-
* The memory a running session holds, broken down by where each fact came
|
|
147
|
-
* from: the seed corpus bands it booted with (keyed by the SAME band name
|
|
148
|
-
* build-chat-seed.mjs/extensions.mjs seed under — "human", "seon",
|
|
149
|
-
* "conceptnet" today, whichever bands a future seed adds tomorrow) plus
|
|
150
|
-
* whatever has been taught THIS session over chat. Reuses memory/trust.mjs's
|
|
151
|
-
* own `provenanceTagToSource` against each fact's already-stored provenance
|
|
152
|
-
* tag(s) (readFactRows' `provenance`, the ' | '-joined compat string) rather
|
|
153
|
-
* than inventing a second provenance parse — the same tag chat.mjs's own
|
|
154
|
-
* "(source: ...)" citation already carries, so this panel and a turn's
|
|
155
|
-
* citation always agree on where a fact came from.
|
|
156
|
-
*
|
|
157
|
-
* Returns { total, bandCounts, taught }: `bandCounts` maps a band label to
|
|
158
|
-
* its fact count ("taught this session" for a teach/operator-sourced fact
|
|
159
|
-
* with no corpus band, "other" for anything provenance can't place);
|
|
160
|
-
* `taught` lists every session-taught fact (subject/predicate/object + its
|
|
161
|
-
* own provenance tag), most-recently-taught last — a stats panel's
|
|
162
|
-
* provenance column reads straight off this, no further lookup needed.
|
|
163
|
-
*/
|
|
164
|
-
export async function memoryStats(memoryDir) {
|
|
165
|
-
const memory = await loadMemory(memoryDir);
|
|
166
|
-
const rows = readFactRows(memory);
|
|
167
|
-
const bandCounts = {};
|
|
168
|
-
const taught = [];
|
|
169
|
-
for (const row of rows) {
|
|
170
|
-
const tags = String(row.provenance || "").split(" | ").filter(Boolean);
|
|
171
|
-
let band = null;
|
|
172
|
-
let isTaught = false;
|
|
173
|
-
let taughtTag = "";
|
|
174
|
-
for (const tag of tags) {
|
|
175
|
-
const src = provenanceTagToSource(tag);
|
|
176
|
-
if (!src) continue;
|
|
177
|
-
// corpusWeak (a /r/RelatedTo-strength triple, e.g. ConceptNet's or
|
|
178
|
-
// SEON's own weaker associations) names the SAME band as corpus (a
|
|
179
|
-
// /r/IsA-strength one) — both carry `src.name`, and a band count that
|
|
180
|
-
// dropped the weak tier would undercount a corpus by exactly its
|
|
181
|
-
// weak-relation facts (SEON: 19 of them, all `corpus-weak:seon`).
|
|
182
|
-
if ((src.kind === "corpus" || src.kind === "corpusWeak") && src.name) band = src.name;
|
|
183
|
-
if (src.kind === "teach" || src.kind === "operator") { isTaught = true; taughtTag = tag; }
|
|
184
|
-
}
|
|
185
|
-
const label = band || (isTaught ? "taught this session" : "other");
|
|
186
|
-
bandCounts[label] = (bandCounts[label] || 0) + 1;
|
|
187
|
-
if (isTaught) taught.push({ subject: row.subject, predicate: row.predicate, object: row.object, tag: taughtTag });
|
|
188
|
-
}
|
|
189
|
-
return { total: rows.length, bandCounts, taught };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* The session's whole triple store as JSONL — one
|
|
194
|
-
* { subject, predicate, object, provenance } object per line, the same shape
|
|
195
|
-
* `tmct extract` and `tmct memory --export` emit. Reads the live memory the
|
|
196
|
-
* same way memoryStats does; the page offers it as a download.
|
|
197
|
-
*/
|
|
198
|
-
export async function exportFactsJsonl(memoryDir) {
|
|
199
|
-
return serializeFactsJsonl(await loadMemory(memoryDir));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
130
|
/**
|
|
203
131
|
* Every fact a "research <topic>" run has stored so far, in storage order —
|
|
204
132
|
* the exposure the "researched this session" panel needs and runTurn's
|
|
@@ -219,4 +147,20 @@ export async function researchedFactRows(memoryDir) {
|
|
|
219
147
|
.map((row) => ({ subject: row.subject, predicate: row.predicate, object: row.object }));
|
|
220
148
|
}
|
|
221
149
|
|
|
222
|
-
|
|
150
|
+
// The page reaches the engine through the one shared surface: `tmct.open()`
|
|
151
|
+
// opens this session, `tmct.turn()` runs the dock, `tmct.ask()` puts a
|
|
152
|
+
// question to the session's own graph. What stays on `tmct.page` is what has
|
|
153
|
+
// no plain-English form — the vendor/provider seams the page registers before
|
|
154
|
+
// the first turn, its IndexedDB wrapper, and the two serializers its export
|
|
155
|
+
// and paste-ingest controls run.
|
|
156
|
+
publishTmctSurface({
|
|
157
|
+
open: createChatSession,
|
|
158
|
+
ask: graphAsk,
|
|
159
|
+
plan: enginePlan,
|
|
160
|
+
page: {
|
|
161
|
+
registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider,
|
|
162
|
+
registerResearchProvider, normFactTerm, vocabExampleHint, memoryStats,
|
|
163
|
+
openPersistedStore, exportFactsJsonl, researchedFactRows,
|
|
164
|
+
splitSentences: splitSentencesPreservingPaths,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
@@ -13,14 +13,19 @@
|
|
|
13
13
|
// Gitignored, built fresh by scripts/build-code-explorer-bundle.mjs; the page
|
|
14
14
|
// degrades to a static view when it is absent (renderCodeExplorerHtml's own
|
|
15
15
|
// contract), so nothing here is ever published.
|
|
16
|
-
import {
|
|
16
|
+
import { vocabExampleHint } from "../../services/chat.mjs";
|
|
17
17
|
import { createInMemoryStore, normFactTerm } from "../../adapters/memory/core.mjs";
|
|
18
18
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
19
19
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
20
20
|
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
21
21
|
import * as source from "../../adapters/source.mjs";
|
|
22
|
-
import { computeCodeExplorerData, computeCodeLedger } from "../../services/code-explorer-viz.mjs";
|
|
22
|
+
import { computeCodeExplorerData, computeCodeLedger, edgePhrase } from "../../services/code-explorer-viz.mjs";
|
|
23
23
|
import { generateCodeHints } from "../../domain/code-explorer-hints.mjs";
|
|
24
|
+
import { createTurnSession } from "./turn-session.mjs";
|
|
25
|
+
import { publishTmctSurface } from "./tmct-surface.mjs";
|
|
26
|
+
import { graphAsk, enginePlan } from "./engine-surface.mjs";
|
|
27
|
+
import { tmct_ask } from "../../tools/handlers/tmct-ask.mjs";
|
|
28
|
+
import { RELATIONS } from "../../domain/ask-vocab.mjs";
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
31
|
* A browser code-explorer session over the real turn engine. Registers the
|
|
@@ -34,7 +39,7 @@ import { generateCodeHints } from "../../domain/code-explorer-hints.mjs";
|
|
|
34
39
|
* payload carries the starter vocabulary. Teaches land in the same in-memory
|
|
35
40
|
* store, so a taught fact never touches disk.
|
|
36
41
|
*
|
|
37
|
-
* Returns { memoryDir, sessionId, turn }, the createChatSession shape the
|
|
42
|
+
* Returns { memoryDir, sessionId, graph, turn }, the createChatSession shape the
|
|
38
43
|
* page's chat expects.
|
|
39
44
|
*/
|
|
40
45
|
export function createCodeExplorerSession({ graphPayload, seedPayload = null, vocabSeeded = false } = {}) {
|
|
@@ -54,42 +59,194 @@ export function createCodeExplorerSession({ graphPayload, seedPayload = null, vo
|
|
|
54
59
|
// ever read, but the code lanes still do path math (join/dirname) on it.
|
|
55
60
|
const config = { graphFile: "graph.json" };
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
const turnSession = createTurnSession({
|
|
63
|
+
memoryDir, graph, lexicon, sessionId, vocabHint,
|
|
64
|
+
// The code explorer is the one page whose turns run against a real
|
|
65
|
+
// provider seam (source.mjs, registered above) and a virtual graph file
|
|
66
|
+
// path, so both override createTurnSession's config:null/source:null
|
|
67
|
+
// defaults on every call.
|
|
68
|
+
buildExtraOptions: () => ({ config, source }),
|
|
69
|
+
});
|
|
60
70
|
|
|
61
71
|
return {
|
|
62
72
|
memoryDir,
|
|
63
73
|
sessionId,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
result = await runTurn(line, {
|
|
68
|
-
config, source, graph, focus, last, memoryDir, sessionId,
|
|
69
|
-
env: {}, lexicon, vocabHint, planState,
|
|
70
|
-
});
|
|
71
|
-
} catch (e) {
|
|
72
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
73
|
-
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null };
|
|
74
|
-
}
|
|
75
|
-
focus = result.focus;
|
|
76
|
-
last = result.last;
|
|
77
|
-
if ("planState" in result) planState = result.planState;
|
|
78
|
-
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null };
|
|
79
|
-
},
|
|
74
|
+
graph,
|
|
75
|
+
turn: turnSession.turn,
|
|
80
76
|
};
|
|
81
77
|
}
|
|
82
78
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
// ---- "what relates to X", asked ------------------------------------------
|
|
80
|
+
|
|
81
|
+
// A symbol-grain predicate asks as its coarse sibling: ask()'s relation
|
|
82
|
+
// vocabulary carries one question per grain pair, not two.
|
|
83
|
+
const COARSE_RELATION = { callsSymbol: "calls", touchesSymbol: "touches" };
|
|
84
|
+
|
|
85
|
+
/** The two questions ask() understands about one relation kind, both spelled
|
|
86
|
+
* from its own vocabulary: `verbs[0]` reads as the reverse direction ("what
|
|
87
|
+
* couples to X"), `bare` as the forward one ("what does X import"). A relation
|
|
88
|
+
* taught to ask-vocab is asked here with no second table to keep in step. */
|
|
89
|
+
function relationQuestions(kind, term) {
|
|
90
|
+
const { bare, verbs } = RELATIONS[kind];
|
|
91
|
+
return [
|
|
92
|
+
{ shape: "reverse", query: `what ${verbs[0]} ${term}` },
|
|
93
|
+
{ shape: "forward", query: `what does ${term} ${bare}` },
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The kinds worth asking about: the ones this graph actually stores, folded
|
|
98
|
+
* onto ask()'s vocabulary keys. A graph carrying only imports asks two
|
|
99
|
+
* questions, not twenty. */
|
|
100
|
+
function askableRelationKinds(graph) {
|
|
101
|
+
const kinds = new Set();
|
|
102
|
+
for (const relation of graph.relations) {
|
|
103
|
+
if (!relation.edges.length) continue;
|
|
104
|
+
const kind = COARSE_RELATION[relation.predicate] || relation.predicate;
|
|
105
|
+
if (Object.hasOwn(RELATIONS, kind)) kinds.add(kind);
|
|
106
|
+
}
|
|
107
|
+
return [...kinds];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Every stored edge as `<subjectId> <objectId>`, keyed by the coarse relation
|
|
111
|
+
* kind so both grains of a pair land in one set, beside a label-to-ids index.
|
|
112
|
+
* An answer names the individuals it found; these two decide whether the row
|
|
113
|
+
* built from that answer is an edge the graph actually holds. */
|
|
114
|
+
function indexEdges(graph) {
|
|
115
|
+
const edgesByKind = new Map();
|
|
116
|
+
for (const relation of graph.relations) {
|
|
117
|
+
const kind = COARSE_RELATION[relation.predicate] || relation.predicate;
|
|
118
|
+
let pairs = edgesByKind.get(kind);
|
|
119
|
+
if (!pairs) edgesByKind.set(kind, (pairs = new Set()));
|
|
120
|
+
for (const edge of relation.edges) pairs.add(`${edge.subject} ${edge.object}`);
|
|
121
|
+
}
|
|
122
|
+
const idsByLabel = new Map();
|
|
123
|
+
for (const individual of graph.individuals) {
|
|
124
|
+
if (!individual?.label || !individual.id) continue;
|
|
125
|
+
const ids = idsByLabel.get(individual.label);
|
|
126
|
+
if (ids) ids.push(individual.id);
|
|
127
|
+
else idsByLabel.set(individual.label, [individual.id]);
|
|
128
|
+
}
|
|
129
|
+
return { edgesByKind, idsByLabel };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// parseEntities walks the whole payload, and a focus click re-asks the same
|
|
133
|
+
// graph, so one parse and one edge index per payload serve every question put
|
|
134
|
+
// to it.
|
|
135
|
+
const readGraphs = new WeakMap();
|
|
136
|
+
function readGraph(payload) {
|
|
137
|
+
let read = readGraphs.get(payload);
|
|
138
|
+
if (!read) {
|
|
139
|
+
const graph = parseEntities(payload);
|
|
140
|
+
read = { graph, ...indexEdges(graph) };
|
|
141
|
+
readGraphs.set(payload, read);
|
|
142
|
+
}
|
|
143
|
+
return read;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const rowKey = (row) => JSON.stringify([row.s, row.kind, row.o]);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* "What relates to <term>", asked instead of computed. Every relation kind the
|
|
150
|
+
* loaded graph carries becomes two real tmct_ask round trips over that graph —
|
|
151
|
+
* the same ask() every chat turn on this page already reaches — and the
|
|
152
|
+
* answers' typed `matches` become the explorer sidebar's rows. The page stops
|
|
153
|
+
* deciding what relates to what, so the panel and the conversation cannot
|
|
154
|
+
* scope or phrase one question two ways.
|
|
155
|
+
*
|
|
156
|
+
* Two things have to hold before an answer becomes a row. ask() must have
|
|
157
|
+
* parsed the question as the question asked: an identifier that is itself a
|
|
158
|
+
* relation verb makes it read "what tests run" as a question about testing, and
|
|
159
|
+
* rows off that answer would describe the wrong term. And the row must be an
|
|
160
|
+
* edge this graph holds between the term and what the answer named — ask()
|
|
161
|
+
* resolves a term against the asked relation's own range, so "what contains
|
|
162
|
+
* Task" legitimately answers about `Task.title`'s container, which is a true
|
|
163
|
+
* sentence about a different pair than the one the sidebar is drawing. A row
|
|
164
|
+
* the graph cannot confirm is dropped rather than shown; the engine's own prose
|
|
165
|
+
* is where an inexact resolution belongs, because there it can say so.
|
|
166
|
+
*
|
|
167
|
+
* `grounded` is false when no question was understood as asked, which is the
|
|
168
|
+
* caller's signal to fall back rather than show an empty neighbourhood for a
|
|
169
|
+
* term the graph plainly has edges for.
|
|
170
|
+
*
|
|
171
|
+
* Returns { term, rows, asked, grounded }: `rows` in computeCodeLedger's own
|
|
172
|
+
* row shape so one renderer draws both, `asked` every question put to the
|
|
173
|
+
* engine beside what came back.
|
|
174
|
+
*/
|
|
175
|
+
export function askRelatedFacts(graphPayload, term) {
|
|
176
|
+
const rows = [];
|
|
177
|
+
const asked = [];
|
|
178
|
+
let grounded = false;
|
|
179
|
+
if (!graphPayload || !term) return { term: term || null, rows, asked, grounded };
|
|
180
|
+
|
|
181
|
+
const { graph, edgesByKind, idsByLabel } = readGraph(graphPayload);
|
|
182
|
+
// An edge group's objectLabel is denormalized and can be shorter than the
|
|
183
|
+
// individual's own ("assignTo" for `Task.assignTo`), so the row list offers
|
|
184
|
+
// clickable terms this graph holds no individual for. There is nothing to ask
|
|
185
|
+
// about one, and saying so leaves the caller its row-list fallback instead of
|
|
186
|
+
// an answered-and-empty neighbourhood.
|
|
187
|
+
const termIds = idsByLabel.get(term) || [];
|
|
188
|
+
if (!termIds.length) return { term, rows, asked, grounded };
|
|
189
|
+
|
|
190
|
+
const seen = new Set();
|
|
191
|
+
for (const kind of askableRelationKinds(graph)) {
|
|
192
|
+
const storedEdges = edgesByKind.get(kind) || new Set();
|
|
193
|
+
const holdsEdge = (subjectId, objectId) => storedEdges.has(`${subjectId} ${objectId}`);
|
|
194
|
+
for (const { shape, query } of relationQuestions(kind, term)) {
|
|
195
|
+
const envelope = tmct_ask({ query }, { graph }).data;
|
|
196
|
+
const parsed = envelope?.parsed;
|
|
197
|
+
const answeredAsAsked = parsed?.shape === shape
|
|
198
|
+
&& parsed?.kind === kind
|
|
199
|
+
&& parsed?.object === term
|
|
200
|
+
&& !envelope.ambiguous;
|
|
201
|
+
if (!answeredAsAsked) {
|
|
202
|
+
asked.push({ query, kind, shape, used: false, parsedAs: parsed?.canonical?.machine ?? null });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
grounded = true;
|
|
206
|
+
const matches = Array.isArray(envelope.matches) ? envelope.matches : [];
|
|
207
|
+
let confirmed = 0;
|
|
208
|
+
for (const match of matches) {
|
|
209
|
+
if (!match?.label || !match.id) continue;
|
|
210
|
+
const aboutTerm = termIds.some((termId) => (shape === "reverse"
|
|
211
|
+
? holdsEdge(match.id, termId)
|
|
212
|
+
: holdsEdge(termId, match.id)));
|
|
213
|
+
if (!aboutTerm) continue;
|
|
214
|
+
confirmed += 1;
|
|
215
|
+
const row = shape === "reverse"
|
|
216
|
+
? { s: match.label, kind, phrase: edgePhrase(kind), o: term, sClass: match.type || "", oClass: "" }
|
|
217
|
+
: { s: term, kind, phrase: edgePhrase(kind), o: match.label, sClass: "", oClass: match.type || "" };
|
|
218
|
+
const key = rowKey(row);
|
|
219
|
+
if (seen.has(key)) continue;
|
|
220
|
+
seen.add(key);
|
|
221
|
+
rows.push(row);
|
|
222
|
+
}
|
|
223
|
+
asked.push({ query, kind, shape, used: true, miss: Boolean(envelope.miss), matched: matches.length, confirmed, traversal: envelope.traversal ?? null });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { term, rows, asked, grounded };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// This page holds a REAL code graph, so `tmct.ask` is the engine answering
|
|
230
|
+
// over it — the same round trip askRelatedFacts already makes twice per
|
|
231
|
+
// relation kind to build the sidebar's rows.
|
|
232
|
+
//
|
|
233
|
+
// `tmct.page` keeps the re-derivation helpers a graph swapped through the
|
|
234
|
+
// desktop picker re-renders with, plus the wink and term-normalizing seams the
|
|
235
|
+
// chat shares with the ledger page. `createCodeExplorerSession` sits there
|
|
236
|
+
// too: this page's client script is still one raw-text block, and it reaches
|
|
237
|
+
// its factory through the bag rather than through `tmct.open()`.
|
|
238
|
+
publishTmctSurface({
|
|
239
|
+
open: createCodeExplorerSession,
|
|
240
|
+
ask: graphAsk,
|
|
241
|
+
plan: enginePlan,
|
|
242
|
+
page: {
|
|
243
|
+
createCodeExplorerSession,
|
|
244
|
+
askRelatedFacts,
|
|
245
|
+
computeCodeExplorerData,
|
|
246
|
+
computeCodeLedger,
|
|
247
|
+
generateCodeHints,
|
|
248
|
+
parseEntities,
|
|
249
|
+
normFactTerm,
|
|
250
|
+
registerWinkModel,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// engine-surface.mjs — the standard `ask` and `plan` routes a browser page
|
|
2
|
+
// wires into publishTmctSurface (tmct-surface.mjs).
|
|
3
|
+
//
|
|
4
|
+
// Both run the SAME machinery the CLI runs, over the session's own in-memory
|
|
5
|
+
// state rather than a repo on disk: `graphAsk` dispatches `tmct_ask` through
|
|
6
|
+
// the tool layer and hands back the structured envelope beside the prose
|
|
7
|
+
// (dispatchToolStructured's `{ content, data }`); `enginePlan` builds the
|
|
8
|
+
// capability planner's context from whatever the session holds and runs one
|
|
9
|
+
// request through it.
|
|
10
|
+
//
|
|
11
|
+
// Kept out of tmct-surface.mjs on purpose. That module is pure wiring and
|
|
12
|
+
// imports nothing, so memory-ask-browser-entry.mjs — the one bundle that is
|
|
13
|
+
// committed and published, and is deliberately small — can publish the same
|
|
14
|
+
// `globalThis.tmct` without pulling the tool layer and the router into its
|
|
15
|
+
// bundle.
|
|
16
|
+
import { dispatchToolStructured } from "../../tools/server.mjs";
|
|
17
|
+
import { capabilityPlanDeps, noCodeGraph } from "../../services/chat.mjs";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One plain-English question, answered against the graph this session holds,
|
|
21
|
+
* through the same `tmct_ask` tool the CLI dispatches. Returns
|
|
22
|
+
* `{ answer, data, miss }` — `data` is ask()'s own envelope (the parse, the
|
|
23
|
+
* matches it found, the traversal it walked), which is what a panel renders
|
|
24
|
+
* rows from and what makes an answer auditable.
|
|
25
|
+
*
|
|
26
|
+
* A session whose graph is empty answers with an honest miss. That is the real
|
|
27
|
+
* state of a memory-only page: its rows are facts in a store, and ask() reads
|
|
28
|
+
* a graph. spider-fly is the page that closed that gap for its own world, by
|
|
29
|
+
* projecting its live board into a graph (worldRelationGraphPayload) before
|
|
30
|
+
* every turn — the same route is open to any page whose rows have a shape
|
|
31
|
+
* worth asking over.
|
|
32
|
+
*/
|
|
33
|
+
export async function graphAsk(request, options, session) {
|
|
34
|
+
const { content, data } = await dispatchToolStructured("tmct_ask", { query: request }, {
|
|
35
|
+
graph: session.graph,
|
|
36
|
+
memoryBackend: session.memoryDir,
|
|
37
|
+
});
|
|
38
|
+
return { answer: content, data, miss: Boolean(data?.miss) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One compound or maintenance-goal request, planned and executed over whatever
|
|
43
|
+
* this session holds — the same capability router `/plan` reaches, with the
|
|
44
|
+
* same memory-only mode a page with no code graph needs (a code-graph
|
|
45
|
+
* capability there refuses by naming the graph it hasn't got).
|
|
46
|
+
*
|
|
47
|
+
* Returns the planner's own result: `{ refused, why, driver, calls, composed,
|
|
48
|
+
* observed }`. No prose is composed here — a page that wants the written
|
|
49
|
+
* answer asks for it in words through `tmct.turn("/plan …")`, which is the
|
|
50
|
+
* same split `dispatchTool` and `dispatchToolStructured` already draw.
|
|
51
|
+
*/
|
|
52
|
+
export async function enginePlan(request, options, session) {
|
|
53
|
+
const graph = session.graph && !noCodeGraph(session.graph) ? session.graph : null;
|
|
54
|
+
const memoryDir = session.memoryDir ?? null;
|
|
55
|
+
if (!graph && !memoryDir) {
|
|
56
|
+
return {
|
|
57
|
+
refused: true,
|
|
58
|
+
why: "nothing to plan over — this session holds neither a code graph nor a memory store",
|
|
59
|
+
driver: null, calls: [], composed: null, observed: null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } =
|
|
63
|
+
await import("../../domain/router/drive.mjs");
|
|
64
|
+
const ctx = await buildCapabilityPlanCtx({
|
|
65
|
+
...capabilityPlanDeps(), config: null, source: null, graph, memoryDir,
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
const result = await runCapabilityPlan(request, declaredCapabilityNames(), ctx);
|
|
69
|
+
return {
|
|
70
|
+
refused: Boolean(result.refused),
|
|
71
|
+
why: Array.isArray(result.why) ? result.why.join("; ") : (result.why ?? null),
|
|
72
|
+
driver: result.driver ?? null,
|
|
73
|
+
calls: result.calls ?? [],
|
|
74
|
+
composed: result.composed ?? null,
|
|
75
|
+
observed: result.observed ?? null,
|
|
76
|
+
};
|
|
77
|
+
} finally {
|
|
78
|
+
// The taught registrations are per-ctx: dispose them so the next call
|
|
79
|
+
// re-reads the store instead of meeting a stale name collision.
|
|
80
|
+
for (const dispose of ctx.disposers || []) dispose();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
// runTurn engine, the same weight class as the chat/ledger bundles, and is
|
|
25
25
|
// never published.
|
|
26
26
|
import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
|
|
27
|
-
import { createInMemoryStore, normFactTerm, loadMemory, readFactRows, appendFact } from "../../adapters/memory/core.mjs";
|
|
28
|
-
import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
|
|
27
|
+
import { createInMemoryStore, normFactTerm, loadMemory, readFactRows, appendFact, applySeedPayload } from "../../adapters/memory/core.mjs";
|
|
29
28
|
import { splitSentencesPreservingPaths, stripCitationResidue } from "../../services/sentences.mjs";
|
|
30
29
|
import { clauseCandidates, optimisticTriples } from "../../services/extract-facts.mjs";
|
|
31
30
|
import { touchedFactRows } from "../../domain/memory/touched-facts.mjs";
|
|
32
31
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
33
32
|
import { registerWinkModel, winkInstance } from "../../adapters/wink-model.mjs";
|
|
34
|
-
import { memoryStats } from "./memory-stats.mjs";
|
|
33
|
+
import { memoryStats, exportFactsJsonl } from "./memory-stats.mjs";
|
|
35
34
|
import { openPersistedStore } from "./idb-persist.mjs";
|
|
35
|
+
import { publishTmctSurface } from "./tmct-surface.mjs";
|
|
36
36
|
|
|
37
37
|
// The pronoun subjects a bounded carry substitutes with the last unique
|
|
38
38
|
// grounded subject in the SAME paragraph. Reset at every blank line, so a
|
|
@@ -178,7 +178,7 @@ export async function groundTextToFacts(text, { memoryDir, sessionId, lexicon, v
|
|
|
178
178
|
*/
|
|
179
179
|
export function createIngestSession({ seedPayload = null, vocabSeeded = false } = {}) {
|
|
180
180
|
const memoryDir = createInMemoryStore();
|
|
181
|
-
|
|
181
|
+
applySeedPayload(memoryDir, seedPayload);
|
|
182
182
|
|
|
183
183
|
const lexicon = loadLexicon();
|
|
184
184
|
const vocabHint = vocabExampleHint(vocabSeeded);
|
|
@@ -193,16 +193,15 @@ export function createIngestSession({ seedPayload = null, vocabSeeded = false }
|
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
};
|
|
196
|
+
// This page grounds pasted prose into facts; it holds no conversation, so
|
|
197
|
+
// `tmct.turn` is unwired and says so, and the one call the page makes is
|
|
198
|
+
// `tmct.session.ingest(text)`. `tmct.page` keeps the recognizer entry point
|
|
199
|
+
// the page also drives directly, the wink seam, the stats fold behind its
|
|
200
|
+
// counters, its IndexedDB wrapper and its JSONL export.
|
|
201
|
+
publishTmctSurface({
|
|
202
|
+
open: createIngestSession,
|
|
203
|
+
page: {
|
|
204
|
+
groundTextToFacts, exportFactsJsonl, registerWinkModel, normFactTerm,
|
|
205
|
+
memoryStats, openPersistedStore,
|
|
206
|
+
},
|
|
207
|
+
});
|