@polycode-projects/the-mechanical-code-talker 1.11.6 → 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 +117 -81
  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} +1238 -680
  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} +38 -7
  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
package/src/server.mjs DELETED
@@ -1,501 +0,0 @@
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/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 switch
5
- // the chat surface and the `cli <tool>` route call into.
6
- //
7
- // Tools (all query-only, bounded output): tmct_search, tmct_describe, tmct_snippet,
8
- // tmct_impact, plus the read-replacing tools tmct_members, tmct_subclasses,
9
- // tmct_architecture, tmct_tests_for, tmct_untested, tmct_history, tmct_callers,
10
- // tmct_callees. Each 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
- // tmct_ask (hot tool): a mechanical, zero-model-call NL query over the graph —
14
- // collapses the search+describe+traversal composition loop a caller would
15
- // otherwise hand-compose into one deterministic round-trip. See ask.mjs.
16
-
17
- import { readFile } from "node:fs/promises";
18
- import { dirname } from "node:path";
19
- import { ToolError } from "./config.mjs";
20
- import { sliceSpan, readSpanSafe } from "./source-slice.mjs";
21
- import * as defaultSource from "./source.mjs";
22
- import {
23
- parseEntities,
24
- resolveSymbol,
25
- renderDescribe,
26
- renderImpact,
27
- renderSearch,
28
- siteOf,
29
- renderMembers,
30
- renderSubclasses,
31
- renderArchitecture,
32
- renderTestsFor,
33
- renderUntested,
34
- renderHistory,
35
- renderCallers,
36
- renderCallees,
37
- renderCochanges,
38
- renderExports,
39
- renderSignature,
40
- contextPlan,
41
- sizeBundle,
42
- bundleMask,
43
- trimBundleMask,
44
- renderContextMore,
45
- renderCalls,
46
- callHint,
47
- renderFileHistory,
48
- renderMethodHistory,
49
- renderClassHistory,
50
- } from "./codegraph.mjs";
51
- import { ask } from "./ask.mjs";
52
- import { createGraphService } from "./providers/graph-service.mjs";
53
- // Read-only consumers of the conversational-memory graph (corpus facts, separate from
54
- // the code-map graph.json). Used by the fall-through bridge below: when the code-map
55
- // resolves nothing for a concept query, answer from the reified isa-family facts instead.
56
- import { loadMemory, readFactRows, normFactTerm } from "./memory/core.mjs";
57
-
58
- const SNIPPET_MAX_LINES = 200;
59
-
60
- // The reified isa-family predicates a memory Fact carries ("<subject> rdfs:subClassOf
61
- // <object>" / "rdf:type"): subject IS-A object. Subclasses of X = facts whose OBJECT is X;
62
- // superclasses of X = facts whose SUBJECT is X. (Matches chat.mjs's ISA_PREDICATES.)
63
- const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
64
- const MEMORY_LIST_CAP = 40;
65
-
66
- /** Load the conversational-memory Facts as trust-bearing rows, failure-tolerant (no memory
67
- * store / unreadable → [], so the tool still returns its honest code-map miss). repoRoot is
68
- * the dir that CONTAINS .tmct/ (graphFile = <repo>/.tmct/graph.json), which is exactly the
69
- * `dir` loadMemory joins MEMORY_GRAPH_REL onto. */
70
- async function memoryFactRows(config) {
71
- try {
72
- return readFactRows(await loadMemory(dirname(dirname(config.graphFile))));
73
- } catch {
74
- return [];
75
- }
76
- }
77
-
78
- /** A short provenance receipt for a set of memory rows — distinct source strings, capped. */
79
- function memoryProvenance(rows) {
80
- const provs = [...new Set(rows.map((r) => r.provenance).filter(Boolean))];
81
- if (!provs.length) return "provenance: memory/corpus facts";
82
- const shown = provs.slice(0, 2).join("; ");
83
- return `provenance: ${shown}${provs.length > 2 ? `, +${provs.length - 2} more source(s)` : ""}`;
84
- }
85
-
86
- /** FALL-THROUGH: subclasses of a concept from the reified isa-family facts (subjects of
87
- * "<subj> subClassOf <term>"). Null when the term names no such facts (so the caller can
88
- * keep the honest code-map miss). Provenance is always cited. */
89
- function renderMemorySubclasses(rows, term) {
90
- const t = normFactTerm(term);
91
- const hits = rows.filter((r) => ISA_PREDICATES.has(r.predicate) && r.object === t);
92
- if (!hits.length) return null;
93
- const labels = [...new Set(hits.map((r) => r.subject))].sort();
94
- const shown = labels.slice(0, MEMORY_LIST_CAP);
95
- const tail = labels.length > MEMORY_LIST_CAP ? `\n …+${labels.length - MEMORY_LIST_CAP} more` : "";
96
- return `"${term}" is not a code-map entity — answering from memory/corpus facts. ` +
97
- `${labels.length} known subclass(es):\n ${shown.join("\n ")}${tail}\n(${memoryProvenance(hits)})`;
98
- }
99
-
100
- /** FALL-THROUGH: a concept's DEFINITION from the isa-family facts — its superclasses ("is
101
- * a …") plus a count/sample of its known subclasses. Null when the term names no facts. */
102
- function renderMemoryDefinition(rows, term) {
103
- const t = normFactTerm(term);
104
- const isa = rows.filter((r) => ISA_PREDICATES.has(r.predicate) && (r.subject === t || r.object === t));
105
- if (!isa.length) return null;
106
- const supers = [...new Set(isa.filter((r) => r.subject === t).map((r) => r.object))];
107
- const subs = [...new Set(isa.filter((r) => r.object === t).map((r) => r.subject))].sort();
108
- const lines = [`"${term}" is not a code-map entity — answering from memory/corpus facts.`];
109
- if (supers.length) lines.push(`is a: ${supers.slice(0, MEMORY_LIST_CAP).join(", ")}`);
110
- if (subs.length) {
111
- const tail = subs.length > MEMORY_LIST_CAP ? `, +${subs.length - MEMORY_LIST_CAP} more` : "";
112
- lines.push(`known subclasses (${subs.length}): ${subs.slice(0, MEMORY_LIST_CAP).join(", ")}${tail}`);
113
- }
114
- lines.push(`(${memoryProvenance(isa)})`);
115
- return lines.join("\n");
116
- }
117
-
118
- // Tiered tool surface: the hot tools carry full descriptions/schemas in this
119
- // catalog; every COLD tool (describe/members/impact/history/…) is still served
120
- // by dispatchTool below and is reachable via the CLI `cli <tool>` route +
121
- // the generated <repo>/.tmct/TOOLS.md catalog (renderToolsCatalog).
122
- export const TOOLS = [
123
- {
124
- name: "tmct_context",
125
- // Lean resident schema (re-billed every turn): the minimum that still steers the agent to
126
- // ONE call → write, not Read.
127
- description:
128
- "START HERE to add/modify code: ONE call returns a sized edit bundle (exemplar source, sibling signatures, registration, insertion region) — then write directly, don't Read.",
129
- inputSchema: {
130
- type: "object",
131
- required: ["symbol"],
132
- properties: {
133
- symbol: { type: "string", description: "Module path (e.g. path/to/module) or a sibling function/class name defined in it." },
134
- depth: { type: "string", enum: ["min", "auto", "full"], default: "auto", description: "auto (sized to the task) | min (leanest) | full (every section)." },
135
- },
136
- },
137
- },
138
- {
139
- name: "tmct_snippet",
140
- description: "EXACT source of one function/class/Class.method by name (its line span only) + a one-line in-repo call hint. Prefer over Read for a single symbol.",
141
- inputSchema: {
142
- type: "object",
143
- required: ["symbol"],
144
- properties: {
145
- symbol: { type: "string", description: "function/class name, Class.method, or fn:<path>#name." },
146
- },
147
- },
148
- },
149
- {
150
- name: "tmct_ask",
151
- description:
152
- "Ask a structural question in plain English: \"which functions call X\", \"what uses X\", \"where is X defined\", \"when did X change\". One call, no model. A clean miss beats a guess.",
153
- inputSchema: {
154
- type: "object",
155
- required: ["query"],
156
- properties: {
157
- query: { type: "string", description: "A free-text question, e.g. \"which functions explicitly couple to logging\"." },
158
- },
159
- },
160
- },
161
- ];
162
-
163
- // Exported so chat.mjs's compare lane can load the SAME graph dispatchTool's
164
- // own tools load — no new loading path, just direct reuse of the existing
165
- // config -> source.fetchEntities -> parseEntities chain, for the case where
166
- // runAsk's own `graph` param is null (the common case; it's only preloaded
167
- // when a caller already has one in hand — see runAsk's own `if (graph && ...)`
168
- // / dispatchTool("tmct_ask", …) split just above).
169
- export async function loadGraph(config, source) {
170
- const payload = await source.fetchEntities(config);
171
- const graph = parseEntities(payload);
172
- if (!graph.individuals.length) {
173
- // Honest miss, never a stack: a fresh repo simply has no graph yet (the chat
174
- // session itself creates one as the conversation folds in).
175
- throw new ToolError(
176
- `the graph at ${config.graphFile} is empty — no entities to answer from yet ` +
177
- "(this repo starts with no graph; the chat session folds the conversation into one).",
178
- );
179
- }
180
- return graph;
181
- }
182
-
183
- // A clean miss on the interface becomes the instructive ToolError the CLI/chat expect —
184
- // message-only, never a stack, no fabricated entity names.
185
- function resolveOrThrow(svc, symbol, what) {
186
- const { match, candidates } = resolveSymbol(svc.graph, symbol);
187
- if (!match) {
188
- throw new ToolError(
189
- `no entity matching ${what} "${symbol}" in the code-map graph. ` +
190
- "Try a repo-relative path (e.g. path/to/module), a basename, or tmct_search for a fuzzy lookup.",
191
- );
192
- }
193
- return { match, candidates };
194
- }
195
-
196
- /**
197
- * Build the tmct_context "edit bundle" for a symbol and return { text, tier, topup }.
198
- * Shared by the tmct_context tool AND the `cli digest` arm (cli.mjs). `trim:true` renders
199
- * a SECONDARY, signatures-only bundle (no bodies/tails) for related-but-not-primary
200
- * digest modules.
201
- *
202
- * Section order is cache-stable: content identical across runs (anchor/registration/
203
- * exemplar/siblings/__all__/insertion region) comes first; more variable, history-derived
204
- * tails (covering tests, co-change) come last, so a stable prefix maximises prompt-cache
205
- * reuse.
206
- */
207
- export async function buildContextBundle(args, { config, source = defaultSource, trim = false, tel = null } = {}) {
208
- const symbol = String(args?.symbol || "").trim();
209
- if (!symbol) throw new ToolError("symbol is required");
210
- const depth = String(args?.depth || "auto").trim().toLowerCase();
211
- // Tuning-flag contract: `min` forces the LEANEST bundle (TINY mask, no top-up) regardless of
212
- // exemplar length; `untuned` reproduces the earlier escalation (tuning #1 bypassed). Neither
213
- // → the tuned default (sizeBundle's anchor-gated escalation).
214
- const min = Boolean(args?.min);
215
- const untuned = Boolean(args?.untuned);
216
- // `max` forces the injection CEILING — FULL tier (every section + inlined depth-1 callee
217
- // bodies) with top-up, and it OVERRIDES trim so even secondary modules get the full bundle. Used
218
- // by the tmct-max arm to test whether more injection re-bloats.
219
- const max = Boolean(args?.max);
220
- const graph = await loadGraph(config, source);
221
- // repo root = the dir containing .tmct/ (graphFile = <repo>/.tmct/graph.json) — computed
222
- // before createGraphService so the RI service can be constructed source-capable (2e): this
223
- // module still does its OWN safe reads below (readSpanSafe/sliceSpan, Item 1) rather than
224
- // delegating to svc.context() — see the module docblock's note on why. Passing sourceAccess
225
- // through anyway keeps svc.snippet()/svc.context() usable by any future/external caller of
226
- // this same service object without a second, divergent construction path. `tel` (optional,
227
- // Item 3.3) is an already-constructed telemetry sink threaded down from the caller (e.g.
228
- // chat.mjs's session-level createTelemetry) — never minted here, so a caller that never
229
- // passes one costs nothing extra (createGraphService's own wrapping loop no-ops on tel:null).
230
- const repoRoot = dirname(dirname(config.graphFile));
231
- const svc = createGraphService(graph, { sourceAccess: true, repoRoot, readFile, tel });
232
- const { match } = resolveOrThrow(svc, symbol, "symbol");
233
- const plan = contextPlan(graph, match);
234
- // #6/B1/B6: pick the section mask by depth — min forces TINY, full/max forces everything, auto
235
- // runs the size classifier (lean TINY default + one-tier top-up when the edit needs it).
236
- let tier;
237
- let mask;
238
- let topup = false;
239
- if (min || depth === "min") { tier = "TINY"; mask = bundleMask("TINY"); }
240
- else if (max || depth === "full") { tier = "FULL"; mask = bundleMask("FULL"); topup = true; }
241
- else ({ tier, mask, topup } = sizeBundle(plan, graph, { untuned }));
242
- if (trim && !max) mask = trimBundleMask(mask); // B2: secondary digest module → signatures + region only (max keeps the full bundle)
243
- let lines = null;
244
- if (plan.moduleLabel) {
245
- try { ({ lines } = await readSpanSafe({ readFile, repoRoot, path: plan.moduleLabel })); }
246
- catch { lines = null; }
247
- }
248
- const lineAt = (n) => (lines && lines[n - 1] != null ? lines[n - 1].trim() : "");
249
- const sliceBody = (start, end) => sliceSpan(lines, start, end, SNIPPET_MAX_LINES).text;
250
- const out = [
251
- `Edit context for ${plan.moduleLabel} [${tier}${trim ? " secondary" : ""}] — assembled from the typed graph + that file. ` +
252
- "You do NOT need to Read it; write the new code directly after reviewing this.",
253
- ];
254
- // ---- cache-stable prefix: identical across runs ----
255
- if (mask.anchor && plan.anchor?.site && lines) {
256
- const { start, end } = plan.anchor.site;
257
- out.push(`\n## anchor: ${plan.anchor.label} (${plan.anchor.class}) @ ${plan.moduleLabel}:${start}-${end}`);
258
- out.push(sliceBody(start, end));
259
- if (plan.callHint) out.push(plan.callHint);
260
- }
261
- if (mask.registration && plan.globals.length) {
262
- out.push(`\n## registration / module globals (replicate this pattern):`);
263
- for (const g of plan.globals) out.push(` ${g.label} = ${g.value}${g.site ? ` [:${g.site.start}]` : ""}`);
264
- }
265
- if (mask.exemplar && plan.exemplar?.site && lines) {
266
- const { start, end } = plan.exemplar.site;
267
- const dec = plan.exemplar.decorators ? ` @${plan.exemplar.decorators}` : "";
268
- out.push(`\n## closest example (full body) — copy this style: ${plan.exemplar.label} (${plan.exemplar.class})${dec} @ ${plan.moduleLabel}:${start}-${end}`);
269
- out.push(sliceBody(start, end));
270
- if (plan.callHint) out.push(plan.callHint);
271
- }
272
- if (mask.inlinedCallees && plan.calleeBodies.length && lines) {
273
- let budget = 120; // INLINE_CALLEE_LOC
274
- for (const cb of plan.calleeBodies) {
275
- if (budget <= 0) break;
276
- const start = cb.site.start;
277
- const fromThisFile = cb.site.path === plan.moduleLabel;
278
- const bodyLines = fromThisFile && lines
279
- ? lines
280
- : await readSpanSafe({ readFile, repoRoot, path: cb.site.path }).then((r) => r.lines).catch(() => null);
281
- if (!bodyLines) continue;
282
- const sliced = sliceSpan(bodyLines, start, cb.site.end, budget);
283
- out.push(`\n## inlined callee body (depth-1 in-repo call): ${cb.label} @ ${cb.site.path}:${start}-${cb.site.end}`);
284
- out.push(sliced.text);
285
- budget -= (sliced.end - start + 1);
286
- }
287
- }
288
- if (mask.classMembers && plan.classMembers && plan.classMembers.members.length) {
289
- 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):`);
290
- for (const m of plan.classMembers.members) {
291
- const short = String(m.label).split(".").pop();
292
- const sig = m.params != null && m.params !== "" ? `(${m.params})${m.returns ? ` -> ${m.returns}` : ""}` : "";
293
- const dec = m.decorators ? `@${m.decorators} ` : "";
294
- const r = m.raises ? ` raises=${m.raises}` : "";
295
- out.push(` ${m.class} ${short}${m.site ? ` :${m.site.start}` : ""} ${dec}${short}${sig}${r}`);
296
- }
297
- }
298
- if (mask.siblings && plan.siblings.length) {
299
- out.push(`\n## sibling symbols to copy the style of (most relevant first; ${plan.siblings.length} total):`);
300
- for (const s of plan.siblings.slice(0, plan.siblingCap)) {
301
- const sig = s.site ? lineAt(s.site.start) : "";
302
- const dec = s.decorators ? `@${s.decorators} ` : "";
303
- const r = s.raises ? ` raises=${s.raises}` : "";
304
- out.push(` ${s.class} ${s.label}${s.site ? ` :${s.site.start}` : ""} ${dec}${sig}${r}`);
305
- }
306
- if (plan.siblings.length > plan.siblingCap) {
307
- out.push(` …+${plan.siblings.length - plan.siblingCap} more (use tmct_search kind=function or tmct_snippet <name> for any of them)`);
308
- }
309
- }
310
- if (mask.allExports && plan.allExports) {
311
- 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}`);
312
- }
313
- if (mask.reexports && plan.exports && plan.exports.length) out.push(`\n## re-exported symbols (resolved __all__ → defining module): ${plan.exports.join(", ")}`);
314
- // #2 + B4: the contiguous insertion region is part of the STABLE prefix — always present
315
- // (even at TINY) so the agent never needs to Read the file to place the edit.
316
- if (mask.insertionRegion && plan.insertionRegion && lines) {
317
- const start = plan.insertionRegion.start;
318
- const end = Math.min(lines.length, start + 40 - 1); // INSERTION_REGION_CAP
319
- out.push(`\n## insertion region (write your new sibling here) — ${plan.moduleLabel}:${start}-${end}`);
320
- out.push(lines.slice(start - 1, end).map((l, i) => `${start + i}\t${l}`).join("\n"));
321
- } else if (plan.insertion) {
322
- out.push(`\n## insert the new sibling after line ~${plan.insertion} (end of the last top-level definition).`);
323
- }
324
- // ---- variable tail: history-derived, kept LAST so the prefix stays cache-stable ----
325
- if (mask.tests && plan.tests.length) out.push(`\n## covering tests: ${plan.tests.join(", ")}`);
326
- if (mask.cochange && plan.cochange && plan.cochange.length) {
327
- out.push(`\n## usually changed together (consider editing these too): ${plan.cochange.map((c) => `${c.label} (×${c.weight})`).join(", ")}`);
328
- }
329
- out.push(`\nYou now have the snippet, the sibling style, the registration anchor and the tests. ` +
330
- `Write the new code with Edit/Write — do NOT Read ${plan.moduleLabel}.`);
331
- if (tier !== "FULL") {
332
- out.push(`(bundle tier ${tier}; for any omitted sections run tmct_context_more {"symbol":"${symbol}"}, or tmct_context with depth="full".)`);
333
- }
334
- return { text: out.join("\n"), tier, topup };
335
- }
336
-
337
- // The full set of tool names dispatchTool serves (hot catalog + cold tools). Used
338
- // to reject an unknown tool before any graph load.
339
- const DISPATCH_TOOLS = new Set([
340
- "tmct_context", "tmct_context_more", "tmct_describe", "tmct_snippet", "tmct_signature",
341
- "tmct_impact", "tmct_search", "tmct_members", "tmct_subclasses", "tmct_architecture",
342
- "tmct_exports", "tmct_untested", "tmct_ask", "tmct_tests_for", "tmct_history",
343
- "tmct_callers", "tmct_callees", "tmct_cochanges", "tmct_calls",
344
- "tmct_file_history", "tmct_method_history", "tmct_class_history",
345
- ]);
346
-
347
- export async function dispatchTool(name, args, { config, source = defaultSource, tel = null } = {}) {
348
- // tmct_context builds (and loads) its own edit bundle — return early so we don't
349
- // double-load the graph for it.
350
- if (name === "tmct_context") {
351
- return (await buildContextBundle(args, { config, source, tel })).text;
352
- }
353
- // Reject an unknown tool BEFORE touching the graph — preserves the original
354
- // ordering (an unknown name never triggers a load).
355
- if (!DISPATCH_TOOLS.has(name)) throw new ToolError(`unknown tool: ${name}`);
356
- // Every other tool reads graph truth: load once and build the typed service
357
- // object (the Repository Interface). dispatchTool is the presentation adapter —
358
- // it delegates resolution + the miss/error contract to the service and formats
359
- // the result with tmct's own render* layer (which reads svc.graph). This is the
360
- // switch's operations extracted into a named, typed seam without changing bytes.
361
- const graph = await loadGraph(config, source);
362
- // repo root = the dir containing .tmct/ (graphFile = <repo>/.tmct/graph.json). Passed through
363
- // to createGraphService (2e) so svc.snippet()/svc.context() are usable directly; this
364
- // dispatcher still does its OWN safe read for tmct_snippet below (readSpanSafe/sliceSpan,
365
- // Item 1) rather than delegating, to keep its richer presentation (candidates, call hints,
366
- // truncation notices) — see the tmct_snippet branch below.
367
- const repoRoot = dirname(dirname(config.graphFile));
368
- const svc = createGraphService(graph, { sourceAccess: true, repoRoot, readFile, tel });
369
- if (name === "tmct_context_more") {
370
- const symbol = String(args?.symbol || "").trim();
371
- if (!symbol) throw new ToolError("symbol is required");
372
- const { match } = resolveOrThrow(svc, symbol, "symbol");
373
- return renderContextMore(contextPlan(graph, match));
374
- }
375
- if (name === "tmct_describe") {
376
- const symbol = String(args?.symbol || "").trim();
377
- if (!symbol) throw new ToolError("symbol is required");
378
- const { match, candidates } = resolveSymbol(svc.graph, symbol);
379
- if (match) return renderDescribe(graph, match, { candidates }); // code-map wins when present
380
- const fb = renderMemoryDefinition(await memoryFactRows(config), symbol);
381
- if (fb) return fb;
382
- resolveOrThrow(svc, symbol, "symbol"); // no code-map + no memory fact → the honest miss
383
- }
384
- if (name === "tmct_snippet") {
385
- const symbol = String(args?.symbol || "").trim();
386
- if (!symbol) throw new ToolError("symbol is required");
387
- const { match, candidates } = resolveOrThrow(svc, symbol, "symbol");
388
- const site = siteOf(match);
389
- if (!site) {
390
- throw new ToolError(
391
- `"${match.label}" (${match.class || "Entity"}) has no source span in the graph — ` +
392
- "it is likely a module. Use tmct_describe for its contents, then tmct_snippet one of the functions/classes it defines.",
393
- );
394
- }
395
- let sliced;
396
- try {
397
- sliced = await readSpanSafe({
398
- readFile, repoRoot, path: site.path, start: site.start, end: site.end, maxLines: SNIPPET_MAX_LINES,
399
- });
400
- } catch (e) {
401
- if (e instanceof ToolError) throw e; // path-traversal guard: message already names the offending path
402
- throw new ToolError(`could not read ${site.path} (${e?.code || e?.message || e})`);
403
- }
404
- const { text: body, truncated } = sliced;
405
- const span = site.end > site.start ? `${site.start}-${site.end}` : `${site.start}`;
406
- const header = `${match.label} — ${match.class || "Entity"} @ ${site.path}:${span}`;
407
- const note = truncated ? `\n… (truncated to ${SNIPPET_MAX_LINES} lines; full span ${span})` : "";
408
- const cand = candidates.length ? `\n(other matches: ${candidates.map((c) => c.label).join(", ")})` : "";
409
- const hint = callHint(graph, match); // #4: one-line "calls in-repo: …" so the agent sees in-repo deps inline
410
- return `${header}\n${body}${note}${hint ? `\n${hint}` : ""}${cand}`;
411
- }
412
- if (name === "tmct_signature") {
413
- const symbol = String(args?.symbol || "").trim();
414
- if (!symbol) throw new ToolError("symbol is required");
415
- const { match } = resolveOrThrow(svc, symbol, "symbol");
416
- return renderSignature(graph, match);
417
- }
418
- if (name === "tmct_impact") {
419
- const module = String(args?.module || "").trim();
420
- if (!module) throw new ToolError("module is required");
421
- const { match } = resolveOrThrow(svc, module, "module");
422
- return renderImpact(graph, match);
423
- }
424
- if (name === "tmct_search") {
425
- const query = String(args?.query || "").trim();
426
- const kind = String(args?.kind || "").trim();
427
- if (!query && !kind) throw new ToolError("query is required");
428
- const out = renderSearch(graph, query, {
429
- kind,
430
- decorator: String(args?.decorator || "").trim(),
431
- name: String(args?.name || "").trim(),
432
- });
433
- // FALL-THROUGH: a code-map miss ("no module matches …") on a plain concept query still
434
- // answers from the memory/corpus isa-family facts when the concept is known there.
435
- if (!kind && /^no module matches/.test(out)) {
436
- const fb = renderMemoryDefinition(await memoryFactRows(config), query);
437
- if (fb) return fb;
438
- }
439
- return out;
440
- }
441
- if (name === "tmct_members") {
442
- const symbol = String(args?.class || "").trim();
443
- if (!symbol) throw new ToolError("class is required");
444
- const { match } = resolveSymbol(svc.graph, symbol);
445
- if (match) return renderMembers(graph, match); // code-map wins when present
446
- // a concept's "members" in the corpus sense are its subclasses (its instances).
447
- const fb = renderMemorySubclasses(await memoryFactRows(config), symbol);
448
- if (fb) return fb;
449
- resolveOrThrow(svc, symbol, "class"); // the honest miss
450
- }
451
- if (name === "tmct_subclasses") {
452
- const symbol = String(args?.class || "").trim();
453
- if (!symbol) throw new ToolError("class is required");
454
- const { match } = resolveSymbol(svc.graph, symbol);
455
- if (match) return renderSubclasses(graph, match); // code-map wins when present
456
- const fb = renderMemorySubclasses(await memoryFactRows(config), symbol);
457
- if (fb) return fb;
458
- resolveOrThrow(svc, symbol, "class"); // no code-map subclass + no memory fact → honest miss
459
- }
460
- if (name === "tmct_architecture") {
461
- return renderArchitecture(graph, { pkg: String(args?.package || "").trim() });
462
- }
463
- if (name === "tmct_exports") {
464
- const module = String(args?.module || "").trim();
465
- if (!module) throw new ToolError("module is required");
466
- const { match } = resolveOrThrow(svc, module, "module");
467
- return renderExports(graph, match);
468
- }
469
- if (name === "tmct_untested") {
470
- return renderUntested(graph);
471
- }
472
- if (name === "tmct_ask") {
473
- const query = String(args?.query || "").trim();
474
- if (!query) throw new ToolError("query is required");
475
- const { content, tmct_ask } = ask(graph, query);
476
- // Every dispatchTool caller (the chat surface, the CLI fallback) expects a plain string —
477
- // append the structured envelope as a delimited, machine-parseable block rather than
478
- // changing that shared contract for one tool.
479
- return `${content}\n\n---tmct_ask---\n${JSON.stringify(tmct_ask, null, 2)}`;
480
- }
481
- if (
482
- name === "tmct_tests_for" || name === "tmct_history" || name === "tmct_callers" ||
483
- name === "tmct_callees" || name === "tmct_cochanges" || name === "tmct_calls" ||
484
- name === "tmct_file_history" || name === "tmct_method_history" || name === "tmct_class_history"
485
- ) {
486
- const symbol = String(args?.symbol || "").trim();
487
- if (!symbol) throw new ToolError("symbol is required");
488
- const { match } = resolveOrThrow(svc, symbol, "symbol");
489
- if (name === "tmct_tests_for") return renderTestsFor(graph, match);
490
- if (name === "tmct_history") return renderHistory(graph, match);
491
- if (name === "tmct_callers") return renderCallers(graph, match);
492
- if (name === "tmct_cochanges") return renderCochanges(graph, match);
493
- if (name === "tmct_calls") return renderCalls(graph, match);
494
- if (name === "tmct_file_history") return renderFileHistory(graph, match);
495
- if (name === "tmct_method_history") return renderMethodHistory(graph, match);
496
- if (name === "tmct_class_history") return renderClassHistory(graph, match);
497
- return renderCallees(graph, match);
498
- }
499
- throw new ToolError(`unknown tool: ${name}`);
500
- }
501
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes