@polycode-projects/the-mechanical-code-talker 1.8.20 → 1.9.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 +27 -6
- package/ROADMAP.md +1 -1
- package/bin/tmct.mjs +167 -20
- package/corpus/generated/README.md +2 -2
- package/corpus/namenet/LICENSE-NOTICE +68 -0
- package/corpus/namenet/generate.mjs +309 -0
- package/corpus/namenet/manifest.json +20 -0
- package/corpus/namenet/namenet.jsonl +7260 -0
- package/corpus/wordnet/LICENSE-NOTICE +49 -0
- package/corpus/wordnet/generate.mjs +333 -0
- package/corpus/wordnet/manifest.json +34 -0
- package/corpus/wordnet/wordnet-full.jsonl +192498 -0
- package/corpus/wordnet/wordnet-xl.jsonl +23805 -0
- package/package.json +6 -2
- package/src/ask-browser-entry.mjs +13 -2
- package/src/ask-browser.bundle.js +272 -19
- package/src/chat.mjs +229 -82
- package/src/cli-args.mjs +20 -1
- package/src/codegraph.mjs +306 -1
- package/src/corpus/conceptnet-map.toml +17 -12
- package/src/corpus/conceptnet.mjs +23 -1
- package/src/extensions.mjs +44 -1
- package/src/init.mjs +99 -46
- package/src/interpret/normalize.mjs +1 -3
- package/src/memory/core.mjs +191 -14
- package/src/memory/trust.mjs +27 -6
- package/src/memory-ask-browser-entry.mjs +36 -0
- package/src/memory-ask-browser.bundle.js +5544 -0
- package/src/toml-config.mjs +11 -1
- package/src/viz.mjs +548 -73
package/src/init.mjs
CHANGED
|
@@ -148,6 +148,27 @@ enabled = ${seed.enabled ? "true" : "false"}
|
|
|
148
148
|
# To cap it, uncomment and set a number (definitional band first):
|
|
149
149
|
${seed.limit != null ? `limit = ${Number(seed.limit)}` : "# limit = 500"}
|
|
150
150
|
`;
|
|
151
|
+
// [memory] backend — ONLY emitted when a caller actually supplies it (an
|
|
152
|
+
// explicit `tmct init --memory-backend <...>`, or a manual override); the
|
|
153
|
+
// plain zero-flag `tmct init` output stays BYTE-IDENTICAL to before this
|
|
154
|
+
// knob existed, same discipline as the extras block below. "default" is
|
|
155
|
+
// written out explicitly rather than omitted, so `--memory-backend default`
|
|
156
|
+
// leaves a self-documenting trace of the choice (mirrors --with-persona's
|
|
157
|
+
// own "make the default explicit" behaviour).
|
|
158
|
+
let out = base;
|
|
159
|
+
if (config.memory && config.memory.backend !== undefined) {
|
|
160
|
+
out += `
|
|
161
|
+
[memory]
|
|
162
|
+
# Storage backend for taught facts + the memory graph (PLAN_SEED.md §6).
|
|
163
|
+
# Precedence: --memory-backend flag > TMCT_MEMORY_BACKEND env > this file >
|
|
164
|
+
# "default" (the built-in fallback).
|
|
165
|
+
# "default" — the flat OWL-labelled JSON file under .tmct/memory/. The default.
|
|
166
|
+
# "memory" — in-process only; nothing written to disk (a library caller's option).
|
|
167
|
+
# "sqlite" — a local SQLite file at .tmct/memory/graph.sqlite.
|
|
168
|
+
backend = ${JSON.stringify(config.memory.backend)}
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
|
|
151
172
|
// Extension-pack / bias sections (src/extensions.mjs) — ONLY emitted when a
|
|
152
173
|
// caller actually supplies them (an explicit `--with-persona`, or a manual
|
|
153
174
|
// override); the plain zero-flag `tmct init` output stays BYTE-IDENTICAL to
|
|
@@ -157,8 +178,8 @@ ${seed.limit != null ? `limit = ${Number(seed.limit)}` : "# limit = 500"}
|
|
|
157
178
|
const extras = {};
|
|
158
179
|
if (config.extensions !== undefined) extras.extensions = config.extensions;
|
|
159
180
|
if (config.bias !== undefined) extras.bias = config.bias;
|
|
160
|
-
if (!Object.keys(extras).length) return
|
|
161
|
-
return `${
|
|
181
|
+
if (!Object.keys(extras).length) return out;
|
|
182
|
+
return `${out}
|
|
162
183
|
# Extension packs + bias (src/extensions.mjs) — written by \`tmct init --with-persona\`
|
|
163
184
|
# or a manual edit. Recognized names (human, seon, conceptnet, tier2-aws,
|
|
164
185
|
# tier2-python, tier2-java, tier2-general) override the shipped defaults; any
|
|
@@ -196,13 +217,21 @@ function seedRequested({ optSeed, configEnabled, env }) {
|
|
|
196
217
|
* only ever sees an already-resolved preset object (or nothing). Has no
|
|
197
218
|
* effect when tmct.toml already exists and `force` isn't set (the existing
|
|
198
219
|
* "preserve a user's tmct.toml" rule wins, same as `seed`/`corpus.tier`).
|
|
220
|
+
* @param {string} [opts.memoryBackend] "default" | "memory" | "sqlite" — merged
|
|
221
|
+
* into the FRESH config's `[memory] backend` before it's written (same
|
|
222
|
+
* "fresh write only" rule as `persona`, above; `tmct init --memory-backend
|
|
223
|
+
* <...>` on an ALREADY-initialized repo is bin/tmct.mjs's own job, mirroring
|
|
224
|
+
* how `--graph` amends an existing tmct.toml post-hoc). Also selects which
|
|
225
|
+
* backend the corpus SEED below (step 3) writes into — src/memory/core.mjs's
|
|
226
|
+
* `openMemoryBackend`, the same resolver chat.mjs's createSession uses, so a
|
|
227
|
+
* seeded fact and a later chat-taught fact always land in the same store.
|
|
199
228
|
* @returns {Promise<{
|
|
200
229
|
* created: string[], config: object, seeded: boolean,
|
|
201
230
|
* alreadyInitialized: boolean, seedResult: (object|null), message: string
|
|
202
231
|
* }>} `created` lists the ABSOLUTE paths this call brought into being (empty on a
|
|
203
232
|
* benign no-op re-init). Never throws on a benign re-init or a corpus failure.
|
|
204
233
|
*/
|
|
205
|
-
export async function initRepo(dir, { force = false, seed, env = process.env, persona = null } = {}) {
|
|
234
|
+
export async function initRepo(dir, { force = false, seed, env = process.env, persona = null, memoryBackend = null } = {}) {
|
|
206
235
|
const root = resolve(dir);
|
|
207
236
|
const created = [];
|
|
208
237
|
const paths = {
|
|
@@ -235,6 +264,7 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
|
|
|
235
264
|
if (persona.extensions && Object.keys(persona.extensions).length) config.extensions = persona.extensions;
|
|
236
265
|
if (persona.bias && Object.keys(persona.bias).length) config.bias = persona.bias;
|
|
237
266
|
}
|
|
267
|
+
if (memoryBackend) config.memory = { ...(config.memory || {}), backend: memoryBackend };
|
|
238
268
|
const tomlPresent = await exists(paths.toml);
|
|
239
269
|
if (!tomlPresent || force) {
|
|
240
270
|
await writeFile(paths.toml, renderTomlConfig(config));
|
|
@@ -266,50 +296,70 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
|
|
|
266
296
|
} else if ((await exists(paths.marker)) && !force) {
|
|
267
297
|
seedNote = "seed skipped (already seeded — marker present)";
|
|
268
298
|
} else {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
299
|
+
// BUG FIX (found in review): this step used to call seedActiveCorpusEntries
|
|
300
|
+
// with the plain `root` string ALWAYS — Backend A only — regardless of
|
|
301
|
+
// `config.memory.backend`. A `tmct init --memory-backend sqlite` repo ended
|
|
302
|
+
// up with its corpus facts trapped in an inert .tmct/memory/graph.json that
|
|
303
|
+
// a sqlite-backend chat session (createSession, which IS backend-aware)
|
|
304
|
+
// createSession resolves it — src/memory/core.mjs's openMemoryBackend — so
|
|
305
|
+
// the seed lands in whichever backend `config.memory.backend` actually
|
|
306
|
+
// names. "memory" is skipped outright: it's an in-process-only store that
|
|
307
|
+
// vanishes the moment this one-shot init process exits, so seeding it is
|
|
308
|
+
// pure wasted work — a later `tmct chat --memory-backend memory` opens a
|
|
309
|
+
// brand new, unrelated in-memory store anyway.
|
|
310
|
+
const backendChoice = String(config.memory?.backend || "").trim().toLowerCase();
|
|
311
|
+
if (backendChoice === "memory") {
|
|
312
|
+
seedNote = "seed skipped (memory backend is in-process only — nothing would persist past this command)";
|
|
313
|
+
} else {
|
|
314
|
+
const { openMemoryBackend } = await import("./memory/core.mjs");
|
|
315
|
+
const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(root, backendChoice);
|
|
316
|
+
try {
|
|
317
|
+
const { resolveExtensions, seedActiveCorpusEntries } = await import("./extensions.mjs");
|
|
318
|
+
const { entries } = await resolveExtensions(root);
|
|
319
|
+
// `tmct.toml`'s `[seed] limit` knob is documented as capping the tier-1
|
|
320
|
+
// ConceptNet band specifically (the curated SEON ontology is small and
|
|
321
|
+
// always seeds whole) — so it overrides ONLY the resolved "conceptnet"
|
|
322
|
+
// entry's limit, exactly like the pre-fix single-corpus seed did.
|
|
323
|
+
if (config.seed?.limit != null && entries.has("conceptnet")) {
|
|
324
|
+
entries.set("conceptnet", { ...entries.get("conceptnet"), limit: Number(config.seed.limit) });
|
|
325
|
+
}
|
|
326
|
+
const { appended, skipped, total, perBundle } = await seedActiveCorpusEntries(memoryDir, entries);
|
|
327
|
+
// seedActiveCorpusEntries is failure-tolerant PER BUNDLE (a bad third-party
|
|
328
|
+
// pack never aborts the others) — but initRepo's own "FAILURE-TOLERANT
|
|
329
|
+
// SEED" contract is about the SEED AS A WHOLE degrading honestly. If every
|
|
330
|
+
// active bundle failed (e.g. the memory graph file itself is unwritable —
|
|
331
|
+
// see test/init.test.mjs "seed failure degrades"), re-throw the first
|
|
332
|
+
// bundle's error so the SAME outer catch below reports the familiar "seed
|
|
333
|
+
// skipped (corpus unavailable: …)" note, rather than claiming success with
|
|
334
|
+
// zero facts actually written.
|
|
335
|
+
const bundleNames = Object.keys(perBundle);
|
|
336
|
+
const allFailed = bundleNames.length > 0 && bundleNames.every((n) => perBundle[n].error);
|
|
337
|
+
if (allFailed) throw new Error(perBundle[bundleNames[0]].error);
|
|
338
|
+
seedResult = {
|
|
339
|
+
appended, skipped, total, perBundle,
|
|
340
|
+
seon: perBundle.seon?.appended || 0,
|
|
341
|
+
conceptnet: perBundle.conceptnet?.appended || 0,
|
|
342
|
+
};
|
|
343
|
+
const markerNew = !(await exists(paths.marker));
|
|
344
|
+
await mkdir(dirname(paths.marker), { recursive: true });
|
|
345
|
+
await writeFile(
|
|
346
|
+
paths.marker,
|
|
347
|
+
JSON.stringify({
|
|
348
|
+
seededAt: new Date().toISOString(),
|
|
349
|
+
limit: config.seed?.limit != null ? Number(config.seed.limit) : SEED_LIMIT,
|
|
350
|
+
appended: seedResult.appended,
|
|
351
|
+
skipped: seedResult.skipped,
|
|
352
|
+
perBundle,
|
|
353
|
+
}) + "\n",
|
|
354
|
+
);
|
|
355
|
+
if (markerNew) created.push(paths.marker);
|
|
356
|
+
seeded = true;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
// Corpus unavailable/broken → an initialised-but-unseeded repo, not a crash.
|
|
359
|
+
seedNote = `seed skipped (corpus unavailable: ${err && err.message ? err.message : err})`;
|
|
360
|
+
} finally {
|
|
361
|
+
await closeMemoryStore();
|
|
278
362
|
}
|
|
279
|
-
const { appended, skipped, total, perBundle } = await seedActiveCorpusEntries(root, entries);
|
|
280
|
-
// seedActiveCorpusEntries is failure-tolerant PER BUNDLE (a bad third-party
|
|
281
|
-
// pack never aborts the others) — but initRepo's own "FAILURE-TOLERANT
|
|
282
|
-
// SEED" contract is about the SEED AS A WHOLE degrading honestly. If every
|
|
283
|
-
// active bundle failed (e.g. the memory graph file itself is unwritable —
|
|
284
|
-
// see test/init.test.mjs "seed failure degrades"), re-throw the first
|
|
285
|
-
// bundle's error so the SAME outer catch below reports the familiar "seed
|
|
286
|
-
// skipped (corpus unavailable: …)" note, rather than claiming success with
|
|
287
|
-
// zero facts actually written.
|
|
288
|
-
const bundleNames = Object.keys(perBundle);
|
|
289
|
-
const allFailed = bundleNames.length > 0 && bundleNames.every((n) => perBundle[n].error);
|
|
290
|
-
if (allFailed) throw new Error(perBundle[bundleNames[0]].error);
|
|
291
|
-
seedResult = {
|
|
292
|
-
appended, skipped, total, perBundle,
|
|
293
|
-
seon: perBundle.seon?.appended || 0,
|
|
294
|
-
conceptnet: perBundle.conceptnet?.appended || 0,
|
|
295
|
-
};
|
|
296
|
-
const markerNew = !(await exists(paths.marker));
|
|
297
|
-
await mkdir(dirname(paths.marker), { recursive: true });
|
|
298
|
-
await writeFile(
|
|
299
|
-
paths.marker,
|
|
300
|
-
JSON.stringify({
|
|
301
|
-
seededAt: new Date().toISOString(),
|
|
302
|
-
limit: config.seed?.limit != null ? Number(config.seed.limit) : SEED_LIMIT,
|
|
303
|
-
appended: seedResult.appended,
|
|
304
|
-
skipped: seedResult.skipped,
|
|
305
|
-
perBundle,
|
|
306
|
-
}) + "\n",
|
|
307
|
-
);
|
|
308
|
-
if (markerNew) created.push(paths.marker);
|
|
309
|
-
seeded = true;
|
|
310
|
-
} catch (err) {
|
|
311
|
-
// Corpus unavailable/broken → an initialised-but-unseeded repo, not a crash.
|
|
312
|
-
seedNote = `seed skipped (corpus unavailable: ${err && err.message ? err.message : err})`;
|
|
313
363
|
}
|
|
314
364
|
}
|
|
315
365
|
|
|
@@ -356,6 +406,9 @@ async function readWrittenConfig(tomlPath, base) {
|
|
|
356
406
|
// normalizeConfig).
|
|
357
407
|
if (raw.extensions !== undefined) cfg.extensions = raw.extensions;
|
|
358
408
|
if (raw.bias !== undefined) cfg.bias = raw.bias;
|
|
409
|
+
if (raw.memory && raw.memory.backend !== undefined) {
|
|
410
|
+
cfg.memory = { ...cfg.memory, backend: raw.memory.backend };
|
|
411
|
+
}
|
|
359
412
|
return cfg;
|
|
360
413
|
} catch {
|
|
361
414
|
return base;
|
|
@@ -119,9 +119,7 @@ const KIND_NOUN_ANAPHORA_RE = /\b(this|that)\s+(class|module|function|method|att
|
|
|
119
119
|
* "file"->"Module" convention every other lane in this grammar already
|
|
120
120
|
* uses)? Returns that class, or null when no such anaphora is present.
|
|
121
121
|
* Deliberately SEPARATE from normalizeQuery's own KIND_NOUN_ANAPHORA_RE
|
|
122
|
-
* replace just above (
|
|
123
|
-
* "this", discarding the kind-noun signal for good, by design — see that
|
|
124
|
-
* replace's own docblock): this never mutates its input and has no effect
|
|
122
|
+
* replace just above (: this never mutates its input and has no effect
|
|
125
123
|
* on normalizeQuery's behavior, signature, or any of its many call sites.
|
|
126
124
|
* A caller that needs BOTH the collapsed pronoun AND the kind it stood for
|
|
127
125
|
* (chat.mjs's runAsk, at its pronoun-reuse site) calls this side-channel on
|
package/src/memory/core.mjs
CHANGED
|
@@ -108,7 +108,7 @@ const MEMORY_VOCABULARY = [
|
|
|
108
108
|
{ prop: DERIVED_FROM_PROP, predicate: "derivedFrom", note: "umbrella: a Fact derived from a Source (or another Fact). ext ref prov:wasDerivedFrom (UNVERIFIED-pending-web-check)" },
|
|
109
109
|
{ prop: STATED_BY_PROP, predicate: "statedBy", note: "subPropertyOf derivedFrom: a Source directly asserts this Fact (one edge per independent source — replaces the factProvenance union)" },
|
|
110
110
|
{ prop: CANONICALISED_FROM_PROP, predicate: "canonicalisedFrom", note: "subPropertyOf derivedFrom: a canonical Fact cleaned from a raw Block/Source, never replacing it" },
|
|
111
|
-
{ prop: "mgx:sourceType", note: "a Source's kind: operator | teach | provider | corpus | web | entailed (the trust-prior key)" },
|
|
111
|
+
{ prop: "mgx:sourceType", note: "a Source's kind: operator | teach | provider | corpus | corpusWeak | extracted | web | entailed (the trust-prior key)" },
|
|
112
112
|
{ prop: "mgx:sourceUrl", note: "a web Source's URL" },
|
|
113
113
|
{ prop: "mgx:sourceRule", note: "an entailed Source's rule id" },
|
|
114
114
|
{ prop: "mgx:sourceReliability", note: "actor-level (session-scoped) trust nudge in [0.5,1.5], neutral 1.0 when absent — materialised by recomputeSourceReliability from a session's asserted-vs-contradicted track record (memory/trust.mjs's sessionReliabilityFrom); folds into computeTrust's per-source prior" },
|
|
@@ -312,6 +312,43 @@ export function closeSqliteMemoryStore(handle) {
|
|
|
312
312
|
if (isSqliteHandle(handle)) handle.db.close();
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Resolve an already-lowercased/trimmed backend token ("memory" | "sqlite" |
|
|
317
|
+
* anything else, including "" or "default") into `{ dir, close }`: the exact
|
|
318
|
+
* `dir` value every dir-taking export in this module (loadMemory, appendFact,
|
|
319
|
+
* appendFacts, seedMemory via src/corpus/conceptnet.mjs, …) already accepts —
|
|
320
|
+
* a plain repo-path string for Backend A, or a live Backend B/C handle — plus
|
|
321
|
+
* an idempotent `close()` cleanup (a no-op for A/B; closes the sqlite
|
|
322
|
+
* connection for C).
|
|
323
|
+
*
|
|
324
|
+
* The ONE shared resolver for the storage-backend seam (PLAN_SEED.md §6):
|
|
325
|
+
* originally inlined only in chat.mjs's createSession, factored out here so
|
|
326
|
+
* `tmct init`'s corpus seed (src/init.mjs) and bin/tmct.mjs's
|
|
327
|
+
* `--corpus`/`--ontology`/`--lexicon` activation seed the SAME backend a
|
|
328
|
+
* later `tmct chat` in that repo will read — a caller resolving "sqlite"
|
|
329
|
+
* independently and picking a different db path (or a different in-memory
|
|
330
|
+
* store) would silently split a repo's memory across two places that can
|
|
331
|
+
* never see each other. `backendChoice` is a precedence result the CALLER
|
|
332
|
+
* already computed (CLI flag > env > tmct.toml > default, or whatever subset
|
|
333
|
+
* applies); this function does no precedence resolution of its own — it only
|
|
334
|
+
* maps the resolved token to a handle. `repoRoot` is used only for Backend C's
|
|
335
|
+
* db path (`<repoRoot>/.tmct/memory/graph.sqlite`); Backend A returns
|
|
336
|
+
* `repoRoot` itself unchanged, byte-identical to every existing plain-string
|
|
337
|
+
* caller.
|
|
338
|
+
*/
|
|
339
|
+
export async function openMemoryBackend(repoRoot, backendChoice) {
|
|
340
|
+
if (backendChoice === BACKEND_MEMORY) {
|
|
341
|
+
return { dir: createInMemoryStore(), close: async () => {} };
|
|
342
|
+
}
|
|
343
|
+
if (backendChoice === BACKEND_SQLITE) {
|
|
344
|
+
const dbPath = join(repoRoot, ".tmct", "memory", "graph.sqlite");
|
|
345
|
+
await mkdir(dirname(dbPath), { recursive: true });
|
|
346
|
+
const handle = await createSqliteMemoryStore(dbPath);
|
|
347
|
+
return { dir: handle, close: async () => closeSqliteMemoryStore(handle) };
|
|
348
|
+
}
|
|
349
|
+
return { dir: repoRoot, close: async () => {} };
|
|
350
|
+
}
|
|
351
|
+
|
|
315
352
|
/** Deep-clone a JSON-safe value. Used two ways here: (1) readSqlitePayload
|
|
316
353
|
* hands every CALLER a clone of the cache, never the live cached object
|
|
317
354
|
* itself, so this backend keeps the same "fresh object every call" contract
|
|
@@ -725,8 +762,56 @@ async function persistMemory(dir, payload) {
|
|
|
725
762
|
* reached). `await fn(payload)` is a documented no-op for every existing
|
|
726
763
|
* SYNC caller (appendUtterance(s), appendFacts) — awaiting a non-Promise
|
|
727
764
|
* value just resolves to it, byte-identical behaviour to calling it plain. */
|
|
765
|
+
// ---- mutateMemory-scoped lookup index (PLAN_GRAPH_SCAN.md Phase 1) ----------
|
|
766
|
+
// syncFactSources's per-fact bookkeeping (upsertSource, upsertIndividual,
|
|
767
|
+
// upsertEdge's statedBy path, statedByObjectsFor, sourcesByIdMap) used to each
|
|
768
|
+
// re-scan payload.individuals or the statedBy edge list from scratch, turning
|
|
769
|
+
// one appendFacts batch of n facts into O(n^2) work. mutateMemory now builds
|
|
770
|
+
// three lookup Maps once per call (one O(n) pass) and attaches them to payload
|
|
771
|
+
// under a Symbol key — JSON.stringify skips Symbol-keyed properties
|
|
772
|
+
// automatically, so persistMemory's graph.json write is byte-identical to
|
|
773
|
+
// before. Every helper below checks for the Symbol slot: present → O(1) Map
|
|
774
|
+
// lookup; absent (a bare payload object built outside mutateMemory, e.g. a
|
|
775
|
+
// test fixture) → today's exact linear-scan fallback, so nothing outside
|
|
776
|
+
// mutateMemory's own call chain can observe a behaviour change. The index is
|
|
777
|
+
// discarded when mutateMemory returns — it never survives across calls, so
|
|
778
|
+
// there is no invalidation logic to get wrong.
|
|
779
|
+
const MEMORY_INDEX = Symbol("mutateMemory lookup index");
|
|
780
|
+
|
|
781
|
+
/** Build the three lookup Maps from the just-loaded payload and attach them
|
|
782
|
+
* under MEMORY_INDEX. Any code that pushes a new individual into
|
|
783
|
+
* payload.individuals, or a new statedBy edge, must also write the matching
|
|
784
|
+
* index entry in that same statement (see upsertIndividual/upsertSource/
|
|
785
|
+
* upsertEdge/appendFacts below) — the same discipline appendFacts's own
|
|
786
|
+
* local `byId` Map already used for the Fact upsert, generalised here. */
|
|
787
|
+
function buildMemoryIndex(payload) {
|
|
788
|
+
const individualsById = new Map();
|
|
789
|
+
const sourcesById = new Map();
|
|
790
|
+
const statedByBySubject = new Map();
|
|
791
|
+
for (const ind of payload.individuals || []) {
|
|
792
|
+
if (!ind?.id) continue;
|
|
793
|
+
individualsById.set(ind.id, ind);
|
|
794
|
+
if (ind.class === SOURCE_CLASS) sourcesById.set(ind.id, ind);
|
|
795
|
+
}
|
|
796
|
+
const statedGroup = (payload.objectProperties || []).find((g) => g?.prop === STATED_BY_PROP);
|
|
797
|
+
for (const e of statedGroup?.examples || []) {
|
|
798
|
+
if (!e?.subject) continue;
|
|
799
|
+
const list = statedByBySubject.get(e.subject);
|
|
800
|
+
if (list) list.push(e.object);
|
|
801
|
+
else statedByBySubject.set(e.subject, [e.object]);
|
|
802
|
+
}
|
|
803
|
+
payload[MEMORY_INDEX] = { individualsById, sourcesById, statedByBySubject };
|
|
804
|
+
return payload[MEMORY_INDEX];
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** The active lookup index for this payload, or null when this payload wasn't
|
|
808
|
+
* built by mutateMemory (a bare test fixture) — callers fall back to a
|
|
809
|
+
* linear scan in that case. */
|
|
810
|
+
const memoryIndexOf = (payload) => payload?.[MEMORY_INDEX] || null;
|
|
811
|
+
|
|
728
812
|
async function mutateMemory(dir, fn) {
|
|
729
813
|
const payload = await loadMemory(dir);
|
|
814
|
+
buildMemoryIndex(payload);
|
|
730
815
|
const out = (await fn(payload)) ?? payload;
|
|
731
816
|
migrateLegacyProvenance(out);
|
|
732
817
|
recomputeSourceReliability(out);
|
|
@@ -771,6 +856,11 @@ function sourceIdFor(desc) {
|
|
|
771
856
|
case "teach": return { id: desc.sessionId ? `${TEACH_SOURCE_ID}:${desc.sessionId}` : TEACH_SOURCE_ID, type: "teach" };
|
|
772
857
|
case "provider": return { id: `src:provider:${desc.name}`, type: "provider" };
|
|
773
858
|
case "corpus": return { id: `src:corpus:${desc.name}`, type: "corpus" };
|
|
859
|
+
// One Source per source-file basename (the corpus precedent above), not per
|
|
860
|
+
// extraction run — re-running scripts/extract-facts-from-text.mjs over the
|
|
861
|
+
// SAME file collapses onto the same Source instead of minting a new one
|
|
862
|
+
// every time, matching corpus's "one Source per named dataset" idiom.
|
|
863
|
+
case "extracted": return { id: `src:extracted:${desc.name}`, type: "extracted" };
|
|
774
864
|
case "web": return { id: `src:learned:web:${fnv1aHex(String(desc.url || ""))}`, type: "web", url: String(desc.url || "") };
|
|
775
865
|
case "entailed": return { id: `src:entailed:${desc.rule}`, type: "entailed", rule: String(desc.rule || "") };
|
|
776
866
|
default: return null;
|
|
@@ -785,9 +875,10 @@ const sourceLabel = (id) => String(id).replace(/^src:/, "");
|
|
|
785
875
|
function upsertSource(payload, desc, createdAtCandidate) {
|
|
786
876
|
const info = sourceIdFor(desc);
|
|
787
877
|
if (!info) return null;
|
|
788
|
-
const
|
|
878
|
+
const idx = memoryIndexOf(payload);
|
|
879
|
+
const prior = idx ? idx.individualsById.get(info.id) : payload.individuals.find((i) => i?.id === info.id);
|
|
789
880
|
const created = firstWriteCreatedAt(prior, desc?.createdAt || createdAtCandidate);
|
|
790
|
-
|
|
881
|
+
const ind = {
|
|
791
882
|
id: info.id, label: sourceLabel(info.id), class: SOURCE_CLASS,
|
|
792
883
|
derived_from: [], mentions: [],
|
|
793
884
|
attributes: [
|
|
@@ -797,7 +888,9 @@ function upsertSource(payload, desc, createdAtCandidate) {
|
|
|
797
888
|
...(info.url ? [{ prop: "mgx:sourceUrl", key: "sourceUrl", value: info.url }] : []),
|
|
798
889
|
...(info.rule ? [{ prop: "mgx:sourceRule", key: "sourceRule", value: info.rule }] : []),
|
|
799
890
|
],
|
|
800
|
-
}
|
|
891
|
+
};
|
|
892
|
+
const stored = upsertIndividual(payload, ind);
|
|
893
|
+
if (idx) idx.sourcesById.set(info.id, stored);
|
|
801
894
|
return info.id;
|
|
802
895
|
}
|
|
803
896
|
|
|
@@ -821,19 +914,32 @@ function parseChatTagRest(rest) {
|
|
|
821
914
|
* set — the inverse the migration and the live write path both name Sources
|
|
822
915
|
* through. The tag formats are exactly what the writers produce:
|
|
823
916
|
* corpus:conceptnet /r/IsA → { kind:"corpus", name:"conceptnet" }
|
|
917
|
+
* corpus-weak:conceptnet /r/RelatedTo → { kind:"corpusWeak", name:"conceptnet" }
|
|
824
918
|
* ace:chat:<session>@<ts> → { kind:"operator", createdAt:<ts>, sessionId:<session> }
|
|
825
919
|
* teach:chat:<session>@<ts> → { kind:"teach", createdAt:<ts>, sessionId:<session> }
|
|
826
920
|
* web:<url> | url:<url> → { kind:"web", url:<url> }
|
|
921
|
+
* extracted:<file-basename> → { kind:"extracted", name:<file-basename> }
|
|
827
922
|
* entailed:<rule> → { kind:"entailed", rule:<rule> }
|
|
828
923
|
* chat:/session: refs map to the operator; an unknown tag → null (no Source).
|
|
924
|
+
* `corpus-weak:` is the SAME corpus provenance shape as `corpus:`, just naming
|
|
925
|
+
* a lower trust-prior kind (memory/trust.mjs SOURCE_PRIOR.corpusWeak) for
|
|
926
|
+
* facts whose underlying relation is real but low-precision (e.g. ConceptNet's
|
|
927
|
+
* undirected /r/RelatedTo) — trust stays computed from the Source's kind, never
|
|
928
|
+
* hand-set on the Fact.
|
|
829
929
|
* The session-id segment (Part B: session-scoped actor-level trust) feeds
|
|
830
930
|
* sourceIdFor, which mints a PER-SESSION Source id when present, instead of
|
|
831
931
|
* collapsing every session onto one singleton operator/teach Source.
|
|
932
|
+
* `extracted:` is scripts/extract-facts-from-text.mjs's own audit tag, layered
|
|
933
|
+
* ADDITIVELY (via appendFact's provenance union) on top of whatever the
|
|
934
|
+
* runTurn recognizer already wrote (ace:/teach:) — see that script for why:
|
|
935
|
+
* it distinguishes "this document evidenced this fact" from ordinary chat
|
|
936
|
+
* speech, at its own trust-prior tier (memory/trust.mjs SOURCE_PRIOR.extracted).
|
|
832
937
|
*/
|
|
833
938
|
export function provenanceTagToSource(tag) {
|
|
834
939
|
const t = String(tag || "").trim();
|
|
835
940
|
if (!t) return null;
|
|
836
941
|
const head = t.split(/\s+/)[0]; // drop trailing " /r/IsA" etc.
|
|
942
|
+
if (head.startsWith("corpus-weak:")) return { kind: "corpusWeak", name: head.slice("corpus-weak:".length) || "unknown" };
|
|
837
943
|
if (head.startsWith("corpus:")) return { kind: "corpus", name: head.slice("corpus:".length) || "unknown" };
|
|
838
944
|
if (head.startsWith("ace:")) return { kind: "operator", ...parseChatTagRest(head.slice("ace:".length)) };
|
|
839
945
|
if (head.startsWith("teach:")) {
|
|
@@ -842,6 +948,7 @@ export function provenanceTagToSource(tag) {
|
|
|
842
948
|
}
|
|
843
949
|
if (head.startsWith("web:")) return { kind: "web", url: head.slice("web:".length) };
|
|
844
950
|
if (head.startsWith("url:")) return { kind: "web", url: head.slice("url:".length) };
|
|
951
|
+
if (head.startsWith("extracted:")) return { kind: "extracted", name: head.slice("extracted:".length) || "unknown" };
|
|
845
952
|
if (head.startsWith("entailed:")) return { kind: "entailed", rule: head.slice("entailed:".length) };
|
|
846
953
|
if (head.startsWith("chat:") || head.startsWith("session:") || head.startsWith("operator")) return { kind: "operator" };
|
|
847
954
|
return null;
|
|
@@ -850,13 +957,23 @@ export function provenanceTagToSource(tag) {
|
|
|
850
957
|
/** Map a payload's Source individuals into the { id: Source } shape computeTrust
|
|
851
958
|
* resolves against. */
|
|
852
959
|
function sourcesByIdMap(payload) {
|
|
960
|
+
const idx = memoryIndexOf(payload);
|
|
853
961
|
const m = {};
|
|
962
|
+
if (idx) {
|
|
963
|
+
// idx.sourcesById is kept incrementally correct by upsertSource, so this
|
|
964
|
+
// is O(distinct Sources) — a handful, roughly one per corpus/provider —
|
|
965
|
+
// never O(all individuals), unlike the fallback rebuild below.
|
|
966
|
+
for (const [id, ind] of idx.sourcesById) m[id] = ind;
|
|
967
|
+
return m;
|
|
968
|
+
}
|
|
854
969
|
for (const i of payload.individuals) if (i?.class === SOURCE_CLASS) m[i.id] = i;
|
|
855
970
|
return m;
|
|
856
971
|
}
|
|
857
972
|
|
|
858
973
|
/** The Source ids a Fact is statedBy, read off the edge group. */
|
|
859
974
|
function statedByObjectsFor(payload, factId) {
|
|
975
|
+
const idx = memoryIndexOf(payload);
|
|
976
|
+
if (idx) return (idx.statedByBySubject.get(factId) || []).slice();
|
|
860
977
|
const g = payload.objectProperties.find((x) => x?.prop === STATED_BY_PROP);
|
|
861
978
|
return (g?.examples || []).filter((e) => e?.subject === factId).map((e) => e.object);
|
|
862
979
|
}
|
|
@@ -983,8 +1100,9 @@ function recomputeSourceReliability(payload) {
|
|
|
983
1100
|
}
|
|
984
1101
|
if (!bySource.size) return;
|
|
985
1102
|
|
|
1103
|
+
const idx = memoryIndexOf(payload);
|
|
986
1104
|
for (const [sid, counts] of bySource) {
|
|
987
|
-
const source = payload.individuals.find((i) => i?.id === sid);
|
|
1105
|
+
const source = idx ? idx.individualsById.get(sid) : payload.individuals.find((i) => i?.id === sid);
|
|
988
1106
|
if (!source) continue;
|
|
989
1107
|
setAttr(source, SOURCE_RELIABILITY_PROP, "sourceReliability", String(sessionReliabilityFrom(counts)));
|
|
990
1108
|
// Own-attribute mutation in place (PLAN_VIZ.md §2) — same reasoning as recomputeFactTrust.
|
|
@@ -997,16 +1115,38 @@ function recomputeSourceReliability(payload) {
|
|
|
997
1115
|
const affected = new Set();
|
|
998
1116
|
for (const e of statedGroup?.examples || []) if (bySource.has(e?.object)) affected.add(e.subject);
|
|
999
1117
|
for (const id of affected) {
|
|
1000
|
-
const ind = payload.individuals.find((i) => i?.id === id);
|
|
1118
|
+
const ind = idx ? idx.individualsById.get(id) : payload.individuals.find((i) => i?.id === id);
|
|
1001
1119
|
if (ind) recomputeFactTrust(payload, ind);
|
|
1002
1120
|
}
|
|
1003
1121
|
}
|
|
1004
1122
|
|
|
1005
|
-
/** Upsert an individual by id (replace-in-place keeps ordering stable).
|
|
1123
|
+
/** Upsert an individual by id (replace-in-place keeps ordering stable).
|
|
1124
|
+
* Returns the individual object actually stored in payload.individuals — the
|
|
1125
|
+
* caller (e.g. upsertSource) should index THAT reference, not `ind` itself,
|
|
1126
|
+
* since the indexed path below merges into the prior object in place rather
|
|
1127
|
+
* than replacing the array slot. When a lookup index is present (built by
|
|
1128
|
+
* mutateMemory), an existing individual is updated via Object.assign — same
|
|
1129
|
+
* array position AND same object identity as before, so it stays trivially
|
|
1130
|
+
* in sync with individualsById without a second Map write, and a brand-new
|
|
1131
|
+
* individual is pushed + indexed in the same statement. Absent an index
|
|
1132
|
+
* (a bare payload built outside mutateMemory), this is EXACTLY the original
|
|
1133
|
+
* findIndex + replace-or-push code. */
|
|
1006
1134
|
function upsertIndividual(payload, ind) {
|
|
1135
|
+
const idx = memoryIndexOf(payload);
|
|
1136
|
+
if (idx) {
|
|
1137
|
+
const prior = idx.individualsById.get(ind.id);
|
|
1138
|
+
if (prior) {
|
|
1139
|
+
Object.assign(prior, ind);
|
|
1140
|
+
return prior;
|
|
1141
|
+
}
|
|
1142
|
+
payload.individuals.push(ind);
|
|
1143
|
+
idx.individualsById.set(ind.id, ind);
|
|
1144
|
+
return ind;
|
|
1145
|
+
}
|
|
1007
1146
|
const i = payload.individuals.findIndex((x) => x?.id === ind.id);
|
|
1008
|
-
if (i >= 0) payload.individuals[i] = ind;
|
|
1009
|
-
|
|
1147
|
+
if (i >= 0) { payload.individuals[i] = ind; return ind; }
|
|
1148
|
+
payload.individuals.push(ind);
|
|
1149
|
+
return ind;
|
|
1010
1150
|
}
|
|
1011
1151
|
|
|
1012
1152
|
/** Upsert one edge into the named relation group (dedupe by subject>object). Stamps `createdAt`
|
|
@@ -1020,6 +1160,27 @@ function upsertEdge(payload, { predicate, prop }, edge) {
|
|
|
1020
1160
|
group = { predicate, prop, count: 0, examples: [] };
|
|
1021
1161
|
payload.objectProperties.push(group);
|
|
1022
1162
|
}
|
|
1163
|
+
// statedBy-only fast path (PLAN_GRAPH_SCAN.md Phase 1): statedByBySubject
|
|
1164
|
+
// tracks, per fact, the small list of Source ids already stated it (almost
|
|
1165
|
+
// always 0-1 during a seed), so the overwhelmingly common case — a brand
|
|
1166
|
+
// new (subject,object) statedBy pair — can append directly without the
|
|
1167
|
+
// find+filter scan of the WHOLE statedBy edge list below. Every other
|
|
1168
|
+
// predicate (saidInSession, inReplyTo, ...) is untouched and always takes
|
|
1169
|
+
// the original path.
|
|
1170
|
+
const idx = prop === STATED_BY_PROP ? memoryIndexOf(payload) : null;
|
|
1171
|
+
if (idx) {
|
|
1172
|
+
const existing = idx.statedByBySubject.get(edge.subject);
|
|
1173
|
+
if (!existing || !existing.includes(edge.object)) {
|
|
1174
|
+
group.examples.push({ ...edge, createdAt: edge.createdAt || nowIso() });
|
|
1175
|
+
group.count = group.examples.length;
|
|
1176
|
+
if (existing) existing.push(edge.object);
|
|
1177
|
+
else idx.statedByBySubject.set(edge.subject, [edge.object]);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
// Rare re-assert of the exact same (subject,object) pair — fall through
|
|
1181
|
+
// to the exact original find+filter dance so first-write-wins createdAt
|
|
1182
|
+
// is preserved; the index is kept accurate below too.
|
|
1183
|
+
}
|
|
1023
1184
|
// Edges are flat ({subject, object, ...}), not attribute-bearing individuals, so this can't
|
|
1024
1185
|
// reuse firstWriteCreatedAt (which reads `.attributes`) directly — same discipline, edge shape:
|
|
1025
1186
|
// the prior edge's OWN createdAt wins if it has one, else the incoming candidate, else now.
|
|
@@ -1030,6 +1191,11 @@ function upsertEdge(payload, { predicate, prop }, edge) {
|
|
|
1030
1191
|
);
|
|
1031
1192
|
group.examples.push({ ...edge, createdAt });
|
|
1032
1193
|
group.count = group.examples.length;
|
|
1194
|
+
if (idx) {
|
|
1195
|
+
const list = idx.statedByBySubject.get(edge.subject) || [];
|
|
1196
|
+
if (!list.includes(edge.object)) list.push(edge.object);
|
|
1197
|
+
idx.statedByBySubject.set(edge.subject, list);
|
|
1198
|
+
}
|
|
1033
1199
|
}
|
|
1034
1200
|
|
|
1035
1201
|
/** Recount `classes[]` from the individuals — every memory class stays counted
|
|
@@ -1285,7 +1451,13 @@ export async function appendFacts(dir, facts) {
|
|
|
1285
1451
|
if (!prepared.length) return { ids, appended: 0, skipped };
|
|
1286
1452
|
await mutateMemory(dir, (payload) => {
|
|
1287
1453
|
// id → individual index for O(1) upsert (the array grows to thousands).
|
|
1288
|
-
|
|
1454
|
+
// When mutateMemory already built the Symbol-keyed lookup index, reuse
|
|
1455
|
+
// THAT Map directly (same object) instead of rescanning payload.individuals
|
|
1456
|
+
// a second time — every `byId.set` below then also keeps
|
|
1457
|
+
// idx.individualsById correct for upsertSource/recomputeSourceReliability's
|
|
1458
|
+
// later lookups in this same mutation, with no extra write.
|
|
1459
|
+
const idx = memoryIndexOf(payload);
|
|
1460
|
+
const byId = idx ? idx.individualsById : new Map(payload.individuals.map((i) => [i?.id, i]));
|
|
1289
1461
|
const touched = [];
|
|
1290
1462
|
const seen = new Set();
|
|
1291
1463
|
const trustOptsById = new Map();
|
|
@@ -1314,10 +1486,15 @@ export async function appendFacts(dir, facts) {
|
|
|
1314
1486
|
...(f.justification && f.justification.length ? [{ prop: "mgx:factJustification", key: "justification", value: f.justification.join(" ") }] : []),
|
|
1315
1487
|
],
|
|
1316
1488
|
};
|
|
1317
|
-
// Upsert
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1489
|
+
// Upsert via the shared helper — O(1) via the index (Object.assign in
|
|
1490
|
+
// place when `prior` exists, push+index when it's new), same as every
|
|
1491
|
+
// other upsert path now. Previously this did its own inline
|
|
1492
|
+
// `payload.individuals.indexOf(prior)` array scan on a re-assert within
|
|
1493
|
+
// the same batch — an O(n) fallback that could still blow up a batch
|
|
1494
|
+
// heavy with within-file duplicate triples; upsertIndividual has no
|
|
1495
|
+
// such case left.
|
|
1496
|
+
const stored = upsertIndividual(payload, ind);
|
|
1497
|
+
byId.set(f.id, stored);
|
|
1321
1498
|
ids.push(f.id);
|
|
1322
1499
|
if (!seen.has(f.id)) { seen.add(f.id); touched.push(f.id); }
|
|
1323
1500
|
// Last-prepared-row-wins per id for the trust hook opts (mirrors the
|
package/src/memory/trust.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// Trust is a COMPUTED attribute of a Fact — never hand-set — a pure function of
|
|
5
5
|
// its Source edges, those Sources' types, and its mgx:createdAt. Three inputs
|
|
6
6
|
// combine:
|
|
7
|
-
// - a Source-TYPE PRIOR (operator > teach > provider > corpus >
|
|
7
|
+
// - a Source-TYPE PRIOR (operator > teach > provider > corpus > corpusWeak >
|
|
8
|
+
// extracted > web > entailed);
|
|
8
9
|
// - CORROBORATION over the fact's distinct Sources by noisy-OR
|
|
9
10
|
// (1 − Π(1 − wᵢ), capped at 1) — two independent web sources (0.4) reach
|
|
10
11
|
// 0.64, a lone operator fact is already 1.0;
|
|
@@ -35,17 +36,37 @@ export const TRUST_SCORE_PROP = "mgx:trustScore";
|
|
|
35
36
|
export const TRUST_INPUTS_PROP = "mgx:trustInputs";
|
|
36
37
|
|
|
37
38
|
/** Source-type priors — the ordering operator > teach > provider-graph >
|
|
38
|
-
* curated-corpus >
|
|
39
|
-
* lane's natural-frame writes
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* curated-corpus > weak-corpus > extracted-document > web > unverified-
|
|
40
|
+
* entailment. `teach` is the chat teach lane's natural-frame writes
|
|
41
|
+
* ("remember that …", "<Name> owns <X>") — still operator speech, but
|
|
42
|
+
* through a looser recognizer than the ACE-parsed operator assert, so it
|
|
43
|
+
* sits just below the operator prior. The entailed value is a FLOOR before
|
|
44
|
+
* premise adjustment (see the entailed hook below).
|
|
45
|
+
*
|
|
46
|
+
* `corpusWeak` is for corpus-sourced facts whose underlying relation is real
|
|
47
|
+
* but low-precision (ConceptNet's /r/RelatedTo — ambiguous, undirected
|
|
48
|
+
* association, unlike the specific typed relations the plain `corpus` prior
|
|
49
|
+
* covers) — still a curated, committed dataset (above `web`), just not
|
|
50
|
+
* asserting the same strength of claim (below `corpus`).
|
|
51
|
+
*
|
|
52
|
+
* `extracted` is scripts/extract-facts-from-text.mjs's batch reader: it runs
|
|
53
|
+
* the SAME deterministic teach/assert recognizer as `teach`/`operator`, but
|
|
54
|
+
* unattended over an arbitrary document nobody in-session vetted sentence by
|
|
55
|
+
* sentence — the recognizer is exact/closed-set (no guessing), but the
|
|
56
|
+
* SOURCE DOCUMENT is unreviewed, so it sits just above `web` (also an
|
|
57
|
+
* unreviewed external source) and below `corpusWeak`/`corpus` (curated,
|
|
58
|
+
* committed datasets) and `teach` (a human typing into the live chat).
|
|
59
|
+
*
|
|
60
|
+
* Both are computed from the Source's type exactly like every other tier —
|
|
61
|
+
* never hand-set on a Fact directly. */
|
|
43
62
|
export const SOURCE_PRIOR = Object.freeze({
|
|
44
63
|
operator: 1.0,
|
|
45
64
|
teach: 0.95,
|
|
46
65
|
provider: 0.9,
|
|
47
66
|
corpus: 0.7,
|
|
67
|
+
corpusWeak: 0.55,
|
|
48
68
|
web: 0.4,
|
|
69
|
+
extracted: 0.45,
|
|
49
70
|
entailed: 0.3,
|
|
50
71
|
});
|
|
51
72
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// memory-ask-browser-entry.mjs — the esbuild entry for `tmct viz`'s embedded
|
|
2
|
+
// "Ask the graph" panel's MEMORY-graph engine (PLAN_VIZ_MEMORY.md Bug 1 fix).
|
|
3
|
+
//
|
|
4
|
+
// Bug 1: the panel bundled ONLY ask.mjs — tmct's code-graph query engine
|
|
5
|
+
// ("which modules import X"), which has no concept of Facts/corpus data at
|
|
6
|
+
// all, so a memory-graph question like "what is a dog" always missed even
|
|
7
|
+
// though the page's own embedded payload had the answer. This is the SECOND,
|
|
8
|
+
// narrow browser entry point src/ask-browser-entry.mjs's own doc comment
|
|
9
|
+
// anticipates: re-exports just `factAnswer` (src/chat.mjs) — tmct's REAL
|
|
10
|
+
// memory-graph answer engine, the same one `npm run chat` uses — plus
|
|
11
|
+
// `createInMemoryStore` (src/memory/core.mjs), which is how the panel hands
|
|
12
|
+
// `factAnswer` the page's already-embedded PAYLOAD with ZERO fs I/O: a
|
|
13
|
+
// Backend-B handle's `loadMemory` branch returns `handle.payload` directly, no
|
|
14
|
+
// bundle-time module shimming needed (see factAnswer's own doc comment,
|
|
15
|
+
// src/chat.mjs, for the full reasoning — a simpler, more robust mechanism than
|
|
16
|
+
// intercepting loadMemory at bundle time, since it reuses machinery the
|
|
17
|
+
// codebase already ships and tests, rather than a new esbuild-only code path
|
|
18
|
+
// that could drift from the real one).
|
|
19
|
+
//
|
|
20
|
+
// `factAnswer` is called with `envelope: null, miss: true` — the exact,
|
|
21
|
+
// already-documented "no envelope available" bootstrap path (chat.mjs's own
|
|
22
|
+
// comments: "the FIRST turn of a graph-less session... leaves `envelope` null
|
|
23
|
+
// for the rest of THIS turn's processing" — a real, tested code path, not a
|
|
24
|
+
// hack) — which arms factAnswer's own bare-question regex fallbacks
|
|
25
|
+
// (BARE_WHATIS_RE and friends) to parse the query directly, with no
|
|
26
|
+
// dependency on the much larger structural-graph parse pipeline
|
|
27
|
+
// (dispatchTool/loadGraph, server.mjs) that pipeline needs a real --repo code
|
|
28
|
+
// index for and this panel has no use for.
|
|
29
|
+
import { factAnswer } from "./chat.mjs";
|
|
30
|
+
import { createInMemoryStore, normFactTerm } from "./memory/core.mjs";
|
|
31
|
+
|
|
32
|
+
// normFactTerm is re-exported too — viz.mjs's client-side "focus follows the
|
|
33
|
+
// memory-engine's answer" heuristic (guessTermIdFromQuery) needs the SAME
|
|
34
|
+
// term normalization the CLI's own `--term` seed flag and factAnswer's own
|
|
35
|
+
// subject/object matching use, never a second hand-rolled copy.
|
|
36
|
+
globalThis.tmctMemoryAsk = { factAnswer, createInMemoryStore, normFactTerm };
|