@polycode-projects/the-mechanical-code-talker 1.9.1 → 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.
- package/README.md +441 -217
- package/bin/tmct.mjs +126 -1
- package/corpus/seon/README.md +1 -2
- package/package.json +4 -2
- package/src/answer-variants.mjs +8 -36
- package/src/ask-browser-entry.mjs +5 -23
- package/src/ask-browser.bundle.js +1 -2
- package/src/ask-nlp.mjs +9 -23
- package/src/ask-vocab.mjs +139 -589
- package/src/ask.mjs +627 -1729
- package/src/chat.mjs +1684 -2874
- package/src/cli-args.mjs +14 -28
- package/src/codegraph.mjs +236 -644
- package/src/completions/complete.mjs +18 -62
- package/src/completions/graph-adapter.mjs +14 -60
- package/src/completions/group.mjs +12 -68
- package/src/completions/infer.mjs +38 -126
- package/src/completions/prune.mjs +17 -70
- package/src/completions/rank.mjs +16 -69
- package/src/completions/search.mjs +8 -31
- package/src/concept.mjs +32 -88
- package/src/conformance.mjs +11 -15
- package/src/corpus/conceptnet.mjs +31 -89
- package/src/corpus/templates.mjs +19 -45
- package/src/corpus/unknown-ingest.mjs +31 -92
- package/src/embed.mjs +10 -22
- package/src/extensions.mjs +50 -154
- package/src/finish.mjs +35 -91
- package/src/grammar/ace.mjs +16 -40
- package/src/grammar/assert.mjs +1 -1
- package/src/grammar/lexicon-core.json +1 -1
- package/src/grammar/lexicon.mjs +9 -27
- package/src/graph-merge.mjs +2 -3
- package/src/hash.mjs +6 -14
- package/src/index.mjs +6 -10
- package/src/init.mjs +38 -125
- package/src/interpret/fuzzy.mjs +10 -29
- package/src/interpret/merge.mjs +9 -27
- package/src/interpret/normalize.mjs +137 -585
- package/src/interpret/pipeline.mjs +23 -71
- package/src/interpret/strategies/ace.mjs +7 -31
- package/src/interpret/strategies/constructions.mjs +14 -41
- package/src/interpret/strategies/grammar.mjs +21 -60
- package/src/interpret/strategies/keywords.mjs +42 -131
- package/src/interpret/strategies/noise-strip.mjs +18 -89
- package/src/memory/bias.mjs +11 -54
- package/src/memory/blocks.mjs +18 -69
- package/src/memory/core.mjs +171 -591
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +7 -25
- package/src/memory/shacl.mjs +10 -39
- package/src/memory/trust.mjs +26 -127
- package/src/memory-ask-browser-entry.mjs +7 -30
- package/src/memory-ask-browser.bundle.js +1 -1
- package/src/paraphrase.mjs +20 -53
- package/src/planning.mjs +15 -157
- package/src/prose-nlp.mjs +4 -17
- package/src/prose.mjs +19 -67
- package/src/providers/bootstrap.mjs +1 -2
- package/src/providers/fixture.mjs +1 -2
- package/src/providers/graph-service.mjs +28 -59
- package/src/repository-interface.mjs +6 -8
- package/src/router/drive.mjs +183 -0
- package/src/router/goal-reasoner.mjs +66 -231
- package/src/router/guardrail.mjs +20 -58
- package/src/router/planner.mjs +15 -46
- package/src/router/registry.mjs +13 -43
- package/src/router/resolver.mjs +46 -131
- package/src/router/results.mjs +231 -0
- package/src/schema-docs.mjs +10 -27
- package/src/server-http.mjs +10 -19
- package/src/server.mjs +22 -28
- package/src/sessions.mjs +15 -30
- package/src/source-slice.mjs +5 -7
- package/src/source.mjs +10 -20
- package/src/syllogise.mjs +187 -575
- package/src/telemetry.mjs +3 -3
- package/src/toml-config.mjs +4 -4
- package/src/tui/app.mjs +9 -19
- package/src/viz.mjs +66 -123
- 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";
|
package/src/schema-docs.mjs
CHANGED
|
@@ -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
|
-
//
|
|
4
|
-
//
|
|
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
|
|
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
|
|
114
|
-
"
|
|
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:
|
package/src/server-http.mjs
CHANGED
|
@@ -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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
//
|
|
11
|
-
//
|
|
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
|
-
//
|
|
21
|
-
//
|
|
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
|
|
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-
|
|
54
|
-
//
|
|
55
|
-
//
|
|
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
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
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
|
-
//
|
|
187
|
-
//
|
|
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)
|
|
205
|
-
*
|
|
206
|
-
*
|
|
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
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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
|
|
23
|
-
//
|
|
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
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
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),
|
|
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");
|
package/src/source-slice.mjs
CHANGED
|
@@ -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.
|
|
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`.
|
|
12
|
-
//
|
|
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
|
|
57
|
-
//
|
|
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 (
|
|
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
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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;
|