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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "1.8.20",
3
+ "version": "1.9.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.",
@@ -80,6 +80,7 @@
80
80
  "wink-nlp": "^2.4.0"
81
81
  },
82
82
  "scripts": {
83
+ "help": "node bin/tmct.mjs --help",
83
84
  "test": "node --test \"test/**/*.test.mjs\"",
84
85
  "chat": "node bin/tmct.mjs",
85
86
  "chat:repo": "node bin/tmct.mjs chat --repo",
@@ -87,6 +88,8 @@
87
88
  "init:persona:human": "node bin/tmct.mjs init --with-persona human",
88
89
  "init:persona:empty": "node bin/tmct.mjs init --with-persona empty",
89
90
  "init:large": "node bin/tmct.mjs init && node bin/tmct.mjs import --corpus seon && node bin/tmct.mjs import --corpus conceptnet && node bin/tmct.mjs import --corpus aws && node bin/tmct.mjs import --corpus python && node bin/tmct.mjs import --corpus java",
91
+ "init:xl": "node bin/tmct.mjs init --persona-size large && node bin/tmct.mjs import --corpus seon && node bin/tmct.mjs import --corpus conceptnet && node bin/tmct.mjs import --corpus aws && node bin/tmct.mjs import --corpus python && node bin/tmct.mjs import --corpus java && node bin/tmct.mjs import --corpus wordnet-xl",
92
+ "init:xxl": "node bin/tmct.mjs init --persona-size large && node bin/tmct.mjs import --corpus seon && node bin/tmct.mjs import --corpus conceptnet && node bin/tmct.mjs import --corpus aws && node bin/tmct.mjs import --corpus python && node bin/tmct.mjs import --corpus java && node bin/tmct.mjs import --corpus wordnet-full && node bin/tmct.mjs import --corpus namenet",
90
93
  "memory": "node bin/tmct.mjs memory",
91
94
  "syllogise": "node bin/tmct.mjs syllogise",
92
95
  "viz": "node bin/tmct.mjs viz",
@@ -100,7 +103,8 @@
100
103
  "audit": "npm audit --audit-level=high",
101
104
  "audit:fix": "npm audit fix",
102
105
  "demo:build": "node scripts/build-demo-site.mjs",
103
- "build:ask-bundle": "node scripts/build-ask-bundle.mjs"
106
+ "build:ask-bundle": "node scripts/build-ask-bundle.mjs",
107
+ "extract:facts": "node scripts/extract-facts-from-text.mjs"
104
108
  },
105
109
  "devDependencies": {
106
110
  "esbuild": "^0.28.1",
@@ -17,10 +17,21 @@
17
17
  import { ask, parseQuery } from "./ask.mjs";
18
18
  import {
19
19
  parseEntities, spiralExpand, mostRecentIndividual, derivedUpdatedAt, MEMORY_SPIRAL_EXPAND_KINDS,
20
- buildVizNodesAndEdges,
20
+ MEMORY_FACT_LINK_KINDS, buildVizNodesAndEdges, deriveFactTermGraph, pickLegendDimension, legendValueFor,
21
+ edgeKindsFor, collapseToTopN,
21
22
  } from "./codegraph.mjs";
22
23
 
24
+ // PLAN_VIZ_MEMORY.md Bug 2 fix + Controls port: deriveFactTermGraph/
25
+ // pickLegendDimension/legendValueFor/MEMORY_FACT_LINK_KINDS/edgeKindsFor/
26
+ // collapseToTopN are exported here (alongside the pre-existing traversal
27
+ // exports) so the viewer page's client-side recentre/edge-kind-toggle/
28
+ // dimension-switcher (a user double-clicking a node, changing which edge
29
+ // kinds the walk follows, or flipping the legend from "split by predicate"
30
+ // to "split by trust source") re-derives the SAME term graph, kind set, and
31
+ // legend the CLI's own generation-time computeVizGraph used — never a second
32
+ // hand-rolled copy that could drift.
23
33
  globalThis.tmctViz = {
24
34
  ask, parseQuery, parseEntities, spiralExpand, mostRecentIndividual, derivedUpdatedAt,
25
- MEMORY_SPIRAL_EXPAND_KINDS, buildVizNodesAndEdges,
35
+ MEMORY_SPIRAL_EXPAND_KINDS, MEMORY_FACT_LINK_KINDS, buildVizNodesAndEdges,
36
+ deriveFactTermGraph, pickLegendDimension, legendValueFor, edgeKindsFor, collapseToTopN,
26
37
  };
@@ -47,6 +47,7 @@
47
47
  var writeFile = unavailable("writeFile");
48
48
  var appendFile = unavailable("appendFile");
49
49
  var mkdir = unavailable("mkdir");
50
+ var mkdtemp = unavailable("mkdtemp");
50
51
  var rename = unavailable("rename");
51
52
  var unlink = unavailable("unlink");
52
53
  var rm = unavailable("rm");
@@ -54,9 +55,14 @@
54
55
  var access = unavailable("access");
55
56
  var copyFile = unavailable("copyFile");
56
57
  var readdir = unavailable("readdir");
58
+ var createReadStream = unavailable("createReadStream");
59
+ var createWriteStream = unavailable("createWriteStream");
57
60
  var randomBytes = unavailable("randomBytes");
58
61
  var createHash = unavailable("createHash");
59
62
  var createRequireFromPath = unavailable("createRequireFromPath");
63
+ var spawnSync = unavailable("spawnSync");
64
+ var createInterface = unavailable("createInterface");
65
+ var DatabaseSync = unavailable("DatabaseSync");
60
66
 
61
67
  // node-stub:node:path
62
68
  var unavailable2 = (name) => () => {
@@ -69,6 +75,7 @@
69
75
  var writeFile2 = unavailable2("writeFile");
70
76
  var appendFile2 = unavailable2("appendFile");
71
77
  var mkdir2 = unavailable2("mkdir");
78
+ var mkdtemp2 = unavailable2("mkdtemp");
72
79
  var rename2 = unavailable2("rename");
73
80
  var unlink2 = unavailable2("unlink");
74
81
  var rm2 = unavailable2("rm");
@@ -76,11 +83,16 @@
76
83
  var access2 = unavailable2("access");
77
84
  var copyFile2 = unavailable2("copyFile");
78
85
  var readdir2 = unavailable2("readdir");
86
+ var createReadStream2 = unavailable2("createReadStream");
87
+ var createWriteStream2 = unavailable2("createWriteStream");
79
88
  var join = (...a) => a.join("/");
80
89
  var dirname = (p) => String(p).replace(/\/[^/]*$/, "");
81
90
  var randomBytes2 = unavailable2("randomBytes");
82
91
  var createHash2 = unavailable2("createHash");
83
92
  var createRequireFromPath2 = unavailable2("createRequireFromPath");
93
+ var spawnSync2 = unavailable2("spawnSync");
94
+ var createInterface2 = unavailable2("createInterface");
95
+ var DatabaseSync2 = unavailable2("DatabaseSync");
84
96
 
85
97
  // node-stub:node:url
86
98
  var unavailable3 = (name) => () => {
@@ -93,6 +105,7 @@
93
105
  var writeFile3 = unavailable3("writeFile");
94
106
  var appendFile3 = unavailable3("appendFile");
95
107
  var mkdir3 = unavailable3("mkdir");
108
+ var mkdtemp3 = unavailable3("mkdtemp");
96
109
  var rename3 = unavailable3("rename");
97
110
  var unlink3 = unavailable3("unlink");
98
111
  var rm3 = unavailable3("rm");
@@ -100,10 +113,15 @@
100
113
  var access3 = unavailable3("access");
101
114
  var copyFile3 = unavailable3("copyFile");
102
115
  var readdir3 = unavailable3("readdir");
116
+ var createReadStream3 = unavailable3("createReadStream");
117
+ var createWriteStream3 = unavailable3("createWriteStream");
103
118
  var fileURLToPath = (u) => String(u);
104
119
  var randomBytes3 = unavailable3("randomBytes");
105
120
  var createHash3 = unavailable3("createHash");
106
121
  var createRequireFromPath3 = unavailable3("createRequireFromPath");
122
+ var spawnSync3 = unavailable3("spawnSync");
123
+ var createInterface3 = unavailable3("createInterface");
124
+ var DatabaseSync3 = unavailable3("DatabaseSync");
107
125
 
108
126
  // node-stub:node:fs/promises
109
127
  var unavailable4 = (name) => () => {
@@ -116,6 +134,7 @@
116
134
  var writeFile4 = unavailable4("writeFile");
117
135
  var appendFile4 = unavailable4("appendFile");
118
136
  var mkdir4 = unavailable4("mkdir");
137
+ var mkdtemp4 = unavailable4("mkdtemp");
119
138
  var rename4 = unavailable4("rename");
120
139
  var unlink4 = unavailable4("unlink");
121
140
  var rm4 = unavailable4("rm");
@@ -123,9 +142,14 @@
123
142
  var access4 = unavailable4("access");
124
143
  var copyFile4 = unavailable4("copyFile");
125
144
  var readdir4 = unavailable4("readdir");
145
+ var createReadStream4 = unavailable4("createReadStream");
146
+ var createWriteStream4 = unavailable4("createWriteStream");
126
147
  var randomBytes4 = unavailable4("randomBytes");
127
148
  var createHash4 = unavailable4("createHash");
128
149
  var createRequireFromPath4 = unavailable4("createRequireFromPath");
150
+ var spawnSync4 = unavailable4("spawnSync");
151
+ var createInterface4 = unavailable4("createInterface");
152
+ var DatabaseSync4 = unavailable4("DatabaseSync");
129
153
 
130
154
  // src/memory/trust.mjs
131
155
  var SOURCE_PRIOR = Object.freeze({
@@ -133,7 +157,9 @@
133
157
  teach: 0.95,
134
158
  provider: 0.9,
135
159
  corpus: 0.7,
160
+ corpusWeak: 0.55,
136
161
  web: 0.4,
162
+ extracted: 0.45,
137
163
  entailed: 0.3
138
164
  });
139
165
  var RECENCY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
@@ -144,6 +170,31 @@
144
170
  var CREATED_AT_PROP = "mgx:createdAt";
145
171
  var UPDATED_AT_PROP = "mgx:updatedAt";
146
172
  var MEMORY_MANIFEST_REL = join(MEMORY_DIR_REL, "manifest.json");
173
+ function parseChatTagRest(rest) {
174
+ const at = rest.indexOf("@");
175
+ const beforeAt = at >= 0 ? rest.slice(0, at) : rest;
176
+ const createdAt = at >= 0 ? rest.slice(at + 1) : "";
177
+ const colon = beforeAt.indexOf(":");
178
+ const sessionId = colon >= 0 ? beforeAt.slice(colon + 1) : "";
179
+ return { createdAt, ...sessionId ? { sessionId } : {} };
180
+ }
181
+ function provenanceTagToSource(tag) {
182
+ const t = String(tag || "").trim();
183
+ if (!t) return null;
184
+ const head = t.split(/\s+/)[0];
185
+ if (head.startsWith("corpus-weak:")) return { kind: "corpusWeak", name: head.slice("corpus-weak:".length) || "unknown" };
186
+ if (head.startsWith("corpus:")) return { kind: "corpus", name: head.slice("corpus:".length) || "unknown" };
187
+ if (head.startsWith("ace:")) return { kind: "operator", ...parseChatTagRest(head.slice("ace:".length)) };
188
+ if (head.startsWith("teach:")) {
189
+ return { kind: "teach", ...parseChatTagRest(head.slice("teach:".length)) };
190
+ }
191
+ if (head.startsWith("web:")) return { kind: "web", url: head.slice("web:".length) };
192
+ if (head.startsWith("url:")) return { kind: "web", url: head.slice("url:".length) };
193
+ if (head.startsWith("extracted:")) return { kind: "extracted", name: head.slice("extracted:".length) || "unknown" };
194
+ if (head.startsWith("entailed:")) return { kind: "entailed", rule: head.slice("entailed:".length) };
195
+ if (head.startsWith("chat:") || head.startsWith("session:") || head.startsWith("operator")) return { kind: "operator" };
196
+ return null;
197
+ }
147
198
  var RULE_KIND_COMPOSE2 = "compose2";
148
199
  var RULE_KIND_FILTER = "filter";
149
200
  var RULE_KIND_RECURSIVE = "recursive";
@@ -216,11 +267,21 @@
216
267
  "mgx:saidinsession": "saidInSession",
217
268
  "mgx:inreplyto": "inReplyTo",
218
269
  "mgx:statedby": "statedBy",
219
- "mgx:canonicalisedfrom": "canonicalisedFrom"
270
+ "mgx:canonicalisedfrom": "canonicalisedFrom",
271
+ // PLAN_VIZ_MEMORY.md Bug 2 fix: the two FIXED structural link kinds
272
+ // deriveFactTermGraph (below) synthesizes on every Fact — Fact -> its own
273
+ // subject/object Term individual. Without these a walk seeded on a Fact (the
274
+ // default mostRecentIndividual seed right after a teach turn) could never
275
+ // reach the term graph at all. Distinct from the per-predicate kinds below
276
+ // (an open-ended, DYNAMIC set — see relationKind's "factrel:" branch), these
277
+ // two are fixed and few, so a plain PROP_KIND row is the simplest fit.
278
+ "mgx:factsubjectterm": "factSubjectTerm",
279
+ "mgx:factobjectterm": "factObjectTerm"
220
280
  };
221
281
  function relationKind(group) {
222
282
  const prop = String(group?.prop || "").toLowerCase();
223
283
  if (PROP_KIND[prop]) return PROP_KIND[prop];
284
+ if (prop.startsWith("factrel:")) return group.predicate || null;
224
285
  const pred = String(group?.predicate || "").toLowerCase();
225
286
  if (/symbol/.test(pred)) {
226
287
  if (/\b(call|invoke)/.test(pred)) return "callsSymbol";
@@ -338,7 +399,8 @@
338
399
  kinds = SPIRAL_EXPAND_KINDS,
339
400
  classPredicate = (ind) => (ind.class || "") === "Module",
340
401
  idNormalizer = null,
341
- seeds: seedsOpt = null
402
+ seeds: seedsOpt = null,
403
+ hubDegree = Infinity
342
404
  } = {}) {
343
405
  const byId = new Map(scored.map((s) => [s.ind.id, s]));
344
406
  let maxSeed = 0;
@@ -412,6 +474,7 @@
412
474
  emitted++;
413
475
  }
414
476
  if (node.hop >= depth) continue;
477
+ if (node.hop > 0 && degree(node.id) > hubDegree) continue;
415
478
  const cands = [];
416
479
  for (const nid of adj.get(node.id) || []) {
417
480
  if (visited.has(nid)) continue;
@@ -499,6 +562,139 @@
499
562
  }
500
563
  return { nodes, edges };
501
564
  }
565
+ var MEMORY_FACT_CLASS = "Fact";
566
+ var MEMORY_TERM_CLASS = "Term";
567
+ function deriveFactTermGraph(graph) {
568
+ const termById = /* @__PURE__ */ new Map();
569
+ const groupByPredicate = /* @__PURE__ */ new Map();
570
+ const subjectLinks = [];
571
+ const objectLinks = [];
572
+ const termId = (t) => `term:${t}`;
573
+ const ensureTerm = (t) => {
574
+ const id = termId(t);
575
+ if (!termById.has(id)) termById.set(id, { id, label: t, class: MEMORY_TERM_CLASS, attributes: [] });
576
+ return id;
577
+ };
578
+ for (const ind of graph?.individuals || []) {
579
+ if ((ind?.class || "") !== MEMORY_FACT_CLASS) continue;
580
+ const attrs = ind.attributes || [];
581
+ const s = attrs.find((a) => a?.prop === "rdf:subject")?.value;
582
+ const p = attrs.find((a) => a?.prop === "rdf:predicate")?.value;
583
+ const o = attrs.find((a) => a?.prop === "rdf:object")?.value;
584
+ if (!s || !p || !o) continue;
585
+ const subjectTermId = ensureTerm(s);
586
+ const objectTermId = ensureTerm(o);
587
+ let group = groupByPredicate.get(p);
588
+ if (!group) {
589
+ group = { predicate: p, prop: `factrel:${p}`, count: 0, edges: [] };
590
+ groupByPredicate.set(p, group);
591
+ }
592
+ group.edges.push({ subject: subjectTermId, object: objectTermId, subjectLabel: s, objectLabel: o });
593
+ group.count = group.edges.length;
594
+ subjectLinks.push({ subject: ind.id, object: subjectTermId, subjectLabel: ind.label, objectLabel: s });
595
+ objectLinks.push({ subject: ind.id, object: objectTermId, subjectLabel: ind.label, objectLabel: o });
596
+ }
597
+ if (!termById.size) return { graph, factRelationKinds: [] };
598
+ const individuals = [...graph.individuals || [], ...termById.values()];
599
+ const byId = new Map(graph.byId);
600
+ for (const term of termById.values()) byId.set(term.id, term);
601
+ const relations = [
602
+ ...graph.relations || [],
603
+ ...groupByPredicate.values(),
604
+ { predicate: "factSubjectTerm", prop: "mgx:factSubjectTerm", count: subjectLinks.length, edges: subjectLinks },
605
+ { predicate: "factObjectTerm", prop: "mgx:factObjectTerm", count: objectLinks.length, edges: objectLinks }
606
+ ];
607
+ return {
608
+ graph: { ...graph, individuals, byId, relations },
609
+ factRelationKinds: [...groupByPredicate.keys()]
610
+ };
611
+ }
612
+ var MEMORY_FACT_LINK_KINDS = ["factSubjectTerm", "factObjectTerm"];
613
+ function edgeKindsFor(mode, factRelationKinds) {
614
+ const relationKinds = [...factRelationKinds, ...MEMORY_FACT_LINK_KINDS];
615
+ if (mode === "meta") return [...MEMORY_SPIRAL_EXPAND_KINDS];
616
+ if (mode === "relation") return relationKinds;
617
+ return [...MEMORY_SPIRAL_EXPAND_KINDS, ...relationKinds];
618
+ }
619
+ var LEGEND_MAX_BUCKETS = 20;
620
+ var LEGEND_MIN_BUCKETS = 2;
621
+ var LEGEND_COLLAPSE_TOP_N = 15;
622
+ function normalizedEntropy(buckets) {
623
+ const k = buckets.length;
624
+ if (k < 2) return 0;
625
+ const total = buckets.reduce((sum, b) => sum + b.count, 0);
626
+ if (!total) return 0;
627
+ let h = 0;
628
+ for (const b of buckets) {
629
+ if (!b.count) continue;
630
+ const p = b.count / total;
631
+ h -= p * Math.log2(p);
632
+ }
633
+ return h / Math.log2(k);
634
+ }
635
+ function collapseToTopN(buckets) {
636
+ if (buckets.length <= LEGEND_MAX_BUCKETS) return buckets;
637
+ const sorted = [...buckets].sort((a, b) => b.count - a.count || (a.value < b.value ? -1 : 1));
638
+ const kept = sorted.slice(0, LEGEND_COLLAPSE_TOP_N);
639
+ const restCount = sorted.slice(LEGEND_COLLAPSE_TOP_N).reduce((sum, b) => sum + b.count, 0);
640
+ return restCount ? [...kept, { value: "Other", count: restCount }] : kept;
641
+ }
642
+ function bucketCounts(values) {
643
+ const counts = /* @__PURE__ */ new Map();
644
+ for (const v of values) {
645
+ if (v == null || v === "") continue;
646
+ counts.set(v, (counts.get(v) || 0) + 1);
647
+ }
648
+ return [...counts.entries()].map(([value, count]) => ({ value, count }));
649
+ }
650
+ function provenanceBucketLabel(rawTag) {
651
+ const tag = String(rawTag || "").split(" | ")[0].trim();
652
+ if (!tag) return null;
653
+ const src = provenanceTagToSource(tag);
654
+ if (!src) return null;
655
+ if (src.kind === "corpus" || src.kind === "corpusWeak") {
656
+ return `${src.kind === "corpusWeak" ? "corpus-weak" : "corpus"}:${src.name || "unknown"}`;
657
+ }
658
+ if (src.kind === "extracted") return `extracted:${src.name || "unknown"}`;
659
+ if (src.kind === "entailed") return `entailed:${src.rule || "unknown"}`;
660
+ if (src.kind === "operator") return "ace:chat";
661
+ if (src.kind === "teach") return "teach:chat";
662
+ if (src.kind === "web") return "web";
663
+ return src.kind;
664
+ }
665
+ function legendValueFor(graph, node, dimension) {
666
+ if (dimension === "class") return node?.class || "(none)";
667
+ if (!node || node.class !== MEMORY_FACT_CLASS) return null;
668
+ const attrs = graph?.byId?.get?.(node.id)?.attributes || [];
669
+ if (dimension === "predicate") return attrs.find((a) => a?.prop === "rdf:predicate")?.value || null;
670
+ if (dimension === "provenance") return provenanceBucketLabel(attrs.find((a) => a?.prop === "mgx:factProvenance")?.value);
671
+ return null;
672
+ }
673
+ function pickLegendDimension(graph, nodes) {
674
+ const classBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "class")));
675
+ const predicateBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "predicate")));
676
+ const provenanceBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "provenance")));
677
+ const score = (rawBuckets) => {
678
+ const buckets = collapseToTopN(rawBuckets);
679
+ const qualifies = buckets.length >= LEGEND_MIN_BUCKETS && buckets.length <= LEGEND_MAX_BUCKETS;
680
+ return { score: normalizedEntropy(buckets), qualifies, buckets };
681
+ };
682
+ const dimensions = {
683
+ class: score(classBuckets),
684
+ predicate: score(predicateBuckets),
685
+ provenance: score(provenanceBuckets)
686
+ };
687
+ let primary = "class";
688
+ let bestScore = -1;
689
+ for (const [name, d] of Object.entries(dimensions)) {
690
+ if (!d.qualifies) continue;
691
+ if (d.score > bestScore) {
692
+ bestScore = d.score;
693
+ primary = name;
694
+ }
695
+ }
696
+ return { primary, dimensions };
697
+ }
502
698
  function moduleIdOfId(graph, id) {
503
699
  const ind = graph.byId?.get?.(id);
504
700
  if (ind) return moduleIdOf(graph, ind);
@@ -1141,7 +1337,16 @@
1141
1337
  "sorta",
1142
1338
  "btw",
1143
1339
  "by the way",
1144
- "you"
1340
+ "you",
1341
+ // "quick q" (BENCHMARK_CONVERSATION_1.8.14.md item 11): the casual abbreviated
1342
+ // sibling of GREETING_PREAMBLE_RE's own "quick question" clause (normalize.mjs)
1343
+ // — that frame requires a delimiter immediately after the greeting word
1344
+ // ("hey, quick question - …"), so it never matches "hey quick q, …" (no
1345
+ // delimiter between "hey" and "quick q"). Filler-stripping instead — this
1346
+ // list is matched word-boundary-anywhere, not anchored — closes the gap
1347
+ // without needing GREETING_PREAMBLE_RE's own stricter delimiter-position
1348
+ // shape.
1349
+ "quick q"
1145
1350
  ]);
1146
1351
  var CONTEXT_PRONOUNS = Object.freeze(["this", "it", "that", "here", "this one", "that one"]);
1147
1352
  var NEGATION_FRAMES = Object.freeze([
@@ -1437,6 +1642,15 @@
1437
1642
  var FOR_DIGIT_EXAMPLE_RE = /\b4\s+(example|instance)\b(?!\s*[a-z])/gi;
1438
1643
  var KIND_NOUN_ANAPHORA_RE = /\b(this|that)\s+(class|module|function|method|attribute|variable|file|commit)\b/gi;
1439
1644
  var VERB_ALTERNATION = Object.keys(VERB_TO_KIND).sort((a, b) => b.length - a.length).map(escapeRegex).join("|");
1645
+ var FILLER_RE = FILLER_WORDS.length ? new RegExp(
1646
+ "\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b\\s*,?",
1647
+ "gi"
1648
+ ) : null;
1649
+ function stripFillerWords(text) {
1650
+ let q = String(text || "");
1651
+ if (FILLER_RE) q = q.replace(FILLER_RE, " ");
1652
+ return q.replace(/\s+/g, " ").trim();
1653
+ }
1440
1654
  var RELATION_VERB_RE = new RegExp(
1441
1655
  "\\b(?:" + Object.keys(VERB_TO_KIND).sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b",
1442
1656
  "i"
@@ -1591,13 +1805,7 @@
1591
1805
  q = applySelfCorrectionFrames(q);
1592
1806
  q = applySubordinationFrames(q);
1593
1807
  q = applyConditionalFrames(q);
1594
- if (FILLER_WORDS.length) {
1595
- const fillerRe = new RegExp(
1596
- "\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b",
1597
- "gi"
1598
- );
1599
- q = q.replace(fillerRe, " ");
1600
- }
1808
+ q = stripFillerWords(q);
1601
1809
  q = q.replace(/\?{2,}\s*$/, "?");
1602
1810
  return q.replace(/\s+/g, " ").trim();
1603
1811
  }
@@ -2386,6 +2594,7 @@
2386
2594
  var writeFile5 = unavailable5("writeFile");
2387
2595
  var appendFile5 = unavailable5("appendFile");
2388
2596
  var mkdir5 = unavailable5("mkdir");
2597
+ var mkdtemp5 = unavailable5("mkdtemp");
2389
2598
  var rename5 = unavailable5("rename");
2390
2599
  var unlink5 = unavailable5("unlink");
2391
2600
  var rm5 = unavailable5("rm");
@@ -2393,9 +2602,14 @@
2393
2602
  var access5 = unavailable5("access");
2394
2603
  var copyFile5 = unavailable5("copyFile");
2395
2604
  var readdir5 = unavailable5("readdir");
2605
+ var createReadStream5 = unavailable5("createReadStream");
2606
+ var createWriteStream5 = unavailable5("createWriteStream");
2396
2607
  var randomBytes5 = unavailable5("randomBytes");
2397
2608
  var createHash5 = unavailable5("createHash");
2398
2609
  var createRequireFromPath5 = unavailable5("createRequireFromPath");
2610
+ var spawnSync5 = unavailable5("spawnSync");
2611
+ var createInterface5 = unavailable5("createInterface");
2612
+ var DatabaseSync5 = unavailable5("DatabaseSync");
2399
2613
 
2400
2614
  // src/answer-variants.mjs
2401
2615
  var import_meta = {};
@@ -2531,7 +2745,7 @@
2531
2745
  function parseComposite(text, nlp) {
2532
2746
  const w = splitWords(text);
2533
2747
  const lc = w.map((x) => x.toLowerCase());
2534
- return parseExistence(w, lc) || parseQualifierCheck(w, lc) || parseNegation(text, nlp, 0) || parseForwardNegation(w, lc, nlp) || parseTemporal(w, lc, nlp, 0) || parseCommitFilter(w, lc) || parseAnaphora(w, lc, nlp) || parseAggregate(w, lc, nlp) || parseSuperlative(w, lc, nlp) || parseFind(w, lc, nlp, 0) || parseList(w, lc, nlp, 0) || parseNested(w, lc, nlp, 0) || parseRelationalOrQualified(w, lc, nlp, 0);
2748
+ return parseExistence(w, lc) || parseQualifierCheck(w, lc) || parseNegation(text, nlp, 0) || parseForwardNegation(w, lc, nlp) || parseTemporal(w, lc, nlp, 0) || parseCommitFilter(w, lc) || parseAnaphora(w, lc, nlp) || parseAggregate(w, lc, nlp) || parseSuperlative(w, lc, nlp) || parseFind(w, lc, nlp, 0) || parseList(w, lc, nlp, 0) || parseNested(w, lc, nlp, 0) || parsePluralAnaphoraObject(w, lc, nlp) || parseRelationalOrQualified(w, lc, nlp, 0);
2535
2749
  }
2536
2750
  function complementAst(entityType, diffAtom) {
2537
2751
  return {
@@ -2634,6 +2848,23 @@
2634
2848
  }
2635
2849
  return null;
2636
2850
  }
2851
+ var PLURAL_ANAPHORA_OBJECT = /* @__PURE__ */ new Set(["those", "them"]);
2852
+ function parsePluralAnaphoraObject(w, lc, nlp) {
2853
+ for (let i = 0; i < lc.length; i += 1) {
2854
+ if (!PLURAL_ANAPHORA_OBJECT.has(lc[i])) continue;
2855
+ if (lc[i - 1] === "of") continue;
2856
+ const isTerminal = i === lc.length - 1;
2857
+ const leadsAVerb = i + 1 < lc.length && !!VERB_TO_KIND[lc[i + 1]];
2858
+ if (!isTerminal && !leadsAVerb) continue;
2859
+ const head = [...w.slice(0, i), NEST_SENTINEL, ...w.slice(i + 1)];
2860
+ const outer = parseSimpleClause(head.join(" "), nlp);
2861
+ if (!outer || outer.shape !== "reverse" && outer.shape !== "forward") continue;
2862
+ if (outer.object !== NEST_SENTINEL) continue;
2863
+ if (outer.modifier && outer.modifier !== "direct") continue;
2864
+ return { node: outer.shape === "reverse" ? "reverseSet" : "forwardSet", kind: outer.kind, entityType: outer.entityType, inner: { node: "prevSet" } };
2865
+ }
2866
+ return null;
2867
+ }
2637
2868
  var TEMPORAL_AUX = /* @__PURE__ */ new Set(["did", "was", "were", "do", "does", "has", "have", "had"]);
2638
2869
  var TEMPORAL_TAIL = /* @__PURE__ */ new Set([
2639
2870
  "change",
@@ -3289,8 +3520,8 @@
3289
3520
  const objs = uniqueById(MEMBERSHIP_KINDS.flatMap((k) => forwardOverSet(graph, k, /* @__PURE__ */ new Set([id]))));
3290
3521
  return entityType ? objs.filter((o) => o.class === entityType) : objs;
3291
3522
  }
3292
- function resolveMembershipOwner(graph, term) {
3293
- const r = resolveObject(graph, term);
3523
+ function resolveMembershipOwner(graph, term, contextId = null) {
3524
+ const r = resolveTermOrContext(graph, term, contextId);
3294
3525
  if (!(r.match && r.tier === 1)) {
3295
3526
  const dirMods = directoryScopeModules(graph, term);
3296
3527
  if (dirMods.length) return { kind: "dir", mods: dirMods };
@@ -3479,8 +3710,17 @@
3479
3710
  const ids = new Set(evalSet(graph, ast.inner, opts).map((i) => i.id));
3480
3711
  return forwardOverSet(graph, ast.kind, ids);
3481
3712
  }
3713
+ // the previous list-shaped answer's own id set (parsePluralAnaphoraObject's
3714
+ // "those"/"them" leaf) — evalComposite's reverseSet/forwardSet dispatch already
3715
+ // intercepts the genuinely-empty (no `prev` at all) case as an honest "needs a
3716
+ // previous answer" miss, same as evalAnaphora's own no-prev branch; this is only
3717
+ // reached with a real, non-empty `prev` in hand.
3718
+ case "prevSet": {
3719
+ const prev = opts && opts.prev;
3720
+ return Array.isArray(prev) ? prev.map((id) => graph.byId.get(id)).filter(Boolean) : [];
3721
+ }
3482
3722
  case "membership": {
3483
- const owner = resolveMembershipOwner(graph, ast.term);
3723
+ const owner = resolveMembershipOwner(graph, ast.term, opts && opts.contextId);
3484
3724
  if (owner.kind === "dir") {
3485
3725
  if (!ast.entityType || ast.entityType === "Module") return owner.mods;
3486
3726
  const ids = new Set(owner.mods.map((m) => m.id));
@@ -3640,7 +3880,7 @@
3640
3880
  const memNode = qualNode ? qualNode.inner : ast;
3641
3881
  const entityType = memNode.entityType;
3642
3882
  const filterFn = qualNode ? (ind) => qualNode.filters.every((f) => qualHolds(graph, ind, QUALIFIERS[f])) : null;
3643
- const owner = resolveMembershipOwner(graph, memNode.term);
3883
+ const owner = resolveMembershipOwner(graph, memNode.term, opts && opts.contextId);
3644
3884
  if (owner.kind === "dir") {
3645
3885
  let objs;
3646
3886
  if (!entityType || entityType === "Module") objs = owner.mods;
@@ -3720,6 +3960,9 @@
3720
3960
  broad: !narrow.length && broad.length > 0
3721
3961
  };
3722
3962
  }
3963
+ if ((ast.node === "reverseSet" || ast.node === "forwardSet") && ast.inner.node === "prevSet" && !(Array.isArray(opts.prev) && opts.prev.length)) {
3964
+ return { compositeMiss: true, reason: "no-prev", matches: [] };
3965
+ }
3723
3966
  return { compositeKind: "set", matches: evalSet(graph, ast, opts), entityType: ast.entityType || null };
3724
3967
  }
3725
3968
  var compositeList = (matches) => listJoin(matches.slice(0, OVERFLOW_CAP).map((m) => ["Function", "Method"].includes(m.class) ? `${m.label}()` : m.label)) + (matches.length > OVERFLOW_CAP ? `, \u2026and ${matches.length - OVERFLOW_CAP} more` : "");
@@ -4204,7 +4447,7 @@
4204
4447
  if (shape === "ask") {
4205
4448
  const subj = resolveTermOrContext(graph, parsed.subject, contextId);
4206
4449
  const obj = resolveTermOrContext(graph, parsed.object, contextId);
4207
- if (!subj.match || !obj.match) {
4450
+ if (!subj.match || !obj.match || subj.ambiguous || obj.ambiguous) {
4208
4451
  return {
4209
4452
  matches: [],
4210
4453
  objMatch: obj.match,
@@ -4398,7 +4641,11 @@
4398
4641
  widenNote = `, widened to ${siblingClass} subjects (no ${entityType} recorded)`;
4399
4642
  }
4400
4643
  }
4401
- return { matches: matches2, objMatch, candidates, traversal: `${symbolKind} edges where object = ${objMatch.label}${widenNote}`, ambiguous, matchedVia };
4644
+ const upRefineEligible = (kind === "touches" || kind === "calls") && objMatch.class === "Class" && !edgesOfKind2(graph, "contains").some((e) => e.subject === objMatch.id);
4645
+ const upRefineModule = upRefineEligible ? graph.byId.get(moduleIdOf2(graph, objMatch) || "") : null;
4646
+ if (matches2.length || !(upRefineEligible && upRefineModule)) {
4647
+ return { matches: matches2, objMatch, candidates, traversal: `${symbolKind} edges where object = ${objMatch.label}${widenNote}`, ambiguous, matchedVia };
4648
+ }
4402
4649
  }
4403
4650
  let gObjMatch = objMatch;
4404
4651
  let gCandidates = candidates;
@@ -4413,7 +4660,7 @@
4413
4660
  gCandidates = retry.candidates;
4414
4661
  gAmbiguous = retry.ambiguous;
4415
4662
  gMatchedVia = retry.matchedVia;
4416
- } else if ((kind === "tests" || kind === "cochange") && gObjMatch.class !== "Module") {
4663
+ } else if (wantClass === "Module") {
4417
4664
  const mid = moduleIdOf2(graph, gObjMatch);
4418
4665
  const mod = mid && graph.byId.get(mid);
4419
4666
  if (mod) {
@@ -5154,6 +5401,12 @@ ${lines.join("\n")}`;
5154
5401
  mostRecentIndividual,
5155
5402
  derivedUpdatedAt,
5156
5403
  MEMORY_SPIRAL_EXPAND_KINDS,
5157
- buildVizNodesAndEdges
5404
+ MEMORY_FACT_LINK_KINDS,
5405
+ buildVizNodesAndEdges,
5406
+ deriveFactTermGraph,
5407
+ pickLegendDimension,
5408
+ legendValueFor,
5409
+ edgeKindsFor,
5410
+ collapseToTopN
5158
5411
  };
5159
5412
  })();