@polycode-projects/the-mechanical-code-talker 2.7.0 → 2.7.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 CHANGED
@@ -369,6 +369,51 @@ asserts the plan is exactly 2^n − 1 moves every time, and a second game
369
369
  conjunction) solves with zero interpreter changes. `--render blocks` writes
370
370
  the plan as a self-contained animated page (see "Two more surfaces" above).
371
371
 
372
+ ## Play a game with it
373
+
374
+ Two games run inside an ordinary chat session, no setup.
375
+
376
+ **Guess the number.** Say `I'm thinking of a number between 1 and 100` and
377
+ tmct guesses by narrowing an interval — answer `higher`, `lower`, or
378
+ `correct`. It finds any number in at most 7 guesses, and if your answers
379
+ contradict each other it names the contradicting pair and stops rather than
380
+ guessing on. Say `think of a number` to swap seats: tmct commits to a secret
381
+ and answers your guesses honestly, reveals on request, and corrects you from
382
+ its own record if you claim it already said `correct`. The behaviour is
383
+ pinned by `test/corpus/games/guess-number.jsonl`, and the home page's demo
384
+ rail plays the guesser side live in your browser.
385
+
386
+ **A text adventure.** Say `start the adventure` (or `play ashcombe hall`)
387
+ and tmct loads a small country-house mystery from a lazily-fetched worlds
388
+ pack (`corpus/worlds/`) into the session's ordinary memory graph — rooms,
389
+ objects and people become graph facts, and the verbs (`go`, `take`, `open`,
390
+ `unlock`, `look`…) are taught action rules, not hard-wired code. Every move
391
+ writes per-turn snapshot facts, `look` is an extractive digest of the graph,
392
+ a blocked action declines by name, and one of the household moves on its own
393
+ schedule whether you are there to see it or not. The full worked mystery is
394
+ pinned step by step in `test/corpus/games/adventure.jsonl`.
395
+
396
+ ## Learning on a miss
397
+
398
+ A question tmct cannot ground is still an honest miss — but on the cleanest
399
+ kind of miss (a recognised word, a clean parse, simply no facts anywhere) it
400
+ now consults two shipped, lazily-loaded packs before giving up:
401
+
402
+ - `corpus/child/` — 93k everyday-world triples filtered from ConceptNet by a
403
+ child-concept seed. Asked `what is a kettle` cold, tmct loads the term's
404
+ triples into memory (provenance `child:conceptnet:kettle`, ranked below
405
+ anything you teach) and answers from them; the next ask answers from
406
+ memory directly.
407
+ - `corpus/reference/` — 3,887 Simple English Wikipedia summaries. When the
408
+ triples cannot answer, a matching article answers as a cited read-out
409
+ (`source: reference article "Otter"…, CC BY-SA 4.0`).
410
+
411
+ Facts first, prose second; if neither pack carries the term, the turn is the
412
+ same honest miss it always was, byte for byte. An unknown word, a parse
413
+ failure, or an ambiguous reading never consults a pack at all. The gate and
414
+ both fallbacks are pinned by `test/corpus/reference.jsonl` and the
415
+ chat-lane tests beside it, and the home page demos the article path live.
416
+
372
417
  ## How it remembers
373
418
 
374
419
  tmct's memory has two layers, both fed by every parsed request and response and
@@ -922,7 +967,7 @@ The remaining tools are **cold**: still served, but not billed to an agent every
922
967
  | --- | --- | --- |
923
968
  | `tmct_describe` | Locate one symbol and list its typed edges (both directions) with provenance. | `symbol` (required) |
924
969
  | `tmct_signature` | One symbol's API surface (params, returns, raises/catches, flags, decorators, doc) without the body. | `symbol` (required) |
925
- | `tmct_impact` | Transitive reverse closure over imports/calls — what breaks if a module changes, by depth, with tests. | `module` (required) |
970
+ | `tmct_impact` | Transitive reverse closure over imports/calls — what breaks if a module or symbol changes, by depth, with tests. | `module` (required) |
926
971
  | `tmct_search` | Free-text/ranked lookup over the code-map to find the right module or symbol. | `query`, `kind`, `decorator`, `name` |
927
972
  | `tmct_members` | A class's methods + attributes (file:line, decorators) in one slice. | `class` (required) |
928
973
  | `tmct_subclasses` | A class's base classes plus the transitive set of classes that extend it. | `class` (required) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.7.0",
3
+ "version": "2.7.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
@@ -466,12 +466,20 @@ export const PLACEHOLDER_NOUNS = Object.freeze([
466
466
 
467
467
  /** Boolean connectives over same-subject clauses -> a set operation on result ids.
468
468
  * "and" = intersection, "or" = union, "but not"/"and not"/"without"/"except" =
469
- * difference. Multi-word keys are matched longest-first by ask.mjs so "but not"
470
- * wins over a bare "not". Left-associative in ask.mjs's fold. */
469
+ * difference. The do-support negations ("but do not import Y", the expanded
470
+ * form every "don't" reaches after the contraction pass) are difference too —
471
+ * the auxiliary is part of the connective, never of the branch. A bare "but"
472
+ * is contrastive coordination, which still intersects ("inheriting from X but
473
+ * untested" = both at once). Multi-word keys are matched longest-first by
474
+ * ask.mjs so "but do not" wins over "but not" wins over a bare "but".
475
+ * Left-associative in ask.mjs's fold. */
471
476
  export const BOOLEAN_CONNECTIVES = Object.freeze({
477
+ "but do not": "difference", "but does not": "difference",
478
+ "and do not": "difference", "and does not": "difference",
472
479
  "but not": "difference", "and not": "difference", "except": "difference",
473
480
  "without": "difference",
474
481
  "and": "intersection", "plus": "intersection",
482
+ "but": "intersection",
475
483
  "or": "union",
476
484
  });
477
485
 
@@ -437,6 +437,12 @@ function parseNested(w, lc, nlp, depth) {
437
437
  if (!noun) continue; // marker not preceded by a noun
438
438
  const head = w.slice(0, r - 1); // outer clause words, minus the placeholder noun
439
439
  if (!head.length) continue; // noun is the leading subject → subject-relative, not this shape
440
+ // A head made ENTIRELY of qualifier adjectives ("tested modules importing
441
+ // X") is an adjective stack over the subject, not an outer clause —
442
+ // "tested" doubles as a relation verb, so parseSimpleClause would read it
443
+ // as reverse(tests) over the inner set and answer the test module itself.
444
+ // parseRelationalOrQualified owns the adjective reading.
445
+ if (head.every((x) => QUALIFIERS[x.toLowerCase()])) continue;
440
446
  const outer = parseSimpleClause([...head, NEST_SENTINEL].join(" "), nlp);
441
447
  if (!outer || (outer.shape !== "reverse" && outer.shape !== "forward")) continue;
442
448
  if (outer.modifier && outer.modifier !== "direct") continue; // no transitive-over-set closure primitive
@@ -8,7 +8,7 @@
8
8
  import {
9
9
  CONTRACTIONS, MISSPELLINGS, WRONG_WORDS, G_DROP, FILLER_WORDS,
10
10
  NEGATION_FRAMES, COMMIT_CONTENT_FRAMES, VERB_TO_KIND, ENTITY_TO_TYPE,
11
- TRAILING_SCOPE_FILLER, TRAILING_TEMPORAL_ADVERBS,
11
+ TRAILING_SCOPE_FILLER, TRAILING_TEMPORAL_ADVERBS, stripTrailingDiscourseTag,
12
12
  } from "../ask-vocab.mjs";
13
13
 
14
14
  export function escapeRegex(s) {
@@ -251,18 +251,29 @@ export function applyPreambleFrames(text) {
251
251
  return q;
252
252
  }
253
253
 
254
- /** Strippable leading framing clause: "since/although/though/while/because/
255
- * whereas/given that/now that <clause>, <Q>" -> "<Q>". Comma-anchored and
256
- * non-empty-remainder-required, same discipline as GREETING_PREAMBLE_RE. */
254
+ /** Strippable leading framing clause: "since/[even] though/although/while/
255
+ * because/[even] if/whereas/given that/now that <clause>, <Q>" -> "<Q>".
256
+ * Comma-anchored and non-empty-remainder-required, same discipline as
257
+ * GREETING_PREAMBLE_RE. */
257
258
  const SUBORDINATION_FRAMES_RE =
258
- /^(?:since|although|though|while|because|whereas|given\s+that|now\s+that)\s+.+?,\s*(.+)$/i;
259
+ /^(?:since|although|(?:even\s+)?though|while|because|whereas|given\s+that|now\s+that)\s+.+?,\s*(.+)$/i;
260
+
261
+ /** The same framing clause TRAILING the question ("<Q>, because the sprint
262
+ * just kicked off") — comma-anchored on the same subordinator set, so the
263
+ * clause never reads as part of the object term. A bare "while"/"since"
264
+ * mid-question without the comma is untouched (those can be content). */
265
+ const TRAILING_SUBORDINATION_RE =
266
+ /^(.+?),\s*(?:since|although|(?:even\s+)?though|while|because|whereas|given\s+that|now\s+that)\s+.+$/i;
259
267
 
260
268
  export function applySubordinationFrames(text) {
261
269
  let q = String(text || "");
262
270
  for (let pass = 0; pass < 3; pass++) {
263
- const m = q.match(SUBORDINATION_FRAMES_RE);
264
- if (!m) break;
265
- q = m[1].trim();
271
+ const before = q;
272
+ let m = q.match(SUBORDINATION_FRAMES_RE);
273
+ if (m) q = m[1].trim();
274
+ m = q.match(TRAILING_SUBORDINATION_RE);
275
+ if (m) q = m[1].trim();
276
+ if (q === before) break;
266
277
  }
267
278
  return q;
268
279
  }
@@ -390,6 +401,14 @@ export function normalizeQuery(text) {
390
401
  q = applySubordinationFrames(q);
391
402
  q = applyConditionalFrames(q);
392
403
  q = stripFillerWords(q);
404
+ // A trailing bare discourse tag ("which modules import a.mjs THEN") is
405
+ // conversational glue, not part of the object term — the same curated call
406
+ // ask-vocab's stripTrailingDiscourseTag already makes for the meta-whatis
407
+ // object. Stripped here, in the shared pre-pass and AFTER the filler strip
408
+ // (the noise wrappers that leave the tag behind sit at the other end), so
409
+ // both parse strategies see one string and the residue guard never has to
410
+ // refuse over a word that carried no content.
411
+ q = stripTrailingDiscourseTag(q);
393
412
  // emphatic trailing punctuation (item 10): a run of terminal "?" collapses to
394
413
  // one — the anchored templates consume exactly one optional trailing "?", so
395
414
  // "…walk.mjs??" otherwise leaks a stray "?" into the captured object term (the
@@ -485,6 +504,11 @@ const PHRASING_FRAMES = Object.freeze([
485
504
  { re: /^what\s+does\s+(.+?)\s+changes?\s+together\s+with\??$/i, to: (m) => `what co-changes with ${m[1]}` },
486
505
  { re: /^what\s+changes?\s+together\s+with\s+(.+?)\??$/i, to: (m) => `what co-changes with ${m[1]}` },
487
506
 
507
+ // COMMIT-COUNT PASSIVE ("how many commits are recorded for X") → the touch
508
+ // phrasing the count restrictor already compiles. "recorded" is no relation
509
+ // verb, so the passive otherwise dies in the restrictor parse.
510
+ { re: /^how\s+many\s+commits\s+(?:are|were)\s+(?:recorded|logged)\s+(?:for|against)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `how many commits touched ${m[1]}` },
511
+
488
512
  // AUTHORSHIP → "who touched X" (tmct's touch edge IS the authorship signal).
489
513
  // A commit sha object is excluded — that dumps the commit's touch-set, not its author.
490
514
  { re: /^who\s+(?:wrote|authored)\s+(?:the\s+)?(?!(?:commit\s+)?[0-9a-f]{7,40}\??$)(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
@@ -35,6 +35,11 @@ function parseChatTagRest(rest) {
35
35
  * same 0.7 prior, same shared Source as the bulk conceptnet import; the
36
36
  * <term> segment records which miss pulled the fact in and is not part of
37
37
  * the Source identity)
38
+ * world:<name>[:turnN] -> { kind:"corpus", name:<name> }
39
+ * (a loaded world's facts are first-party authored shipped content — the
40
+ * same tier the hand-written tier2 corpus already scores at; the :turnN
41
+ * segment a snapshot write carries records when, not who, and is not
42
+ * part of the Source identity)
38
43
  * ace:chat:<session>@<ts> -> { kind:"operator", createdAt:<ts>, sessionId:<session> }
39
44
  * teach:chat:<session>@<ts> -> { kind:"teach", createdAt:<ts>, sessionId:<session> }
40
45
  * web:<url> | url:<url> -> { kind:"web", url:<url> }
@@ -62,6 +67,10 @@ export function provenanceTagToSource(tag) {
62
67
  // child:<pack>:<term> — the lazy child triples pack, scored at the corpus tier
63
68
  // under the pack's shared Source; the per-term tail is dropped from the id.
64
69
  if (head.startsWith("child:")) return { kind: "corpus", name: head.slice("child:".length).split(":")[0] || "unknown" };
70
+ // world:<name>[:turnN] — a loaded world's facts and snapshots, first-party
71
+ // authored shipped content scored at the corpus tier; the per-turn tail is
72
+ // dropped from the id so every write of one world corroborates one Source.
73
+ if (head.startsWith("world:")) return { kind: "corpus", name: head.slice("world:".length).split(":")[0] || "unknown" };
65
74
  if (head.startsWith("ace:")) return { kind: "operator", ...parseChatTagRest(head.slice("ace:".length)) };
66
75
  if (head.startsWith("teach:")) {
67
76
  // the chat teach lane's natural frames — chat.mjs's teachProvenanceTag
@@ -13,7 +13,7 @@ import { parseImperative } from "../domain/grammar/ace.mjs";
13
13
  import { createCompletionsGraphAdapter } from "../domain/completions/graph-adapter.mjs";
14
14
  import { actionFamilies } from "../domain/router/taught.mjs";
15
15
  import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
16
- import { appendFacts, appendRule, loadMemory, readFactRows, readRuleRows } from "../adapters/memory/core.mjs";
16
+ import { appendFacts, appendRule, loadMemory, normFactTerm, readFactRows, readRuleRows } from "../adapters/memory/core.mjs";
17
17
  import { COMPLETIONS_STORE, generateCompletion } from "./completions.mjs";
18
18
 
19
19
  // ---- recognizers: the closed opening/stop set --------------------------------
@@ -579,6 +579,53 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
579
579
  );
580
580
  }
581
581
 
582
+ // A mid-game "where is X" aside. Optional trailing "now": the question means
583
+ // the same with or without it, and the fold IS the now.
584
+ const WORLD_WHERE_RE = /^where(?:'s|\s+is|\s+are)\s+(?:the\s+|a\s+|an\s+)?(.+?)(?:\s+now)?[?.!\s]*$/i;
585
+
586
+ /** A locative aside about a placed world thing, answered from the SAME @turnN
587
+ * fold every other world reader uses — never the raw base rows, whose
588
+ * superseded placements would answer where things stood at load time. Null
589
+ * when the asked thing has no placement in the world, so an ordinary
590
+ * locative question (a code symbol, a taught board piece) keeps its lane. A
591
+ * hidden thing is declined without naming its hiding place. */
592
+ async function worldWhereAnswer(line, { memoryDir }) {
593
+ const m = String(line).match(WORLD_WHERE_RE);
594
+ if (!m) return null;
595
+ const thing = normFactTerm(m[1]);
596
+ let rows;
597
+ try { rows = readFactRows(await loadMemory(memoryDir)); } catch { return null; }
598
+ const state = foldWorldState(rows);
599
+ const place = state.placements.get(thing);
600
+ if (!place) return null;
601
+ if (place.predicate === "mgx:hidden-in") {
602
+ return answer(
603
+ `nothing you've seen says where the ${thing} is.`,
604
+ `ADVENTURE — where-aside: ${thing} is hidden; declined without naming the hiding place`,
605
+ { miss: true, goal: `locate the ${thing}` },
606
+ );
607
+ }
608
+ if (thing === "player") {
609
+ return answer(
610
+ `you are in the ${place.object}.`,
611
+ "ADVENTURE — where-aside: the player's own room, from the current world fold",
612
+ { goal: "check where you are" },
613
+ );
614
+ }
615
+ if (place.object === "player") {
616
+ return answer(
617
+ `you are carrying the ${thing}.`,
618
+ `ADVENTURE — where-aside: ${thing} is carried, from the current world fold`,
619
+ { goal: `locate the ${thing}` },
620
+ );
621
+ }
622
+ return answer(
623
+ `the ${thing} is in the ${place.object}.`,
624
+ `ADVENTURE — where-aside: ${thing}'s current placement from the world fold (as of turn ${place.turn})`,
625
+ { goal: `locate the ${thing}` },
626
+ );
627
+ }
628
+
582
629
  async function inventoryAnswer({ memoryDir, graph }) {
583
630
  const memory = await loadMemory(memoryDir);
584
631
  const rows = readFactRows(memory);
@@ -665,5 +712,7 @@ export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "
665
712
  if (INVENTORY_RE.test(line)) return inventoryAnswer({ memoryDir, graph });
666
713
  const cmd = parseImperative(line, lexicon ?? undefined);
667
714
  if (cmd) return runWorldCommand(cmd, { world: adventure.world, memoryDir, env, graph, cache });
715
+ const whereAside = await worldWhereAnswer(line, { memoryDir });
716
+ if (whereAside) return whereAside;
668
717
  return null; // a mid-game aside — the ordinary lanes answer, world untouched
669
718
  }
@@ -48,6 +48,8 @@ import { loadLexicon, lookupNoun } from "../domain/grammar/lexicon.mjs";
48
48
  import { pickPhrase } from "../domain/answer-variants.mjs";
49
49
  import { REFERENCE_PACK_NAME, cleanMissReferenceTerm, renderReferenceAnswer, referenceProvenanceTag } from "../domain/reference-pack.mjs";
50
50
  import { getReferencePackProvider } from "../adapters/corpus/reference-pack.mjs";
51
+ import { CHILD_PACK_NAME, childProvenanceTag } from "../domain/child-pack.mjs";
52
+ import { getChildPackProvider } from "../adapters/corpus/child-pack.mjs";
51
53
  import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
52
54
  import { relatedForTerm } from "../domain/skos-view.mjs";
53
55
  import { adventureTurn } from "./adventure.mjs";
@@ -2794,6 +2796,16 @@ const GENERAL_VERB_DETERMINER_TEACH_RE = new RegExp(
2794
2796
  * stored, and cited, as "len"). */
2795
2797
  const QUANTIFIED_HAS_TEACH_RE = /^(every|each|all)\s+([\w'-]+)\s+(?:has|have)\s+(.+?)[.!?]*$/i;
2796
2798
  const quantifiedHasSubject = (m) => (/^all$/i.test(m[1]) ? singularizeSurface(m[2]) : m[2]);
2799
+ /** The OBJECT side of the same fold: "all dogs have tails" states one tail
2800
+ * per dog, so the plural sentence form's object stores as its singular
2801
+ * ("tail" — the spelling the have-questions and the seeded corpus's own hasA
2802
+ * facts read back). Only the "all" form folds, the same gate the subject
2803
+ * uses: "every"/"each" take singular grammar, so their object's number is
2804
+ * the speaker's own ("every dog has fur"). The last word folds so a
2805
+ * modified object ("all dogs have long tails") keeps its modifier. */
2806
+ const quantifiedHasObject = (m) => (/^all$/i.test(m[1])
2807
+ ? m[3].replace(/[\w'-]+$/, (w) => singularizeSurface(w))
2808
+ : m[3]);
2797
2809
  /** Verbs owned by an earlier, more specific recognizer in this lane — is/are
2798
2810
  * (class-membership/property, above) and owns/maintains (ownership, above).
2799
2811
  * generalVerbTeach declines outright on these so it can never race a more
@@ -2942,7 +2954,7 @@ async function generalVerbTeach(payload) {
2942
2954
  if (quantHas) {
2943
2955
  subjectRaw = quantifiedHasSubject(quantHas);
2944
2956
  verbRaw = "has";
2945
- objectRaw = quantHas[3];
2957
+ objectRaw = quantifiedHasObject(quantHas);
2946
2958
  } else {
2947
2959
  const det = p.match(GENERAL_VERB_DETERMINER_TEACH_RE);
2948
2960
  if (!det) return null; // not a bare-name subject, and no preposition to pin the verb
@@ -9045,14 +9057,15 @@ async function curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon })
9045
9057
  return { text: `${def} (source: corpus/seon)`, term };
9046
9058
  }
9047
9059
 
9048
- // ---- learn-on-miss: the shipped reference pack behind the cleanest miss ----
9060
+ // ---- learn-on-miss: the shipped child + reference packs behind the cleanest miss ----
9049
9061
 
9050
- /** The learn-on-miss gate, shared by the articled miss hook and the bare-form
9051
- * fallback so the two can never disagree. Fires only on the CLEANEST miss: a
9052
- * definition-shaped term the lexicon knows, resolving to no graph entity and
9053
- * no remembered fact — then, and only then, the pack provider is consulted.
9054
- * Null means the turn proceeds byte-identically to a pack-less run. */
9055
- async function referencePackMissAnswer(term, { graph, memoryDir, lexicon, env, cache }) {
9062
+ /** The learn-on-miss gate, shared by the child-pack hook, the articled
9063
+ * reference hook and the bare-form fallback so the three can never disagree.
9064
+ * Passes only on the CLEANEST miss: a definition-shaped term the lexicon
9065
+ * knows, resolving to no graph entity and no remembered fact — then, and
9066
+ * only then, may a pack provider be consulted. Null means the turn proceeds
9067
+ * byte-identically to a pack-less run. */
9068
+ async function cleanMissPackKey(term, { graph, memoryDir, lexicon, cache }) {
9056
9069
  if (!term || !memoryDir) return null;
9057
9070
  let key = null;
9058
9071
  try { key = cleanMissReferenceTerm(term, lexicon ?? undefined); } catch { key = null; }
@@ -9064,12 +9077,43 @@ async function referencePackMissAnswer(term, { graph, memoryDir, lexicon, env, c
9064
9077
  variants.add(key);
9065
9078
  const rows = await factRows(memoryDir, cache);
9066
9079
  if (rows.some((f) => variants.has(f.subject) || variants.has(f.object))) return null;
9080
+ return key;
9081
+ }
9082
+
9083
+ /** The reference-pack lookup for an already-gated key: the article, or null
9084
+ * (absent pack, missing term, any read failure — all byte-identical). */
9085
+ async function referencePackAnswerForKey(key, env) {
9067
9086
  let article = null;
9068
9087
  try { article = await getReferencePackProvider(env).lookup(key); } catch { article = null; }
9069
9088
  if (!article) return null;
9070
9089
  return { key, article, text: renderReferenceAnswer(key, article) };
9071
9090
  }
9072
9091
 
9092
+ /** The gate + the reference lookup in one call, for a caller that has a term
9093
+ * rather than a gated key. */
9094
+ async function referencePackMissAnswer(term, { graph, memoryDir, lexicon, env, cache }) {
9095
+ const key = await cleanMissPackKey(term, { graph, memoryDir, lexicon, cache });
9096
+ return key ? referencePackAnswerForKey(key, env) : null;
9097
+ }
9098
+
9099
+ /** The child-pack half of learn-on-miss, for an already-gated key: look the
9100
+ * key up in the shipped child triples pack and append every fact under child
9101
+ * provenance, so the SAME question can be re-asked from the store. Null on a
9102
+ * pack miss or any failure — the turn then proceeds byte-identically. */
9103
+ async function childPackFactsForKey(key, { memoryDir, env, cache }) {
9104
+ let row = null;
9105
+ try { row = await getChildPackProvider(env).lookup(key); } catch { row = null; }
9106
+ if (!row?.facts?.length) return null;
9107
+ try {
9108
+ const { appendFacts } = await import("../adapters/memory/core.mjs");
9109
+ await appendFacts(memoryDir, row.facts.map(({ subject, predicate, object }) => ({
9110
+ subject, predicate, object, provenance: childProvenanceTag(key),
9111
+ })));
9112
+ } catch { return null; }
9113
+ if (cache) cache.rows = null;
9114
+ return { key, count: row.facts.length };
9115
+ }
9116
+
9073
9117
  /** Store the article's first-sentence isa as a subClassOf fact carrying
9074
9118
  * reference provenance — AFTER the cited answer composed, and failure-
9075
9119
  * tolerated: the answer stands whether or not the fact lands. */
@@ -10492,9 +10536,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
10492
10536
  // actually stores it, the ORIGINAL ask-engine answer is restored unchanged
10493
10537
  // (see "COLLISION RESTORE" below).
10494
10538
  const trimmedQuery = String(query).trim();
10495
- const relaxedTeachCollision = !!(envelope?.relaxed?.dropped?.length) && !askMiss && memoryDir
10539
+ // Boolean() and not a bare && chain: with no memoryDir the chain would
10540
+ // short-circuit to null, and `miss` below (askMiss || THIS) would record a
10541
+ // null miss flag on every relaxation-rescued turn of a memory-less session.
10542
+ const relaxedTeachCollision = Boolean(!!(envelope?.relaxed?.dropped?.length) && !askMiss && memoryDir
10496
10543
  && !QUESTION_LEAD_RE.test(trimmedQuery) && !/\?\s*$/.test(trimmedQuery)
10497
- && DECLARATIVE_KIND_OF_RE.test(trimmedQuery);
10544
+ && DECLARATIVE_KIND_OF_RE.test(trimmedQuery));
10498
10545
  const preCollisionAnswer = relaxedTeachCollision ? answer : null;
10499
10546
  const miss = askMiss || relaxedTeachCollision;
10500
10547
  // Answer provenance (W1): "composed" is the ask engine's productive band; the
@@ -10789,13 +10836,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
10789
10836
  const def = await curatedDefinitionAnswer(factQuery, envelope, { memoryDir, lexicon });
10790
10837
  if (def) bareMetaHit = { text: def.text, replace: true };
10791
10838
  }
10792
- // The reference pack's bare-form fallback, beside the curated one and
10793
- // under the IDENTICAL clean-miss gate the articled hook (4h) applies —
10794
- // "what is otter" reaches the pack exactly as "what is an otter" does.
10839
+ // The learn-on-miss packs' bare-form fallback, beside the curated one
10840
+ // and under the IDENTICAL clean-miss gate the articled hook (4h)
10841
+ // applies — "what is otter" reaches the packs exactly as "what is an
10842
+ // otter" does, child triples first, article prose second.
10795
10843
  if (!bareMetaHit) {
10796
10844
  const refTerm = metaTermOf(factQuery, envelope);
10797
- const ref = refTerm ? await referencePackMissAnswer(refTerm, { graph, memoryDir, lexicon, env, cache }) : null;
10798
- if (ref) bareMetaHit = { text: ref.text, replace: true, reference: ref };
10845
+ const key = refTerm ? await cleanMissPackKey(refTerm, { graph, memoryDir, lexicon, cache }) : null;
10846
+ const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache }) : null;
10847
+ if (learned) {
10848
+ const fact = (await factAnswer(memoryDir, factQuery, envelope, miss, biasByBundle, cache, newFocus?.label))
10849
+ ?? (await factReadBack(memoryDir, factQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
10850
+ if (fact && !fact.miss) bareMetaHit = { text: fact.text, replace: true, child: learned };
10851
+ }
10852
+ if (!bareMetaHit && key) {
10853
+ const ref = await referencePackAnswerForKey(key, env);
10854
+ if (ref) bareMetaHit = { text: ref.text, replace: true, reference: ref };
10855
+ }
10799
10856
  }
10800
10857
  }
10801
10858
  // A bare "what is X" naming a REAL code-graph entity (not a taught fact,
@@ -10851,8 +10908,13 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
10851
10908
  if (!bareMetaHit.miss) { via = "fact"; recordMiss = false; }
10852
10909
  handled = true;
10853
10910
  if (bareMetaHit.pending) factPending = bareMetaHit.pending;
10854
- note(trace, "lane: (2b) BARE META FACT — \"what is X\" (no article) / \"is X <adjective>\" resolved to a remembered fact before the conversational catch-all could claim it");
10855
- note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
10911
+ if (bareMetaHit.child) {
10912
+ note(trace, "lane: (2b) CHILD PACK — a bare \"what is X\" clean miss pulled the term's triples from the shipped child pack into memory, and the question was re-answered from the store");
10913
+ note(trace, `source: child pack ${CHILD_PACK_NAME} — ${bareMetaHit.child.count} fact(s) appended as ${childProvenanceTag(bareMetaHit.child.key)}, answer served from .tmct/memory Facts`);
10914
+ } else {
10915
+ note(trace, "lane: (2b) BARE META FACT — \"what is X\" (no article) / \"is X <adjective>\" resolved to a remembered fact before the conversational catch-all could claim it");
10916
+ note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
10917
+ }
10856
10918
  } else if (isConversationalCandidate && habitualGroundingHint) {
10857
10919
  // A bare habitual teach ("penguins swim") naming a subject grounded
10858
10920
  // nowhere: an honest, actionable grounding hint beats the orientation
@@ -11212,22 +11274,40 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
11212
11274
  note(trace, `lane: (4g) FUZZY-VERB DECLINE — "${from}" only became a verb through the edit-distance repair tier ("${to}"), and the two words are different verbs, so the repaired sentence's graph answer is dropped rather than shown as an answer to what was typed`);
11213
11275
  }
11214
11276
  }
11215
- // (4h) REFERENCE PACK — the cleanest miss consults the shipped reference
11216
- // pack: a definition-shaped term the lexicon knows, no graph entity, no
11217
- // remembered fact. A hit answers with the article's summary, always cited;
11218
- // a null from any gate leaves the turn byte-identical. After the answer
11219
- // composes, the article's first-sentence isa is stored as a subClassOf fact
11220
- // with reference provenance, so the NEXT ask answers from memory.
11277
+ // (4h) LEARN-ON-MISS PACKS — the cleanest miss consults the shipped packs:
11278
+ // a definition-shaped term the lexicon knows, no graph entity, no
11279
+ // remembered fact. The CHILD triples pack goes first (facts before prose):
11280
+ // a hit appends the term's triples under child provenance and the SAME
11281
+ // question is re-asked from the store, so the answer is an ordinary cited
11282
+ // fact answer. Only when the store still cannot answer does the reference
11283
+ // pack's article speak, cited as before. Both packs missing leaves the
11284
+ // honest miss byte-identical.
11221
11285
  if (miss && recordMiss && via === "composed" && memoryDir) {
11222
11286
  const refTerm = metaTermOf(query, envelope);
11223
- const ref = refTerm ? await referencePackMissAnswer(refTerm, { graph, memoryDir, lexicon, env, cache }) : null;
11224
- if (ref) {
11225
- answer = ref.text;
11226
- via = "reference";
11227
- recordMiss = false;
11228
- note(trace, "lane: (4h) REFERENCE PACK — a clean miss on a lexicon term answered from the shipped reference pack, cited");
11229
- note(trace, `source: reference pack ${REFERENCE_PACK_NAME} article "${ref.article.title}" (revid ${ref.article.revid})`);
11230
- await appendReferenceIsaFact(memoryDir, ref.key, ref.article, cache);
11287
+ const key = refTerm ? await cleanMissPackKey(refTerm, { graph, memoryDir, lexicon, cache }) : null;
11288
+ const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache }) : null;
11289
+ if (learned) {
11290
+ const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle, cache, newFocus?.label))
11291
+ ?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
11292
+ if (fact && !fact.miss) {
11293
+ answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
11294
+ via = "fact";
11295
+ recordMiss = false;
11296
+ if (fact.pending) factPending = fact.pending;
11297
+ note(trace, "lane: (4h) CHILD PACK — a clean miss on a lexicon term pulled the term's triples from the shipped child pack into memory, and the question was re-answered from the store");
11298
+ note(trace, `source: child pack ${CHILD_PACK_NAME} — ${learned.count} fact(s) appended as ${childProvenanceTag(key)}, answer served from .tmct/memory Facts`);
11299
+ }
11300
+ }
11301
+ if (miss && recordMiss && via === "composed" && key) {
11302
+ const ref = await referencePackAnswerForKey(key, env);
11303
+ if (ref) {
11304
+ answer = ref.text;
11305
+ via = "reference";
11306
+ recordMiss = false;
11307
+ note(trace, "lane: (4h) REFERENCE PACK — a clean miss on a lexicon term answered from the shipped reference pack, cited");
11308
+ note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${ref.article.title}" (revid ${ref.article.revid})`);
11309
+ await appendReferenceIsaFact(memoryDir, ref.key, ref.article, cache);
11310
+ }
11231
11311
  }
11232
11312
  }
11233
11313
  // (5) #1 SHORT TAILORED MISS — replace ONLY the engine's full grammar cheat-sheet
@@ -12532,7 +12612,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
12532
12612
  if (advTurn) {
12533
12613
  note(trace, `lane: ${advTurn.note}`);
12534
12614
  if (advTurn.goal) note(trace, `goal: ${advTurn.goal}`);
12535
- const result = plainTurn(workingLine, advTurn.text, { via: "game", focus });
12615
+ const result = plainTurn(workingLine, advTurn.text, { via: "game", miss: !!advTurn.miss, focus });
12536
12616
  if (advTurn.goal) result.goal = advTurn.goal;
12537
12617
  result.lane = advTurn.lane;
12538
12618
  const rec = withLast(result, advTurn.goal ?? "play the adventure");
@@ -673,12 +673,17 @@
673
673
  "symbols"
674
674
  ]);
675
675
  BOOLEAN_CONNECTIVES = Object.freeze({
676
+ "but do not": "difference",
677
+ "but does not": "difference",
678
+ "and do not": "difference",
679
+ "and does not": "difference",
676
680
  "but not": "difference",
677
681
  "and not": "difference",
678
682
  "except": "difference",
679
683
  "without": "difference",
680
684
  "and": "intersection",
681
685
  "plus": "intersection",
686
+ "but": "intersection",
682
687
  "or": "union"
683
688
  });
684
689
  QUALIFIERS = Object.freeze({
@@ -985,6 +990,7 @@
985
990
  if (head.startsWith("corpus-weak:")) return { kind: "corpusWeak", name: head.slice("corpus-weak:".length) || "unknown" };
986
991
  if (head.startsWith("corpus:")) return { kind: "corpus", name: head.slice("corpus:".length) || "unknown" };
987
992
  if (head.startsWith("child:")) return { kind: "corpus", name: head.slice("child:".length).split(":")[0] || "unknown" };
993
+ if (head.startsWith("world:")) return { kind: "corpus", name: head.slice("world:".length).split(":")[0] || "unknown" };
988
994
  if (head.startsWith("ace:")) return { kind: "operator", ...parseChatTagRest(head.slice("ace:".length)) };
989
995
  if (head.startsWith("teach:")) {
990
996
  return { kind: "teach", ...parseChatTagRest(head.slice("teach:".length)) };
@@ -2752,9 +2758,12 @@ ${shown.join("\n")}${tail}`;
2752
2758
  function applySubordinationFrames(text) {
2753
2759
  let q = String(text || "");
2754
2760
  for (let pass = 0; pass < 3; pass++) {
2755
- const m = q.match(SUBORDINATION_FRAMES_RE);
2756
- if (!m) break;
2757
- q = m[1].trim();
2761
+ const before = q;
2762
+ let m = q.match(SUBORDINATION_FRAMES_RE);
2763
+ if (m) q = m[1].trim();
2764
+ m = q.match(TRAILING_SUBORDINATION_RE);
2765
+ if (m) q = m[1].trim();
2766
+ if (q === before) break;
2758
2767
  }
2759
2768
  return q;
2760
2769
  }
@@ -2799,6 +2808,7 @@ ${shown.join("\n")}${tail}`;
2799
2808
  q = applySubordinationFrames(q);
2800
2809
  q = applyConditionalFrames(q);
2801
2810
  q = stripFillerWords(q);
2811
+ q = stripTrailingDiscourseTag(q);
2802
2812
  q = q.replace(/\?{2,}\s*$/, "?");
2803
2813
  return q.replace(/\s+/g, " ").trim();
2804
2814
  }
@@ -2824,7 +2834,7 @@ ${shown.join("\n")}${tail}`;
2824
2834
  if (!predicate) return null;
2825
2835
  return { entWord, predicate };
2826
2836
  }
2827
- var tableRe, CONTRACTION_RE, correctionRe, MISSPELLING_RE, WRONG_WORD_RE, W_SLASH_RE, FOR_DIGIT_THANKS_RE, FOR_DIGIT_EXAMPLE_RE, KIND_NOUN_ANAPHORA_RE, VERB_ALTERNATION, FILLER_RE, RELATION_VERB_RE, INTERROGATIVE_LEAD_RE, LISTING_TAIL_KINDS, BARE_KIND_RE, isListingRemainder, GREETING_PREAMBLE_RE, THANKS_PREAMBLE_RE, ACK_PREAMBLE_RE, BROWSING_PREAMBLE_RE, HEDGE_ADVERB_PREAMBLE_RE, TROUBLE_ASIDE_RE, MODAL_WRAPPER_RE, EXPLAIN_WRAPPER_RE, TELL_ME_WRAPPER_RE, KNOW_WRAPPER_RE, WANT_KNOW_WRAPPER_RE, WONDERING_WRAPPER_RE, EMBEDDED_WHATIS_RE, EMBEDDED_MEANS_RE, SHOW_GIVE_ME_RE, LEADING_CONNECTIVE_RE, QUESTION_AUX_LEAD_RE, TOPIC_SWITCH_PREAMBLE_RE, SUBORDINATION_FRAMES_RE, SELF_CORRECTION_RE, CONDITIONAL_VERB_GERUND, CONDITIONAL_KIND_PLURAL, CONDITIONAL_QUALIFIER_SRC, CONDITIONAL_QUALIFIER_RE, COUNTERFACTUAL_RE, WHERE_TRAILING_TEMPORAL_RE, PHRASING_FRAMES, NEGATION_SET_RE, STOPWORDS2, splitWords, wordsOf;
2837
+ var tableRe, CONTRACTION_RE, correctionRe, MISSPELLING_RE, WRONG_WORD_RE, W_SLASH_RE, FOR_DIGIT_THANKS_RE, FOR_DIGIT_EXAMPLE_RE, KIND_NOUN_ANAPHORA_RE, VERB_ALTERNATION, FILLER_RE, RELATION_VERB_RE, INTERROGATIVE_LEAD_RE, LISTING_TAIL_KINDS, BARE_KIND_RE, isListingRemainder, GREETING_PREAMBLE_RE, THANKS_PREAMBLE_RE, ACK_PREAMBLE_RE, BROWSING_PREAMBLE_RE, HEDGE_ADVERB_PREAMBLE_RE, TROUBLE_ASIDE_RE, MODAL_WRAPPER_RE, EXPLAIN_WRAPPER_RE, TELL_ME_WRAPPER_RE, KNOW_WRAPPER_RE, WANT_KNOW_WRAPPER_RE, WONDERING_WRAPPER_RE, EMBEDDED_WHATIS_RE, EMBEDDED_MEANS_RE, SHOW_GIVE_ME_RE, LEADING_CONNECTIVE_RE, QUESTION_AUX_LEAD_RE, TOPIC_SWITCH_PREAMBLE_RE, SUBORDINATION_FRAMES_RE, TRAILING_SUBORDINATION_RE, SELF_CORRECTION_RE, CONDITIONAL_VERB_GERUND, CONDITIONAL_KIND_PLURAL, CONDITIONAL_QUALIFIER_SRC, CONDITIONAL_QUALIFIER_RE, COUNTERFACTUAL_RE, WHERE_TRAILING_TEMPORAL_RE, PHRASING_FRAMES, NEGATION_SET_RE, STOPWORDS2, splitWords, wordsOf;
2828
2838
  var init_normalize = __esm({
2829
2839
  "src/domain/interpret/normalize.mjs"() {
2830
2840
  init_ask_vocab();
@@ -2893,7 +2903,8 @@ ${shown.join("\n")}${tail}`;
2893
2903
  LEADING_CONNECTIVE_RE = /^(?:and|also|so|then|now|but)\s+(.+)$/i;
2894
2904
  QUESTION_AUX_LEAD_RE = /^(?:does|do|did|is|are|was|were|has|have|had|can|could|will|would|should)\b/i;
2895
2905
  TOPIC_SWITCH_PREAMBLE_RE = /^(?:(?:actually|no\s+wait|wait|hold\s+on|never\s+mind|scratch\s+that|on\s+second\s+thought|i\s+mean(?:t)?)[\s,.]+)+(.+)$/i;
2896
- SUBORDINATION_FRAMES_RE = /^(?:since|although|though|while|because|whereas|given\s+that|now\s+that)\s+.+?,\s*(.+)$/i;
2906
+ SUBORDINATION_FRAMES_RE = /^(?:since|although|(?:even\s+)?though|while|because|whereas|given\s+that|now\s+that)\s+.+?,\s*(.+)$/i;
2907
+ TRAILING_SUBORDINATION_RE = /^(.+?),\s*(?:since|although|(?:even\s+)?though|while|because|whereas|given\s+that|now\s+that)\s+.+$/i;
2897
2908
  SELF_CORRECTION_RE = /^.+?(?:\s*(?:--|—|-)\s*)?\b(?:sorry|i\s+mean)\b\s*(?:--|—|-|,|:)\s*(.+)$/i;
2898
2909
  CONDITIONAL_VERB_GERUND = Object.freeze({
2899
2910
  imports: "importing",
@@ -2992,6 +3003,10 @@ ${shown.join("\n")}${tail}`;
2992
3003
  // CO-CHANGE → "what co-changes with X" (the plainest phrasing a developer types).
2993
3004
  { re: /^what\s+does\s+(.+?)\s+changes?\s+together\s+with\??$/i, to: (m) => `what co-changes with ${m[1]}` },
2994
3005
  { re: /^what\s+changes?\s+together\s+with\s+(.+?)\??$/i, to: (m) => `what co-changes with ${m[1]}` },
3006
+ // COMMIT-COUNT PASSIVE ("how many commits are recorded for X") → the touch
3007
+ // phrasing the count restrictor already compiles. "recorded" is no relation
3008
+ // verb, so the passive otherwise dies in the restrictor parse.
3009
+ { re: /^how\s+many\s+commits\s+(?:are|were)\s+(?:recorded|logged)\s+(?:for|against)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `how many commits touched ${m[1]}` },
2995
3010
  // AUTHORSHIP → "who touched X" (tmct's touch edge IS the authorship signal).
2996
3011
  // A commit sha object is excluded — that dumps the commit's touch-set, not its author.
2997
3012
  { re: /^who\s+(?:wrote|authored)\s+(?:the\s+)?(?!(?:commit\s+)?[0-9a-f]{7,40}\??$)(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
@@ -3908,6 +3923,7 @@ ${shown.join("\n")}${tail}`;
3908
3923
  if (!noun) continue;
3909
3924
  const head = w.slice(0, r - 1);
3910
3925
  if (!head.length) continue;
3926
+ if (head.every((x) => QUALIFIERS[x.toLowerCase()])) continue;
3911
3927
  const outer = parseSimpleClause([...head, NEST_SENTINEL].join(" "), nlp);
3912
3928
  if (!outer || outer.shape !== "reverse" && outer.shape !== "forward") continue;
3913
3929
  if (outer.modifier && outer.modifier !== "direct") continue;
@@ -22013,7 +22029,7 @@ ${codeblock}`, options);
22013
22029
  });
22014
22030
 
22015
22031
  // src/adapters/corpus/conceptnet.mjs
22016
- var import_meta6, PKG_ROOT4, SLICE_FILE, MAP_FILE, SEON_CONCEPTS_FILE, SEON_DEFINITIONS_FILE, TIER2_DIR, TIER2_MANIFEST_FILE, WORDNET_DIR, WORDNET_MANIFEST_FILE;
22032
+ var import_meta7, PKG_ROOT5, SLICE_FILE, MAP_FILE, SEON_CONCEPTS_FILE, SEON_DEFINITIONS_FILE, TIER2_DIR, TIER2_MANIFEST_FILE, WORDNET_DIR, WORDNET_MANIFEST_FILE;
22017
22033
  var init_conceptnet = __esm({
22018
22034
  "src/adapters/corpus/conceptnet.mjs"() {
22019
22035
  init_node_fs();
@@ -22023,15 +22039,15 @@ ${codeblock}`, options);
22023
22039
  init_node_path();
22024
22040
  init_dist();
22025
22041
  init_core();
22026
- import_meta6 = {};
22027
- PKG_ROOT4 = join(dirname(fileURLToPath2(import_meta6.url)), "..", "..", "..");
22028
- SLICE_FILE = join(PKG_ROOT4, "corpus", "conceptnet", "slice.jsonl");
22029
- MAP_FILE = join(PKG_ROOT4, "src", "adapters", "corpus", "conceptnet-map.toml");
22030
- SEON_CONCEPTS_FILE = join(PKG_ROOT4, "corpus", "seon", "concepts.jsonl");
22031
- SEON_DEFINITIONS_FILE = join(PKG_ROOT4, "corpus", "seon", "definitions.jsonl");
22032
- TIER2_DIR = join(PKG_ROOT4, "corpus", "tier2");
22042
+ import_meta7 = {};
22043
+ PKG_ROOT5 = join(dirname(fileURLToPath2(import_meta7.url)), "..", "..", "..");
22044
+ SLICE_FILE = join(PKG_ROOT5, "corpus", "conceptnet", "slice.jsonl");
22045
+ MAP_FILE = join(PKG_ROOT5, "src", "adapters", "corpus", "conceptnet-map.toml");
22046
+ SEON_CONCEPTS_FILE = join(PKG_ROOT5, "corpus", "seon", "concepts.jsonl");
22047
+ SEON_DEFINITIONS_FILE = join(PKG_ROOT5, "corpus", "seon", "definitions.jsonl");
22048
+ TIER2_DIR = join(PKG_ROOT5, "corpus", "tier2");
22033
22049
  TIER2_MANIFEST_FILE = join(TIER2_DIR, "manifest.json");
22034
- WORDNET_DIR = join(PKG_ROOT4, "corpus", "wordnet");
22050
+ WORDNET_DIR = join(PKG_ROOT5, "corpus", "wordnet");
22035
22051
  WORDNET_MANIFEST_FILE = join(WORDNET_DIR, "manifest.json");
22036
22052
  }
22037
22053
  });
@@ -22125,7 +22141,7 @@ ${codeblock}`, options);
22125
22141
  }
22126
22142
  };
22127
22143
  }
22128
- var import_meta7, NAMENET_DIR, EXTENSION_KINDS, CONCEPTNET_PREFER, BUILTIN_EXTENSIONS;
22144
+ var import_meta8, NAMENET_DIR, EXTENSION_KINDS, CONCEPTNET_PREFER, BUILTIN_EXTENSIONS;
22129
22145
  var init_extensions = __esm({
22130
22146
  "src/services/extensions.mjs"() {
22131
22147
  init_node_path();
@@ -22133,8 +22149,8 @@ ${codeblock}`, options);
22133
22149
  init_node_url();
22134
22150
  init_toml_config();
22135
22151
  init_conceptnet();
22136
- import_meta7 = {};
22137
- NAMENET_DIR = join(dirname(fileURLToPath2(import_meta7.url)), "..", "..", "corpus", "namenet");
22152
+ import_meta8 = {};
22153
+ NAMENET_DIR = join(dirname(fileURLToPath2(import_meta8.url)), "..", "..", "corpus", "namenet");
22138
22154
  EXTENSION_KINDS = Object.freeze(["corpus", "lexicon", "templates", "pack", "ontology"]);
22139
22155
  CONCEPTNET_PREFER = ["rdfs:subClassOf", "rdf:type", "mgx:usedFor", "mgx:partOf", "mgx:capableOf"];
22140
22156
  BUILTIN_EXTENSIONS = Object.freeze(builtinExtensions());
@@ -22300,8 +22316,8 @@ ${codeblock}`, options);
22300
22316
  {
22301
22317
  name: "tmct_impact",
22302
22318
  tier: "cold",
22303
- summary: "Transitive reverse closure over imports/calls \u2014 what breaks if a module changes, by depth, with tests.",
22304
- inputSchema: moduleArg("The module whose dependents you want."),
22319
+ summary: "Transitive reverse closure over imports/calls \u2014 what breaks if a module or symbol changes, by depth, with tests.",
22320
+ inputSchema: moduleArg("The module (or a symbol it defines) whose dependents you want."),
22305
22321
  example: { module: "django/utils/text.py" }
22306
22322
  },
22307
22323
  {
@@ -23233,6 +23249,17 @@ ${JSON.stringify(envelope, null, 2)}`;
23233
23249
  var import_meta4 = {};
23234
23250
  var PKG_ROOT2 = join(dirname(fileURLToPath2(import_meta4.url)), "..", "..", "..");
23235
23251
 
23252
+ // src/domain/child-pack.mjs
23253
+ init_hash();
23254
+
23255
+ // src/adapters/corpus/child-pack.mjs
23256
+ init_node_fs();
23257
+ init_node_url();
23258
+ init_node_path();
23259
+ init_hash();
23260
+ var import_meta5 = {};
23261
+ var PKG_ROOT3 = join(dirname(fileURLToPath2(import_meta5.url)), "..", "..", "..");
23262
+
23236
23263
  // src/domain/dialogue-acts.mjs
23237
23264
  var DIALOGUE_ACT_DIMENSIONS = Object.freeze([
23238
23265
  "task",
@@ -23392,10 +23419,10 @@ ${JSON.stringify(envelope, null, 2)}`;
23392
23419
  init_node_fs();
23393
23420
  init_node_url();
23394
23421
  init_node_path();
23395
- var import_meta5 = {};
23396
- var PKG_ROOT3 = join(dirname(fileURLToPath2(import_meta5.url)), "..", "..", "..");
23422
+ var import_meta6 = {};
23423
+ var PKG_ROOT4 = join(dirname(fileURLToPath2(import_meta6.url)), "..", "..", "..");
23397
23424
  function worldsPackDir(env = process.env) {
23398
- return env?.TMCT_WORLDS_PACK_DIR || join(PKG_ROOT3, "corpus", "worlds");
23425
+ return env?.TMCT_WORLDS_PACK_DIR || join(PKG_ROOT4, "corpus", "worlds");
23399
23426
  }
23400
23427
  var indexCacheByDir = /* @__PURE__ */ new Map();
23401
23428
  var worldCacheByKey = /* @__PURE__ */ new Map();
@@ -146,8 +146,8 @@ export const TOOL_DEFINITIONS = Object.freeze([
146
146
  {
147
147
  name: "tmct_impact",
148
148
  tier: "cold",
149
- summary: "Transitive reverse closure over imports/calls — what breaks if a module changes, by depth, with tests.",
150
- inputSchema: moduleArg("The module whose dependents you want."),
149
+ summary: "Transitive reverse closure over imports/calls — what breaks if a module or symbol changes, by depth, with tests.",
150
+ inputSchema: moduleArg("The module (or a symbol it defines) whose dependents you want."),
151
151
  example: { module: "django/utils/text.py" },
152
152
  },
153
153
  {