@polycode-projects/the-mechanical-code-talker 3.0.5 → 3.0.7

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.
Files changed (68) hide show
  1. package/README.md +41 -16
  2. package/bin/tmct.mjs +1 -1
  3. package/corpus/child/README.md +2 -2
  4. package/corpus/child/index.json.gz +0 -0
  5. package/corpus/child/manifest.json +74 -74
  6. package/corpus/child/shards/child-00.jsonl.gz +0 -0
  7. package/corpus/child/shards/child-01.jsonl.gz +0 -0
  8. package/corpus/child/shards/child-02.jsonl.gz +0 -0
  9. package/corpus/child/shards/child-03.jsonl.gz +0 -0
  10. package/corpus/child/shards/child-04.jsonl.gz +0 -0
  11. package/corpus/child/shards/child-05.jsonl.gz +0 -0
  12. package/corpus/child/shards/child-06.jsonl.gz +0 -0
  13. package/corpus/child/shards/child-07.jsonl.gz +0 -0
  14. package/corpus/child/shards/child-08.jsonl.gz +0 -0
  15. package/corpus/child/shards/child-09.jsonl.gz +0 -0
  16. package/corpus/child/shards/child-0a.jsonl.gz +0 -0
  17. package/corpus/child/shards/child-0b.jsonl.gz +0 -0
  18. package/corpus/child/shards/child-0c.jsonl.gz +0 -0
  19. package/corpus/child/shards/child-0d.jsonl.gz +0 -0
  20. package/corpus/child/shards/child-0e.jsonl.gz +0 -0
  21. package/corpus/child/shards/child-0f.jsonl.gz +0 -0
  22. package/corpus/child/shards/child-10.jsonl.gz +0 -0
  23. package/corpus/child/shards/child-11.jsonl.gz +0 -0
  24. package/corpus/child/shards/child-12.jsonl.gz +0 -0
  25. package/corpus/child/shards/child-13.jsonl.gz +0 -0
  26. package/corpus/child/shards/child-14.jsonl.gz +0 -0
  27. package/corpus/child/shards/child-15.jsonl.gz +0 -0
  28. package/corpus/child/shards/child-16.jsonl.gz +0 -0
  29. package/corpus/child/shards/child-17.jsonl.gz +0 -0
  30. package/corpus/child/shards/child-18.jsonl.gz +0 -0
  31. package/corpus/child/shards/child-19.jsonl.gz +0 -0
  32. package/corpus/child/shards/child-1a.jsonl.gz +0 -0
  33. package/corpus/child/shards/child-1b.jsonl.gz +0 -0
  34. package/corpus/child/shards/child-1c.jsonl.gz +0 -0
  35. package/corpus/child/shards/child-1d.jsonl.gz +0 -0
  36. package/corpus/child/shards/child-1e.jsonl.gz +0 -0
  37. package/corpus/child/shards/child-1f.jsonl.gz +0 -0
  38. package/corpus/conceptnet/quality-filter.mjs +28 -4
  39. package/corpus/tier2/generate.mjs +4 -8
  40. package/corpus/tier2/human-large.jsonl +0 -2
  41. package/corpus/tier2/human-medium.jsonl +0 -2
  42. package/corpus/tier2/manifest.json +6 -6
  43. package/data/templates/constructions/digest-sentence-structures.toml +146 -1
  44. package/package.json +18 -15
  45. package/src/adapters/corpus/digest-bank.mjs +15 -3
  46. package/src/domain/answer-variants.json +1 -1
  47. package/src/domain/ask-vocab.mjs +4 -0
  48. package/src/domain/ask.mjs +101 -12
  49. package/src/domain/codegraph.mjs +25 -8
  50. package/src/domain/digest/compose.mjs +142 -10
  51. package/src/domain/digest/config.json +5 -0
  52. package/src/domain/digest/structures.mjs +39 -14
  53. package/src/domain/interpret/normalize.mjs +2 -2
  54. package/src/domain/reference-pack.mjs +12 -3
  55. package/src/domain/router/call-validator.mjs +1 -1
  56. package/src/domain/router/drive.mjs +2 -2
  57. package/src/domain/router/set-algebra.mjs +1 -1
  58. package/src/services/adventure.mjs +34 -19
  59. package/src/services/chat-page-viz.mjs +16 -5
  60. package/src/services/chat.mjs +290 -88
  61. package/src/services/extract-facts.mjs +46 -4
  62. package/src/services/index.mjs +6 -3
  63. package/src/services/ledger-viz.mjs +1 -1
  64. package/src/services/research.mjs +80 -3
  65. package/src/surfaces/web/chat-browser-entry.mjs +23 -3
  66. package/src/surfaces/web/digest-client.mjs +8 -3
  67. package/src/surfaces/web/graph-ask-browser-entry.mjs +41 -0
  68. package/src/surfaces/web/memory-ask-browser.bundle.js +113 -113
@@ -53,7 +53,8 @@ import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises";
53
53
  import { tmpdir } from "node:os";
54
54
  import { basename, join, resolve } from "node:path";
55
55
 
56
- import { runTurn, uuidv7 } from "./chat.mjs";
56
+ import { runTurn, uuidv7, stripLeadingDiscourseAdverb } from "./chat.mjs";
57
+ import { beginsWithVowelSound, grammarRules } from "./finish.mjs";
57
58
  import { splitSentencesPreservingPaths, stripCitationResidue } from "./sentences.mjs";
58
59
  import { loadMemory, readFactRows, appendFact } from "../adapters/memory/core.mjs";
59
60
  import { loadConfig } from "../adapters/config.mjs";
@@ -125,6 +126,12 @@ const RELATIVE_PRONOUNS = new Set(["that", "which", "who", "whom", "whose"]);
125
126
  // At most this many triples from one sentence — a bound so a run-on can never
126
127
  // shatter into noise, not a first-wins cap.
127
128
  const MAX_TRIPLES_PER_SENTENCE = 4;
129
+ // How far a copula object scan walks past an attributive-adjective compound
130
+ // (wink tokenizes "medium-sized" as NOUN + "-" + VERB and never re-fuses it) to
131
+ // reach the real head noun through a coordinate modifier list
132
+ // (", burrowing, nocturnal mammal"). A small, explicit bound: past it the object
133
+ // abstains rather than guess, so a long noun pile never mints a stray class.
134
+ const ATTRIBUTIVE_CHAIN_MAX_HOPS = 8;
128
135
 
129
136
  /** Fold an entity surface to its stored key: a lexicon noun's lemma, else the
130
137
  * word's own normFactTerm (the optimistic tier mints unlisted content nouns
@@ -242,6 +249,25 @@ function optimisticTriplesPos(sentence, lexicon, nlp) {
242
249
  if (!isNounish(j)) continue;
243
250
  let hi = j;
244
251
  while (hi + 1 < values.length && isNounish(hi + 1)) hi += 1;
252
+ // A NOUN immediately followed by "-" then a VERB or ADJ is the left half of
253
+ // an attributive-adjective compound wink never re-fused ("medium-sized"),
254
+ // not the class. Walk forward through the coordinate modifier list (hyphens,
255
+ // commas, "and", further ADJ/VERB tokens) to the real head noun and re-point
256
+ // there; abstain if none appears within the bound, never mint the modifier.
257
+ if (values[hi + 1] === "-" && (pos[hi + 2] === "VERB" || pos[hi + 2] === "ADJ")) {
258
+ let head = null;
259
+ let k = hi + 1;
260
+ for (let hop = 0; hop < ATTRIBUTIVE_CHAIN_MAX_HOPS && k < values.length; hop += 1, k += 1) {
261
+ if (isNounish(k)) { head = k; break; }
262
+ const w = values[k]?.toLowerCase();
263
+ if (w === "-" || w === "," || w === "and" || pos[k] === "ADJ" || pos[k] === "VERB" || pos[k] === "CCONJ") continue;
264
+ break;
265
+ }
266
+ if (head === null) return null;
267
+ j = head;
268
+ hi = head;
269
+ while (hi + 1 < values.length && isNounish(hi + 1)) hi += 1;
270
+ }
245
271
  const headWord = String(values[hi]).toLowerCase();
246
272
  const nextIsOf = values[hi + 1]?.toLowerCase() === "of";
247
273
  if (!nextIsOf) return { label: entityRunAt(j), hi };
@@ -434,6 +460,17 @@ export function clauseCandidates(sentence, { nlp } = {}) {
434
460
  // live focus, never a stale paragraph carry.
435
461
  const PRONOUN_LEAD_RE = /^(?:they|it|these|those|this)\b\s*/i;
436
462
 
463
+ /** Re-article a bare carried subject so the retried sentence is a grammatical
464
+ * habitual surface the recognizer accepts: "cell" → "a cell", "orbit" → "an
465
+ * orbit". Uses the same vowel-sound-aware article rule (grammar-rules.toml)
466
+ * the chat recognizer's own capability rewrite uses, rather than a hardcoded
467
+ * "a". */
468
+ function articledSubject(subject) {
469
+ const articleRule = grammarRules().find((r) => r.kind === "article");
470
+ const article = articleRule && beginsWithVowelSound(subject, articleRule) ? "an" : "a";
471
+ return `${article} ${subject}`;
472
+ }
473
+
437
474
  /** A readable predicate for canonical output: the local part of an rdfs:/ace:
438
475
  * CURIE, otherwise the predicate verbatim. */
439
476
  const readablePredicate = (predicate) => String(predicate).replace(/^[a-z]+:/i, "");
@@ -532,9 +569,14 @@ export async function ingestText(text, {
532
569
  }
533
570
  // Bounded pronoun carry: a "they/it/these/those/this …" sentence the
534
571
  // recognizer skipped is retried once with the paragraph's last grounded
535
- // subject in the pronoun's place. Never a chat turn — ingest only.
536
- if (!rows && carrySubject && PRONOUN_LEAD_RE.test(cleaned)) {
537
- rows = await strictRows(cleaned.replace(PRONOUN_LEAD_RE, `${carrySubject} `));
572
+ // subject in the pronoun's place. A leading ordinal/temporal discourse
573
+ // adverb ("Then it splits.") is stripped first so the pronoun reaches
574
+ // the sentence front; the carried subject is re-articled ("a cell") so
575
+ // the retry is a grammatical habitual surface. Never a chat turn —
576
+ // ingest only.
577
+ const threaded = stripLeadingDiscourseAdverb(cleaned);
578
+ if (!rows && carrySubject && PRONOUN_LEAD_RE.test(threaded)) {
579
+ rows = await strictRows(threaded.replace(PRONOUN_LEAD_RE, `${articledSubject(carrySubject)} `));
538
580
  }
539
581
  if (rows) {
540
582
  recognizedSentences += 1;
@@ -6,7 +6,7 @@
6
6
  // ask.mjs keeps the core primitives (resolveObject, traverse, render) and
7
7
  // the ask() orchestration.
8
8
 
9
- // Chat surface (also reachable as the `./chat` subpath export).
9
+ // Chat surface.
10
10
  // createSession is the library's session sink — the same focus/last/planState
11
11
  // threading and memory-backend seam every shell shares; runTurn is the pure
12
12
  // single-turn engine underneath it.
@@ -35,6 +35,9 @@ setConstructionBanks(readConstructionFiles);
35
35
  // Graph traversal primitives.
36
36
  export { relationKind, impactClosure } from "../domain/codegraph.mjs";
37
37
 
38
+ // Graph-service construction for a provider-supplied code graph.
39
+ export { createGraphService } from "../adapters/providers/graph-service.mjs";
40
+
38
41
  // Tool dispatch (slash-commands and CLI tool calls route through here).
39
42
  export { dispatchTool } from "../tools/server.mjs";
40
43
 
@@ -49,13 +52,13 @@ export { foldSessionLogs } from "./fold.mjs";
49
52
  // fetchEntities() is the one read path.
50
53
  export { fetchEntities, registerProvider } from "../adapters/source.mjs";
51
54
 
52
- // `tmct init` onboarding (also reachable as the `./init` subpath export).
55
+ // `tmct init` onboarding.
53
56
  // init.mjs and toml-config.mjs each export a same-named `CONFIG_FILE`
54
57
  // constant ("tmct.toml") — aliased here so both can ride the one `.` entry
55
58
  // point without colliding.
56
59
  export { initRepo, defaultConfig, renderTomlConfig, PERSONA_PRESETS, CONFIG_FILE as INIT_CONFIG_FILE } from "./init.mjs";
57
60
 
58
- // tmct.toml loading (also reachable as the `./toml-config` subpath export).
61
+ // tmct.toml loading.
59
62
  export { CONFIG_FILE as TOML_CONFIG_FILE } from "../adapters/toml-config.mjs";
60
63
 
61
64
  // The "detailed answer" completions pipeline (also reachable as the
@@ -795,7 +795,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
795
795
  // LEDGER (declared "let" in the embed script above this one — a classic,
796
796
  // non-module script's top-level bindings are reachable as bare identifiers
797
797
  // by every later script tag in the document, never as window.LEDGER; see
798
- // e2e/pages-ledger.test.mjs's own note on this) and its indexes start as
798
+ // test-e2e/pages-ledger.test.mjs's own note on this) and its indexes start as
799
799
  // the server-rendered snapshot but are REASSIGNED wholesale after a live
800
800
  // teach (applyLedgerData, below) — a successful teach through the dock
801
801
  // changes the underlying graph, and the page has to show that, not keep
@@ -25,6 +25,7 @@
25
25
 
26
26
  import { normFactTerm } from "../domain/hash.mjs";
27
27
  import { loadLexicon, lookupNoun } from "../domain/grammar/lexicon.mjs";
28
+ import { defaultNlp } from "../domain/interpret/nlp-registry.mjs";
28
29
 
29
30
  /** The search key a topic folds to: normFactTerm, then the lexicon lemma
30
31
  * when the noun is known ("owls" → "owl") — the same fold the live
@@ -210,12 +211,87 @@ function queuedFolds(state) {
210
211
  return seen;
211
212
  }
212
213
 
214
+ // The lexical-token split a fan-out candidate and the seed topic both fold
215
+ // through before comparison — lowercased, split on anything that isn't a
216
+ // letter or digit, empty pieces dropped.
217
+ const LEXICAL_SPLIT_RE = /[^a-z0-9]+/i;
218
+ function lexicalTokens(text) {
219
+ return String(text || "").toLowerCase().split(LEXICAL_SPLIT_RE).filter(Boolean);
220
+ }
221
+ function sharesLexicalToken(title, seedTokens) {
222
+ if (!seedTokens.size) return false;
223
+ for (const tok of lexicalTokens(title)) if (seedTokens.has(tok)) return true;
224
+ return false;
225
+ }
226
+
227
+ // A citation-shaped candidate: an ISBN/DOI prefix, a bare year, or a
228
+ // "Nth century" phrase — the shape raw Wikipedia link lists carry for their
229
+ // source apparatus rather than a kin article.
230
+ const CITATION_PREFIX_RE = /^(isbn|doi)[\s:.-]/i;
231
+ const CITATION_BARE_YEAR_RE = /^\d{3,4}$/;
232
+ const CITATION_CENTURY_RE = /\b\d{1,2}(?:st|nd|rd|th)\s+century\b/i;
233
+ function isCitationShaped(title) {
234
+ const t = String(title || "").trim();
235
+ if (!t) return false;
236
+ return CITATION_PREFIX_RE.test(t) || CITATION_BARE_YEAR_RE.test(t) || CITATION_CENTURY_RE.test(t);
237
+ }
238
+
239
+ const SINGLE_WORD_TITLE_RE = /^[A-Za-z][A-Za-z'-]*$/;
240
+ // A short, neutral sentence to tag a bare candidate word inside — wink-nlp
241
+ // reads an isolated capitalized word as PROPN with no sentence context to
242
+ // tell it otherwise, so the word is lowercased and read at this fixed slot
243
+ // inside real subject/verb/object context instead.
244
+ const NOUN_CARRIER_PREFIX = ["this", "is", "about", "the"];
245
+ const NOUN_CARRIER_SUFFIX = ["and", "its", "history"];
246
+ const NOUN_CARRIER_WORD_INDEX = NOUN_CARRIER_PREFIX.length;
247
+
248
+ /** True when `title` is a single word a general-English POS tagger reads as
249
+ * a common noun (hub articles like "Earth"/"Geology") rather than a proper
250
+ * noun (kin articles like "Hawaii") — false whenever `nlp` is unavailable,
251
+ * never a throw. */
252
+ function readsAsCommonNoun(title, nlp) {
253
+ if (!nlp || typeof nlp.posTags !== "function") return false;
254
+ const t = String(title || "").trim();
255
+ if (!SINGLE_WORD_TITLE_RE.test(t)) return false;
256
+ const carrier = [...NOUN_CARRIER_PREFIX, t.toLowerCase(), ...NOUN_CARRIER_SUFFIX];
257
+ let tags;
258
+ try { tags = nlp.posTags(carrier); } catch { return false; }
259
+ return Array.isArray(tags) && tags[NOUN_CARRIER_WORD_INDEX] === "NOUN";
260
+ }
261
+
262
+ /** Stable-sorts fan-out candidates into relevance tiers — never reorders
263
+ * within a tier, only reprioritizes between them — from information the
264
+ * runtime actually has (title strings, the seed topic, an optional POS
265
+ * tagger), never bench-only ground truth:
266
+ * 0. shares a lexical token with `seedTopic` ("Active volcano" ~ "Volcano");
267
+ * 1. everything else, in original document order (the fallback tier);
268
+ * 2. a single-word title a POS tagger reads as a common noun rather than a
269
+ * proper noun — the hub-article signal ("Earth", "Geology");
270
+ * 3. a citation-shaped title (ISBN/DOI prefix, bare year, "Nth century").
271
+ * With no `nlp` adapter registered, tier 2 never fires (everything that
272
+ * would have landed there stays in tier 1) — degrades gracefully, never
273
+ * throws, never drops a candidate. */
274
+ export function relevanceOrder(titles, seedTopic, nlp = null) {
275
+ const list = Array.isArray(titles) ? titles : [];
276
+ const seedTokens = new Set(lexicalTokens(seedTopic));
277
+ const tiers = [[], [], [], []];
278
+ for (const title of list) {
279
+ if (sharesLexicalToken(title, seedTokens)) { tiers[0].push(title); continue; }
280
+ if (isCitationShaped(title)) { tiers[3].push(title); continue; }
281
+ if (readsAsCommonNoun(title, nlp)) { tiers[2].push(title); continue; }
282
+ tiers[1].push(title);
283
+ }
284
+ return [...tiers[0], ...tiers[1], ...tiers[2], ...tiers[3]];
285
+ }
286
+
213
287
  /** Queue `article`'s lead-section links at `fromDepth + 1`, subject to the run's
214
288
  * depth ceiling, its per-fan-out cap and — crucially — its TOTAL node budget:
215
289
  * the number added never pushes grounded+pending past `maxTopics`. Sets
216
290
  * `state.nodeCapReached` when the budget (not the depth, not a lack of links)
217
- * is what stopped the fan-out, so the progress line can say so. Returns the
218
- * titles it enqueued. */
291
+ * is what stopped the fan-out, so the progress line can say so. Candidates are
292
+ * relevance-ordered (relevanceOrder) before the fan-out cap truncates them, so
293
+ * a capped fetch keeps kin articles over generic hubs. Returns the titles it
294
+ * enqueued. */
219
295
  async function enqueueFrom(state, article, fromDepth, provider) {
220
296
  const childDepth = fromDepth + 1;
221
297
  if (childDepth > runMaxDepth(state)) return [];
@@ -226,10 +302,11 @@ async function enqueueFrom(state, article, fromDepth, provider) {
226
302
  if (want <= 0) { state.nodeCapReached = true; return []; }
227
303
  let linked = null;
228
304
  try { linked = await provider.linkedTitles(article.title, { limit: want + 2 }); } catch { linked = null; }
305
+ const ordered = relevanceOrder(linked || [], article.title, defaultNlp());
229
306
  const seen = queuedFolds(state);
230
307
  if (!state.depths) state.depths = {};
231
308
  const added = [];
232
- for (const title of linked || []) {
309
+ for (const title of ordered) {
233
310
  const folded = normFactTerm(title);
234
311
  if (!folded || seen.has(folded)) continue;
235
312
  seen.add(folded);
@@ -10,12 +10,21 @@
10
10
  // built seed payload (scripts/build-chat-seed.mjs), so teach turns, recall,
11
11
  // proof chains and the honest miss all run client-side with zero I/O.
12
12
  //
13
- // Two browser traps this file owns so no caller can fall into them:
13
+ // Three browser traps this file owns so no caller can fall into them:
14
14
  // - runTurn defaults `env` to process.env, and a browser has no `process`
15
15
  // global — every turn here passes `env: {}` explicitly;
16
16
  // - the uuid adapter needs node:crypto — the session id comes from the
17
17
  // Web Crypto API instead, with a Date.now fallback for contexts
18
- // without it.
18
+ // without it;
19
+ // - setDigestStructures is imported from adapters/corpus/digest-bank.mjs.
20
+ // Under the bundle build, scripts/build-chat-bundle.mjs swaps that import
21
+ // for a live in-memory twin, and this call actually feeds it the page's
22
+ // embedded structure rows; this file is ALSO imported directly by plain
23
+ // Node (test-e2e/web-chat-memory.test.mjs, exercising the same engine contract
24
+ // without a browser), where the import resolves to the real fs+TOML
25
+ // adapter — its own setDigestStructures is a documented no-op there
26
+ // (see that module's header), so the call is harmless either way, never
27
+ // a load error and never a behavior change on the Node side.
19
28
  import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
20
29
  import { createInMemoryStore, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
21
30
  import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
@@ -24,6 +33,7 @@ import { provenanceTagToSource } from "../../domain/memory/trust.mjs";
24
33
  import { parseEntities } from "../../domain/codegraph.mjs";
25
34
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
26
35
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
36
+ import { setDigestStructures } from "../../adapters/corpus/digest-bank.mjs";
27
37
  // The reference-pack provider seam: the page registers a fetch-backed
28
38
  // provider over public/reference-pack/ so the engine's pack lookups work
29
39
  // where the gzipped fs layout cannot (the module's own fs loader degrades to
@@ -50,11 +60,21 @@ import { openPersistedStore } from "./idb-persist.mjs";
50
60
  * payload carries the starter vocabulary, so an unseeded session offers the
51
61
  * teach pointer instead of an example it cannot answer.
52
62
  *
63
+ * `digestStructures` (optional) is the page's build-time-embedded
64
+ * [[structure]] rows from the digest sentence-structure bank (chat-page-viz.mjs's
65
+ * DIGEST_STRUCTURES) — fed once to the bundle's live digest-bank twin so a
66
+ * long "what is X" answer leads with a composed digest instead of always
67
+ * falling back to the flat fact list (the bundle's stub used to return null
68
+ * unconditionally). Harmless to call more than once per page load: the table
69
+ * is module-scope, last write wins, and every session created after this one
70
+ * shares it.
71
+ *
53
72
  * Returns { memoryDir, sessionId, turn }. `turn(line)` resolves to
54
73
  * { answer, end, record, plan } and threads focus/last/planState between
55
74
  * calls exactly as the CLI session does.
56
75
  */
57
- export function createChatSession({ seedPayload = null, vocabSeeded = false, liveReference = false, onLiveLookup = null, synthesisBudget = 12 } = {}) {
76
+ export function createChatSession({ seedPayload = null, vocabSeeded = false, liveReference = false, onLiveLookup = null, synthesisBudget = 12, digestStructures = null } = {}) {
77
+ setDigestStructures(digestStructures || []);
58
78
  const memoryDir = createInMemoryStore();
59
79
  // Spread onto the store's own empty payload so a partial seed (individuals
60
80
  // and objectProperties only) still carries the classes/prefixes scaffolding
@@ -14,7 +14,8 @@
14
14
 
15
15
  import { buildStructureTable, digestTerm } from "../../domain/digest/index.mjs";
16
16
  import { digestStoreStats, chainsForObjects, isaObjectsOf } from "../../domain/digest/store-stats.mjs";
17
- import { readFactRows } from "../../adapters/memory/core.mjs";
17
+ import { readFactRows, normFactTerm } from "../../adapters/memory/core.mjs";
18
+ import { factTermVariants } from "../../services/chat.mjs";
18
19
 
19
20
  /**
20
21
  * Digest one term end to end from fact rows and a pre-parsed structure table.
@@ -40,14 +41,18 @@ export function digestTermFromRowsBrowser(term, termRows, allRows, structures, o
40
41
  * in-browser surface already holds (the ledger's embedded PAYLOAD, the ledger
41
42
  * dock's live memoryDir.payload, the research session's store). Scans the
42
43
  * payload once for the term's own rows and the whole-store statistics, so a
43
- * caller never has to run readFactRows itself.
44
+ * caller never has to run readFactRows itself. Matches `term` against a fact's
45
+ * subject through the same spelling-variant fold `factTermVariants` gives the
46
+ * "what is X" fact reader (plural/irregular-plural folding), so a term seeded
47
+ * as "dog" is still found when queried as "dogs".
44
48
  *
45
49
  * Returns the render-ready view (see digestViewFromArticle) or null when the
46
50
  * term has no rows, no structures were supplied, or the selector kept nothing.
47
51
  */
48
52
  export function digestTermFromPayloadBrowser(payload, term, structures, opts = {}) {
49
53
  const rows = readFactRows(payload || { individuals: [], objectProperties: [] });
50
- const termRows = rows.filter((r) => r.subject === term);
54
+ const variants = factTermVariants(normFactTerm, term);
55
+ const termRows = rows.filter((r) => variants.has(r.subject));
51
56
  if (!termRows.length) return null;
52
57
  const article = digestTermFromRowsBrowser(term, termRows, rows, structures, opts);
53
58
  return digestViewFromArticle(article);
@@ -0,0 +1,41 @@
1
+ // graph-ask-browser-entry.mjs — the public, browser-safe entry point for
2
+ // tmct's CODE-graph query engine (`./ask-browser` package export).
3
+ //
4
+ // A plain re-export, zero side effects, zero `globalThis` assignment: unlike
5
+ // this directory's other *-browser-entry.mjs files (built into an IIFE and
6
+ // inlined into a specific tmct page), this one is meant to be imported by a
7
+ // DOWNSTREAM consumer's own bundler (e.g. seonix's website chat panel) as a
8
+ // genuine ESM module, not inlined by tmct's own build scripts.
9
+ //
10
+ // It exists because the package root (`src/services/index.mjs`, and its
11
+ // `.`/exports subpath) also runs Node-side composition wiring at module-load
12
+ // time — `setDefaultNlpAdapter(nlpAdapter)` from `../adapters/ask-nlp.mjs`
13
+ // (wink-nlp, a ~4MB model) and `setConstructionBanks(readConstructionFiles)`
14
+ // from `../adapters/corpus/construction-banks.mjs` (fs reads) — neither of
15
+ // which can run or bundle in a browser. This file imports only ask.mjs and
16
+ // graph-service.mjs directly, so none of that wiring is reachable from here.
17
+ //
18
+ // Verified with `esbuild --bundle --platform=browser --format=esm`
19
+ // (scripts/verify-ask-browser-entry.mjs). ask()'s optional strategies
20
+ // (ace.mjs, constructions.mjs) and the wink lemma/POS adapter are all reached
21
+ // through nlp-registry.mjs's `defaultNlp()` registry seam and each call
22
+ // site's own `typeof X !== "undefined"` guard, not a static import, so a
23
+ // consumer bundling this entry point alone never links wink-nlp or any
24
+ // fs-reading corpus module — none of the ask-nlp.mjs/ace.mjs/
25
+ // constructions.mjs/construction-banks.mjs/digest-bank.mjs family of stubs
26
+ // scripts/build-ask-bundle.mjs needs for chat.mjs is required here.
27
+ //
28
+ // One real gap remains: `createGraphService` pulls in source-slice.mjs,
29
+ // which imports `node:path` (join/resolve/sep) for span math, so a browser
30
+ // bundler with no Node shims will fail on that one specifier. Apply the
31
+ // same generic `stubNodeBuiltins` plugin scripts/lib/browser-bundle.mjs
32
+ // already exports for tmct's own bundles (or an equivalent node:path shim —
33
+ // e.g. esbuild's own `--define`/polyfill options, or a bundler that already
34
+ // ships a node:path shim, as most do). Re-run
35
+ // `node scripts/verify-ask-browser-entry.mjs` (clean) and
36
+ // `node scripts/verify-ask-browser-entry.mjs --with-stubs` (with the shim)
37
+ // after touching ask.mjs's or graph-service.mjs's import graph — if the
38
+ // no-stub run ever passes clean, or a new specifier shows up, update this
39
+ // comment to match what esbuild actually reports.
40
+ export { ask, resolveObject } from "../../domain/ask.mjs";
41
+ export { createGraphService } from "../../adapters/providers/graph-service.mjs";