@polycode-projects/the-mechanical-code-talker 1.12.0 → 2.0.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.
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} +65 -26
  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
@@ -0,0 +1,163 @@
1
+ // tmct_context — the sized "edit bundle" for one symbol: exemplar source, sibling
2
+ // signatures, the registration anchor and the insertion region, in one call.
3
+
4
+ import { readFile } from "node:fs/promises";
5
+ import { dirname } from "node:path";
6
+ import { ToolError } from "../../adapters/config.mjs";
7
+ import { sliceSpan, readSpanSafe } from "../../adapters/source-slice.mjs";
8
+ import * as defaultSource from "../../adapters/source.mjs";
9
+ import { contextPlan, sizeBundle, bundleMask, trimBundleMask } from "../../domain/codegraph.mjs";
10
+ import { ask } from "../../domain/ask.mjs";
11
+ import { createGraphService } from "../../adapters/providers/graph-service.mjs";
12
+ import { loadGraph } from "../graph-load.mjs";
13
+ import { resolveOrThrow, SNIPPET_MAX_LINES } from "./kit.mjs";
14
+
15
+ /**
16
+ * Build the tmct_context "edit bundle" for a symbol and return { text, tier, topup }.
17
+ * Shared by the tmct_context tool AND the `cli digest` arm (bin/tmct.mjs). `trim:true` renders
18
+ * a SECONDARY, signatures-only bundle (no bodies/tails) for related-but-not-primary
19
+ * digest modules.
20
+ *
21
+ * Section order is cache-stable: content identical across runs (anchor/registration/
22
+ * exemplar/siblings/__all__/insertion region) comes first; more variable, history-derived
23
+ * tails (covering tests, co-change) come last, so a stable prefix maximises prompt-cache
24
+ * reuse.
25
+ */
26
+ export async function buildContextBundle(args, { config, source = defaultSource, trim = false, tel = null } = {}) {
27
+ const symbol = String(args?.symbol || "").trim();
28
+ if (!symbol) throw new ToolError("symbol is required");
29
+ const depth = String(args?.depth || "auto").trim().toLowerCase();
30
+ // Tuning-flag contract: `min` forces the LEANEST bundle (TINY mask, no top-up) regardless of
31
+ // exemplar length; `untuned` reproduces the earlier escalation (tuning #1 bypassed). Neither
32
+ // → the tuned default (sizeBundle's anchor-gated escalation).
33
+ const min = Boolean(args?.min);
34
+ const untuned = Boolean(args?.untuned);
35
+ // `max` forces the injection CEILING — FULL tier (every section + inlined depth-1 callee
36
+ // bodies) with top-up, and it OVERRIDES trim so even secondary modules get the full bundle. Used
37
+ // by the tmct-max arm to test whether more injection re-bloats.
38
+ const max = Boolean(args?.max);
39
+ const graph = await loadGraph(config, source);
40
+ // repo root = the dir containing .tmct/ (graphFile = <repo>/.tmct/graph.json) — computed
41
+ // before createGraphService so the RI service can be constructed source-capable: this
42
+ // module still does its OWN safe reads below (readSpanSafe/sliceSpan) rather than
43
+ // delegating to svc.context(). Passing sourceAccess through anyway keeps
44
+ // svc.snippet()/svc.context() usable by any future/external caller of this same service
45
+ // object without a second, divergent construction path. `tel` (optional) is an
46
+ // already-constructed telemetry sink threaded down from the caller (e.g. chat.mjs's
47
+ // session-level createTelemetry) — never minted here, so a caller that never passes one
48
+ // costs nothing extra (createGraphService's own wrapping loop no-ops on tel:null).
49
+ const repoRoot = dirname(dirname(config.graphFile));
50
+ const svc = createGraphService(graph, { sourceAccess: true, repoRoot, readFile, tel, ask });
51
+ const { match } = resolveOrThrow(svc, symbol, "symbol");
52
+ const plan = contextPlan(graph, match);
53
+ // Pick the section mask by depth — min forces TINY, full/max forces everything, auto runs
54
+ // the size classifier (lean TINY default + one-tier top-up when the edit needs it).
55
+ let tier;
56
+ let mask;
57
+ let topup = false;
58
+ if (min || depth === "min") { tier = "TINY"; mask = bundleMask("TINY"); }
59
+ else if (max || depth === "full") { tier = "FULL"; mask = bundleMask("FULL"); topup = true; }
60
+ else ({ tier, mask, topup } = sizeBundle(plan, graph, { untuned }));
61
+ if (trim && !max) mask = trimBundleMask(mask);
62
+ let lines = null;
63
+ if (plan.moduleLabel) {
64
+ try { ({ lines } = await readSpanSafe({ readFile, repoRoot, path: plan.moduleLabel })); }
65
+ catch { lines = null; }
66
+ }
67
+ const lineAt = (n) => (lines && lines[n - 1] != null ? lines[n - 1].trim() : "");
68
+ const sliceBody = (start, end) => sliceSpan(lines, start, end, SNIPPET_MAX_LINES).text;
69
+ const out = [
70
+ `Edit context for ${plan.moduleLabel} [${tier}${trim ? " secondary" : ""}] — assembled from the typed graph + that file. ` +
71
+ "You do NOT need to Read it; write the new code directly after reviewing this.",
72
+ ];
73
+ // ---- cache-stable prefix: identical across runs ----
74
+ if (mask.anchor && plan.anchor?.site && lines) {
75
+ const { start, end } = plan.anchor.site;
76
+ out.push(`\n## anchor: ${plan.anchor.label} (${plan.anchor.class}) @ ${plan.moduleLabel}:${start}-${end}`);
77
+ out.push(sliceBody(start, end));
78
+ if (plan.callHint) out.push(plan.callHint);
79
+ }
80
+ if (mask.registration && plan.globals.length) {
81
+ out.push(`\n## registration / module globals (replicate this pattern):`);
82
+ for (const g of plan.globals) out.push(` ${g.label} = ${g.value}${g.site ? ` [:${g.site.start}]` : ""}`);
83
+ }
84
+ if (mask.exemplar && plan.exemplar?.site && lines) {
85
+ const { start, end } = plan.exemplar.site;
86
+ const dec = plan.exemplar.decorators ? ` @${plan.exemplar.decorators}` : "";
87
+ out.push(`\n## closest example (full body) — copy this style: ${plan.exemplar.label} (${plan.exemplar.class})${dec} @ ${plan.moduleLabel}:${start}-${end}`);
88
+ out.push(sliceBody(start, end));
89
+ if (plan.callHint) out.push(plan.callHint);
90
+ }
91
+ if (mask.inlinedCallees && plan.calleeBodies.length && lines) {
92
+ let budget = 120; // INLINE_CALLEE_LOC
93
+ for (const cb of plan.calleeBodies) {
94
+ if (budget <= 0) break;
95
+ const start = cb.site.start;
96
+ const fromThisFile = cb.site.path === plan.moduleLabel;
97
+ const bodyLines = fromThisFile && lines
98
+ ? lines
99
+ : await readSpanSafe({ readFile, repoRoot, path: cb.site.path }).then((r) => r.lines).catch(() => null);
100
+ if (!bodyLines) continue;
101
+ const sliced = sliceSpan(bodyLines, start, cb.site.end, budget);
102
+ out.push(`\n## inlined callee body (depth-1 in-repo call): ${cb.label} @ ${cb.site.path}:${start}-${cb.site.end}`);
103
+ out.push(sliced.text);
104
+ budget -= (sliced.end - start + 1);
105
+ }
106
+ }
107
+ if (mask.classMembers && plan.classMembers && plan.classMembers.members.length) {
108
+ out.push(`\n## members of ${plan.classMembers.className} (the edit likely lives INSIDE this class — copy a member's shape, do not read the class body):`);
109
+ for (const m of plan.classMembers.members) {
110
+ const short = String(m.label).split(".").pop();
111
+ const sig = m.params != null && m.params !== "" ? `(${m.params})${m.returns ? ` -> ${m.returns}` : ""}` : "";
112
+ const dec = m.decorators ? `@${m.decorators} ` : "";
113
+ const r = m.raises ? ` raises=${m.raises}` : "";
114
+ out.push(` ${m.class} ${short}${m.site ? ` :${m.site.start}` : ""} ${dec}${short}${sig}${r}`);
115
+ }
116
+ }
117
+ if (mask.siblings && plan.siblings.length) {
118
+ out.push(`\n## sibling symbols to copy the style of (most relevant first; ${plan.siblings.length} total):`);
119
+ for (const s of plan.siblings.slice(0, plan.siblingCap)) {
120
+ const sig = s.site ? lineAt(s.site.start) : "";
121
+ const dec = s.decorators ? `@${s.decorators} ` : "";
122
+ const r = s.raises ? ` raises=${s.raises}` : "";
123
+ out.push(` ${s.class} ${s.label}${s.site ? ` :${s.site.start}` : ""} ${dec}${sig}${r}`);
124
+ }
125
+ if (plan.siblings.length > plan.siblingCap) {
126
+ out.push(` …+${plan.siblings.length - plan.siblingCap} more (use tmct_search kind=function or tmct_snippet <name> for any of them)`);
127
+ }
128
+ }
129
+ if (mask.allExports && plan.allExports) {
130
+ out.push(`\n## module __all__ — this module curates its public API; ADD your new public symbol to this list so it is importable:\n ${plan.allExports}`);
131
+ }
132
+ if (mask.reexports && plan.exports && plan.exports.length) out.push(`\n## re-exported symbols (resolved __all__ → defining module): ${plan.exports.join(", ")}`);
133
+ // The contiguous insertion region is part of the STABLE prefix — always present (even at
134
+ // TINY) so the agent never needs to Read the file to place the edit.
135
+ if (mask.insertionRegion && plan.insertionRegion && lines) {
136
+ const start = plan.insertionRegion.start;
137
+ const end = Math.min(lines.length, start + 40 - 1); // INSERTION_REGION_CAP
138
+ out.push(`\n## insertion region (write your new sibling here) — ${plan.moduleLabel}:${start}-${end}`);
139
+ out.push(lines.slice(start - 1, end).map((l, i) => `${start + i}\t${l}`).join("\n"));
140
+ } else if (plan.insertion) {
141
+ out.push(`\n## insert the new sibling after line ~${plan.insertion} (end of the last top-level definition).`);
142
+ }
143
+ // ---- variable tail: history-derived, kept LAST so the prefix stays cache-stable ----
144
+ if (mask.tests && plan.tests.length) out.push(`\n## covering tests: ${plan.tests.join(", ")}`);
145
+ if (mask.cochange && plan.cochange && plan.cochange.length) {
146
+ out.push(`\n## usually changed together (consider editing these too): ${plan.cochange.map((c) => `${c.label} (×${c.weight})`).join(", ")}`);
147
+ }
148
+ out.push(`\nYou now have the snippet, the sibling style, the registration anchor and the tests. ` +
149
+ `Write the new code with Edit/Write — do NOT Read ${plan.moduleLabel}.`);
150
+ if (tier !== "FULL") {
151
+ out.push(`(bundle tier ${tier}; for any omitted sections run tmct_context_more {"symbol":"${symbol}"}, or tmct_context with depth="full".)`);
152
+ }
153
+ return { text: out.join("\n"), tier, topup };
154
+ }
155
+
156
+ export async function tmct_context(args, { config, source, tel }) {
157
+ return (await buildContextBundle(args, { config, source, tel })).text;
158
+ }
159
+
160
+ // buildContextBundle validates `symbol` before it loads anything and then loads the graph
161
+ // itself, so dispatch hands this handler the raw config and skips its own load rather than
162
+ // loading the graph twice.
163
+ tmct_context.ownsGraphLoad = true;
@@ -0,0 +1,15 @@
1
+ // tmct_describe — locate one symbol and list its typed edges, falling through to the
2
+ // memory/corpus facts when the concept is known there but absent from the code-map.
3
+
4
+ import { renderDescribe, resolveSymbol } from "../../domain/codegraph.mjs";
5
+ import { memoryFactRows, renderMemoryDefinition } from "../memory-fallthrough.mjs";
6
+ import { requiredArg, resolveOrThrow } from "./kit.mjs";
7
+
8
+ export async function tmct_describe(args, { graph, svc, config }) {
9
+ const symbol = requiredArg(args, "symbol");
10
+ const { match, candidates } = resolveSymbol(svc.graph, symbol);
11
+ if (match) return renderDescribe(graph, match, { candidates }); // code-map wins when present
12
+ const fallback = renderMemoryDefinition(await memoryFactRows(config), symbol);
13
+ if (fallback) return fallback;
14
+ resolveOrThrow(svc, symbol, "symbol"); // no code-map + no memory fact → the honest miss
15
+ }
@@ -0,0 +1,9 @@
1
+ // tmct_exports — a module's public __all__ surface, each name resolved to its definer.
2
+
3
+ import { renderExports } from "../../domain/codegraph.mjs";
4
+ import { requiredArg, resolveOrThrow } from "./kit.mjs";
5
+
6
+ export function tmct_exports(args, { graph, svc }) {
7
+ const { match } = resolveOrThrow(svc, requiredArg(args, "module"), "module");
8
+ return renderExports(graph, match);
9
+ }
@@ -0,0 +1,6 @@
1
+ // tmct_file_history — commits that touched a symbol's module, with author / date / subject.
2
+
3
+ import { renderFileHistory } from "../../domain/codegraph.mjs";
4
+ import { symbolHandler } from "./kit.mjs";
5
+
6
+ export const tmct_file_history = symbolHandler(renderFileHistory);
@@ -0,0 +1,6 @@
1
+ // tmct_history — recent commits that touched a symbol's module, newest first.
2
+
3
+ import { renderHistory } from "../../domain/codegraph.mjs";
4
+ import { symbolHandler } from "./kit.mjs";
5
+
6
+ export const tmct_history = symbolHandler(renderHistory);
@@ -0,0 +1,9 @@
1
+ // tmct_impact — transitive reverse closure over imports/calls: what breaks if a module changes.
2
+
3
+ import { renderImpact } from "../../domain/codegraph.mjs";
4
+ import { requiredArg, resolveOrThrow } from "./kit.mjs";
5
+
6
+ export function tmct_impact(args, { graph, svc }) {
7
+ const { match } = resolveOrThrow(svc, requiredArg(args, "module"), "module");
8
+ return renderImpact(graph, match);
9
+ }
@@ -0,0 +1,16 @@
1
+ // tmct_members — a class's methods and attributes. A concept's "members" in the corpus
2
+ // sense are its subclasses (its instances), which is the fall-through when the code-map
3
+ // has no such class.
4
+
5
+ import { renderMembers, resolveSymbol } from "../../domain/codegraph.mjs";
6
+ import { memoryFactRows, renderMemorySubclasses } from "../memory-fallthrough.mjs";
7
+ import { requiredArg, resolveOrThrow } from "./kit.mjs";
8
+
9
+ export async function tmct_members(args, { graph, svc, config }) {
10
+ const symbol = requiredArg(args, "class");
11
+ const { match } = resolveSymbol(svc.graph, symbol);
12
+ if (match) return renderMembers(graph, match); // code-map wins when present
13
+ const fallback = renderMemorySubclasses(await memoryFactRows(config), symbol);
14
+ if (fallback) return fallback;
15
+ resolveOrThrow(svc, symbol, "class"); // the honest miss
16
+ }
@@ -0,0 +1,6 @@
1
+ // tmct_method_history — commits that touched one method symbol.
2
+
3
+ import { renderMethodHistory } from "../../domain/codegraph.mjs";
4
+ import { symbolHandler } from "./kit.mjs";
5
+
6
+ export const tmct_method_history = symbolHandler(renderMethodHistory);
@@ -0,0 +1,22 @@
1
+ // tmct_search — free-text/ranked lookup over the code-map, falling through to the
2
+ // memory/corpus facts when a plain concept query misses the code-map entirely.
3
+
4
+ import { ToolError } from "../../adapters/config.mjs";
5
+ import { renderSearch } from "../../domain/codegraph.mjs";
6
+ import { memoryFactRows, renderMemoryDefinition } from "../memory-fallthrough.mjs";
7
+
8
+ export async function tmct_search(args, { graph, config }) {
9
+ const query = String(args?.query || "").trim();
10
+ const kind = String(args?.kind || "").trim();
11
+ if (!query && !kind) throw new ToolError("query is required");
12
+ const out = renderSearch(graph, query, {
13
+ kind,
14
+ decorator: String(args?.decorator || "").trim(),
15
+ name: String(args?.name || "").trim(),
16
+ });
17
+ if (!kind && /^no module matches/.test(out)) {
18
+ const fallback = renderMemoryDefinition(await memoryFactRows(config), query);
19
+ if (fallback) return fallback;
20
+ }
21
+ return out;
22
+ }
@@ -0,0 +1,6 @@
1
+ // tmct_signature — one symbol's API surface without the body.
2
+
3
+ import { renderSignature } from "../../domain/codegraph.mjs";
4
+ import { symbolHandler } from "./kit.mjs";
5
+
6
+ export const tmct_signature = symbolHandler(renderSignature);
@@ -0,0 +1,37 @@
1
+ // tmct_snippet — the exact source of one symbol's line span, plus a one-line in-repo
2
+ // call hint. Does its own safe span read rather than delegating to svc.snippet(), which
3
+ // would drop the candidates / call hint / truncation notice this presentation carries.
4
+
5
+ import { readFile } from "node:fs/promises";
6
+ import { ToolError } from "../../adapters/config.mjs";
7
+ import { readSpanSafe } from "../../adapters/source-slice.mjs";
8
+ import { siteOf, callHint } from "../../domain/codegraph.mjs";
9
+ import { requiredArg, resolveOrThrow, SNIPPET_MAX_LINES } from "./kit.mjs";
10
+
11
+ export async function tmct_snippet(args, { graph, svc, repoRoot }) {
12
+ const symbol = requiredArg(args, "symbol");
13
+ const { match, candidates } = resolveOrThrow(svc, symbol, "symbol");
14
+ const site = siteOf(match);
15
+ if (!site) {
16
+ throw new ToolError(
17
+ `"${match.label}" (${match.class || "Entity"}) has no source span in the graph — ` +
18
+ "it is likely a module. Use tmct_describe for its contents, then tmct_snippet one of the functions/classes it defines.",
19
+ );
20
+ }
21
+ let sliced;
22
+ try {
23
+ sliced = await readSpanSafe({
24
+ readFile, repoRoot, path: site.path, start: site.start, end: site.end, maxLines: SNIPPET_MAX_LINES,
25
+ });
26
+ } catch (e) {
27
+ if (e instanceof ToolError) throw e; // path-traversal guard: message already names the offending path
28
+ throw new ToolError(`could not read ${site.path} (${e?.code || e?.message || e})`);
29
+ }
30
+ const { text: body, truncated } = sliced;
31
+ const span = site.end > site.start ? `${site.start}-${site.end}` : `${site.start}`;
32
+ const header = `${match.label} — ${match.class || "Entity"} @ ${site.path}:${span}`;
33
+ const note = truncated ? `\n… (truncated to ${SNIPPET_MAX_LINES} lines; full span ${span})` : "";
34
+ const cand = candidates.length ? `\n(other matches: ${candidates.map((c) => c.label).join(", ")})` : "";
35
+ const hint = callHint(graph, match);
36
+ return `${header}\n${body}${note}${hint ? `\n${hint}` : ""}${cand}`;
37
+ }
@@ -0,0 +1,16 @@
1
+ // tmct_subclasses — a class's bases plus the transitive set of classes that extend it,
2
+ // falling through to the memory/corpus isa-family facts for a concept the code-map
3
+ // doesn't hold.
4
+
5
+ import { renderSubclasses, resolveSymbol } from "../../domain/codegraph.mjs";
6
+ import { memoryFactRows, renderMemorySubclasses } from "../memory-fallthrough.mjs";
7
+ import { requiredArg, resolveOrThrow } from "./kit.mjs";
8
+
9
+ export async function tmct_subclasses(args, { graph, svc, config }) {
10
+ const symbol = requiredArg(args, "class");
11
+ const { match } = resolveSymbol(svc.graph, symbol);
12
+ if (match) return renderSubclasses(graph, match); // code-map wins when present
13
+ const fallback = renderMemorySubclasses(await memoryFactRows(config), symbol);
14
+ if (fallback) return fallback;
15
+ resolveOrThrow(svc, symbol, "class"); // no code-map subclass + no memory fact → honest miss
16
+ }
@@ -0,0 +1,6 @@
1
+ // tmct_tests_for — the test modules covering a symbol or module.
2
+
3
+ import { renderTestsFor } from "../../domain/codegraph.mjs";
4
+ import { symbolHandler } from "./kit.mjs";
5
+
6
+ export const tmct_tests_for = symbolHandler(renderTestsFor);
@@ -0,0 +1,7 @@
1
+ // tmct_untested — source modules with no covering test module.
2
+
3
+ import { renderUntested } from "../../domain/codegraph.mjs";
4
+
5
+ export function tmct_untested(_args, { graph }) {
6
+ return renderUntested(graph);
7
+ }
@@ -0,0 +1,65 @@
1
+ // Read-only consumers of the conversational-memory graph (corpus facts, separate from
2
+ // the code-map graph.json). The fall-through the concept-shaped tools use: when the
3
+ // code-map resolves nothing for a concept query, answer from the reified isa-family
4
+ // facts instead, always citing provenance.
5
+
6
+ import { dirname } from "node:path";
7
+ import { loadMemory, readFactRows, normFactTerm } from "../adapters/memory/core.mjs";
8
+
9
+ // The reified isa-family predicates a memory Fact carries ("<subject> rdfs:subClassOf
10
+ // <object>" / "rdf:type"): subject IS-A object. Subclasses of X = facts whose OBJECT is X;
11
+ // superclasses of X = facts whose SUBJECT is X. (Matches chat.mjs's ISA_PREDICATES.)
12
+ const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
13
+ const MEMORY_LIST_CAP = 40;
14
+
15
+ /** Load the conversational-memory Facts as trust-bearing rows, failure-tolerant (no memory
16
+ * store / unreadable → [], so the tool still returns its honest code-map miss). repoRoot is
17
+ * the dir that CONTAINS .tmct/ (graphFile = <repo>/.tmct/graph.json), which is exactly the
18
+ * `dir` loadMemory joins MEMORY_GRAPH_REL onto. */
19
+ export async function memoryFactRows(config) {
20
+ try {
21
+ return readFactRows(await loadMemory(dirname(dirname(config.graphFile))));
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
26
+
27
+ /** A short provenance receipt for a set of memory rows — distinct source strings, capped. */
28
+ function memoryProvenance(rows) {
29
+ const provs = [...new Set(rows.map((r) => r.provenance).filter(Boolean))];
30
+ if (!provs.length) return "provenance: memory/corpus facts";
31
+ const shown = provs.slice(0, 2).join("; ");
32
+ return `provenance: ${shown}${provs.length > 2 ? `, +${provs.length - 2} more source(s)` : ""}`;
33
+ }
34
+
35
+ /** Subclasses of a concept from the reified isa-family facts (subjects of
36
+ * "<subj> subClassOf <term>"). Null when the term names no such facts, so the caller can
37
+ * keep the honest code-map miss. Provenance is always cited. */
38
+ export function renderMemorySubclasses(rows, term) {
39
+ const t = normFactTerm(term);
40
+ const hits = rows.filter((r) => ISA_PREDICATES.has(r.predicate) && r.object === t);
41
+ if (!hits.length) return null;
42
+ const labels = [...new Set(hits.map((r) => r.subject))].sort();
43
+ const shown = labels.slice(0, MEMORY_LIST_CAP);
44
+ const tail = labels.length > MEMORY_LIST_CAP ? `\n …+${labels.length - MEMORY_LIST_CAP} more` : "";
45
+ return `"${term}" is not a code-map entity — answering from memory/corpus facts. ` +
46
+ `${labels.length} known subclass(es):\n ${shown.join("\n ")}${tail}\n(${memoryProvenance(hits)})`;
47
+ }
48
+
49
+ /** A concept's DEFINITION from the isa-family facts — its superclasses ("is a …") plus a
50
+ * count/sample of its known subclasses. Null when the term names no facts. */
51
+ export function renderMemoryDefinition(rows, term) {
52
+ const t = normFactTerm(term);
53
+ const isa = rows.filter((r) => ISA_PREDICATES.has(r.predicate) && (r.subject === t || r.object === t));
54
+ if (!isa.length) return null;
55
+ const supers = [...new Set(isa.filter((r) => r.subject === t).map((r) => r.object))];
56
+ const subs = [...new Set(isa.filter((r) => r.object === t).map((r) => r.subject))].sort();
57
+ const lines = [`"${term}" is not a code-map entity — answering from memory/corpus facts.`];
58
+ if (supers.length) lines.push(`is a: ${supers.slice(0, MEMORY_LIST_CAP).join(", ")}`);
59
+ if (subs.length) {
60
+ const tail = subs.length > MEMORY_LIST_CAP ? `, +${subs.length - MEMORY_LIST_CAP} more` : "";
61
+ lines.push(`known subclasses (${subs.length}): ${subs.slice(0, MEMORY_LIST_CAP).join(", ")}${tail}`);
62
+ }
63
+ lines.push(`(${memoryProvenance(isa)})`);
64
+ return lines.join("\n");
65
+ }
@@ -1,5 +1,5 @@
1
1
  // schema-docs.mjs — the single source of truth for tmct's own ontology documentation:
2
- // every entity class and predicate tmct's typed graph (src/graph-build.mjs's
2
+ // every entity class and predicate tmct's typed graph (src/adapters/graph-build.mjs's
3
3
  // buildEntities) actually emits, verified against real `prop:`/`class:` literals.
4
4
  // `ingestSchemaDocs()` merges it into a graph build so "what does cochange mean" or
5
5
  // "what is a Commit" is answerable by querying the graph like any other question.
@@ -0,0 +1,61 @@
1
+ // tmct tool layer — query-only tools over the deterministic typed code-graph
2
+ // artifact (<repo>/.tmct/graph.json). The graph source is a LOCAL file
3
+ // (src/adapters/source.mjs) and tmct_search is a LOCAL lexical lookup (no remote API,
4
+ // no LLM, no model calls anywhere). dispatchTool is the single internal entry
5
+ // the chat surface and the `cli <tool>` route call into.
6
+ //
7
+ // This module is the dispatch entry only: it maps a tool name to the handler module
8
+ // that owns it (handlers/, one module per tool), loads the graph once, builds the
9
+ // typed service object (the Repository Interface) and hands both to the handler.
10
+ // Each tool answers one question in ONE compact call so the caller need not
11
+ // Read/Grep. Errors reach the caller as clean tool errors — message only, never a stack.
12
+
13
+ import { readFile } from "node:fs/promises";
14
+ import { dirname } from "node:path";
15
+ import { HOT_TOOLS } from "./definitions.mjs";
16
+ import { ToolError } from "../adapters/config.mjs";
17
+ import * as defaultSource from "../adapters/source.mjs";
18
+ import { ask } from "../domain/ask.mjs";
19
+ import { createGraphService } from "../adapters/providers/graph-service.mjs";
20
+ import { loadGraph } from "./graph-load.mjs";
21
+ import { HANDLERS } from "./handlers/index.mjs";
22
+ import { setDefaultNlpAdapter } from "../domain/interpret/nlp-registry.mjs";
23
+ import { setConstructionBanks } from "../domain/interpret/strategies/constructions.mjs";
24
+ import { nlpAdapter } from "../adapters/ask-nlp.mjs";
25
+ import { readConstructionFiles } from "../adapters/corpus/construction-banks.mjs";
26
+
27
+ // Composition: the tool layer supplies the domain parser's default lemma/POS
28
+ // adapter and the construction-grammar banks (lazy loader), so dispatchTool-
29
+ // only consumers get the same NL tiers chat wires.
30
+ setDefaultNlpAdapter(nlpAdapter);
31
+ setConstructionBanks(readConstructionFiles);
32
+
33
+ export { loadGraph } from "./graph-load.mjs";
34
+ export { buildContextBundle } from "./handlers/tmct-context.mjs";
35
+
36
+ // Tiered tool surface: the hot tools carry full descriptions/schemas in this
37
+ // catalog; every COLD tool (describe/members/impact/history/…) is still served
38
+ // by dispatchTool below and is reachable via the CLI `cli <tool>` route +
39
+ // the <repo>/.tmct/TOOLS.md catalog `tmct init` writes (renderToolsCatalog). Both
40
+ // tiers, and the schemas themselves, come from the tool definitions.
41
+ export const TOOLS = HOT_TOOLS.map(({ name, agentDescription, inputSchema }) => ({
42
+ name,
43
+ description: agentDescription,
44
+ inputSchema,
45
+ }));
46
+
47
+ export async function dispatchTool(name, args, { config, source = defaultSource, tel = null } = {}) {
48
+ // Reject an unknown tool BEFORE touching the graph — an unknown name never
49
+ // triggers a load. hasOwn, so an inherited name ("constructor", "toString")
50
+ // is unknown rather than a callable found on the prototype chain.
51
+ if (!Object.hasOwn(HANDLERS, name)) throw new ToolError(`unknown tool: ${name}`);
52
+ const handle = HANDLERS[name];
53
+ if (handle.ownsGraphLoad) return handle(args, { config, source, tel });
54
+ const graph = await loadGraph(config, source);
55
+ // repo root = the dir containing .tmct/ (graphFile = <repo>/.tmct/graph.json). Passed to
56
+ // createGraphService so svc.snippet()/svc.context() are usable directly, and on to the
57
+ // handlers that do their own safe source reads.
58
+ const repoRoot = dirname(dirname(config.graphFile));
59
+ const svc = createGraphService(graph, { sourceAccess: true, repoRoot, readFile, tel, ask });
60
+ return handle(args, { graph, svc, config, repoRoot });
61
+ }
@@ -1,39 +0,0 @@
1
- // answer-variants.mjs — deterministic, committed answer-phrasing variety for
2
- // a curated set of answer templates (answer-variants.json).
3
- //
4
- // Selection is a pure hash of (poolId, key), never Math.random/Date.now, so
5
- // output is byte-identical across runs (pinnable in tests). Load is lazy and
6
- // failure-tolerant (missing file, or a stubbed node:fs in the browser bundle,
7
- // both degrade to returning `base`) rather than throwing.
8
-
9
- import { readFileSync } from "node:fs";
10
- import { fileURLToPath } from "node:url";
11
- import { dirname, join } from "node:path";
12
- import { createHash } from "node:crypto";
13
-
14
- const DATA_FILE = join(dirname(fileURLToPath(import.meta.url)), "answer-variants.json");
15
-
16
- let dataCache; // undefined = not yet attempted; null = load failed/unavailable
17
- function loadData() {
18
- if (dataCache !== undefined) return dataCache;
19
- try {
20
- dataCache = JSON.parse(readFileSync(DATA_FILE, "utf8"));
21
- } catch {
22
- dataCache = null;
23
- }
24
- return dataCache;
25
- }
26
-
27
- /** Deterministically choose `base` or one of `poolId`'s committed variants,
28
- * keyed on `key`. Any failure (unknown pool, unreadable data, falsy `key`)
29
- * falls back to `base`. */
30
- export function pickPhrase(poolId, key, base) {
31
- if (!key) return base;
32
- const data = loadData();
33
- const variants = data?.pools?.[poolId]?.variants;
34
- if (!Array.isArray(variants) || !variants.length) return base;
35
- const forms = [base, ...variants];
36
- const digest = createHash("sha256").update(`${poolId}:${String(key)}`).digest();
37
- const idx = digest[0] % forms.length;
38
- return forms[idx];
39
- }
package/src/hash.mjs DELETED
@@ -1,24 +0,0 @@
1
- // hash.mjs — the single home for tmct's content-address hash.
2
- //
3
- // FNV-1a 32-bit is deliberately home-grown: it must be synchronous, browser-safe,
4
- // dependency-free, and — critically — CROSS-VERSION STABLE, because fact ids are
5
- // content-addressed by it and a fact's id is its identity across the whole memory
6
- // graph. Every library candidate fails at least one of those; this eight-line
7
- // function fails none. It lives here, once, so the fact-id contract has exactly
8
- // one definition.
9
-
10
- /** FNV-1a 32-bit. Returns the unsigned 32-bit integer (0 … 2^32−1). */
11
- export function fnv1a32(str) {
12
- let h = 0x811c9dc5;
13
- for (let i = 0; i < str.length; i += 1) {
14
- h ^= str.charCodeAt(i);
15
- h = Math.imul(h, 0x01000193);
16
- }
17
- return h >>> 0;
18
- }
19
-
20
- /** FNV-1a 32-bit as a zero-padded 8-char hex string — the stable content-address
21
- * used for fact ids (`fact:<hex>`). Same (s,p,o) → same id → upsert, never a dup. */
22
- export function fnv1aHex(str) {
23
- return fnv1a32(str).toString(16).padStart(8, "0");
24
- }