@polycode-projects/the-mechanical-code-talker 1.12.0 → 2.0.2

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 (148) hide show
  1. package/README.md +244 -48
  2. package/ROADMAP.md +23 -34
  3. package/bin/tmct.mjs +107 -71
  4. package/corpus/LICENSES.json +118 -0
  5. package/corpus/README.md +17 -13
  6. package/corpus/conceptnet/README.md +5 -5
  7. package/corpus/conceptnet/fetch-slice.mjs +1 -1
  8. package/corpus/conceptnet/filter-dump.mjs +1 -1
  9. package/corpus/generated/README.md +9 -10
  10. package/corpus/namenet/README.md +39 -0
  11. package/corpus/seon/README.md +2 -2
  12. package/corpus/tier2/generate.mjs +58 -9
  13. package/corpus/tier2/manifest.json +44 -0
  14. package/corpus/wordnet/README.md +37 -0
  15. package/corpus/wordnet/generate.mjs +1 -1
  16. package/data/templates/constructions/agent-noun-relations.toml +2 -2
  17. package/data/templates/grammar-rules.toml +1 -1
  18. package/package.json +13 -22
  19. package/src/{ask-nlp.mjs → adapters/ask-nlp.mjs} +1 -1
  20. package/src/{config.mjs → adapters/config.mjs} +1 -1
  21. package/src/{corpus → adapters/corpus}/conceptnet-map.toml +4 -4
  22. package/src/{corpus → adapters/corpus}/conceptnet.mjs +3 -3
  23. package/src/adapters/corpus/construction-banks.mjs +43 -0
  24. package/src/{corpus → adapters/corpus}/templates.mjs +1 -1
  25. package/src/{embed.mjs → adapters/embed.mjs} +1 -11
  26. package/src/{graph-build.mjs → adapters/graph-build.mjs} +6 -6
  27. package/src/{memory → adapters/memory}/blocks.mjs +2 -2
  28. package/src/{memory → adapters/memory}/core.mjs +38 -94
  29. package/src/adapters/prose-tokens.mjs +98 -0
  30. package/src/{providers → adapters/providers}/bootstrap.mjs +2 -2
  31. package/src/{providers → adapters/providers}/fixture.mjs +3 -3
  32. package/src/{providers → adapters/providers}/graph-service.mjs +9 -4
  33. package/src/{source-slice.mjs → adapters/source-slice.mjs} +2 -2
  34. package/src/{source.mjs → adapters/source.mjs} +1 -1
  35. package/src/{toml-config.mjs → adapters/toml-config.mjs} +1 -1
  36. package/src/{answer-variants.json → domain/answer-variants.json} +1 -1
  37. package/src/domain/answer-variants.mjs +23 -0
  38. package/src/{ask-vocab.mjs → domain/ask-vocab.mjs} +37 -5
  39. package/src/{ask.mjs → domain/ask.mjs} +216 -52
  40. package/src/{codegraph.mjs → domain/codegraph.mjs} +31 -315
  41. package/src/{completions → domain/completions}/complete.mjs +16 -10
  42. package/src/{completions → domain/completions}/graph-adapter.mjs +10 -4
  43. package/src/{completions → domain/completions}/group.mjs +14 -6
  44. package/src/{completions → domain/completions}/infer.mjs +57 -39
  45. package/src/domain/completions/injected.mjs +21 -0
  46. package/src/{completions → domain/completions}/rank.mjs +15 -8
  47. package/src/{completions → domain/completions}/search.mjs +4 -2
  48. package/src/{grammar → domain/grammar}/ace.mjs +3 -3
  49. package/src/{grammar → domain/grammar}/assert.mjs +12 -8
  50. package/src/{grammar → domain/grammar}/lexicon-core.json +1 -1
  51. package/src/{grammar → domain/grammar}/lexicon.mjs +4 -6
  52. package/src/domain/hash.mjs +147 -0
  53. package/src/{interpret → domain/interpret}/fuzzy.mjs +42 -4
  54. package/src/domain/interpret/nlp-registry.mjs +20 -0
  55. package/src/{interpret → domain/interpret}/normalize.mjs +35 -5
  56. package/src/{interpret → domain/interpret}/pipeline.mjs +1 -5
  57. package/src/{interpret → domain/interpret}/strategies/ace.mjs +1 -1
  58. package/src/{interpret → domain/interpret}/strategies/constructions.mjs +30 -52
  59. package/src/{interpret → domain/interpret}/strategies/keywords.mjs +48 -26
  60. package/src/domain/memory/capability.mjs +235 -0
  61. package/src/domain/memory/fold.mjs +54 -0
  62. package/src/domain/memory/session-turns.mjs +7 -0
  63. package/src/{memory → domain/memory}/trust.mjs +48 -0
  64. package/src/{paraphrase.mjs → domain/paraphrase.mjs} +2 -2
  65. package/src/{prose.mjs → domain/prose.mjs} +1 -1
  66. package/src/domain/real-word-collisions.json +1 -0
  67. package/src/{router → domain/router}/call-validator.mjs +1 -1
  68. package/src/{router → domain/router}/drive.mjs +34 -25
  69. package/src/{router → domain/router}/goal-reasoner.mjs +1 -1
  70. package/src/{router → domain/router}/guardrail.mjs +1 -1
  71. package/src/{router → domain/router}/planner.mjs +1 -1
  72. package/src/{router → domain/router}/registry.mjs +5 -5
  73. package/src/{router → domain/router}/resolver.mjs +16 -13
  74. package/src/{router → domain/router}/results.mjs +1 -1
  75. package/src/{router → domain/router}/set-algebra.mjs +1 -1
  76. package/src/{router → domain/router}/taught.mjs +10 -9
  77. package/src/{syllogise.mjs → domain/syllogise.mjs} +21 -4
  78. package/src/domain/vector.mjs +12 -0
  79. package/src/services/chat-session.mjs +451 -0
  80. package/src/{chat.mjs → services/chat.mjs} +1209 -684
  81. package/src/{cli-args.mjs → services/cli-args.mjs} +2 -2
  82. package/src/services/completions.mjs +55 -0
  83. package/src/{extensions.mjs → services/extensions.mjs} +7 -7
  84. package/src/{finish.mjs → services/finish.mjs} +2 -2
  85. package/src/{memory → services}/fold.mjs +0 -0
  86. package/src/{import-file.mjs → services/import-file.mjs} +3 -3
  87. package/src/{index.mjs → services/index.mjs} +21 -12
  88. package/src/{init.mjs → services/init.mjs} +9 -9
  89. package/src/{ledger-viz.mjs → services/ledger-viz.mjs} +3 -3
  90. package/src/{plan-viz.mjs → services/plan-viz.mjs} +98 -28
  91. package/src/{sentences.mjs → services/sentences.mjs} +1 -1
  92. package/src/{sessions.mjs → services/sessions.mjs} +4 -5
  93. package/src/{telemetry.mjs → services/telemetry.mjs} +1 -1
  94. package/src/{server-http.mjs → surfaces/http/server-http.mjs} +11 -65
  95. package/src/{tui → surfaces/tui}/app.mjs +3 -3
  96. package/src/{memory-ask-browser-entry.mjs → surfaces/web/memory-ask-browser-entry.mjs} +5 -5
  97. package/src/{memory-ask-browser.bundle.js → surfaces/web/memory-ask-browser.bundle.js} +9465 -6366
  98. package/src/tools/catalog.mjs +29 -0
  99. package/src/{conformance.mjs → tools/conformance.mjs} +2 -2
  100. package/src/tools/definitions.mjs +288 -0
  101. package/src/tools/graph-load.mjs +20 -0
  102. package/src/tools/handlers/index.mjs +54 -0
  103. package/src/tools/handlers/kit.mjs +33 -0
  104. package/src/tools/handlers/tmct-architecture.mjs +7 -0
  105. package/src/tools/handlers/tmct-ask.mjs +14 -0
  106. package/src/tools/handlers/tmct-callees.mjs +6 -0
  107. package/src/tools/handlers/tmct-callers.mjs +6 -0
  108. package/src/tools/handlers/tmct-calls.mjs +6 -0
  109. package/src/tools/handlers/tmct-class-history.mjs +6 -0
  110. package/src/tools/handlers/tmct-cochanges.mjs +6 -0
  111. package/src/tools/handlers/tmct-context-more.mjs +9 -0
  112. package/src/tools/handlers/tmct-context.mjs +163 -0
  113. package/src/tools/handlers/tmct-describe.mjs +15 -0
  114. package/src/tools/handlers/tmct-exports.mjs +9 -0
  115. package/src/tools/handlers/tmct-file-history.mjs +6 -0
  116. package/src/tools/handlers/tmct-history.mjs +6 -0
  117. package/src/tools/handlers/tmct-impact.mjs +9 -0
  118. package/src/tools/handlers/tmct-members.mjs +16 -0
  119. package/src/tools/handlers/tmct-method-history.mjs +6 -0
  120. package/src/tools/handlers/tmct-search.mjs +22 -0
  121. package/src/tools/handlers/tmct-signature.mjs +6 -0
  122. package/src/tools/handlers/tmct-snippet.mjs +37 -0
  123. package/src/tools/handlers/tmct-subclasses.mjs +16 -0
  124. package/src/tools/handlers/tmct-tests-for.mjs +6 -0
  125. package/src/tools/handlers/tmct-untested.mjs +7 -0
  126. package/src/tools/memory-fallthrough.mjs +65 -0
  127. package/src/{schema-docs.mjs → tools/schema-docs.mjs} +1 -1
  128. package/src/tools/server.mjs +61 -0
  129. package/src/answer-variants.mjs +0 -39
  130. package/src/hash.mjs +0 -24
  131. package/src/server.mjs +0 -501
  132. /package/src/{corpus → adapters/corpus}/unknown-ingest.mjs +0 -0
  133. /package/src/{graph-merge.mjs → adapters/graph-merge.mjs} +0 -0
  134. /package/src/{memory → adapters/memory}/inspect.mjs +0 -0
  135. /package/src/{memory → adapters/memory}/shacl.mjs +0 -0
  136. /package/src/{prose-nlp.mjs → adapters/prose-nlp.mjs} +0 -0
  137. /package/src/{repository-interface.mjs → adapters/repository-interface.mjs} +0 -0
  138. /package/src/{uuid.mjs → adapters/uuid.mjs} +0 -0
  139. /package/src/{wink-model.mjs → adapters/wink-model.mjs} +0 -0
  140. /package/src/{completions → domain/completions}/prune.mjs +0 -0
  141. /package/src/{concept.mjs → domain/concept.mjs} +0 -0
  142. /package/src/{domain.mjs → domain/domain.mjs} +0 -0
  143. /package/src/{interpret → domain/interpret}/merge.mjs +0 -0
  144. /package/src/{interpret → domain/interpret}/strategies/grammar.mjs +0 -0
  145. /package/src/{interpret → domain/interpret}/strategies/noise-strip.mjs +0 -0
  146. /package/src/{memory → domain/memory}/bias.mjs +0 -0
  147. /package/src/{planning.mjs → domain/planning.mjs} +0 -0
  148. /package/src/{viz-theme.mjs → services/viz-theme.mjs} +0 -0
@@ -1,10 +1,10 @@
1
1
  import { lookupByProseTokens, proseLayerHits, splitIdentifierWords } from "./prose.mjs";
2
- import { cosine } from "./embed.mjs";
3
- import { CREATED_AT_PROP, UPDATED_AT_PROP, provenanceTagToSource } from "./memory/core.mjs";
2
+ import { cosine } from "./vector.mjs";
3
+ import { CREATED_AT_PROP, UPDATED_AT_PROP } from "./memory/trust.mjs";
4
4
 
5
5
  // Pure (no-network, no-fs) query logic over the typed `entities` payload that the
6
6
  // deterministic indexer writes to <repo>/.tmct/graph.json (shape produced by
7
- // src/graph-build.mjs):
7
+ // src/adapters/graph-build.mjs):
8
8
  //
9
9
  // {
10
10
  // generated_at, classes: [{name, count, sample[]}],
@@ -47,6 +47,14 @@ export function parseEntities(payload) {
47
47
  };
48
48
  }
49
49
 
50
+ /** Code entities (Modules) in the loaded graph — the "is there a code graph here"
51
+ * test. 0 means a graph-less bootstrap OR a graph.json with no code entities (the
52
+ * degenerate trap); both orient rather than over-promise. */
53
+ export function moduleCountOf(graph) {
54
+ if (!graph || !Array.isArray(graph.individuals)) return 0;
55
+ return graph.individuals.filter((i) => (i.class || "") === "Module").length;
56
+ }
57
+
50
58
  // ---- relation-kind classifier (for impact + tests-coverage) -------------------
51
59
 
52
60
  const KINDS = ["imports", "calls", "defines", "tests", "touches", "contains", "inherits", "callsSymbol", "touchesSymbol"];
@@ -83,16 +91,11 @@ const PROP_KIND = {
83
91
  "mgx:inreplyto": "inReplyTo",
84
92
  "mgx:statedby": "statedBy",
85
93
  "mgx:canonicalisedfrom": "canonicalisedFrom",
86
- // structural links deriveFactTermGraph synthesizes on every Fact (Fact -> its own subject/object Term)
87
- "mgx:factsubjectterm": "factSubjectTerm",
88
- "mgx:factobjectterm": "factObjectTerm",
89
94
  };
90
95
 
91
96
  export function relationKind(group) {
92
97
  const prop = String(group?.prop || "").toLowerCase();
93
98
  if (PROP_KIND[prop]) return PROP_KIND[prop];
94
- // deriveFactTermGraph namespaces taught predicates as "factrel:<predicate>"
95
- if (prop.startsWith("factrel:")) return group.predicate || null;
96
99
  const pred = String(group?.predicate || "").toLowerCase();
97
100
  // symbol-granular fallbacks first, so a near-miss still classifies fine-grained
98
101
  if (/symbol/.test(pred)) {
@@ -536,9 +539,6 @@ const SPIRAL_DEPTH_DEFAULT = 3;
536
539
  const SPIRAL_NODE_LIMIT_DEFAULT = 12;
537
540
  const SPIRAL_Q_DEFAULT = 0.9; // keep only the least-connected 90% at each step
538
541
  const SPIRAL_EXPAND_KINDS = ["imports", "calls", "callsSymbol", "inherits"]; // cochange dropped
539
- // The memory graph's edge-kind inventory a memory-graph spiralExpand call walks
540
- // instead of code-graph Modules. mgx:asksAbout is excluded — it lives in the code graph.
541
- export const MEMORY_SPIRAL_EXPAND_KINDS = ["saidInSession", "inReplyTo", "statedBy", "canonicalisedFrom"];
542
542
  const SPIRAL_EMIT_FRAC = 0.5;
543
543
  const SPIRAL_HOP_DECAY = 0.6;
544
544
  const SPIRAL_PROX_FRAC = 0.2;
@@ -769,19 +769,6 @@ export function spiralExpand(graph, scored = [], {
769
769
  return results;
770
770
  }
771
771
 
772
- /** The individual with the most recent `createdAtProp` attribute (ties break
773
- * on lowest id); null if none carry the attribute. ISO-8601 timestamps
774
- * compare correctly as plain strings, so no Date parsing is needed. */
775
- export function mostRecentIndividual(graph, createdAtProp = CREATED_AT_PROP) {
776
- let best = null; // { ind, v }
777
- for (const ind of graph?.individuals || []) {
778
- const v = (ind?.attributes || []).find((a) => a?.prop === createdAtProp)?.value;
779
- if (!v) continue;
780
- if (!best || v > best.v || (v === best.v && String(ind.id) < String(best.ind.id))) best = { ind, v };
781
- }
782
- return best ? best.ind : null;
783
- }
784
-
785
772
  /** Shared module-ranking core behind renderSearch and searchModulesRanked.
786
773
  * IDF-weights each query token, scores path/symbol/exact-symbol matches, and
787
774
  * re-ranks with a bounded import-proximity bonus. Pure; deterministic. */
@@ -1117,247 +1104,6 @@ export function edgesOfKind(graph, kind) {
1117
1104
  return out;
1118
1105
  }
1119
1106
 
1120
- /** A node's "last touched" moment: its own updatedAt/createdAt attribute, or
1121
- * the max `createdAt` over every edge touching it, whichever is newer. ""
1122
- * when nothing carries a timestamp. Skips edges with no `createdAt` rather than throwing. */
1123
- export function derivedUpdatedAt(graph, ind, { createdAtProp = CREATED_AT_PROP, updatedAtProp = UPDATED_AT_PROP } = {}) {
1124
- if (!ind) return "";
1125
- const attrs = ind.attributes || [];
1126
- const own = attrs.find((a) => a?.prop === updatedAtProp)?.value || attrs.find((a) => a?.prop === createdAtProp)?.value || "";
1127
- let best = own || "";
1128
- for (const g of graph?.relations || []) {
1129
- for (const e of g.edges || []) {
1130
- if (!e || (e.subject !== ind.id && e.object !== ind.id)) continue;
1131
- const c = e.createdAt;
1132
- if (!c) continue; // no timestamp on this edge — skip, never throw
1133
- if (!best || c > best) best = c;
1134
- }
1135
- }
1136
- return best;
1137
- }
1138
-
1139
- /** Turn a `spiralExpand` walk into the `{nodes, edges}` shape `tmct viz`
1140
- * renders, shared between the CLI and the browser bundle's client-side
1141
- * re-walk. `edges` includes any relation-group edge connecting two walked
1142
- * nodes, not just kinds the walk itself traversed, de-duped on (subject, object, predicate). */
1143
- export function buildVizNodesAndEdges(graph, walked, { createdAtProp = CREATED_AT_PROP, updatedAtProp = UPDATED_AT_PROP } = {}) {
1144
- const nodeIds = new Set(walked.map((w) => w.id));
1145
- const nodes = walked.map(({ id, hop }) => {
1146
- const ind = graph.byId.get(id) || null;
1147
- const attrs = ind?.attributes || [];
1148
- const createdAt = attrs.find((a) => a?.prop === createdAtProp)?.value || "";
1149
- return {
1150
- id, hop, label: ind?.label || id, class: ind?.class || "", createdAt,
1151
- updatedAt: derivedUpdatedAt(graph, ind, { createdAtProp, updatedAtProp }),
1152
- };
1153
- });
1154
- const edges = [];
1155
- const seen = new Set();
1156
- for (const group of graph.relations || []) {
1157
- for (const e of group.edges || []) {
1158
- if (!nodeIds.has(e.subject) || !nodeIds.has(e.object)) continue;
1159
- const key = `${e.subject} ${e.object} ${group.predicate}`;
1160
- if (seen.has(key)) continue;
1161
- seen.add(key);
1162
- edges.push({ source: e.subject, target: e.object, kind: group.predicate });
1163
- }
1164
- }
1165
- return { nodes, edges };
1166
- }
1167
-
1168
- const MEMORY_FACT_CLASS = "Fact";
1169
- const MEMORY_TERM_CLASS = "Term";
1170
-
1171
- /** Derive a term-relation view of a memory graph's reified Facts (never
1172
- * mutates `graph`, viz-only). A Fact stores subject/predicate/object as
1173
- * plain string attributes, so the concept structure is invisible to a walk
1174
- * over `graph.relations` as-is; this synthesizes one Term individual per
1175
- * distinct subject/object string, one relation group per distinct fact
1176
- * predicate, and two fixed Fact->Term link groups so a walk seeded on a Fact
1177
- * can reach the term graph at all. Returns `{ graph: <augmented graph>,
1178
- * factRelationKinds: [<predicate>, …] }`; a graph with no Facts is a no-op. */
1179
- export function deriveFactTermGraph(graph) {
1180
- const termById = new Map(); // term:<t> -> individual
1181
- const groupByPredicate = new Map(); // predicate -> relation group
1182
- const subjectLinks = []; // Fact -> its subject Term
1183
- const objectLinks = []; // Fact -> its object Term
1184
- const termId = (t) => `term:${t}`;
1185
- const ensureTerm = (t) => {
1186
- const id = termId(t);
1187
- if (!termById.has(id)) termById.set(id, { id, label: t, class: MEMORY_TERM_CLASS, attributes: [] });
1188
- return id;
1189
- };
1190
-
1191
- for (const ind of graph?.individuals || []) {
1192
- if ((ind?.class || "") !== MEMORY_FACT_CLASS) continue;
1193
- const attrs = ind.attributes || [];
1194
- const s = attrs.find((a) => a?.prop === "rdf:subject")?.value;
1195
- const p = attrs.find((a) => a?.prop === "rdf:predicate")?.value;
1196
- const o = attrs.find((a) => a?.prop === "rdf:object")?.value;
1197
- if (!s || !p || !o) continue; // a malformed/legacy Fact — skip, never throw
1198
- const subjectTermId = ensureTerm(s);
1199
- const objectTermId = ensureTerm(o);
1200
- let group = groupByPredicate.get(p);
1201
- if (!group) {
1202
- group = { predicate: p, prop: `factrel:${p}`, count: 0, edges: [] };
1203
- groupByPredicate.set(p, group);
1204
- }
1205
- group.edges.push({ subject: subjectTermId, object: objectTermId, subjectLabel: s, objectLabel: o });
1206
- group.count = group.edges.length;
1207
- subjectLinks.push({ subject: ind.id, object: subjectTermId, subjectLabel: ind.label, objectLabel: s });
1208
- objectLinks.push({ subject: ind.id, object: objectTermId, subjectLabel: ind.label, objectLabel: o });
1209
- }
1210
-
1211
- if (!termById.size) return { graph, factRelationKinds: [] };
1212
-
1213
- const individuals = [...(graph.individuals || []), ...termById.values()];
1214
- const byId = new Map(graph.byId);
1215
- for (const term of termById.values()) byId.set(term.id, term);
1216
- const relations = [
1217
- ...(graph.relations || []),
1218
- ...groupByPredicate.values(),
1219
- { predicate: "factSubjectTerm", prop: "mgx:factSubjectTerm", count: subjectLinks.length, edges: subjectLinks },
1220
- { predicate: "factObjectTerm", prop: "mgx:factObjectTerm", count: objectLinks.length, edges: objectLinks },
1221
- ];
1222
-
1223
- return {
1224
- graph: { ...graph, individuals, byId, relations },
1225
- factRelationKinds: [...groupByPredicate.keys()],
1226
- };
1227
- }
1228
-
1229
- /** The two fixed structural link kinds `deriveFactTermGraph` always emits.
1230
- * Bundled into the "relation" (concept) walk, not "meta" (provenance), so a
1231
- * user toggling to meta-only still gets the provenance-only view. */
1232
- export const MEMORY_FACT_LINK_KINDS = ["factSubjectTerm", "factObjectTerm"];
1233
-
1234
- /** The combined kinds list a memory-graph walk uses for a given edge-kind
1235
- * mode: "meta" (provenance-only), "relation" (concept view), or "both"
1236
- * (default). Lives here, not viz.mjs, so the CLI and the browser bundle's
1237
- * client-side re-walk share the same function (viz.mjs does real fs I/O and
1238
- * can't be bundled for the browser). */
1239
- export function edgeKindsFor(mode, factRelationKinds) {
1240
- const relationKinds = [...factRelationKinds, ...MEMORY_FACT_LINK_KINDS];
1241
- if (mode === "meta") return [...MEMORY_SPIRAL_EXPAND_KINDS];
1242
- if (mode === "relation") return relationKinds;
1243
- return [...MEMORY_SPIRAL_EXPAND_KINDS, ...relationKinds]; // "both" (default)
1244
- }
1245
-
1246
- const LEGEND_MAX_BUCKETS = 20; // too many chips to be usable
1247
- const LEGEND_MIN_BUCKETS = 2; // nothing to filter with only one bucket
1248
- const LEGEND_COLLAPSE_TOP_N = 15; // "top 15 by count, rest grouped as Other" — stays under the max
1249
-
1250
- /** Normalized Shannon entropy (`H / log2(k)`, `k` = bucket count) of a
1251
- * {value, count} bucket list — 1.0 for a perfectly even split, ~0 for one
1252
- * dominant bucket swallowing everything, undefined (returns 0) for k<2. */
1253
- function normalizedEntropy(buckets) {
1254
- const k = buckets.length;
1255
- if (k < 2) return 0;
1256
- const total = buckets.reduce((sum, b) => sum + b.count, 0);
1257
- if (!total) return 0;
1258
- let h = 0;
1259
- for (const b of buckets) {
1260
- if (!b.count) continue;
1261
- const p = b.count / total;
1262
- h -= p * Math.log2(p);
1263
- }
1264
- return h / Math.log2(k);
1265
- }
1266
-
1267
- /** Collapse a raw {value,count} bucket list down to at most LEGEND_MAX_BUCKETS
1268
- * entries: keep the top LEGEND_COLLAPSE_TOP_N by count, fold the rest into a
1269
- * single "Other" bucket, applied generically (not predicate-only) so
1270
- * any dimension that happens to be high-cardinality degrades the same way.
1271
- * A no-op (returns `buckets` unchanged, same array) when already <= the cap.
1272
- * Exported (not just used internally by pickLegendDimension) so the browser
1273
- * bundle's client-side legend (live per-view recomputation, viz.mjs's own
1274
- * computeLegendBuckets) collapses high-cardinality dimensions the SAME way,
1275
- * never a second hand-rolled copy that could drift. */
1276
- export function collapseToTopN(buckets) {
1277
- if (buckets.length <= LEGEND_MAX_BUCKETS) return buckets;
1278
- const sorted = [...buckets].sort((a, b) => b.count - a.count || (a.value < b.value ? -1 : 1));
1279
- const kept = sorted.slice(0, LEGEND_COLLAPSE_TOP_N);
1280
- const restCount = sorted.slice(LEGEND_COLLAPSE_TOP_N).reduce((sum, b) => sum + b.count, 0);
1281
- return restCount ? [...kept, { value: "Other", count: restCount }] : kept;
1282
- }
1283
-
1284
- function bucketCounts(values) {
1285
- const counts = new Map();
1286
- for (const v of values) {
1287
- if (v == null || v === "") continue;
1288
- counts.set(v, (counts.get(v) || 0) + 1);
1289
- }
1290
- return [...counts.entries()].map(([value, count]) => ({ value, count }));
1291
- }
1292
-
1293
- /** The normalized provenance-prefix label for a Fact's FIRST recorded
1294
- * provenance tag (a Fact may carry a " | "-joined union of several; the
1295
- * legend buckets on the primary/first-recorded one, not a multiset) —
1296
- * reuses memory/core.mjs's own provenanceTagToSource parser (the SAME
1297
- * collapse-the-session-id/timestamp-suffix, keep-the-corpus/source-name
1298
- * logic the trust layer already relies on) rather than re-deriving it.
1299
- * Null when the Fact carries no provenance tag at all (a legacy/malformed
1300
- * row) or the tag doesn't parse to a known Source kind. */
1301
- function provenanceBucketLabel(rawTag) {
1302
- const tag = String(rawTag || "").split(" | ")[0].trim();
1303
- if (!tag) return null;
1304
- const src = provenanceTagToSource(tag);
1305
- if (!src) return null;
1306
- if (src.kind === "corpus" || src.kind === "corpusWeak") {
1307
- return `${src.kind === "corpusWeak" ? "corpus-weak" : "corpus"}:${src.name || "unknown"}`;
1308
- }
1309
- if (src.kind === "extracted") return `extracted:${src.name || "unknown"}`;
1310
- if (src.kind === "entailed") return `entailed:${src.rule || "unknown"}`;
1311
- if (src.kind === "operator") return "ace:chat";
1312
- if (src.kind === "teach") return "teach:chat";
1313
- if (src.kind === "web") return "web";
1314
- return src.kind;
1315
- }
1316
-
1317
- /** A single walked node's bucket value under one legend dimension. `"class"`
1318
- * reads every node; `"predicate"`/`"provenance"` only return non-null for a
1319
- * Fact-class node. */
1320
- export function legendValueFor(graph, node, dimension) {
1321
- if (dimension === "class") return node?.class || "(none)";
1322
- if (!node || node.class !== MEMORY_FACT_CLASS) return null;
1323
- const attrs = graph?.byId?.get?.(node.id)?.attributes || [];
1324
- if (dimension === "predicate") return attrs.find((a) => a?.prop === "rdf:predicate")?.value || null;
1325
- if (dimension === "provenance") return provenanceBucketLabel(attrs.find((a) => a?.prop === "mgx:factProvenance")?.value);
1326
- return null;
1327
- }
1328
-
1329
- /** Auto-pick the filter/legend dimension: since Fact dominates class-based
1330
- * legends once real memory-graph data is seeded, this scores class/predicate/
1331
- * provenance by normalized Shannon entropy over their bucket distribution
1332
- * and picks the best-scoring qualifying one (LEGEND_MIN_BUCKETS..MAX_BUCKETS).
1333
- * Falls back to "class" when nothing qualifies. Returns `{ primary,
1334
- * dimensions: { class, predicate, provenance } }`. */
1335
- export function pickLegendDimension(graph, nodes) {
1336
- const classBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "class")));
1337
- const predicateBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "predicate")));
1338
- const provenanceBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "provenance")));
1339
-
1340
- const score = (rawBuckets) => {
1341
- const buckets = collapseToTopN(rawBuckets);
1342
- const qualifies = buckets.length >= LEGEND_MIN_BUCKETS && buckets.length <= LEGEND_MAX_BUCKETS;
1343
- return { score: normalizedEntropy(buckets), qualifies, buckets };
1344
- };
1345
-
1346
- const dimensions = {
1347
- class: score(classBuckets),
1348
- predicate: score(predicateBuckets),
1349
- provenance: score(provenanceBuckets),
1350
- };
1351
-
1352
- let primary = "class";
1353
- let bestScore = -1;
1354
- for (const [name, d] of Object.entries(dimensions)) {
1355
- if (!d.qualifies) continue;
1356
- if (d.score > bestScore) { bestScore = d.score; primary = name; }
1357
- }
1358
- return { primary, dimensions };
1359
- }
1360
-
1361
1107
  /** moduleIdOf by raw edge-endpoint id: resolves through byId when the individual exists,
1362
1108
  * else falls back to parsing an `fn:<path>#name` id directly (callsSymbol objects may name
1363
1109
  * symbols with no individual of their own). Null if it cannot be mapped. */
@@ -1528,16 +1274,32 @@ export function renderArchitecture(graph, { pkg = "" } = {}) {
1528
1274
  }
1529
1275
 
1530
1276
  const COVERAGE_CAP = 40;
1277
+ const TESTS_GRAIN_NOTE = "tests edges are recorded module to module, so this is module-grain coverage.";
1531
1278
 
1532
1279
  /** The test modules covering a symbol/module — from the `tests` (mgx:testsCoverage)
1533
- * relation. Replaces grepping `tests/` for who imports the target. */
1280
+ * relation. Replaces grepping `tests/` for who imports the target.
1281
+ *
1282
+ * `tests` edges run module to module, so a FUNCTION has no coverage of its
1283
+ * own to report and the answer is its defining module's. Asked about a
1284
+ * function, say both: which module the question hopped to, and that the
1285
+ * coverage is module-grain. Without that the answer names a module the user
1286
+ * never mentioned and silently passes off module coverage as the function's.
1287
+ * The grain note sits outside the indented list on purpose — indented, it
1288
+ * reads as one more test module. */
1534
1289
  export function renderTestsFor(graph, ind) {
1535
1290
  const modId = moduleIdOf(graph, ind);
1536
1291
  if (!modId) return `cannot map ${ind.label} to a module.`;
1537
- const modLabel = graph.byId.get(modId)?.label || modId;
1292
+ // A symbol's module id is derived from its own recorded site, so it can name
1293
+ // a module the index doesn't carry an individual for. The site's path is
1294
+ // then the only honest spelling of it — never the raw id.
1295
+ const modLabel = graph.byId.get(modId)?.label || siteOf(ind)?.path || modId;
1538
1296
  const tests = [...new Set(edgesOfKind(graph, "tests").filter((e) => e.object === modId).map((e) => e.subjectLabel || e.subject))];
1539
- if (!tests.length) return `${modLabel}: no covering tests recorded (no test module imports it).`;
1540
- return `${modLabel}: covered by ${tests.length} test module(s):\n ${capJoin(tests, COVERAGE_CAP, "\n ")}`;
1297
+ const covered = tests.length > 0;
1298
+ const verdict = covered
1299
+ ? `covered by ${tests.length} test module(s):\n ${capJoin(tests, COVERAGE_CAP, "\n ")}`
1300
+ : "no covering tests recorded (no test module imports it).";
1301
+ if (modId === ind.id) return `${modLabel}: ${verdict}`;
1302
+ return `${ind.label} is defined in ${modLabel}, which ${covered ? "is" : "has"} ${verdict}\n${TESTS_GRAIN_NOTE}`;
1541
1303
  }
1542
1304
 
1543
1305
  /** Source modules with no covering test module — a coverage gap view. Test
@@ -2217,52 +1979,6 @@ export function renderGraphOnlyBundle(plan, mask) {
2217
1979
  return out.join("\n");
2218
1980
  }
2219
1981
 
2220
- // ---- cold-tool catalog (written to <repo>/.tmct/TOOLS.md by the index step) -----
2221
-
2222
- /** Markdown catalog of the COLD tools (everything except the hot catalog tools): each
2223
- * with a one-line purpose and the exact Bash invocation via the CLI `cli <tool>` route.
2224
- * Pure — `cliPath` is the absolute path to bin/cli.mjs the caller wants embedded. */
2225
- export function renderToolsCatalog(cliPath) {
2226
- const cold = [
2227
- ["tmct_describe", "Locate one symbol and list its typed edges (both directions) with provenance.", { symbol: "django/utils/text.py" }],
2228
- ["tmct_signature", "One symbol's API surface (params, returns, raises/catches, flags, decorators, doc) without the body.", { symbol: "Truncator.chars" }],
2229
- ["tmct_impact", "Transitive reverse closure over imports/calls — what breaks if a module changes, by depth, with tests.", { module: "django/utils/text.py" }],
2230
- ["tmct_search", "Free-text/ranked lookup over the code-map to find the right module or symbol.", { query: "template filters", kind: "function" }],
2231
- ["tmct_members", "A class's methods + attributes (file:line, decorators) in one slice.", { class: "Truncator" }],
2232
- ["tmct_subclasses", "A class's base classes plus the transitive set of classes that extend it.", { class: "Field" }],
2233
- ["tmct_architecture", "Package/module map + the most-imported hub modules (optionally scoped to a package).", { package: "django/template" }],
2234
- ["tmct_exports", "A module's public __all__ surface, each name resolved to the module that defines it.", { module: "django/db/models/__init__.py" }],
2235
- ["tmct_tests_for", "The test modules covering a symbol or module, from the typed test edges.", { symbol: "django/utils/text.py" }],
2236
- ["tmct_untested", "Source modules with no covering test module — a coverage-gap view (no arguments).", {}],
2237
- ["tmct_history", "Recent commits that touched a symbol's module (newest first).", { symbol: "django/utils/text.py" }],
2238
- ["tmct_file_history", "Commits that touched a symbol's module, each with author / date / subject.", { symbol: "django/utils/text.py" }],
2239
- ["tmct_method_history", "Commits that touched a specific method symbol (fine-grained), with author / date / subject.", { symbol: "Truncator.chars" }],
2240
- ["tmct_class_history", "Commits that touched a specific class symbol (fine-grained), with author / date / subject.", { symbol: "Truncator" }],
2241
- ["tmct_callers", "Modules that call into a symbol's module (one hop).", { symbol: "django/utils/text.py" }],
2242
- ["tmct_callees", "Modules a symbol's module calls into (one hop).", { symbol: "django/utils/text.py" }],
2243
- ["tmct_calls", "The in-repo symbols a function calls (fn→fn), each with file:line.", { symbol: "slugify" }],
2244
- ["tmct_cochanges", "Modules that historically change in the same commit as a symbol's module (git co-change).", { symbol: "django/utils/text.py" }],
2245
- ["tmct_context_more", "The bundle sections a lean tmct_context omitted (siblings / tests / cochange / class members / re-exports).", { symbol: "django/utils/text.py" }],
2246
- ];
2247
- const lines = [
2248
- "# tmct cold-tool catalog",
2249
- "",
2250
- "The hot tools — `tmct_context` (start here to add/modify code; supports `depth: min|auto|full`) and `tmct_snippet` (exact source of one symbol) — carry full schemas in the TOOLS catalog.",
2251
- "",
2252
- "The cold tools below invoke via the CLI:",
2253
- "",
2254
- ];
2255
- for (const [name, purpose, args] of cold) {
2256
- lines.push(`## ${name}`);
2257
- lines.push(purpose);
2258
- lines.push("```bash");
2259
- lines.push(`node ${cliPath} cli ${name} '${JSON.stringify(args)}'`);
2260
- lines.push("```");
2261
- lines.push("");
2262
- }
2263
- return lines.join("\n");
2264
- }
2265
-
2266
1982
  // ---- change-coupling (git co-change) — "what usually changes together" ----------
2267
1983
 
2268
1984
  /** [{label, weight}] modules co-changed with modId, sorted by count desc. Pure. */
@@ -7,13 +7,12 @@ import { groupHits } from "./group.mjs";
7
7
  import { rankSentences } from "./rank.mjs";
8
8
  import { inferRelations } from "./infer.mjs";
9
9
  import { pruneCompletion } from "./prune.mjs";
10
- import { loadMemory } from "../memory/core.mjs";
11
- import { finish, grammarRules } from "../finish.mjs";
10
+ import { requireInjected } from "./injected.mjs";
12
11
 
13
12
  /** grammarRules() with sentence-capitalisation force-enabled (disabled in live chat only to
14
13
  * protect single-answer lowercase-opener goldens, which don't apply to a multi-sentence
15
14
  * completion). */
16
- function completionGrammarRules() {
15
+ function completionGrammarRules(grammarRules) {
17
16
  return grammarRules().map((r) => (r.id === "sentence-capitalisation" ? { ...r, enabled: true } : r));
18
17
  }
19
18
 
@@ -36,8 +35,11 @@ const DEFAULT_MAX_SENTENCES_PER_GROUP = 3; // see prune.mjs's own file header fo
36
35
  * @param {string} [opts.query] the query rankSentences()/pruning focus on; defaults to `prompt`;
37
36
  * pass `null` for self-weighted (LexRank-style) ranking instead
38
37
  * @param {number} [opts.maxSentencesPerGroup=3] prune.mjs's top-K-per-group cutoff
39
- * @param {object} [opts.graph] optional loaded graph (src/codegraph.mjs parseEntities() shape)
38
+ * @param {object} [opts.graph] optional loaded graph (src/domain/codegraph.mjs parseEntities() shape)
40
39
  * handed to finish()'s maskSegments to protect known entity labels during the grammar pass
40
+ * @param {object} opts.store REQUIRED — the memory/block store handles this pipeline reads
41
+ * through; `loadMemory` here, plus whatever each stage below requires of its own
42
+ * @param {object} opts.finisher REQUIRED — the prose finisher's `{ finish, grammarRules }`
41
43
  * @returns {Promise<{
42
44
  * text: string,
43
45
  * sourceSpans: Array<{sourceBlockId:string, groupId:string, sentence:string}>,
@@ -51,22 +53,26 @@ export async function generateCompletion(dir, prompt, opts = {}) {
51
53
  const {
52
54
  blockK, graphService = null, graphLimit, overlapMin,
53
55
  memory: memoryOpt, query = prompt, maxSentencesPerGroup = DEFAULT_MAX_SENTENCES_PER_GROUP,
54
- graph,
56
+ graph, store, finisher,
55
57
  } = opts;
58
+ const { loadMemory } = requireInjected(store, ["loadMemory"], { caller: "generateCompletion", option: "store" });
59
+ const { finish, grammarRules } = requireInjected(
60
+ finisher, ["finish", "grammarRules"], { caller: "generateCompletion", option: "finisher" },
61
+ );
56
62
 
57
63
  // Stage 1 — broad search
58
- const hits = await broadSearch(dir, prompt, { blockK, graphService, graphLimit });
64
+ const hits = await broadSearch(dir, prompt, { blockK, graphService, graphLimit, store });
59
65
 
60
66
  // Stage 2 — grouping
61
- const groups = groupHits(hits, { overlapMin });
67
+ const groups = groupHits(hits, { overlapMin, store });
62
68
 
63
69
  // Stage 3 — cross-group inference
64
70
  const memory = memoryOpt || await loadMemory(dir);
65
- const relations = groups.length >= 2 ? await inferRelations(groups, memory) : [];
71
+ const relations = groups.length >= 2 ? await inferRelations(groups, memory, { store }) : [];
66
72
 
67
73
  // Stage 4 — extractive sentence ranking, per group (query-focused unless the caller opted out)
68
74
  const rankedByGroup = {};
69
- for (const g of groups) rankedByGroup[g.id] = rankSentences(g, { query });
75
+ for (const g of groups) rankedByGroup[g.id] = rankSentences(g, { query, store });
70
76
 
71
77
  // Stage 5 — pruning: decide keep/drop, with an itemized, auditable drop log
72
78
  const { kept, dropped } = pruneCompletion(
@@ -84,7 +90,7 @@ export async function generateCompletion(dir, prompt, opts = {}) {
84
90
  const rawText = kept.map((k) => k.sentence).join(" ");
85
91
 
86
92
  // Stage 6 — grammar/voice pass (reuses finish() verbatim; see completionGrammarRules() above).
87
- const finished = finish({ answer: rawText, via: "completion" }, { graph, rules: completionGrammarRules() });
93
+ const finished = finish({ answer: rawText, via: "completion" }, { graph, rules: completionGrammarRules(grammarRules) });
88
94
 
89
95
  // sourceSpans traces every output sentence back to its block id + group id, from `kept`
90
96
  // (pre-grammar-pass) — stays index-aligned since finish() only mutates casing/punctuation.
@@ -7,9 +7,9 @@
7
7
  // renderDescribe and readFactRows(memory), falling back to svc.ask() only when it genuinely
8
8
  // parsed the term.
9
9
 
10
- import { createGraphService } from "../providers/graph-service.mjs";
11
10
  import { resolveSymbol, renderDescribe } from "../codegraph.mjs";
12
- import { readFactRows } from "../memory/core.mjs";
11
+ import { ask } from "../ask.mjs";
12
+ import { requireInjected } from "./injected.mjs";
13
13
 
14
14
  /** renderDescribe() renders one line per fact with no terminal punctuation of its own;
15
15
  * rank.mjs's splitSentences() treats each line as its own candidate sentence, and
@@ -27,10 +27,16 @@ function withTerminalPunctuation(text) {
27
27
  /**
28
28
  * @param {object|null} graph a parseEntities() result, or null when no code graph is loaded
29
29
  * @param {object|null} [memory=null] a loadMemory() payload, or null when there's no Fact store
30
+ * @param {object} [opts]
31
+ * @param {object} opts.store REQUIRED — the `{ createGraphService, readFactRows }` handles
32
+ * this adapter builds its two sentence sources from
30
33
  * @returns {{search: Function, ask: Function}} a Repository-Interface-shaped graphService
31
34
  */
32
- export function createCompletionsGraphAdapter(graph, memory = null) {
33
- const svc = graph ? createGraphService(graph) : null;
35
+ export function createCompletionsGraphAdapter(graph, memory = null, { store } = {}) {
36
+ const { createGraphService, readFactRows } = requireInjected(
37
+ store, ["createGraphService", "readFactRows"], { caller: "createCompletionsGraphAdapter", option: "store" },
38
+ );
39
+ const svc = graph ? createGraphService(graph, { ask }) : null;
34
40
 
35
41
  return {
36
42
  search(q, { limit } = {}) {
@@ -3,8 +3,8 @@
3
3
  // memory/blocks.mjs's buildNeighbours()/OVERLAP_MIN). Block granularity, no sub-block spans.
4
4
  // Each group's label is its top shared-IDF tokens (df/N over the hit set, not the corpus).
5
5
 
6
- import { buildNeighbours, OVERLAP_MIN, tokenizeBlock } from "../memory/blocks.mjs";
7
6
  import { STOPWORDS } from "../prose.mjs";
7
+ import { requireInjected } from "./injected.mjs";
8
8
 
9
9
  const LABEL_TOKEN_COUNT = 5;
10
10
 
@@ -14,8 +14,8 @@ const isContentToken = (t) => /^[a-z0-9]+$/.test(t) && !STOPWORDS.has(t);
14
14
 
15
15
  /** tokenizeBlock(text), narrowed to real content tokens (see isContentToken above) — the
16
16
  * token set this module actually clusters and labels on. */
17
- function contentTokens(text) {
18
- return tokenizeBlock(text).filter(isContentToken);
17
+ function makeContentTokens(tokenizeBlock) {
18
+ return (text) => tokenizeBlock(text).filter(isContentToken);
19
19
  }
20
20
 
21
21
  /** Plain union-find (path halving, union-by-index) — small N here (a single broad search's
@@ -41,13 +41,21 @@ function unionFind(n) {
41
41
  *
42
42
  * @param {Array<{id:string, text:string}>} hits
43
43
  * @param {object} [opts]
44
- * @param {number} [opts.overlapMin=OVERLAP_MIN] shared-token threshold for a similarity edge
44
+ * @param {number} [opts.overlapMin] shared-token threshold for a similarity edge; defaults to
45
+ * the store's own OVERLAP_MIN
46
+ * @param {object} opts.store REQUIRED — the block store's `{ buildNeighbours, tokenizeBlock,
47
+ * OVERLAP_MIN }` clustering handles
45
48
  * @returns {Array<{ id: string, members: Array<{id:string, text:string}>, memberIds: string[],
46
49
  * tokens: string[], label: string }>}
47
50
  * One entry per connected component (a singleton hit is still a one-member group).
48
51
  * Deterministic order: groups by lowest member id; members within a group by id.
49
52
  */
50
- export function groupHits(hits, { overlapMin = OVERLAP_MIN } = {}) {
53
+ export function groupHits(hits, { overlapMin, store } = {}) {
54
+ const { buildNeighbours, tokenizeBlock, OVERLAP_MIN } = requireInjected(
55
+ store, ["buildNeighbours", "tokenizeBlock", "OVERLAP_MIN"], { caller: "groupHits", option: "store" },
56
+ );
57
+ const contentTokens = makeContentTokens(tokenizeBlock);
58
+ const edgeThreshold = overlapMin ?? OVERLAP_MIN;
51
59
  const list = Array.isArray(hits) ? hits.filter((h) => h && h.id != null) : [];
52
60
  if (!list.length) return [];
53
61
 
@@ -63,7 +71,7 @@ export function groupHits(hits, { overlapMin = OVERLAP_MIN } = {}) {
63
71
  const tokensById = {};
64
72
  for (const h of deduped) tokensById[h.id] = contentTokens(h.text || "");
65
73
 
66
- const { ids, neighbours } = buildNeighbours(tokensById, overlapMin);
74
+ const { ids, neighbours } = buildNeighbours(tokensById, edgeThreshold);
67
75
  const { find, union } = unionFind(ids.length);
68
76
  for (let i = 0; i < ids.length; i += 1) {
69
77
  for (const j of neighbours[i]) union(i, j);