@polycode-projects/the-mechanical-code-talker 1.8.20 → 1.9.0

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/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 base;
161
- return `${base}
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
- try {
270
- const { resolveExtensions, seedActiveCorpusEntries } = await import("./extensions.mjs");
271
- const { entries } = await resolveExtensions(root);
272
- // `tmct.toml`'s `[seed] limit` knob is documented as capping the tier-1
273
- // ConceptNet band specifically (the curated SEON ontology is small and
274
- // always seeds whole)so it overrides ONLY the resolved "conceptnet"
275
- // entry's limit, exactly like the pre-fix single-corpus seed did.
276
- if (config.seed?.limit != null && entries.has("conceptnet")) {
277
- entries.set("conceptnet", { ...entries.get("conceptnet"), limit: Number(config.seed.limit) });
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 itsrc/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 (which permanently collapses "this file" to bare
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
@@ -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
@@ -771,6 +808,11 @@ function sourceIdFor(desc) {
771
808
  case "teach": return { id: desc.sessionId ? `${TEACH_SOURCE_ID}:${desc.sessionId}` : TEACH_SOURCE_ID, type: "teach" };
772
809
  case "provider": return { id: `src:provider:${desc.name}`, type: "provider" };
773
810
  case "corpus": return { id: `src:corpus:${desc.name}`, type: "corpus" };
811
+ // One Source per source-file basename (the corpus precedent above), not per
812
+ // extraction run — re-running scripts/extract-facts-from-text.mjs over the
813
+ // SAME file collapses onto the same Source instead of minting a new one
814
+ // every time, matching corpus's "one Source per named dataset" idiom.
815
+ case "extracted": return { id: `src:extracted:${desc.name}`, type: "extracted" };
774
816
  case "web": return { id: `src:learned:web:${fnv1aHex(String(desc.url || ""))}`, type: "web", url: String(desc.url || "") };
775
817
  case "entailed": return { id: `src:entailed:${desc.rule}`, type: "entailed", rule: String(desc.rule || "") };
776
818
  default: return null;
@@ -821,19 +863,32 @@ function parseChatTagRest(rest) {
821
863
  * set — the inverse the migration and the live write path both name Sources
822
864
  * through. The tag formats are exactly what the writers produce:
823
865
  * corpus:conceptnet /r/IsA → { kind:"corpus", name:"conceptnet" }
866
+ * corpus-weak:conceptnet /r/RelatedTo → { kind:"corpusWeak", name:"conceptnet" }
824
867
  * ace:chat:<session>@<ts> → { kind:"operator", createdAt:<ts>, sessionId:<session> }
825
868
  * teach:chat:<session>@<ts> → { kind:"teach", createdAt:<ts>, sessionId:<session> }
826
869
  * web:<url> | url:<url> → { kind:"web", url:<url> }
870
+ * extracted:<file-basename> → { kind:"extracted", name:<file-basename> }
827
871
  * entailed:<rule> → { kind:"entailed", rule:<rule> }
828
872
  * chat:/session: refs map to the operator; an unknown tag → null (no Source).
873
+ * `corpus-weak:` is the SAME corpus provenance shape as `corpus:`, just naming
874
+ * a lower trust-prior kind (memory/trust.mjs SOURCE_PRIOR.corpusWeak) for
875
+ * facts whose underlying relation is real but low-precision (e.g. ConceptNet's
876
+ * undirected /r/RelatedTo) — trust stays computed from the Source's kind, never
877
+ * hand-set on the Fact.
829
878
  * The session-id segment (Part B: session-scoped actor-level trust) feeds
830
879
  * sourceIdFor, which mints a PER-SESSION Source id when present, instead of
831
880
  * collapsing every session onto one singleton operator/teach Source.
881
+ * `extracted:` is scripts/extract-facts-from-text.mjs's own audit tag, layered
882
+ * ADDITIVELY (via appendFact's provenance union) on top of whatever the
883
+ * runTurn recognizer already wrote (ace:/teach:) — see that script for why:
884
+ * it distinguishes "this document evidenced this fact" from ordinary chat
885
+ * speech, at its own trust-prior tier (memory/trust.mjs SOURCE_PRIOR.extracted).
832
886
  */
833
887
  export function provenanceTagToSource(tag) {
834
888
  const t = String(tag || "").trim();
835
889
  if (!t) return null;
836
890
  const head = t.split(/\s+/)[0]; // drop trailing " /r/IsA" etc.
891
+ if (head.startsWith("corpus-weak:")) return { kind: "corpusWeak", name: head.slice("corpus-weak:".length) || "unknown" };
837
892
  if (head.startsWith("corpus:")) return { kind: "corpus", name: head.slice("corpus:".length) || "unknown" };
838
893
  if (head.startsWith("ace:")) return { kind: "operator", ...parseChatTagRest(head.slice("ace:".length)) };
839
894
  if (head.startsWith("teach:")) {
@@ -842,6 +897,7 @@ export function provenanceTagToSource(tag) {
842
897
  }
843
898
  if (head.startsWith("web:")) return { kind: "web", url: head.slice("web:".length) };
844
899
  if (head.startsWith("url:")) return { kind: "web", url: head.slice("url:".length) };
900
+ if (head.startsWith("extracted:")) return { kind: "extracted", name: head.slice("extracted:".length) || "unknown" };
845
901
  if (head.startsWith("entailed:")) return { kind: "entailed", rule: head.slice("entailed:".length) };
846
902
  if (head.startsWith("chat:") || head.startsWith("session:") || head.startsWith("operator")) return { kind: "operator" };
847
903
  return null;
@@ -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 > web > entailed);
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 > web > unverified-entailment. `teach` is the chat teach
39
- * lane's natural-frame writes ("remember that …", "<Name> owns <X>") — still
40
- * operator speech, but through a looser recognizer than the ACE-parsed
41
- * operator assert, so it sits just below the operator prior. The entailed
42
- * value is a FLOOR before premise adjustment (see the entailed hook below). */
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 };