@polycode-projects/the-mechanical-code-talker 1.9.2 → 1.10.0

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 (80) hide show
  1. package/README.md +441 -202
  2. package/bin/tmct.mjs +126 -1
  3. package/package.json +4 -2
  4. package/src/answer-variants.mjs +8 -36
  5. package/src/ask-browser-entry.mjs +5 -23
  6. package/src/ask-browser.bundle.js +1 -2
  7. package/src/ask-nlp.mjs +9 -23
  8. package/src/ask-vocab.mjs +139 -589
  9. package/src/ask.mjs +627 -1729
  10. package/src/chat.mjs +1684 -2872
  11. package/src/cli-args.mjs +14 -28
  12. package/src/codegraph.mjs +236 -644
  13. package/src/completions/complete.mjs +18 -62
  14. package/src/completions/graph-adapter.mjs +14 -60
  15. package/src/completions/group.mjs +12 -68
  16. package/src/completions/infer.mjs +38 -126
  17. package/src/completions/prune.mjs +17 -70
  18. package/src/completions/rank.mjs +16 -69
  19. package/src/completions/search.mjs +8 -31
  20. package/src/concept.mjs +32 -88
  21. package/src/conformance.mjs +11 -15
  22. package/src/corpus/conceptnet.mjs +31 -89
  23. package/src/corpus/templates.mjs +19 -45
  24. package/src/corpus/unknown-ingest.mjs +31 -92
  25. package/src/embed.mjs +10 -22
  26. package/src/extensions.mjs +50 -154
  27. package/src/finish.mjs +35 -91
  28. package/src/grammar/ace.mjs +16 -40
  29. package/src/grammar/assert.mjs +1 -1
  30. package/src/grammar/lexicon-core.json +1 -1
  31. package/src/grammar/lexicon.mjs +9 -27
  32. package/src/graph-merge.mjs +2 -3
  33. package/src/hash.mjs +6 -14
  34. package/src/index.mjs +6 -10
  35. package/src/init.mjs +38 -125
  36. package/src/interpret/fuzzy.mjs +10 -29
  37. package/src/interpret/merge.mjs +9 -27
  38. package/src/interpret/normalize.mjs +137 -585
  39. package/src/interpret/pipeline.mjs +23 -71
  40. package/src/interpret/strategies/ace.mjs +7 -31
  41. package/src/interpret/strategies/constructions.mjs +14 -41
  42. package/src/interpret/strategies/grammar.mjs +21 -60
  43. package/src/interpret/strategies/keywords.mjs +42 -131
  44. package/src/interpret/strategies/noise-strip.mjs +18 -89
  45. package/src/memory/bias.mjs +11 -54
  46. package/src/memory/blocks.mjs +18 -69
  47. package/src/memory/core.mjs +171 -591
  48. package/src/memory/fold.mjs +0 -0
  49. package/src/memory/inspect.mjs +7 -25
  50. package/src/memory/shacl.mjs +10 -39
  51. package/src/memory/trust.mjs +26 -127
  52. package/src/memory-ask-browser-entry.mjs +7 -30
  53. package/src/memory-ask-browser.bundle.js +1 -1
  54. package/src/paraphrase.mjs +20 -53
  55. package/src/planning.mjs +15 -157
  56. package/src/prose-nlp.mjs +4 -17
  57. package/src/prose.mjs +19 -67
  58. package/src/providers/bootstrap.mjs +1 -2
  59. package/src/providers/fixture.mjs +1 -2
  60. package/src/providers/graph-service.mjs +28 -59
  61. package/src/repository-interface.mjs +6 -8
  62. package/src/router/drive.mjs +183 -0
  63. package/src/router/goal-reasoner.mjs +66 -231
  64. package/src/router/guardrail.mjs +20 -58
  65. package/src/router/planner.mjs +15 -46
  66. package/src/router/registry.mjs +13 -43
  67. package/src/router/resolver.mjs +46 -131
  68. package/src/router/results.mjs +231 -0
  69. package/src/schema-docs.mjs +10 -27
  70. package/src/server-http.mjs +10 -19
  71. package/src/server.mjs +22 -28
  72. package/src/sessions.mjs +15 -30
  73. package/src/source-slice.mjs +5 -7
  74. package/src/source.mjs +10 -20
  75. package/src/syllogise.mjs +187 -575
  76. package/src/telemetry.mjs +3 -3
  77. package/src/toml-config.mjs +4 -4
  78. package/src/tui/app.mjs +9 -19
  79. package/src/viz.mjs +66 -123
  80. package/src/wink-model.mjs +10 -24
@@ -0,0 +1,231 @@
1
+ // src/router/results.mjs — the RESULT-EXECUTION layer for the capability router.
2
+ //
3
+ // Grading (and the live `tmct plan` surface) needs more than a call PLAN — it
4
+ // needs the executed, COMPOSED answer: an "of the modules impacted by X, which
5
+ // are untested" request passes by emitting tmct_impact then tmct_untested IN
6
+ // ORDER, but the actual set-intersection still has to be computed and checked.
7
+ // This module supplies that, in two halves, both PURE over the parsed graph
8
+ // (no I/O, no Date.now, no LLM):
9
+ //
10
+ // 1. resultSetOf(graph, name, input, resolvedInd) — the STRUCTURED RESULT of a
11
+ // single grounded call: the SET of entity LABELS the query produces (the
12
+ // impacted modules, the untested modules, the callers, the members, …),
13
+ // computed from the SAME codegraph primitives dispatchTool renders as text.
14
+ // A driver threads this onto ctx.dispatch's return (`result`) — the
15
+ // structured payload a composer reads. It is the machine-checkable twin of
16
+ // the human text a tool_result carries.
17
+ //
18
+ // 2. the COMPOSITION OPERATORS — intersect / fallback-if-empty / guard-if-empty
19
+ // (set-algebra.mjs) — the small set-algebra a multi-step plan needs to fold
20
+ // its threaded step results into ONE composed answer. A driver picks the
21
+ // operator from the router's OWN HTN method (relative-filter -> intersect;
22
+ // conditional -> fallback/guard).
23
+ //
24
+ // Why recompute here rather than parse the rendered text: the text is capped and
25
+ // human-formatted (renderImpact caps lists "for brevity"); the label SET is the
26
+ // honest, uncapped truth the composition needs. These extractors mirror the
27
+ // dispatchTool render* functions edge-for-edge.
28
+
29
+ import { impactClosure, edgesOfKind, siteOf } from "../codegraph.mjs";
30
+ import { uniqSort } from "./set-algebra.mjs";
31
+
32
+ // A test-path label, mirroring codegraph.mjs's private isTestLabel (untested view).
33
+ const isTestLabel = (s) =>
34
+ /(^|\/)tests?\//.test(s) || /(^|\/)test_[^/]*\.py$/.test(s) || /\.tests(\.|$)/.test(s);
35
+
36
+ /** The module id an individual belongs to — a Module is itself; a fine symbol maps
37
+ * through its site span (`mod:<path>`), else an `fn:<path>#name` id. Mirrors the
38
+ * private codegraph.moduleIdOf so tests_for/history resolve to the same key
39
+ * dispatchTool uses. Null if unmappable. */
40
+ function moduleIdOf(graph, ind) {
41
+ if ((ind?.class || "") === "Module") return ind.id;
42
+ const site = siteOf(ind);
43
+ if (site) return `mod:${site.path}`;
44
+ const m = String(ind?.id || "").match(/^fn:(.+)#/);
45
+ return m ? `mod:${m[1]}` : null;
46
+ }
47
+
48
+ // ---- per-tool structured result sets (label sets), mirroring render* ----------
49
+
50
+ /** Source modules with no covering test module (mirrors renderUntested). */
51
+ export function untestedModules(graph) {
52
+ const covered = new Set();
53
+ const testModules = new Set();
54
+ for (const e of edgesOfKind(graph, "tests")) { covered.add(e.object); testModules.add(e.subject); }
55
+ return uniqSort(
56
+ graph.individuals
57
+ .filter((i) => (i.class || "") === "Module"
58
+ && !testModules.has(i.id)
59
+ && !isTestLabel(String(i.label).toLowerCase())
60
+ && !covered.has(i.id))
61
+ .map((i) => i.label),
62
+ );
63
+ }
64
+
65
+ /** The reverse impact closure of a module — every transitive dependent's label
66
+ * (mirrors renderImpact/impactClosure, uncapped). */
67
+ export function impactLabels(graph, ind) {
68
+ const labels = [];
69
+ for (const level of impactClosure(graph, ind)) for (const dep of level) labels.push(dep.label);
70
+ return uniqSort(labels);
71
+ }
72
+
73
+ /** The test modules covering a symbol's module (mirrors renderTestsFor). [] = untested. */
74
+ export function testsForLabels(graph, ind) {
75
+ const modId = moduleIdOf(graph, ind);
76
+ if (!modId) return [];
77
+ return uniqSort(edgesOfKind(graph, "tests").filter((e) => e.object === modId).map((e) => e.subjectLabel || e.subject));
78
+ }
79
+
80
+ /** The methods/attributes of a class (mirrors renderMembers — the `contains` edge). */
81
+ export function membersLabels(graph, ind) {
82
+ return uniqSort(edgesOfKind(graph, "contains").filter((e) => e.subject === ind.id).map((e) => e.objectLabel || e.object));
83
+ }
84
+
85
+ const CALL_SYMBOL_CLASSES = new Set(["Function", "Method"]);
86
+
87
+ /** The member INDIVIDUALS of a class — the contains-edge object IDS mapped
88
+ * through a by-id index of graph.individuals to the FULL individuals (dotted
89
+ * labels like Widget.render, real classes like Method/Attribute). This is the
90
+ * GRAIN FIX the member-filter hop needs: the contains edge's objectLabel is the
91
+ * short member name ("render"), but the call graph (callsSymbol) and the
92
+ * tmct_callees arg both speak the individual's own dotted label — so we index
93
+ * to the individual and read ITS label, never string-concatenate one. */
94
+ export function memberIndividuals(graph, classInd) {
95
+ const byId = new Map(graph.individuals.map((i) => [i.id, i]));
96
+ return edgesOfKind(graph, "contains")
97
+ .filter((e) => e.subject === classInd.id)
98
+ .map((e) => byId.get(e.object))
99
+ .filter(Boolean);
100
+ }
101
+
102
+ /** Members of a class (Method|Function grain only) whose BOUNDED transitive
103
+ * callsSymbol closure reaches the target label — the member-filter fold
104
+ * ("which methods of X end up calling Y"). Depth-bounded BFS over the fine
105
+ * call graph (maxDepth mirrors the planner/subclasses bound); an unknown
106
+ * target composes ∅ (an honest empty answer, never a guess). */
107
+ export function membersReaching(graph, classInd, targetLabel, maxDepth = 8) {
108
+ const target = graph.individuals.find((i) => String(i.label) === String(targetLabel));
109
+ if (!target) return [];
110
+ const out = new Map();
111
+ for (const e of edgesOfKind(graph, "callsSymbol")) {
112
+ if (!out.has(e.subject)) out.set(e.subject, []);
113
+ out.get(e.subject).push(e.object);
114
+ }
115
+ const reaches = (startId) => {
116
+ const visited = new Set([startId]);
117
+ let frontier = [startId];
118
+ for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
119
+ const next = [];
120
+ for (const id of frontier) {
121
+ for (const o of out.get(id) || []) {
122
+ if (o === target.id) return true;
123
+ if (!visited.has(o)) { visited.add(o); next.push(o); }
124
+ }
125
+ }
126
+ frontier = next;
127
+ }
128
+ return false;
129
+ };
130
+ return uniqSort(
131
+ memberIndividuals(graph, classInd)
132
+ .filter((m) => CALL_SYMBOL_CLASSES.has(m.class))
133
+ .filter((m) => reaches(m.id))
134
+ .map((m) => m.label),
135
+ );
136
+ }
137
+
138
+ /** Callers of a symbol (mirrors renderCallers — callsSymbol for fine symbols,
139
+ * else module-coarse calls). */
140
+ export function callersLabels(graph, ind) {
141
+ if (CALL_SYMBOL_CLASSES.has(ind.class)) {
142
+ return uniqSort(edgesOfKind(graph, "callsSymbol").filter((e) => e.object === ind.id).map((e) => e.subjectLabel || e.subject));
143
+ }
144
+ const modId = moduleIdOf(graph, ind);
145
+ if (!modId) return [];
146
+ return uniqSort(edgesOfKind(graph, "calls").filter((e) => e.object === modId).map((e) => e.subjectLabel || e.subject));
147
+ }
148
+
149
+ /** Callees of a symbol (mirrors renderCallees). */
150
+ export function calleesLabels(graph, ind) {
151
+ if (CALL_SYMBOL_CLASSES.has(ind.class)) {
152
+ return uniqSort(edgesOfKind(graph, "callsSymbol").filter((e) => e.subject === ind.id).map((e) => e.objectLabel || e.object));
153
+ }
154
+ const modId = moduleIdOf(graph, ind);
155
+ if (!modId) return [];
156
+ return uniqSort(edgesOfKind(graph, "calls").filter((e) => e.subject === modId).map((e) => e.objectLabel || e.object));
157
+ }
158
+
159
+ /** Subclasses (transitive) of a class (mirrors renderSubclasses closure). */
160
+ export function subclassesLabels(graph, ind) {
161
+ const inherits = edgesOfKind(graph, "inherits");
162
+ const childrenOf = new Map();
163
+ for (const e of inherits) {
164
+ if (!childrenOf.has(e.object)) childrenOf.set(e.object, []);
165
+ childrenOf.get(e.object).push({ id: e.subject, label: e.subjectLabel || e.subject });
166
+ }
167
+ const labels = [];
168
+ const visited = new Set([ind.id]);
169
+ let frontier = [ind.id];
170
+ for (let depth = 1; depth <= 8 && frontier.length; depth += 1) {
171
+ const next = [];
172
+ for (const id of frontier) {
173
+ for (const c of childrenOf.get(id) || []) {
174
+ if (visited.has(c.id)) continue;
175
+ visited.add(c.id);
176
+ labels.push(c.label);
177
+ next.push(c.id);
178
+ }
179
+ }
180
+ frontier = next;
181
+ }
182
+ return uniqSort(labels);
183
+ }
184
+
185
+ /** Modules change-coupled with a symbol's module (mirrors renderCochanges /
186
+ * cochangeNeighbours EDGE-FOR-EDGE: the `cochange` edge is read SYMMETRICALLY —
187
+ * a hit whether the module is the edge's subject OR its object). The render
188
+ * caps at 20 for brevity; this is the uncapped honest label SET). */
189
+ export function cochangesLabels(graph, ind) {
190
+ const modId = moduleIdOf(graph, ind);
191
+ if (!modId) return [];
192
+ const labels = [];
193
+ for (const e of edgesOfKind(graph, "cochange")) {
194
+ if (e.subject === modId) labels.push(e.objectLabel || e.object);
195
+ else if (e.object === modId) labels.push(e.subjectLabel || e.subject);
196
+ }
197
+ return uniqSort(labels);
198
+ }
199
+
200
+ /** A module's public exports (mirrors renderExports — the `reexports` edge). */
201
+ export function exportsLabels(graph, ind) {
202
+ const modId = moduleIdOf(graph, ind);
203
+ if (!modId) return [];
204
+ return uniqSort(edgesOfKind(graph, "reexports").filter((e) => e.subject === modId).map((e) => e.objectLabel || e.object));
205
+ }
206
+
207
+ /** The STRUCTURED RESULT SET of one grounded call — the label set the query
208
+ * produces, for the composer to thread. `resolvedInd` is the bound entity
209
+ * (ctx.dispatch's `resolved`); a no-arg tool (untested) ignores it. A tool with
210
+ * no set semantics returns the bound entity itself as a singleton (describe /
211
+ * signature — "the thing looked up"). Never throws: an unbindable call already
212
+ * refused upstream, so this is only reached on a grounded call. */
213
+ export function resultSetOf(graph, name, _input, resolvedInd) {
214
+ switch (name) {
215
+ case "tmct_untested": return untestedModules(graph);
216
+ case "tmct_impact": return resolvedInd ? impactLabels(graph, resolvedInd) : [];
217
+ case "tmct_tests_for": return resolvedInd ? testsForLabels(graph, resolvedInd) : [];
218
+ case "tmct_members": return resolvedInd ? membersLabels(graph, resolvedInd) : [];
219
+ case "tmct_callers": return resolvedInd ? callersLabels(graph, resolvedInd) : [];
220
+ case "tmct_callees": return resolvedInd ? calleesLabels(graph, resolvedInd) : [];
221
+ case "tmct_subclasses": return resolvedInd ? subclassesLabels(graph, resolvedInd) : [];
222
+ case "tmct_cochanges": return resolvedInd ? cochangesLabels(graph, resolvedInd) : [];
223
+ case "tmct_exports": return resolvedInd ? exportsLabels(graph, resolvedInd) : [];
224
+ default: return resolvedInd ? [resolvedInd.label] : [];
225
+ }
226
+ }
227
+
228
+ // ---- the composition operators (the multi-step fold) -------------------------
229
+ // Re-exported here (uniqSort included) so a caller can get both halves from one
230
+ // module; they LIVE in set-algebra.mjs.
231
+ export { uniqSort, intersect, fallbackIfEmpty, guardIfEmpty } from "./set-algebra.mjs";
@@ -1,25 +1,11 @@
1
- // schema-docs.mjs — the single source of truth for tmct's own ontology documentation.
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
3
+ // buildEntities) actually emits, verified against real `prop:`/`class:` literals.
4
+ // `ingestSchemaDocs()` merges it into a graph build so "what does cochange mean" or
5
+ // "what is a Commit" is answerable by querying the graph like any other question.
2
6
  //
3
- // tmct's typed graph (src/graph-build.mjs's buildEntities, queried by codegraph.mjs)
4
- // has had its schema documented since day one, but only in scattered code comments
5
- // (graph-build.mjs's header, the `note` fields on some — not all — of the `vocabulary`
6
- // array entries it emits) — readable by a human editing the source, invisible to anything
7
- // that only sees graph.json. This file completes that documentation (every entity class,
8
- // every predicate actually emitted — verified against real `prop:`/`class:` literals in
9
- // graph-build.mjs, not just what the existing vocabulary array already claimed) and
10
- // `ingestSchemaDocs()` (called by a graph writer after buildEntities) merges it
11
- // into a graph build so a question like "what does cochange mean" or "what is a
12
- // Commit" is answerable by querying the SAME graph the same way any other question is —
13
- // not via separate hardcoded documentation logic.
14
- //
15
- // GLOBAL, NOT PER-REPO: this documentation does not vary by repository — "Module" means
16
- // the same thing indexing Django or a JS library — so it is static, committed data
17
- // (this file), never recomputed at index time. Ingesting it is a fixed-size merge, not a
18
- // computation (see the ~0ms measured delta in schema-docs.test.mjs).
19
- //
20
- // Mirrors the pattern in marginalia's app/lib/vocab.mjs (a `comment`/description per
21
- // schema term, single-sourced) minus the RDF/OWL external-alignment machinery, which
22
- // doesn't apply here — tmct's schema is code-relationship-specific, not general-domain.
7
+ // GLOBAL, NOT PER-REPO: this documentation doesn't vary by repository, so it's static,
8
+ // committed data, never recomputed at index time.
23
9
 
24
10
  // ---- entity classes (7 — verified against every `class: "X"` individual literal in
25
11
  // graph-build.mjs) -----------------------------------------------------------------------
@@ -58,9 +44,7 @@ export const CLASS_DOCS = Object.freeze([
58
44
  ]);
59
45
 
60
46
  // ---- predicates / attributes (every `prop:` token actually emitted by buildEntities,
61
- // verified by grep against graph-build.mjs — not just what the pre-existing `vocabulary`
62
- // array already documented; three real gaps found this way: seon:startsAt, mgx:value,
63
- // mgx:dotted, plus the prose second-pass's mgx:hasProseTokens) ------------------------
47
+ // verified against graph-build.mjs) ----------------------------------------------------
64
48
  export const PREDICATE_DOCS = Object.freeze([
65
49
  // ---- object properties (edges between individuals) ----
66
50
  { prop: "mgx:importsNamespace", kind: "imports", description:
@@ -110,9 +94,8 @@ export const PREDICATE_DOCS = Object.freeze([
110
94
  { prop: "mgx:hasProseTokens", kind: "prose", description:
111
95
  "Individual → word tokens (an attribute, not a between-individuals edge). The " +
112
96
  "decomposed word sequence extracted from an identifier's name (camelCase/snake_case " +
113
- "split) and any doc-comment prose, used by the second-pass prose-to-symbol " +
114
- "cross-reference index (PLAN_PROSE_INDEX.md) to resolve free-text object terms that " +
115
- "don't exact-match an identifier." },
97
+ "split) and any doc-comment prose, used by the prose-to-symbol cross-reference index " +
98
+ "to resolve free-text object terms that don't exact-match an identifier." },
116
99
 
117
100
  // ---- attributes (individual-scoped facts, not edges) ----
118
101
  { prop: "seon:startsAt", kind: "attribute", description:
@@ -1,27 +1,18 @@
1
1
  // server-http.mjs — `tmct serve`: an Anthropic Messages API-compatible HTTP
2
2
  // endpoint (POST /v1/messages) over tmct's existing zero-model engine.
3
3
  //
4
- // This is Phase A of the capability-router plan (PLAN_CAPABILITY_ROUTER.md): the
5
- // COMMON INTERFACE a tool-loop client (Claude Code) already speaks. It is a
6
- // deterministic serialization/HTTP shim NO model, ever. A request carries
7
- // { model, messages[], tools[], max_tokens, system? }; a response is a message
8
- // with `content` blocks (text and/or tool_use) and a `stop_reason`:
4
+ // A deterministic serialization/HTTP shim no model, ever. A request carries
5
+ // { model, messages[], tools[], max_tokens, system? }; a response is a message with
6
+ // `content` blocks and a `stop_reason`: "end_turn" runs the latest user text through
7
+ // runTurn (chat.mjs) for a cited read-only answer; "tool_use" emits a
8
+ // { type:"tool_use", id, name, input } block backed by dispatchTool (server.mjs) when
9
+ // the request maps to a declared graph-query tool.
9
10
  //
10
- // - TEXT ANSWER (stop_reason "end_turn"): the latest user text is run through
11
- // runTurn (src/chat.mjs) over the configured graph the same cited,
12
- // read-only answer the chat surface gives. Emitted when no tools are
13
- // declared, or when nothing maps to a declared graph-query tool.
14
- // - TOOL_USE (stop_reason "tool_use"): when tools[] are declared and the
15
- // request maps to a declared graph-query tool, a { type:"tool_use", id,
16
- // name, input } block is emitted — `name`+`input` are backed by dispatchTool
17
- // (src/server.mjs). The caller executes it and returns a tool_result block;
18
- // the next request closes the loop with an end_turn text answer.
11
+ // Every response's `usage` is { input_tokens: 0, output_tokens: 0 } — tmct is the $0
12
+ // floor, priced as free by the meter.
19
13
  //
20
- // bedrock-meter-pluggable: every response's `usage` is { input_tokens: 0,
21
- // output_tokens: 0 } — tmct is the $0 floor, priced as free by the meter.
22
- //
23
- // NOTE: src/server.mjs is the TOOL-DISPATCH layer (dispatchTool), NOT an HTTP
24
- // server; this module is the HTTP surface and imports that layer's exports.
14
+ // src/server.mjs is the tool-dispatch layer, not an HTTP server; this module is the
15
+ // HTTP surface.
25
16
 
26
17
  import { createServer } from "node:http";
27
18
  import { runTurn, COMMANDS, asBareCommand, isConversational } from "./chat.mjs";
package/src/server.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  // the chat surface and the `cli <tool>` route call into.
6
6
  //
7
7
  // Tools (all query-only, bounded output): tmct_search, tmct_describe, tmct_snippet,
8
- // tmct_impact, plus the §9 read-replacing tools tmct_members, tmct_subclasses,
8
+ // tmct_impact, plus the read-replacing tools tmct_members, tmct_subclasses,
9
9
  // tmct_architecture, tmct_tests_for, tmct_untested, tmct_history, tmct_callers,
10
10
  // tmct_callees. Each answers one question in ONE compact call so the caller need not
11
11
  // Read/Grep. Errors reach the caller as clean tool errors — message only, never a stack.
@@ -50,11 +50,9 @@ import {
50
50
  } from "./codegraph.mjs";
51
51
  import { ask } from "./ask.mjs";
52
52
  import { createGraphService } from "./providers/graph-service.mjs";
53
- // Read-ONLY consumers of the conversational-memory graph (src/memory/core.mjs) the 500
54
- // corpus facts live there, NOT in the code-map graph.json every tool below loads. Used by
55
- // the FALL-THROUGH bridge (below): when the code-map resolves NOTHING for a concept query
56
- // (/subclasses /describe /members /find), answer from the reified isa-family facts instead
57
- // of a flat "no entity". Never written here; memory writes stay owned by memory/*.
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.
58
56
  import { loadMemory, readFactRows, normFactTerm } from "./memory/core.mjs";
59
57
 
60
58
  const SNIPPET_MAX_LINES = 200;
@@ -162,13 +160,12 @@ export const TOOLS = [
162
160
  },
163
161
  ];
164
162
 
165
- // Exported (was module-private) so chat.mjs's compare lane can load the SAME
166
- // graph dispatchTool's own tools load — no new loading path, just direct reuse
167
- // of the existing config -> source.fetchEntities -> parseEntities chain, for
168
- // the case where runAsk's own `graph` param is null (the common case; it's
169
- // only preloaded when a caller already has one in hand — see runAsk's own
170
- // `if (graph && ...)` / dispatchTool("tmct_ask", …) split just above the
171
- // compare lane's call site for the existing precedent).
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).
172
169
  export async function loadGraph(config, source) {
173
170
  const payload = await source.fetchEntities(config);
174
171
  const graph = parseEntities(payload);
@@ -183,11 +180,8 @@ export async function loadGraph(config, source) {
183
180
  return graph;
184
181
  }
185
182
 
186
- // Resolution + the miss→ToolError bridge, threaded through the typed service
187
- // object (createGraphService). The service is the named seam; tmct's own
188
- // presentation (render*) reads its raw graph (svc.graph) and formats. A clean
189
- // miss on the interface becomes the instructive ToolError the CLI/chat expect —
190
- // message-only, never a stack, no fabricated entity names (generic placeholder).
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.
191
185
  function resolveOrThrow(svc, symbol, what) {
192
186
  const { match, candidates } = resolveSymbol(svc.graph, symbol);
193
187
  if (!match) {
@@ -201,14 +195,14 @@ function resolveOrThrow(svc, symbol, what) {
201
195
 
202
196
  /**
203
197
  * Build the tmct_context "edit bundle" for a symbol and return { text, tier, topup }.
204
- * Shared by the tmct_context tool AND the `cli digest` arm (cli.mjs), so both
205
- * benefit from the size-adaptive sizing for free. `trim:true` (B2) renders a SECONDARY,
206
- * signatures-only bundle (no bodies/tails) for related-but-not-primary digest modules.
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.
207
201
  *
208
- * Section ORDER is cache-stable (B7): the content that is identical across runs (anchor /
209
- * registration / exemplar / siblings / __all__ / insertion region) comes FIRST; the more
210
- * variable, history-derived tails (covering tests, co-change) come LAST, so a stable prefix
211
- * maximises prompt-cache reuse.
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.
212
206
  */
213
207
  export async function buildContextBundle(args, { config, source = defaultSource, trim = false, tel = null } = {}) {
214
208
  const symbol = String(args?.symbol || "").trim();
@@ -257,7 +251,7 @@ export async function buildContextBundle(args, { config, source = defaultSource,
257
251
  `Edit context for ${plan.moduleLabel} [${tier}${trim ? " secondary" : ""}] — assembled from the typed graph + that file. ` +
258
252
  "You do NOT need to Read it; write the new code directly after reviewing this.",
259
253
  ];
260
- // ---- cache-stable prefix (B7): identical across runs ----
254
+ // ---- cache-stable prefix: identical across runs ----
261
255
  if (mask.anchor && plan.anchor?.site && lines) {
262
256
  const { start, end } = plan.anchor.site;
263
257
  out.push(`\n## anchor: ${plan.anchor.label} (${plan.anchor.class}) @ ${plan.moduleLabel}:${start}-${end}`);
@@ -327,7 +321,7 @@ export async function buildContextBundle(args, { config, source = defaultSource,
327
321
  } else if (plan.insertion) {
328
322
  out.push(`\n## insert the new sibling after line ~${plan.insertion} (end of the last top-level definition).`);
329
323
  }
330
- // ---- variable tail (B7): history-derived, kept LAST so the prefix stays cache-stable ----
324
+ // ---- variable tail: history-derived, kept LAST so the prefix stays cache-stable ----
331
325
  if (mask.tests && plan.tests.length) out.push(`\n## covering tests: ${plan.tests.join(", ")}`);
332
326
  if (mask.cochange && plan.cochange && plan.cochange.length) {
333
327
  out.push(`\n## usually changed together (consider editing these too): ${plan.cochange.map((c) => `${c.label} (×${c.weight})`).join(", ")}`);
@@ -481,7 +475,7 @@ export async function dispatchTool(name, args, { config, source = defaultSource,
481
475
  const { content, tmct_ask } = ask(graph, query);
482
476
  // Every dispatchTool caller (the chat surface, the CLI fallback) expects a plain string —
483
477
  // append the structured envelope as a delimited, machine-parseable block rather than
484
- // changing that shared contract for one tool. PLAN_MECHANICAL_CHAT.md §6.2.
478
+ // changing that shared contract for one tool.
485
479
  return `${content}\n\n---tmct_ask---\n${JSON.stringify(tmct_ask, null, 2)}`;
486
480
  }
487
481
  if (
package/src/sessions.mjs CHANGED
@@ -7,21 +7,14 @@
7
7
  // {"type":"turn", ts, query, via, resolvedIds, answeredIds, miss} (one per turn, flushed)
8
8
  // {"type":"end", ts} (clean close marker)
9
9
  //
10
- // From the sidecar the session enters the typed graph twice:
11
- // - READ TIME (chat.mjs, per turn): appendSessionToGraph() upserts one `Session`
12
- // individual (`session:<uuidv7>`) + `mgx:asksAbout` edges into graph.json
13
- // atomically (temp + rename), re-reading the file first so a re-index that
14
- // happened mid-session is tolerated: edges whose targets vanished are dropped,
15
- // never left dangling (honest degradation).
16
- // - REBUILD (any future graph writer): readSessionRecords() + foldInSessions()
17
- // re-attach every recorded session to a FRESH graph, re-resolving each recorded
18
- // entity id (by id first, then by unique label derived from the id shape);
19
- // unresolvable references are dropped and counted on the session node
20
- // (mgx:sessionDroppedEdges) — never a guessed edge.
10
+ // From the sidecar the session enters the typed graph twice: at READ TIME (chat.mjs, per
11
+ // turn) appendSessionToGraph() upserts one `Session` individual + `mgx:asksAbout` edges,
12
+ // atomically, dropping edges whose targets vanished; at REBUILD, readSessionRecords() +
13
+ // foldInSessions() re-attach every recorded session to a fresh graph, re-resolving each
14
+ // entity id and dropping unresolvable references rather than guessing.
21
15
  //
22
- // Sessions are runtime observations, not source derivations: they record what a human
23
- // asked the graph about and which entities answered, so re-indexing re-attaches them
24
- // rather than re-deriving them from source.
16
+ // Sessions are runtime observations, not source derivations, so re-indexing re-attaches
17
+ // them rather than re-deriving them from source.
25
18
 
26
19
  import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
27
20
  import { basename, dirname, join } from "node:path";
@@ -177,10 +170,6 @@ export async function appendSessionToGraph(graphFile, record, { memory = true, r
177
170
  }
178
171
  const res = upsertSession(entities, record);
179
172
  await atomicWriteJson(graphFile, entities);
180
- // ALSO record the turn(s) into tmct's OWN memory graph (.tmct/memory/ — item 9),
181
- // and fold the transcript into the text-block corpus once the session has ended.
182
- // Best-effort by design: memory must never degrade the graph append that already
183
- // succeeded, so every failure here is swallowed (mirrors chat.mjs's own stance).
184
173
  if (memory) {
185
174
  try { await recordSessionMemory(graphFile, record, repoDir); } catch { /* best-effort */ }
186
175
  }
@@ -197,15 +186,13 @@ function repoDirFromGraphFile(graphFile) {
197
186
  return basename(tmctDir) === ".tmct" ? dirname(tmctDir) : null;
198
187
  }
199
188
 
200
- /** The memory side-write for one session append (item 9's chat wiring, placed
201
- * HERE so chat.mjs needs no change — it already calls appendSessionToGraph
202
- * every turn). Each recorded turn becomes an a-visitor-said Utterance; the
203
- * response prose is recovered from the human transcript (the only artifact
204
- * that carries answer TEXT the sidecar records ids) and recorded alongside
205
- * as a tmct Utterance replying to it. Deterministic utterance ids make the
206
- * per-turn replay idempotent. Once the sidecar carries its end marker (chat
207
- * writes it before the final graph upsert), the session is folded into the
208
- * text-block corpus (memory/fold.mjs). */
189
+ /** The memory side-write for one session append, placed here so chat.mjs needs no
190
+ * change — it already calls appendSessionToGraph every turn. Each recorded turn becomes
191
+ * an a-visitor-said Utterance; the response prose is recovered from the human transcript
192
+ * (the sidecar only records ids) and recorded alongside as a tmct Utterance replying to
193
+ * it. Deterministic utterance ids make the per-turn replay idempotent. Once the sidecar
194
+ * carries its end marker, the session is folded into the text-block corpus
195
+ * (memory/fold.mjs). */
209
196
  async function recordSessionMemory(graphFile, record, repoDirOverride = null) {
210
197
  const repoDir = repoDirOverride ?? repoDirFromGraphFile(graphFile);
211
198
  if (!repoDir || !record?.id) return;
@@ -298,9 +285,7 @@ export const turnKey = (ts, query) => `${ts}${query}`;
298
285
  * Parse a human-readable session transcript (.tmct/session-<id>.log) into a
299
286
  * Map of turnKey(ts, query) → answer text. The transcript is the ONLY session
300
287
  * artifact that carries the answer PROSE (the structured sidecar records ids,
301
- * not text), so the memory write-path recovers response text from here.
302
- * Tolerant by design: a block is `ts` line + "> query" line + answer lines
303
- * until the next block; header/footer and torn tails just don't match.
288
+ * not text),
304
289
  */
305
290
  export function parseSessionLog(text) {
306
291
  const lines = String(text ?? "").split("\n");
@@ -3,13 +3,11 @@
3
3
  //
4
4
  // Two halves, deliberately split:
5
5
  // - sliceSpan — PURE. Given an in-memory `lines` array, extracts + line-numbers
6
- // one span. No fs, no path logic. Lifted byte-identical from the
7
- // slicing logic that used to live inline in server.mjs (buildContextBundle's
8
- // `sliceBody` closure and the tmct_snippet dispatch branch).
6
+ // one span. No fs, no path logic.
9
7
  // - readSpanSafe — the fs-touching half. Resolves `join(repoRoot, path)` with Node's
10
8
  // `resolve()` and REFUSES to read anything that resolves outside
11
- // `repoRoot` before ever calling the injected `readFile`. This is the
12
- // fix for a real path-traversal gap: graph.json's `site.path` values are
9
+ // `repoRoot` before ever calling the injected `readFile`. Guards against
10
+ // a real path-traversal gap: graph.json's `site.path` values are
13
11
  // data (parsed from a JSON artifact on disk), not trusted input — a
14
12
  // crafted or corrupted graph with a `../../etc/passwd`-shaped path must
15
13
  // never reach an unguarded readFile.
@@ -53,8 +51,8 @@ export function sliceSpan(lines, start, end, maxLines) {
53
51
  export async function readSpanSafe({ readFile, repoRoot, path, start, end, maxLines }) {
54
52
  // Normalize repoRoot to absolute here too (defense in depth) — resolve(repoRoot, path)
55
53
  // is always absolute, so comparing it against a RELATIVE repoRoot would make this guard
56
- // reject every read, not just traversal attempts (the actual bug this normalization
57
- // fixes; callers should already pass an absolute repoRoot via src/config.mjs.
54
+ // reject every read, not just traversal attempts. Callers should already pass an
55
+ // absolute repoRoot via src/config.mjs.
58
56
  const root = resolve(repoRoot);
59
57
  const resolved = resolve(root, path);
60
58
  if (resolved !== root && !resolved.startsWith(root + sep)) {
package/src/source.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // stub it); in production it reads the JSON artifact the deterministic indexer
4
4
  // wrote to config.graphFile. No network, no model calls.
5
5
  //
6
- // This module is the PROVIDER SEAM (ROADMAP item 14, docs/adapter-contract.md):
6
+ // This module is the PROVIDER SEAM (docs/adapter-contract.md):
7
7
  // any graph producer can feed tmct either by writing the entities-payload JSON
8
8
  // where config.graphFile points, or by registering a custom loader with
9
9
  // registerProvider() — no indexer is ever imported here. tmct only READS
@@ -53,10 +53,9 @@ export function emptyEntities() {
53
53
  };
54
54
  }
55
55
 
56
- /** Read + parse ONE graph artifact file the same per-file tolerance the
57
- * single-graph path below has always had: a MISSING file (ENOENT) is not an
58
- * error, it's the bootstrap payload; any other read/parse failure is a clean
59
- * ToolError naming the file. Shared by the single- and multi-graph paths. */
56
+ /** Read + parse ONE graph artifact file: a MISSING file (ENOENT) is not an error, it's
57
+ * the bootstrap payload; any other read/parse failure is a clean ToolError naming the
58
+ * file. Shared by the single- and multi-graph paths. */
60
59
  async function readOneGraphFile(file) {
61
60
  let text;
62
61
  try {
@@ -93,21 +92,12 @@ async function fetchMergedEntities(config) {
93
92
  return merged;
94
93
  }
95
94
 
96
- /** Fetch the entities payload through the provider seam. With a registered
97
- * provider, its result is returned as-is (uncached a live provider owns its
98
- * own caching/refresh policy); a non-object result is a clean ToolError.
99
- * Default: read + parse the local graph artifact, cached per file for the
100
- * process. A MISSING artifact (ENOENT) is not an error: the chat surface
101
- * starts from an empty graph and the first session fold-in creates the file —
102
- * so we return the bootstrap payload (uncached, so the freshly written file is
103
- * picked up next fetch). Every other failure still throws a clean ToolError.
104
- *
105
- * MULTI-GRAPH: when `config.graphFiles` names more than one file, this
106
- * delegates to fetchMergedEntities (src/graph-merge.mjs) instead — a
107
- * SEPARATE code path from the block below. The single-graph case (one
108
- * `config.graphFile`, or a one-element `config.graphFiles`) always falls
109
- * through to the unchanged block below — byte-identical to before multi-graph
110
- * support existed. */
95
+ /** Fetch the entities payload through the provider seam. With a registered provider, its
96
+ * result is returned as-is (uncached); a non-object result is a clean ToolError. Default:
97
+ * read + parse the local graph artifact, cached per file. A missing artifact (ENOENT)
98
+ * returns the bootstrap payload, uncached, so a freshly written file is picked up next
99
+ * fetch. When `config.graphFiles` names more than one file, delegates to
100
+ * fetchMergedEntities instead. */
111
101
  export async function fetchEntities(config) {
112
102
  if (provider) {
113
103
  let payload;