@ctxo/cli 0.14.0 → 0.15.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 +10 -1
- package/dist/{chunk-ZL36YS2V.js → chunk-A4URKKPP.js} +3 -177
- package/dist/{chunk-SRQZDGFZ.js → chunk-BBSJB4CG.js} +2 -2
- package/dist/{chunk-X4TVFJ4C.js → chunk-EC6INGPV.js} +1 -1
- package/dist/{chunk-7HSA5JVP.js → chunk-I7RPXT6L.js} +193 -1
- package/dist/chunk-IT426P3F.js +1036 -0
- package/dist/{chunk-G4BMKCKG.js → chunk-JDL6GGTO.js} +16 -5
- package/dist/{cli-router-AZCDNX5R.js → cli-router-W56INOQ3.js} +10 -10
- package/dist/index.js +3 -3
- package/dist/internal.d.ts +90 -3
- package/dist/internal.js +4 -4
- package/dist/{json-index-reader-DBW32TCN.js → json-index-reader-DKEIP2QR.js} +2 -2
- package/dist/{mcp-server-6YG2QE6E.js → mcp-server-IYCCE74E.js} +178 -446
- package/dist/module-host-KTGDODES.js +13 -0
- package/llms-full.txt +68 -3
- package/llms.txt +14 -2
- package/package.json +4 -3
- package/dist/atomic-write-BAFH464Q.js.map +0 -1
- package/dist/chunk-2Q76LH2H.js.map +0 -1
- package/dist/chunk-5DSKZJNN.js.map +0 -1
- package/dist/chunk-72CEQEQD.js.map +0 -1
- package/dist/chunk-7HSA5JVP.js.map +0 -1
- package/dist/chunk-G4BMKCKG.js.map +0 -1
- package/dist/chunk-L2CAUER6.js +0 -508
- package/dist/chunk-L2CAUER6.js.map +0 -1
- package/dist/chunk-LAX6VVBU.js.map +0 -1
- package/dist/chunk-PSKDDLSF.js.map +0 -1
- package/dist/chunk-RFEXMBTG.js.map +0 -1
- package/dist/chunk-SRQZDGFZ.js.map +0 -1
- package/dist/chunk-X4TVFJ4C.js.map +0 -1
- package/dist/chunk-ZL36YS2V.js.map +0 -1
- package/dist/cli-router-AZCDNX5R.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.js.map +0 -1
- package/dist/json-index-reader-DBW32TCN.js.map +0 -1
- package/dist/mcp-server-6YG2QE6E.js.map +0 -1
- package/dist/module-host-KNMKS62R.js +0 -13
- package/dist/module-host-KNMKS62R.js.map +0 -1
- package/dist/module-loader-B3LYF3W7.js.map +0 -1
- package/dist/plugin-discovery-XRXI3IJF.js.map +0 -1
- package/dist/staleness-detector-4HJYBQ7Z.js.map +0 -1
package/dist/chunk-L2CAUER6.js
DELETED
|
@@ -1,508 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
SymbolGraph
|
|
4
|
-
} from "./chunk-7HSA5JVP.js";
|
|
5
|
-
import {
|
|
6
|
-
DetailLevelSchema,
|
|
7
|
-
JsonIndexReader
|
|
8
|
-
} from "./chunk-X4TVFJ4C.js";
|
|
9
|
-
import {
|
|
10
|
-
createLogger
|
|
11
|
-
} from "./chunk-5DSKZJNN.js";
|
|
12
|
-
|
|
13
|
-
// src/core/response-envelope.ts
|
|
14
|
-
var DEFAULT_THRESHOLD = 8192;
|
|
15
|
-
var workspaceMeta;
|
|
16
|
-
function setWorkspaceMeta(workspace) {
|
|
17
|
-
workspaceMeta = workspace;
|
|
18
|
-
}
|
|
19
|
-
var TRUNCATABLE_FIELDS = {
|
|
20
|
-
impactedSymbols: "Use search_symbols to find specific impacted symbols, or get_blast_radius with a confidence filter.",
|
|
21
|
-
importers: "Use find_importers with edgeKinds filter or maxDepth to narrow results.",
|
|
22
|
-
deadSymbols: "Use search_symbols with kind filter to find specific dead symbols.",
|
|
23
|
-
unusedExports: "Use find_importers to check specific symbols.",
|
|
24
|
-
results: "Use search_symbols or get_ranked_context with a narrower query.",
|
|
25
|
-
rankings: "Use get_symbol_importance with kind or filePattern filter to narrow results.",
|
|
26
|
-
context: "Use get_context_for_task with a smaller tokenBudget.",
|
|
27
|
-
hierarchies: "Use get_class_hierarchy with a specific symbolId.",
|
|
28
|
-
scaffolding: "Review the scaffolding markers directly in the listed files.",
|
|
29
|
-
deadFiles: "Use find_dead_code with includeTests to adjust scope.",
|
|
30
|
-
files: "Use get_changed_symbols with a narrower since ref or smaller maxFiles.",
|
|
31
|
-
clusters: "Use get_architectural_overlay with mode=communities and layer filter for a focused view.",
|
|
32
|
-
candidates: "Use get_architectural_overlay with mode=contexts and candidate: <id> for one candidate in full, or ctxo contexts --json for the full contexts view.",
|
|
33
|
-
clustersAffected: "Use get_blast_radius on individual changed symbols for per-cluster impact."
|
|
34
|
-
};
|
|
35
|
-
function getThreshold() {
|
|
36
|
-
const env = process.env["CTXO_RESPONSE_LIMIT"];
|
|
37
|
-
if (env) {
|
|
38
|
-
const n = parseInt(env, 10);
|
|
39
|
-
if (!isNaN(n) && n > 0) return n;
|
|
40
|
-
}
|
|
41
|
-
return DEFAULT_THRESHOLD;
|
|
42
|
-
}
|
|
43
|
-
function findTruncatableArray(data) {
|
|
44
|
-
let best = null;
|
|
45
|
-
for (const [key, value] of Object.entries(data)) {
|
|
46
|
-
if (Array.isArray(value) && key in TRUNCATABLE_FIELDS) {
|
|
47
|
-
if (!best || value.length > best.arr.length) {
|
|
48
|
-
best = { key, arr: value };
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return best;
|
|
53
|
-
}
|
|
54
|
-
function buildMeta(base, extras) {
|
|
55
|
-
const meta = workspaceMeta ? { ...base, workspace: workspaceMeta } : base;
|
|
56
|
-
if (extras?.snapshotStaleness) {
|
|
57
|
-
return { ...meta, snapshotStaleness: extras.snapshotStaleness };
|
|
58
|
-
}
|
|
59
|
-
return meta;
|
|
60
|
-
}
|
|
61
|
-
function wrapResponse(data, extras) {
|
|
62
|
-
const threshold = getThreshold();
|
|
63
|
-
const fullJson = JSON.stringify(data);
|
|
64
|
-
const totalBytes = Buffer.byteLength(fullJson, "utf-8");
|
|
65
|
-
const truncatable = findTruncatableArray(data);
|
|
66
|
-
const totalItems = truncatable ? truncatable.arr.length : 0;
|
|
67
|
-
if (totalBytes <= threshold) {
|
|
68
|
-
return {
|
|
69
|
-
...data,
|
|
70
|
-
_meta: buildMeta(
|
|
71
|
-
{
|
|
72
|
-
totalItems,
|
|
73
|
-
returnedItems: totalItems,
|
|
74
|
-
truncated: false,
|
|
75
|
-
totalBytes
|
|
76
|
-
},
|
|
77
|
-
extras
|
|
78
|
-
)
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
if (!truncatable || truncatable.arr.length <= 1) {
|
|
82
|
-
return {
|
|
83
|
-
...data,
|
|
84
|
-
_meta: buildMeta(
|
|
85
|
-
{
|
|
86
|
-
totalItems,
|
|
87
|
-
returnedItems: totalItems,
|
|
88
|
-
truncated: false,
|
|
89
|
-
totalBytes
|
|
90
|
-
},
|
|
91
|
-
extras
|
|
92
|
-
)
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
let lo = 1;
|
|
96
|
-
let hi = truncatable.arr.length;
|
|
97
|
-
while (lo < hi) {
|
|
98
|
-
const mid = Math.ceil((lo + hi) / 2);
|
|
99
|
-
const trial = {
|
|
100
|
-
...data,
|
|
101
|
-
[truncatable.key]: truncatable.arr.slice(0, mid),
|
|
102
|
-
_meta: buildMeta(
|
|
103
|
-
{
|
|
104
|
-
totalItems,
|
|
105
|
-
returnedItems: mid,
|
|
106
|
-
truncated: true,
|
|
107
|
-
totalBytes,
|
|
108
|
-
hint: TRUNCATABLE_FIELDS[truncatable.key]
|
|
109
|
-
},
|
|
110
|
-
extras
|
|
111
|
-
)
|
|
112
|
-
};
|
|
113
|
-
const size = Buffer.byteLength(JSON.stringify(trial), "utf-8");
|
|
114
|
-
if (size <= threshold) {
|
|
115
|
-
lo = mid;
|
|
116
|
-
} else {
|
|
117
|
-
hi = mid - 1;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const hint = TRUNCATABLE_FIELDS[truncatable.key];
|
|
121
|
-
return {
|
|
122
|
-
...data,
|
|
123
|
-
[truncatable.key]: truncatable.arr.slice(0, lo),
|
|
124
|
-
_meta: buildMeta(
|
|
125
|
-
{
|
|
126
|
-
totalItems,
|
|
127
|
-
returnedItems: lo,
|
|
128
|
-
truncated: true,
|
|
129
|
-
totalBytes,
|
|
130
|
-
hint
|
|
131
|
-
},
|
|
132
|
-
extras
|
|
133
|
-
)
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// src/core/describe-error.ts
|
|
138
|
-
function describeError(err) {
|
|
139
|
-
try {
|
|
140
|
-
if (err instanceof Error) {
|
|
141
|
-
const message = err.message;
|
|
142
|
-
if (typeof message === "string") return message;
|
|
143
|
-
}
|
|
144
|
-
if (typeof err === "string") return err;
|
|
145
|
-
return String(err);
|
|
146
|
-
} catch {
|
|
147
|
-
try {
|
|
148
|
-
return Object.prototype.toString.call(err);
|
|
149
|
-
} catch {
|
|
150
|
-
return "[unprintable error]";
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// src/adapters/mcp/enrich-payload.ts
|
|
156
|
-
var log = createLogger("ctxo:modules");
|
|
157
|
-
function applyEnricher(enrich, payload, args, tool) {
|
|
158
|
-
if (enrich === void 0) return payload;
|
|
159
|
-
try {
|
|
160
|
-
const enriched = enrich(structuredClone(payload), args);
|
|
161
|
-
if (!isPlainObject(enriched) || !isPlainData(enriched, /* @__PURE__ */ new Set())) {
|
|
162
|
-
log.warn(
|
|
163
|
-
`The ${tool} enricher returned something other than a plain JSON payload object; its fields are left out of this response`
|
|
164
|
-
);
|
|
165
|
-
return payload;
|
|
166
|
-
}
|
|
167
|
-
JSON.stringify(enriched);
|
|
168
|
-
return enriched;
|
|
169
|
-
} catch (err) {
|
|
170
|
-
log.warn(`The ${tool} enricher failed: ${describeError(err)}; its fields are left out of this response`);
|
|
171
|
-
return payload;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
function isPlainObject(value) {
|
|
175
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
176
|
-
const proto = Object.getPrototypeOf(value);
|
|
177
|
-
return proto === Object.prototype || proto === null;
|
|
178
|
-
}
|
|
179
|
-
function isPlainData(value, path) {
|
|
180
|
-
if (value === null) return true;
|
|
181
|
-
const type = typeof value;
|
|
182
|
-
if (type === "string" || type === "number" || type === "boolean") return true;
|
|
183
|
-
if (type !== "object") return false;
|
|
184
|
-
const node = value;
|
|
185
|
-
if (path.has(node)) return false;
|
|
186
|
-
if (!Array.isArray(node) && !isPlainObject(node)) return false;
|
|
187
|
-
path.add(node);
|
|
188
|
-
try {
|
|
189
|
-
for (const child of Array.isArray(node) ? node : Object.values(node)) {
|
|
190
|
-
if (child !== void 0 && !isPlainData(child, path)) return false;
|
|
191
|
-
}
|
|
192
|
-
} finally {
|
|
193
|
-
path.delete(node);
|
|
194
|
-
}
|
|
195
|
-
return true;
|
|
196
|
-
}
|
|
197
|
-
function collectEnrichers(modules) {
|
|
198
|
-
const chains = /* @__PURE__ */ new Map();
|
|
199
|
-
for (const module of modules) {
|
|
200
|
-
let entries;
|
|
201
|
-
try {
|
|
202
|
-
entries = Object.entries(module.registration.enrich ?? {});
|
|
203
|
-
} catch (err) {
|
|
204
|
-
log.warn(`${module.name} enrichers could not be read: ${describeError(err)}`);
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
for (const [tool, enrich] of entries) {
|
|
208
|
-
if (typeof enrich !== "function") continue;
|
|
209
|
-
const chain = chains.get(tool) ?? [];
|
|
210
|
-
chain.push(enrich);
|
|
211
|
-
chains.set(tool, chain);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const enrichers = /* @__PURE__ */ new Map();
|
|
215
|
-
for (const [tool, chain] of chains) {
|
|
216
|
-
enrichers.set(
|
|
217
|
-
tool,
|
|
218
|
-
chain.length === 1 ? chain[0] : (payload, args) => chain.reduce((current, enrich) => applyEnricher(enrich, current, args, tool), payload)
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
return enrichers;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// src/adapters/mcp/get-logic-slice.ts
|
|
225
|
-
import { z } from "zod";
|
|
226
|
-
|
|
227
|
-
// src/core/logic-slice/logic-slice-query.ts
|
|
228
|
-
var LogicSliceQuery = class {
|
|
229
|
-
getLogicSlice(graph, symbolId, maxDepth = Infinity) {
|
|
230
|
-
const root = graph.getNode(symbolId);
|
|
231
|
-
if (!root) return void 0;
|
|
232
|
-
const visited = /* @__PURE__ */ new Set([root.symbolId]);
|
|
233
|
-
const dependencies = [];
|
|
234
|
-
const collectedEdges = [];
|
|
235
|
-
const queue = [{ id: root.symbolId, depth: 0 }];
|
|
236
|
-
while (queue.length > 0) {
|
|
237
|
-
const current = queue.shift();
|
|
238
|
-
if (current.depth >= maxDepth) continue;
|
|
239
|
-
const edges = graph.getForwardEdges(current.id);
|
|
240
|
-
for (const edge of edges) {
|
|
241
|
-
const depNode = graph.getNode(edge.to);
|
|
242
|
-
if (!depNode) continue;
|
|
243
|
-
collectedEdges.push(edge);
|
|
244
|
-
if (visited.has(edge.to)) continue;
|
|
245
|
-
visited.add(edge.to);
|
|
246
|
-
dependencies.push(depNode);
|
|
247
|
-
queue.push({ id: edge.to, depth: current.depth + 1 });
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return { root, dependencies, edges: collectedEdges };
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
// src/core/detail-levels/detail-formatter.ts
|
|
255
|
-
var L1_MAX_LINES = 150;
|
|
256
|
-
var L4_TOKEN_BUDGET = 8e3;
|
|
257
|
-
function withoutAnnotations(node) {
|
|
258
|
-
if (node.annotations === void 0) return node;
|
|
259
|
-
const { annotations: _annotations, ...rest } = node;
|
|
260
|
-
return rest;
|
|
261
|
-
}
|
|
262
|
-
var DetailFormatter = class {
|
|
263
|
-
format(slice, level) {
|
|
264
|
-
switch (level) {
|
|
265
|
-
case 1:
|
|
266
|
-
return this.formatL1(slice);
|
|
267
|
-
case 2:
|
|
268
|
-
return this.formatL2(slice);
|
|
269
|
-
case 3:
|
|
270
|
-
return this.formatL3(slice);
|
|
271
|
-
case 4:
|
|
272
|
-
return this.formatL4(slice);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
formatL1(slice) {
|
|
276
|
-
const root = slice.root;
|
|
277
|
-
const lineCount = root.endLine - root.startLine + 1;
|
|
278
|
-
const clampedRoot = lineCount > L1_MAX_LINES ? { ...root, endLine: root.startLine + L1_MAX_LINES - 1 } : root;
|
|
279
|
-
return {
|
|
280
|
-
root: withoutAnnotations(clampedRoot),
|
|
281
|
-
dependencies: [],
|
|
282
|
-
edges: [],
|
|
283
|
-
level: 1,
|
|
284
|
-
levelDescription: "L1: Root symbol signature only (no dependencies)",
|
|
285
|
-
...lineCount > L1_MAX_LINES ? { truncation: { truncated: true, reason: "token_budget_exceeded" } } : {}
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
formatL2(slice) {
|
|
289
|
-
const directEdges = slice.edges.filter((e) => e.from === slice.root.symbolId);
|
|
290
|
-
const directDepIds = new Set(directEdges.map((e) => e.to));
|
|
291
|
-
const directDeps = slice.dependencies.filter((d) => directDepIds.has(d.symbolId));
|
|
292
|
-
return {
|
|
293
|
-
root: withoutAnnotations(slice.root),
|
|
294
|
-
dependencies: directDeps.map(withoutAnnotations),
|
|
295
|
-
edges: directEdges,
|
|
296
|
-
level: 2,
|
|
297
|
-
levelDescription: "L2: Root + direct dependencies (depth 1)"
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
formatL3(slice) {
|
|
301
|
-
return {
|
|
302
|
-
root: withoutAnnotations(slice.root),
|
|
303
|
-
dependencies: slice.dependencies.map(withoutAnnotations),
|
|
304
|
-
edges: slice.edges,
|
|
305
|
-
level: 3,
|
|
306
|
-
levelDescription: "L3: Full transitive dependency closure"
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
formatL4(slice) {
|
|
310
|
-
const estimatedTokens = this.estimateTokens(slice);
|
|
311
|
-
if (estimatedTokens > L4_TOKEN_BUDGET) {
|
|
312
|
-
const truncated = this.truncateToTokenBudget(slice, L4_TOKEN_BUDGET);
|
|
313
|
-
return {
|
|
314
|
-
...truncated,
|
|
315
|
-
level: 4,
|
|
316
|
-
levelDescription: "L4: Full closure with 8K token budget (truncated)",
|
|
317
|
-
truncation: {
|
|
318
|
-
truncated: true,
|
|
319
|
-
reason: "token_budget_exceeded"
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
return {
|
|
324
|
-
root: slice.root,
|
|
325
|
-
dependencies: slice.dependencies,
|
|
326
|
-
edges: slice.edges,
|
|
327
|
-
level: 4,
|
|
328
|
-
levelDescription: "L4: Full closure with 8K token budget"
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
estimateTokens(slice) {
|
|
332
|
-
let chars = this.symbolCharEstimate(slice.root);
|
|
333
|
-
for (const dep of slice.dependencies) {
|
|
334
|
-
chars += this.symbolCharEstimate(dep);
|
|
335
|
-
}
|
|
336
|
-
for (const edge of slice.edges) {
|
|
337
|
-
chars += edge.from.length + edge.to.length + edge.kind.length + 10;
|
|
338
|
-
}
|
|
339
|
-
return Math.ceil(chars / 4);
|
|
340
|
-
}
|
|
341
|
-
symbolCharEstimate(node) {
|
|
342
|
-
if (node.startOffset !== void 0 && node.endOffset !== void 0) {
|
|
343
|
-
return node.endOffset - node.startOffset;
|
|
344
|
-
}
|
|
345
|
-
return (node.endLine - node.startLine + 1) * 40;
|
|
346
|
-
}
|
|
347
|
-
truncateToTokenBudget(slice, budget) {
|
|
348
|
-
let currentTokens = this.symbolCharEstimate(slice.root) / 4;
|
|
349
|
-
const deps = [];
|
|
350
|
-
const includedIds = /* @__PURE__ */ new Set([slice.root.symbolId]);
|
|
351
|
-
for (const dep of slice.dependencies) {
|
|
352
|
-
const depTokens = this.symbolCharEstimate(dep) / 4;
|
|
353
|
-
if (currentTokens + depTokens > budget) break;
|
|
354
|
-
currentTokens += depTokens;
|
|
355
|
-
deps.push(dep);
|
|
356
|
-
includedIds.add(dep.symbolId);
|
|
357
|
-
}
|
|
358
|
-
const edges = slice.edges.filter(
|
|
359
|
-
(e) => includedIds.has(e.from) && includedIds.has(e.to)
|
|
360
|
-
);
|
|
361
|
-
return { root: slice.root, dependencies: deps, edges };
|
|
362
|
-
}
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
// src/core/intent-filter.ts
|
|
366
|
-
function extractKeywords(intent) {
|
|
367
|
-
return intent.toLowerCase().split(/\s+/).filter((w) => w.length >= 2);
|
|
368
|
-
}
|
|
369
|
-
function matchesAny(keywords, values) {
|
|
370
|
-
const lower = values.map((v) => v.toLowerCase());
|
|
371
|
-
return keywords.some((kw) => lower.some((v) => v.includes(kw)));
|
|
372
|
-
}
|
|
373
|
-
function collectMatchableStrings(entry) {
|
|
374
|
-
const strings = [];
|
|
375
|
-
for (const key of ["symbolId", "file", "name", "kind", "edgeKind", "reason", "confidence"]) {
|
|
376
|
-
const v = entry[key];
|
|
377
|
-
if (typeof v === "string") strings.push(v);
|
|
378
|
-
}
|
|
379
|
-
const edgeKinds = entry["edgeKinds"];
|
|
380
|
-
if (Array.isArray(edgeKinds)) {
|
|
381
|
-
for (const ek of edgeKinds) {
|
|
382
|
-
if (typeof ek === "string") strings.push(ek);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return strings;
|
|
386
|
-
}
|
|
387
|
-
function filterByIntent(items, intent) {
|
|
388
|
-
if (!intent || intent.trim().length === 0) return items;
|
|
389
|
-
const keywords = extractKeywords(intent);
|
|
390
|
-
if (keywords.length === 0) return items;
|
|
391
|
-
return items.filter((item) => {
|
|
392
|
-
const strings = collectMatchableStrings(item);
|
|
393
|
-
return matchesAny(keywords, strings);
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// src/adapters/mcp/get-logic-slice.ts
|
|
398
|
-
var InputSchema = z.object({
|
|
399
|
-
symbolId: z.string().min(1).optional(),
|
|
400
|
-
symbolIds: z.array(z.string().min(1)).optional(),
|
|
401
|
-
level: DetailLevelSchema.optional().default(3),
|
|
402
|
-
intent: z.string().optional().describe('Filter dependencies by intent keywords (e.g., "core", "adapter", "storage")')
|
|
403
|
-
}).refine(
|
|
404
|
-
(data) => data.symbolId || data.symbolIds && data.symbolIds.length > 0,
|
|
405
|
-
{ message: "Either symbolId or symbolIds must be provided" }
|
|
406
|
-
);
|
|
407
|
-
function buildGraphFromStorage(storage) {
|
|
408
|
-
const graph = new SymbolGraph();
|
|
409
|
-
for (const sym of storage.getAllSymbols()) {
|
|
410
|
-
graph.addNode(sym);
|
|
411
|
-
}
|
|
412
|
-
for (const edge of storage.getAllEdges()) {
|
|
413
|
-
graph.addEdge(edge);
|
|
414
|
-
}
|
|
415
|
-
return graph;
|
|
416
|
-
}
|
|
417
|
-
function buildGraphAndFilesFromJsonIndex(ctxoRoot) {
|
|
418
|
-
const reader = new JsonIndexReader(ctxoRoot);
|
|
419
|
-
const indices = reader.readAll();
|
|
420
|
-
return { graph: buildGraphFromIndices(indices), indexedFiles: indices.map((fileIndex) => fileIndex.file) };
|
|
421
|
-
}
|
|
422
|
-
function buildGraphFromIndices(indices) {
|
|
423
|
-
const graph = new SymbolGraph();
|
|
424
|
-
for (const fileIndex of indices) {
|
|
425
|
-
for (const sym of fileIndex.symbols) {
|
|
426
|
-
graph.addNode(sym);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
for (const fileIndex of indices) {
|
|
430
|
-
for (const edge of fileIndex.edges) {
|
|
431
|
-
graph.addEdge(edge);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return graph;
|
|
435
|
-
}
|
|
436
|
-
function getGraphAndFiles(ctxoRoot, storage) {
|
|
437
|
-
const json = buildGraphAndFilesFromJsonIndex(ctxoRoot);
|
|
438
|
-
if (json.graph.nodeCount > 0) return json;
|
|
439
|
-
return { graph: buildGraphFromStorage(storage), indexedFiles: storage.listIndexedFiles() };
|
|
440
|
-
}
|
|
441
|
-
function handleGetLogicSlice(storage, masking, staleness, ctxoRoot = ".ctxo", enrich) {
|
|
442
|
-
const query = new LogicSliceQuery();
|
|
443
|
-
const formatter = new DetailFormatter();
|
|
444
|
-
const getGraph = () => getGraphAndFiles(ctxoRoot, storage);
|
|
445
|
-
const handler = (args) => {
|
|
446
|
-
try {
|
|
447
|
-
const parsed = InputSchema.safeParse(args);
|
|
448
|
-
if (!parsed.success) {
|
|
449
|
-
return {
|
|
450
|
-
content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
const { symbolId, symbolIds, level, intent } = parsed.data;
|
|
454
|
-
const ids = symbolIds ?? (symbolId ? [symbolId] : []);
|
|
455
|
-
const { graph, indexedFiles } = getGraph();
|
|
456
|
-
const results = [];
|
|
457
|
-
for (const id of ids) {
|
|
458
|
-
const slice = query.getLogicSlice(graph, id);
|
|
459
|
-
if (slice) {
|
|
460
|
-
const formatted = formatter.format(slice, level);
|
|
461
|
-
if (intent && Array.isArray(formatted["dependencies"])) {
|
|
462
|
-
formatted["dependencies"] = filterByIntent(
|
|
463
|
-
formatted["dependencies"],
|
|
464
|
-
intent
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
results.push(formatted);
|
|
468
|
-
} else {
|
|
469
|
-
results.push({ found: false, symbolId: id, hint: 'Symbol not found. Run "ctxo index".' });
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
const responseData = ids.length === 1 ? results[0] : { batch: true, results };
|
|
473
|
-
const body = applyEnricher(enrich, responseData, parsed.data, "get_logic_slice");
|
|
474
|
-
const payload = masking.mask(JSON.stringify(wrapResponse(body)));
|
|
475
|
-
const content = [];
|
|
476
|
-
if (staleness) {
|
|
477
|
-
const warning = staleness.check(indexedFiles);
|
|
478
|
-
if (warning) {
|
|
479
|
-
content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
content.push({ type: "text", text: payload });
|
|
483
|
-
return { content };
|
|
484
|
-
} catch (err) {
|
|
485
|
-
return {
|
|
486
|
-
content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
};
|
|
490
|
-
return handler;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
export {
|
|
494
|
-
LogicSliceQuery,
|
|
495
|
-
withoutAnnotations,
|
|
496
|
-
setWorkspaceMeta,
|
|
497
|
-
getThreshold,
|
|
498
|
-
wrapResponse,
|
|
499
|
-
filterByIntent,
|
|
500
|
-
describeError,
|
|
501
|
-
applyEnricher,
|
|
502
|
-
collectEnrichers,
|
|
503
|
-
buildGraphFromStorage,
|
|
504
|
-
buildGraphFromIndices,
|
|
505
|
-
getGraphAndFiles,
|
|
506
|
-
handleGetLogicSlice
|
|
507
|
-
};
|
|
508
|
-
//# sourceMappingURL=chunk-L2CAUER6.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/response-envelope.ts","../src/core/describe-error.ts","../src/adapters/mcp/enrich-payload.ts","../src/adapters/mcp/get-logic-slice.ts","../src/core/logic-slice/logic-slice-query.ts","../src/core/detail-levels/detail-formatter.ts","../src/core/intent-filter.ts"],"sourcesContent":["/**\n * Response envelope — adds _meta field and truncates large payloads.\n *\n * Applied between payload construction and masking in every MCP handler.\n * Threshold default: 8192 bytes (~8KB). Configurable via CTXO_RESPONSE_LIMIT env.\n */\n\nconst DEFAULT_THRESHOLD = 8192;\n\nexport interface ResponseMeta {\n totalItems: number;\n returnedItems: number;\n truncated: boolean;\n totalBytes: number;\n hint?: string;\n /**\n * Workspace descriptor (forward-compat for Monorepo Workspace PRD).\n * v0.7: `root` is set at startup; `package` is always undefined.\n */\n workspace?: {\n root: string;\n package?: string;\n };\n /**\n * How far the community snapshot has drifted from current git HEAD.\n * Only populated on cluster-aware MCP tools (overlay, blast-radius,\n * why-context, pr-impact). Tells AI agents whether the cluster / drift /\n * boundary data they are about to act on is fresh or N commits stale.\n */\n snapshotStaleness?: {\n snapshotCommit: string;\n currentHeadCommit?: string;\n commitsBehind?: number;\n hint?: string;\n };\n}\n\nlet workspaceMeta: ResponseMeta['workspace'] | undefined;\n\n/**\n * Set the workspace descriptor attached to every subsequent `_meta` envelope.\n * Called once at composition root after `detectWorkspace()` resolves.\n */\nexport function setWorkspaceMeta(workspace: ResponseMeta['workspace']): void {\n workspaceMeta = workspace;\n}\n\nexport function getWorkspaceMeta(): ResponseMeta['workspace'] | undefined {\n return workspaceMeta;\n}\n\n/**\n * Known array field names in tool responses that can be truncated.\n * Maps field name → drill-in hint for the LLM.\n */\nconst TRUNCATABLE_FIELDS: Record<string, string> = {\n impactedSymbols: 'Use search_symbols to find specific impacted symbols, or get_blast_radius with a confidence filter.',\n importers: 'Use find_importers with edgeKinds filter or maxDepth to narrow results.',\n deadSymbols: 'Use search_symbols with kind filter to find specific dead symbols.',\n unusedExports: 'Use find_importers to check specific symbols.',\n results: 'Use search_symbols or get_ranked_context with a narrower query.',\n rankings: 'Use get_symbol_importance with kind or filePattern filter to narrow results.',\n context: 'Use get_context_for_task with a smaller tokenBudget.',\n hierarchies: 'Use get_class_hierarchy with a specific symbolId.',\n scaffolding: 'Review the scaffolding markers directly in the listed files.',\n deadFiles: 'Use find_dead_code with includeTests to adjust scope.',\n files: 'Use get_changed_symbols with a narrower since ref or smaller maxFiles.',\n clusters: 'Use get_architectural_overlay with mode=communities and layer filter for a focused view.',\n candidates:\n 'Use get_architectural_overlay with mode=contexts and candidate: <id> for one candidate in full, or ctxo contexts --json for the full contexts view.',\n clustersAffected: 'Use get_blast_radius on individual changed symbols for per-cluster impact.',\n};\n\n/** The response limit in bytes: `CTXO_RESPONSE_LIMIT` when it is a positive integer, else 8192. */\nexport function getThreshold(): number {\n const env = process.env['CTXO_RESPONSE_LIMIT'];\n if (env) {\n const n = parseInt(env, 10);\n if (!isNaN(n) && n > 0) return n;\n }\n return DEFAULT_THRESHOLD;\n}\n\n/**\n * Find the largest array field in a payload object that is in our truncatable list.\n */\nfunction findTruncatableArray(data: Record<string, unknown>): { key: string; arr: unknown[] } | null {\n let best: { key: string; arr: unknown[] } | null = null;\n for (const [key, value] of Object.entries(data)) {\n if (Array.isArray(value) && key in TRUNCATABLE_FIELDS) {\n if (!best || value.length > best.arr.length) {\n best = { key, arr: value };\n }\n }\n }\n return best;\n}\n\n/**\n * Wrap a payload object with _meta and truncate if needed.\n *\n * @param data - The raw response object (before JSON.stringify)\n * @returns The data object with _meta added (and arrays truncated if over threshold)\n */\nexport interface WrapResponseExtras {\n snapshotStaleness?: ResponseMeta['snapshotStaleness'];\n}\n\nfunction buildMeta(\n base: Omit<ResponseMeta, 'workspace' | 'snapshotStaleness'>,\n extras?: WrapResponseExtras,\n): ResponseMeta {\n const meta: ResponseMeta = workspaceMeta ? { ...base, workspace: workspaceMeta } : base;\n if (extras?.snapshotStaleness) {\n return { ...meta, snapshotStaleness: extras.snapshotStaleness };\n }\n return meta;\n}\n\nexport function wrapResponse(\n data: Record<string, unknown>,\n extras?: WrapResponseExtras,\n): Record<string, unknown> {\n const threshold = getThreshold();\n const fullJson = JSON.stringify(data);\n const totalBytes = Buffer.byteLength(fullJson, 'utf-8');\n\n const truncatable = findTruncatableArray(data);\n const totalItems = truncatable ? truncatable.arr.length : 0;\n\n // Under threshold — return with _meta, no truncation\n if (totalBytes <= threshold) {\n return {\n ...data,\n _meta: buildMeta(\n {\n totalItems,\n returnedItems: totalItems,\n truncated: false,\n totalBytes,\n },\n extras,\n ),\n };\n }\n\n // Over threshold — truncate the largest array\n if (!truncatable || truncatable.arr.length <= 1) {\n return {\n ...data,\n _meta: buildMeta(\n {\n totalItems,\n returnedItems: totalItems,\n truncated: false,\n totalBytes,\n },\n extras,\n ),\n };\n }\n\n // Binary search for the number of items that fit within threshold\n let lo = 1;\n let hi = truncatable.arr.length;\n\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const trial = {\n ...data,\n [truncatable.key]: truncatable.arr.slice(0, mid),\n _meta: buildMeta(\n {\n totalItems,\n returnedItems: mid,\n truncated: true,\n totalBytes,\n hint: TRUNCATABLE_FIELDS[truncatable.key],\n },\n extras,\n ),\n };\n const size = Buffer.byteLength(JSON.stringify(trial), 'utf-8');\n if (size <= threshold) {\n lo = mid;\n } else {\n hi = mid - 1;\n }\n }\n\n const hint = TRUNCATABLE_FIELDS[truncatable.key];\n\n return {\n ...data,\n [truncatable.key]: truncatable.arr.slice(0, lo),\n _meta: buildMeta(\n {\n totalItems,\n returnedItems: lo,\n truncated: true,\n totalBytes,\n hint,\n },\n extras,\n ),\n };\n}\n","/**\n * Text for a thrown value, safe for a log line or an error payload. It never throws: a non-Error throw\n * (a string, a null-prototype object, a value with a hostile `message` or `toString`) still gives a string.\n */\nexport function describeError(err: unknown): string {\n try {\n if (err instanceof Error) {\n const message = err.message;\n if (typeof message === 'string') return message;\n }\n if (typeof err === 'string') return err;\n return String(err);\n } catch {\n try {\n return Object.prototype.toString.call(err);\n } catch {\n return '[unprintable error]';\n }\n }\n}\n","import type { CoreToolName, LoadedModule, ResponseEnricher } from '../../ports/i-ctxo-module.js';\nimport { createLogger } from '../../core/logger.js';\nimport { describeError } from '../../core/describe-error.js';\n\nconst log = createLogger('ctxo:modules');\n\n/**\n * The response enricher seam (ADR-018): a module adds fields to a core tool's payload before the\n * `_meta` envelope, truncation and masking see it, so all three treat the added fields like the core's.\n */\n\n/**\n * `payload` with the fields `enrich` adds. The enricher works on a copy, so a throwing enricher, or one\n * that returns something other than an object, leaves the response exactly as the core built it; either\n * case logs one warning for the call. Without an enricher the payload itself is returned.\n *\n * @param args - the tool's validated arguments\n */\nexport function applyEnricher(\n enrich: ResponseEnricher | undefined,\n payload: Record<string, unknown>,\n args: Readonly<Record<string, unknown>>,\n tool: CoreToolName,\n): Record<string, unknown> {\n if (enrich === undefined) return payload;\n try {\n const enriched: unknown = enrich(structuredClone(payload), args);\n if (!isPlainObject(enriched) || !isPlainData(enriched, new Set())) {\n log.warn(\n `The ${tool} enricher returned something other than a plain JSON payload object; its fields are left out of this response`,\n );\n return payload;\n }\n // The response is serialised with JSON; a value JSON cannot hold (a BigInt) is caught here, not later.\n JSON.stringify(enriched);\n return enriched;\n } catch (err) {\n log.warn(`The ${tool} enricher failed: ${describeError(err)}; its fields are left out of this response`);\n return payload;\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;\n const proto: unknown = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Whether `value` holds only JSON data: strings, numbers, booleans, null, arrays and plain objects (an\n * `Object.prototype` or null prototype), nested to any depth and without a cycle. A Promise, Map, Set,\n * Date, class instance or function fails it.\n */\nfunction isPlainData(value: unknown, path: Set<object>): boolean {\n if (value === null) return true;\n const type = typeof value;\n if (type === 'string' || type === 'number' || type === 'boolean') return true;\n if (type !== 'object') return false;\n const node = value as object;\n if (path.has(node)) return false;\n if (!Array.isArray(node) && !isPlainObject(node)) return false;\n path.add(node);\n try {\n for (const child of Array.isArray(node) ? node : Object.values(node)) {\n // JSON drops an undefined property; an undefined array slot becomes null. Both are harmless.\n if (child !== undefined && !isPlainData(child, path)) return false;\n }\n } finally {\n path.delete(node);\n }\n return true;\n}\n\n/**\n * The enricher for each core tool that a loaded module enriches. Several modules enriching one tool run in\n * load order, each on the previous one's output, and each is guarded on its own: a later enricher that\n * throws drops only its own fields. A module whose `enrich` cannot be read contributes nothing.\n */\nexport function collectEnrichers(modules: readonly LoadedModule[]): Map<CoreToolName, ResponseEnricher> {\n const chains = new Map<CoreToolName, ResponseEnricher[]>();\n for (const module of modules) {\n let entries: Array<[string, unknown]>;\n try {\n entries = Object.entries(module.registration.enrich ?? {});\n } catch (err) {\n log.warn(`${module.name} enrichers could not be read: ${describeError(err)}`);\n continue;\n }\n for (const [tool, enrich] of entries) {\n if (typeof enrich !== 'function') continue;\n const chain = chains.get(tool as CoreToolName) ?? [];\n chain.push(enrich as ResponseEnricher);\n chains.set(tool as CoreToolName, chain);\n }\n }\n const enrichers = new Map<CoreToolName, ResponseEnricher>();\n for (const [tool, chain] of chains) {\n enrichers.set(\n tool,\n chain.length === 1\n ? chain[0]!\n : (payload, args) => chain.reduce((current, enrich) => applyEnricher(enrich, current, args, tool), payload),\n );\n }\n return enrichers;\n}\n","import { z } from 'zod';\nimport type { IStoragePort } from '../../ports/i-storage-port.js';\nimport type { IMaskingPort } from '../../ports/i-masking-port.js';\nimport { SymbolGraph } from '../../core/graph/symbol-graph.js';\nimport { JsonIndexReader } from '../storage/json-index-reader.js';\nimport { LogicSliceQuery } from '../../core/logic-slice/logic-slice-query.js';\nimport { DetailFormatter } from '../../core/detail-levels/detail-formatter.js';\nimport { DetailLevelSchema, type FileIndex } from '../../core/types.js';\nimport { wrapResponse } from '../../core/response-envelope.js';\nimport { filterByIntent } from '../../core/intent-filter.js';\nimport type { ResponseEnricher } from '../../ports/i-ctxo-module.js';\nimport { applyEnricher } from './enrich-payload.js';\n\nexport const InputSchema = z.object({\n symbolId: z.string().min(1).optional(),\n symbolIds: z.array(z.string().min(1)).optional(),\n level: DetailLevelSchema.optional().default(3),\n intent: z.string().optional().describe('Filter dependencies by intent keywords (e.g., \"core\", \"adapter\", \"storage\")'),\n}).refine(\n (data) => data.symbolId || (data.symbolIds && data.symbolIds.length > 0),\n { message: 'Either symbolId or symbolIds must be provided' },\n);\n\nexport type GetLogicSliceInput = z.infer<typeof InputSchema>;\n\nexport function getLogicSliceInputSchema() {\n return {\n type: 'object' as const,\n properties: {\n symbolId: { type: 'string' as const, description: 'The symbol ID to retrieve (format: file::name::kind)' },\n level: { type: 'number' as const, enum: [1, 2, 3, 4], description: 'Detail level (1=minimal, 4=full)', default: 3 },\n },\n required: ['symbolId'] as const,\n };\n}\n\nexport function buildGraphFromStorage(storage: IStoragePort): SymbolGraph {\n const graph = new SymbolGraph();\n for (const sym of storage.getAllSymbols()) {\n graph.addNode(sym);\n }\n for (const edge of storage.getAllEdges()) {\n graph.addEdge(edge);\n }\n return graph;\n}\n\n/**\n * Build graph + return file list from a single JSON index read.\n * Returning both from one source prevents staleness warnings from diverging\n * from the graph data (see issue #40).\n */\nexport function buildGraphAndFilesFromJsonIndex(\n ctxoRoot: string,\n): { graph: SymbolGraph; indexedFiles: string[] } {\n const reader = new JsonIndexReader(ctxoRoot);\n const indices = reader.readAll();\n return { graph: buildGraphFromIndices(indices), indexedFiles: indices.map((fileIndex) => fileIndex.file) };\n}\n\n/** Build a graph from file indices already read: every node first, then every edge. */\nexport function buildGraphFromIndices(indices: readonly FileIndex[]): SymbolGraph {\n const graph = new SymbolGraph();\n for (const fileIndex of indices) {\n for (const sym of fileIndex.symbols) {\n graph.addNode(sym);\n }\n }\n for (const fileIndex of indices) {\n for (const edge of fileIndex.edges) {\n graph.addEdge(edge);\n }\n }\n return graph;\n}\n\n/**\n * Build graph directly from JSON index files (bypasses SQLite cache).\n * Always reads fresh data from disk — works even if ctxo index ran in another process.\n */\nexport function buildGraphFromJsonIndex(ctxoRoot: string): SymbolGraph {\n return buildGraphAndFilesFromJsonIndex(ctxoRoot).graph;\n}\n\n/**\n * Returns graph + indexedFiles from JSON index, falling back to SQLite storage\n * when JSON is empty. Keeps graph data and staleness check file list aligned.\n */\nexport function getGraphAndFiles(\n ctxoRoot: string,\n storage: IStoragePort,\n): { graph: SymbolGraph; indexedFiles: string[] } {\n const json = buildGraphAndFilesFromJsonIndex(ctxoRoot);\n if (json.graph.nodeCount > 0) return json;\n return { graph: buildGraphFromStorage(storage), indexedFiles: storage.listIndexedFiles() };\n}\n\nexport interface StalenessCheck {\n check(indexedFiles: readonly string[]): { message: string } | undefined;\n}\n\nexport function handleGetLogicSlice(\n storage: IStoragePort,\n masking: IMaskingPort,\n staleness?: StalenessCheck,\n ctxoRoot = '.ctxo',\n enrich?: ResponseEnricher,\n) {\n const query = new LogicSliceQuery();\n const formatter = new DetailFormatter();\n\n // Build graph fresh on each call — try JSON index first (always up to date),\n // fall back to SQLite storage (for tests and when JSON not available).\n const getGraph = () => getGraphAndFiles(ctxoRoot, storage);\n\n const handler = (args: Record<string, unknown>) => {\n try {\n const parsed = InputSchema.safeParse(args);\n if (!parsed.success) {\n return {\n content: [{ type: 'text' as const, text: JSON.stringify({ error: true, message: parsed.error.message }) }],\n };\n }\n\n const { symbolId, symbolIds, level, intent } = parsed.data;\n const ids = symbolIds ?? (symbolId ? [symbolId] : []);\n\n const { graph, indexedFiles } = getGraph();\n\n // Batch support: query multiple symbols\n const results = [];\n for (const id of ids) {\n const slice = query.getLogicSlice(graph, id);\n if (slice) {\n const formatted = formatter.format(slice, level) as unknown as Record<string, unknown>;\n // Apply intent filter to dependencies if present\n if (intent && Array.isArray(formatted['dependencies'])) {\n formatted['dependencies'] = filterByIntent(\n formatted['dependencies'] as Record<string, unknown>[],\n intent,\n );\n }\n results.push(formatted);\n } else {\n results.push({ found: false, symbolId: id, hint: 'Symbol not found. Run \"ctxo index\".' });\n }\n }\n\n // Single symbol: return directly. Batch: return array.\n const responseData = ids.length === 1 ? results[0] : { batch: true, results };\n\n // A module's enricher sees the single slice or the { batch, results } object; then envelope and masking.\n const body = applyEnricher(enrich, responseData as Record<string, unknown>, parsed.data, 'get_logic_slice');\n const payload = masking.mask(JSON.stringify(wrapResponse(body)));\n\n // Check staleness\n const content: Array<{ type: 'text'; text: string }> = [];\n if (staleness) {\n const warning = staleness.check(indexedFiles);\n if (warning) {\n content.push({ type: 'text', text: `⚠️ ${warning.message}` });\n }\n }\n content.push({ type: 'text', text: payload });\n\n return { content };\n } catch (err) {\n return {\n content: [{ type: 'text' as const, text: JSON.stringify({ error: true, message: (err as Error).message }) }],\n };\n }\n };\n\n return handler;\n}\n","import type { SymbolNode, GraphEdge, LogicSliceResult } from '../types.js';\nimport type { SymbolGraph } from '../graph/symbol-graph.js';\n\nexport class LogicSliceQuery {\n getLogicSlice(\n graph: SymbolGraph,\n symbolId: string,\n maxDepth: number = Infinity,\n ): LogicSliceResult | undefined {\n // Resolve the id once and walk from the node it names: a 2-part data id walks its winning definition, and\n // a full id of another copy walks that copy's own edges.\n const root = graph.getNode(symbolId);\n if (!root) return undefined;\n\n const visited = new Set<string>([root.symbolId]);\n const dependencies: SymbolNode[] = [];\n const collectedEdges: GraphEdge[] = [];\n\n // BFS transitive closure\n const queue: Array<{ id: string; depth: number }> = [{ id: root.symbolId, depth: 0 }];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n\n if (current.depth >= maxDepth) continue;\n\n const edges = graph.getForwardEdges(current.id);\n for (const edge of edges) {\n const depNode = graph.getNode(edge.to);\n // Only include edges whose target node exists in the graph\n if (!depNode) continue;\n\n collectedEdges.push(edge);\n\n if (visited.has(edge.to)) continue;\n visited.add(edge.to);\n\n dependencies.push(depNode);\n queue.push({ id: edge.to, depth: current.depth + 1 });\n }\n }\n\n return { root, dependencies, edges: collectedEdges };\n }\n}\n","import type {\n LogicSliceResult,\n FormattedSlice,\n DetailLevel,\n SymbolNode,\n GraphEdge,\n} from '../types.js';\n\nconst L1_MAX_LINES = 150;\nconst L4_TOKEN_BUDGET = 8000;\n\n/**\n * `node` without its `annotations`. Node output leaves them out everywhere except logic slice L4, which\n * carries them unchanged: up to 20 attributes per C# symbol plus whole column lists would shrink every\n * other slice before its dependencies.\n *\n * Only a graph built from the JSON index has annotations. The SQLite fallback stores none, so even L4\n * carries none when the graph comes from there.\n */\nexport function withoutAnnotations(node: SymbolNode): SymbolNode {\n if (node.annotations === undefined) return node;\n const { annotations: _annotations, ...rest } = node;\n return rest;\n}\n\nexport class DetailFormatter {\n format(slice: LogicSliceResult, level: DetailLevel): FormattedSlice {\n switch (level) {\n case 1:\n return this.formatL1(slice);\n case 2:\n return this.formatL2(slice);\n case 3:\n return this.formatL3(slice);\n case 4:\n return this.formatL4(slice);\n }\n }\n\n private formatL1(slice: LogicSliceResult): FormattedSlice {\n const root = slice.root;\n const lineCount = root.endLine - root.startLine + 1;\n\n const clampedRoot: SymbolNode = lineCount > L1_MAX_LINES\n ? { ...root, endLine: root.startLine + L1_MAX_LINES - 1 }\n : root;\n\n return {\n root: withoutAnnotations(clampedRoot),\n dependencies: [],\n edges: [],\n level: 1,\n levelDescription: 'L1: Root symbol signature only (no dependencies)',\n ...(lineCount > L1_MAX_LINES ? { truncation: { truncated: true as const, reason: 'token_budget_exceeded' as const } } : {}),\n };\n }\n\n private formatL2(slice: LogicSliceResult): FormattedSlice {\n // L2: root + depth-1 direct dependencies\n const directEdges = slice.edges.filter((e) => e.from === slice.root.symbolId);\n const directDepIds = new Set(directEdges.map((e) => e.to));\n const directDeps = slice.dependencies.filter((d) => directDepIds.has(d.symbolId));\n\n return {\n root: withoutAnnotations(slice.root),\n dependencies: directDeps.map(withoutAnnotations),\n edges: directEdges,\n level: 2,\n levelDescription: 'L2: Root + direct dependencies (depth 1)',\n };\n }\n\n private formatL3(slice: LogicSliceResult): FormattedSlice {\n return {\n root: withoutAnnotations(slice.root),\n dependencies: slice.dependencies.map(withoutAnnotations),\n edges: slice.edges,\n level: 3,\n levelDescription: 'L3: Full transitive dependency closure',\n };\n }\n\n private formatL4(slice: LogicSliceResult): FormattedSlice {\n // L4: full closure with token budget enforcement. Nodes keep their annotations (see withoutAnnotations;\n // a graph from the SQLite fallback has none to keep).\n const estimatedTokens = this.estimateTokens(slice);\n\n if (estimatedTokens > L4_TOKEN_BUDGET) {\n // Truncate dependencies to fit budget\n const truncated = this.truncateToTokenBudget(slice, L4_TOKEN_BUDGET);\n return {\n ...truncated,\n level: 4,\n levelDescription: 'L4: Full closure with 8K token budget (truncated)',\n truncation: {\n truncated: true,\n reason: 'token_budget_exceeded',\n },\n };\n }\n\n return {\n root: slice.root,\n dependencies: slice.dependencies,\n edges: slice.edges,\n level: 4,\n levelDescription: 'L4: Full closure with 8K token budget',\n };\n }\n\n private estimateTokens(slice: LogicSliceResult): number {\n // Rough estimate: ~4 chars per token\n let chars = this.symbolCharEstimate(slice.root);\n for (const dep of slice.dependencies) {\n chars += this.symbolCharEstimate(dep);\n }\n for (const edge of slice.edges) {\n chars += (edge.from.length + edge.to.length + edge.kind.length + 10);\n }\n return Math.ceil(chars / 4);\n }\n\n private symbolCharEstimate(node: SymbolNode): number {\n // Use byte offsets for exact size when available, otherwise estimate from line count\n if (node.startOffset !== undefined && node.endOffset !== undefined) {\n return node.endOffset - node.startOffset;\n }\n return (node.endLine - node.startLine + 1) * 40;\n }\n\n private truncateToTokenBudget(\n slice: LogicSliceResult,\n budget: number,\n ): { root: SymbolNode; dependencies: SymbolNode[]; edges: GraphEdge[] } {\n let currentTokens = this.symbolCharEstimate(slice.root) / 4;\n const deps: SymbolNode[] = [];\n const includedIds = new Set<string>([slice.root.symbolId]);\n\n for (const dep of slice.dependencies) {\n const depTokens = this.symbolCharEstimate(dep) / 4;\n if (currentTokens + depTokens > budget) break;\n currentTokens += depTokens;\n deps.push(dep);\n includedIds.add(dep.symbolId);\n }\n\n const edges = slice.edges.filter(\n (e) => includedIds.has(e.from) && includedIds.has(e.to),\n );\n\n return { root: slice.root, dependencies: deps, edges };\n }\n}\n","/**\n * Intent-based result filtering for MCP tool responses.\n *\n * When an `intent` string is provided, filters array results to only include\n * entries whose text fields (symbolId, file, name, kind, edgeKind, reason)\n * match at least one keyword from the intent.\n *\n * Keywords are extracted by splitting the intent on whitespace and lowercasing.\n * A match is found when any keyword appears as a substring of any text field.\n */\n\n/**\n * Extract lowercase keywords from an intent string.\n * Filters out very short words (< 2 chars) to avoid noise.\n */\nfunction extractKeywords(intent: string): string[] {\n return intent\n .toLowerCase()\n .split(/\\s+/)\n .filter(w => w.length >= 2);\n}\n\n/**\n * Check if any keyword matches any of the given text values (case-insensitive substring).\n */\nfunction matchesAny(keywords: string[], values: string[]): boolean {\n const lower = values.map(v => v.toLowerCase());\n return keywords.some(kw => lower.some(v => v.includes(kw)));\n}\n\n/**\n * Collect all string values from a record for matching.\n * Extracts from known fields: symbolId, file, name, kind, edgeKind, edgeKinds, reason, confidence.\n */\nfunction collectMatchableStrings(entry: Record<string, unknown>): string[] {\n const strings: string[] = [];\n for (const key of ['symbolId', 'file', 'name', 'kind', 'edgeKind', 'reason', 'confidence']) {\n const v = entry[key];\n if (typeof v === 'string') strings.push(v);\n }\n const edgeKinds = entry['edgeKinds'];\n if (Array.isArray(edgeKinds)) {\n for (const ek of edgeKinds) {\n if (typeof ek === 'string') strings.push(ek);\n }\n }\n return strings;\n}\n\n/**\n * Filter an array of result entries by intent keywords.\n * Returns only entries where at least one keyword matches a text field.\n *\n * @param items - Array of result objects\n * @param intent - User intent string (e.g., \"test coverage\", \"adapter\", \"security\")\n * @returns Filtered array (empty if no matches)\n */\nexport function filterByIntent<T extends Record<string, unknown>>(\n items: T[],\n intent: string | undefined,\n): T[] {\n if (!intent || intent.trim().length === 0) return items;\n\n const keywords = extractKeywords(intent);\n if (keywords.length === 0) return items;\n\n return items.filter(item => {\n const strings = collectMatchableStrings(item);\n return matchesAny(keywords, strings);\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/modules/module-loader.ts"],"sourcesContent":["import type { CtxoHost, CtxoModuleRegister, CtxoModuleRegistration, LoadedModule } from '../../ports/i-ctxo-module.js';\nimport { createLogger } from '../../core/logger.js';\nimport { resolvePackageCandidates, type ResolvedPackage } from '../language/plugin-discovery.js';\n\nconst log = createLogger('ctxo:modules');\n\nexport const DOMAIN_MODULE_PACKAGE = '@ctxo/domain';\n\nexport type ModuleStatus = 'active' | 'inactive' | 'version-mismatch' | 'load-failed' | 'not-installed';\n\nexport interface ModuleReport {\n readonly name: string;\n readonly status: ModuleStatus;\n readonly version?: string;\n readonly source?: 'project' | 'cli';\n readonly reason?: string;\n}\n\nexport type { LoadedModule };\n\nexport interface ModuleLoadResult {\n readonly modules: readonly LoadedModule[];\n readonly reports: readonly ModuleReport[];\n}\n\nexport interface LoadCtxoModulesOptions {\n /** Resolve again instead of returning the memoized result. */\n readonly refresh?: boolean;\n /** A directory standing in for the CLI's location (tests); defaults to the running CLI. */\n readonly cliAnchor?: string;\n}\n\n/**\n * Whether two versions share a numeric major and minor. A module runs only against the CLI minor it was\n * built for (ADR-018 Decision 3). `@ctxo/domain` carries its own copy of this rule, on purpose: it cannot\n * import the CLI at runtime.\n */\nexport function sameMinor(a: string, b: string): boolean {\n const pa = /^(\\d+)\\.(\\d+)/.exec(a);\n const pb = /^(\\d+)\\.(\\d+)/.exec(b);\n if (!pa || !pb) return false;\n return Number(pa[1]) === Number(pb[1]) && Number(pa[2]) === Number(pb[2]);\n}\n\nconst memo = new Map<string, Promise<ModuleLoadResult>>();\n\ntype Outcome =\n | { readonly kind: 'active'; readonly loaded: LoadedModule; readonly report: ModuleReport }\n | { readonly kind: 'stop'; readonly report: ModuleReport };\n\nasync function loadCandidate(candidate: ResolvedPackage, host: CtxoHost): Promise<Outcome> {\n const base = { name: DOMAIN_MODULE_PACKAGE, version: candidate.version, source: candidate.source } as const;\n const failed = (reason: string): Outcome => {\n log.warn(`${DOMAIN_MODULE_PACKAGE} failed to load: ${reason}; domain features are off.`);\n return { kind: 'stop', report: { ...base, status: 'load-failed', reason } };\n };\n try {\n const mod = (await import(candidate.entryUrl)) as { register?: unknown };\n if (typeof mod.register !== 'function') return failed('missing register export');\n const registration: CtxoModuleRegistration | undefined = await (mod.register as CtxoModuleRegister)(host);\n if (registration === undefined) {\n return { kind: 'stop', report: { ...base, status: 'inactive' } };\n }\n if (registration === null || typeof registration !== 'object' || registration.id !== 'domain') {\n return failed('register returned a registration with an unexpected id');\n }\n return {\n kind: 'active',\n loaded: { name: DOMAIN_MODULE_PACKAGE, version: candidate.version, registration },\n report: { ...base, status: 'active' },\n };\n } catch (err) {\n return failed((err as Error).message);\n }\n}\n\nasync function resolveModules(host: CtxoHost, cliAnchor: string | undefined): Promise<ModuleLoadResult> {\n const candidates = resolvePackageCandidates(DOMAIN_MODULE_PACKAGE, host.projectRoot, cliAnchor);\n if (candidates.length === 0) {\n return { modules: [], reports: [{ name: DOMAIN_MODULE_PACKAGE, status: 'not-installed' }] };\n }\n let firstMismatch: ResolvedPackage | undefined;\n for (const candidate of candidates) {\n if (!sameMinor(candidate.version, host.cliVersion)) {\n firstMismatch ??= candidate;\n continue;\n }\n const outcome = await loadCandidate(candidate, host);\n if (outcome.kind === 'active') {\n return { modules: [outcome.loaded], reports: [outcome.report] };\n }\n return { modules: [], reports: [outcome.report] };\n }\n const mismatch = firstMismatch!;\n log.warn(\n `${DOMAIN_MODULE_PACKAGE} ${mismatch.version} does not match @ctxo/cli ${host.cliVersion}; domain features are off. ` +\n `Install ${DOMAIN_MODULE_PACKAGE}@${host.cliVersion} next to the CLI you run.`,\n );\n return {\n modules: [],\n reports: [\n {\n name: DOMAIN_MODULE_PACKAGE,\n status: 'version-mismatch',\n version: mismatch.version,\n source: mismatch.source,\n reason: `cli ${host.cliVersion}`,\n },\n ],\n };\n}\n\n/**\n * Finds `@ctxo/domain` (the project's copy, then the copy next to the CLI), checks that its major.minor\n * equals the CLI's and calls its `register(host)`. Never throws: every failure is a report plus a warning.\n * Memoized per project root and mode; `refresh` resolves again (after an install).\n */\nexport function loadCtxoModules(host: CtxoHost, options: LoadCtxoModulesOptions = {}): Promise<ModuleLoadResult> {\n const key = `${host.projectRoot}|${host.mode}|${options.cliAnchor ?? ''}`;\n if (!options.refresh) {\n const cached = memo.get(key);\n if (cached) return cached;\n }\n const pending = resolveModules(host, options.cliAnchor).catch((err: unknown): ModuleLoadResult => {\n const reason = (err as Error).message;\n log.warn(`${DOMAIN_MODULE_PACKAGE} failed to load: ${reason}; domain features are off.`);\n return { modules: [], reports: [{ name: DOMAIN_MODULE_PACKAGE, status: 'load-failed', reason }] };\n });\n memo.set(key, pending);\n return pending;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/staleness/staleness-detector.ts"],"sourcesContent":["import { statSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport interface StalenessWarning {\n readonly staleFiles: string[];\n readonly message: string;\n}\n\nexport class StalenessDetector {\n private readonly projectRoot: string;\n private readonly indexDir: string;\n\n constructor(projectRoot: string, ctxoRoot: string) {\n this.projectRoot = projectRoot;\n this.indexDir = join(ctxoRoot, 'index');\n }\n\n check(indexedFiles: readonly string[]): StalenessWarning | undefined {\n if (!existsSync(this.indexDir)) return undefined;\n\n const staleFiles: string[] = [];\n\n for (const relativePath of indexedFiles) {\n const sourcePath = join(this.projectRoot, relativePath);\n const indexPath = join(this.indexDir, `${relativePath}.json`);\n\n if (!existsSync(sourcePath) || !existsSync(indexPath)) continue;\n\n try {\n const sourceMtime = Math.floor(statSync(sourcePath).mtimeMs / 1000);\n const indexMtime = Math.floor(statSync(indexPath).mtimeMs / 1000);\n\n if (sourceMtime > indexMtime) {\n staleFiles.push(relativePath);\n }\n } catch {\n // Skip files we can't stat\n }\n }\n\n if (staleFiles.length === 0) return undefined;\n\n return {\n staleFiles,\n message: `Index may be stale for ${staleFiles.length} file(s). Run \"ctxo index\" to refresh.`,\n };\n }\n}\n"],"mappings":";;;AAAA,SAAS,UAAU,kBAAkB;AACrC,SAAS,YAAY;AAOd,IAAM,oBAAN,MAAwB;AAAA,EACZ;AAAA,EACA;AAAA,EAEjB,YAAY,aAAqB,UAAkB;AACjD,SAAK,cAAc;AACnB,SAAK,WAAW,KAAK,UAAU,OAAO;AAAA,EACxC;AAAA,EAEA,MAAM,cAA+D;AACnE,QAAI,CAAC,WAAW,KAAK,QAAQ,EAAG,QAAO;AAEvC,UAAM,aAAuB,CAAC;AAE9B,eAAW,gBAAgB,cAAc;AACvC,YAAM,aAAa,KAAK,KAAK,aAAa,YAAY;AACtD,YAAM,YAAY,KAAK,KAAK,UAAU,GAAG,YAAY,OAAO;AAE5D,UAAI,CAAC,WAAW,UAAU,KAAK,CAAC,WAAW,SAAS,EAAG;AAEvD,UAAI;AACF,cAAM,cAAc,KAAK,MAAM,SAAS,UAAU,EAAE,UAAU,GAAI;AAClE,cAAM,aAAa,KAAK,MAAM,SAAS,SAAS,EAAE,UAAU,GAAI;AAEhE,YAAI,cAAc,YAAY;AAC5B,qBAAW,KAAK,YAAY;AAAA,QAC9B;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,QAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,WAAO;AAAA,MACL;AAAA,MACA,SAAS,0BAA0B,WAAW,MAAM;AAAA,IACtD;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/storage/atomic-write.ts"],"sourcesContent":["import { readdirSync, renameSync, statSync, unlinkSync, writeFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { createLogger } from '../../core/logger.js';\n\nconst log = createLogger('ctxo:storage');\n\nconst TMP_SUFFIX_RE = /\\.\\d+\\.tmp$/;\nconst TMP_STALE_THRESHOLD_MS = 60_000; // anything older than 60 s is from a dead process\n\nexport function atomicWrite(targetPath: string, content: string): void {\n const tmpPath = `${targetPath}.${process.pid}.tmp`;\n writeFileSync(tmpPath, content, 'utf-8');\n renameSync(tmpPath, targetPath);\n}\n\n/**\n * Remove orphaned .<pid>.tmp files left behind when a previous write crashed\n * (SIGKILL / power loss / ENOSPC). Only files older than a threshold are\n * removed so we never race a concurrent writer.\n *\n * Safe to call on every startup — walks the supplied directory non-recursively.\n */\nexport function sweepStaleTmpFiles(dir: string, now: number = Date.now()): number {\n if (!existsSync(dir)) return 0;\n let cleaned = 0;\n try {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n cleaned += sweepStaleTmpFiles(join(dir, entry.name), now);\n continue;\n }\n if (!TMP_SUFFIX_RE.test(entry.name)) continue;\n const full = join(dir, entry.name);\n try {\n const age = now - statSync(full).mtimeMs;\n if (age < TMP_STALE_THRESHOLD_MS) continue;\n unlinkSync(full);\n cleaned++;\n } catch (err) {\n log.error(`failed to sweep ${entry.name}: ${(err as Error).message}`);\n }\n }\n } catch (err) {\n log.error(`failed to scan ${dir}: ${(err as Error).message}`);\n }\n return cleaned;\n}\n"],"mappings":";;;;;;AAAA,SAAS,aAAa,YAAY,UAAU,YAAY,eAAe,kBAAkB;AACzF,SAAS,YAAY;AAIrB,IAAM,MAAM,aAAa,cAAc;AAEvC,IAAM,gBAAgB;AACtB,IAAM,yBAAyB;AAExB,SAAS,YAAY,YAAoB,SAAuB;AACrE,QAAM,UAAU,GAAG,UAAU,IAAI,QAAQ,GAAG;AAC5C,gBAAc,SAAS,SAAS,OAAO;AACvC,aAAW,SAAS,UAAU;AAChC;AASO,SAAS,mBAAmB,KAAa,MAAc,KAAK,IAAI,GAAW;AAChF,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO;AAC7B,MAAI,UAAU;AACd,MAAI;AACF,eAAW,SAAS,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;AAC7D,UAAI,MAAM,eAAe,EAAG;AAC5B,UAAI,MAAM,YAAY,GAAG;AACvB,mBAAW,mBAAmB,KAAK,KAAK,MAAM,IAAI,GAAG,GAAG;AACxD;AAAA,MACF;AACA,UAAI,CAAC,cAAc,KAAK,MAAM,IAAI,EAAG;AACrC,YAAM,OAAO,KAAK,KAAK,MAAM,IAAI;AACjC,UAAI;AACF,cAAM,MAAM,MAAM,SAAS,IAAI,EAAE;AACjC,YAAI,MAAM,uBAAwB;AAClC,mBAAW,IAAI;AACf;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,MAAM,mBAAmB,MAAM,IAAI,KAAM,IAAc,OAAO,EAAE;AAAA,MACtE;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,QAAI,MAAM,kBAAkB,GAAG,KAAM,IAAc,OAAO,EAAE;AAAA,EAC9D;AACA,SAAO;AACT;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/module-commands.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { LoadedModule } from '../adapters/modules/module-loader.js';\nimport type { CtxoHost, ModuleCommand, ModuleCommandContext } from '../ports/i-ctxo-module.js';\nimport { compareCodepoint } from '../core/compare-codepoint.js';\nimport { createLogger } from '../core/logger.js';\nimport { locateGitRoot } from './cli-paths.js';\n\n/**\n * Commands that opt-in modules contribute (ADR-018), and the fast route for hook commands.\n *\n * This file is loaded on every run (the entry imports `FAST_ROUTE_COMMANDS`), so its static imports stay\n * light: the module host and the module loader are imported on demand, and neither the CLI router nor the\n * MCP stack is ever imported here.\n */\n\nconst log = createLogger('ctxo:modules');\n\n/**\n * The module commands the CLI knows by name, and the package that provides each. Without the package a\n * known name prints an install hint and exits 1 (the hook command exits 0 silently), so CI never passes\n * on a command that did not run.\n */\nexport const MODULE_COMMAND_PACKAGES: Readonly<Record<string, string>> = Object.freeze({\n 'verify-domain': '@ctxo/domain',\n 'contexts-touched': '@ctxo/domain',\n domain: '@ctxo/domain',\n 'domain-guard-hook': '@ctxo/domain',\n});\n\n/** Commands the entry runs without the CLI router or the MCP stack (hook commands with a time budget). */\nexport const FAST_ROUTE_COMMANDS: ReadonlySet<string> = new Set(['domain-guard-hook']);\n\n/** The commands the CLI router handles itself. A module command with one of these names is dropped. */\nexport const CORE_COMMANDS: ReadonlySet<string> = new Set([\n 'index',\n 'sync',\n 'status',\n 'verify-index',\n 'init',\n 'watch',\n 'stats',\n 'doctor',\n 'visualize',\n 'version',\n 'install',\n 'update',\n 'blast-radius',\n 'contexts',\n 'gate',\n 'gate-hook',\n]);\n\n/** True when `command` is one of the module command names the CLI knows (own keys only). */\nexport function isKnownModuleCommand(command: string): boolean {\n return Object.hasOwn(MODULE_COMMAND_PACKAGES, command);\n}\n\n/**\n * The commands the loaded modules register, by name. A command named like a core command, or like a\n * command an earlier module registered, is dropped with a warning. A module whose `registerCommands`\n * throws contributes nothing and the others still count.\n */\nexport function collectModuleCommands(\n modules: readonly LoadedModule[],\n coreCommands: ReadonlySet<string>,\n): Map<string, ModuleCommand> {\n const commands = new Map<string, ModuleCommand>();\n for (const module of modules) {\n const register = module.registration.registerCommands;\n if (typeof register !== 'function') continue;\n try {\n register.call(module.registration, {\n registerCommand: (command: ModuleCommand) => {\n if (coreCommands.has(command.name)) {\n log.warn(`${module.name} command \"${command.name}\" has the name of a core command and was ignored`);\n } else if (commands.has(command.name)) {\n log.warn(`${module.name} command \"${command.name}\" is already registered and was ignored`);\n } else {\n commands.set(command.name, command);\n }\n },\n });\n } catch (err) {\n log.warn(`${module.name} could not register its commands: ${(err as Error).message}`);\n }\n }\n return commands;\n}\n\n/** All of stdin as UTF-8 text; empty on a terminal, where nothing is piped in. */\nasync function readAllStdin(): Promise<string> {\n if (process.stdin.isTTY) return '';\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : (chunk as Buffer));\n }\n return Buffer.concat(chunks).toString('utf-8');\n}\n\n/**\n * Runs the module command `command` with `args` (the words after the command name) and resolves to its\n * exit code, or to undefined when no loaded module registers it. `--help` or `-h` prints the command's\n * full usage (`usage`, else `help`) and runs nothing. A throwing command rejects; the caller decides the exit code.\n */\nexport async function runModuleCommand(\n command: string,\n args: readonly string[],\n options: { readonly projectRoot: string; readonly modules: readonly LoadedModule[] },\n): Promise<number | undefined> {\n const found = collectModuleCommands(options.modules, CORE_COMMANDS).get(command);\n if (found === undefined) return undefined;\n if (args.some((arg) => arg === '--help' || arg === '-h')) {\n console.error((found.usage ?? found.help).join('\\n'));\n return 0;\n }\n const ctx: ModuleCommandContext = {\n projectRoot: options.projectRoot,\n ctxoRoot: join(options.projectRoot, '.ctxo'),\n readStdin: readAllStdin,\n };\n return found.run(args, ctx);\n}\n\n/**\n * The block `ctxo --help` ends with: the visible module commands' usage lines under a heading, sorted by\n * command name, or one install hint line when no module is loaded.\n */\nexport function moduleHelpLines(modules: readonly LoadedModule[]): string[] {\n if (modules.length === 0) return [' Domain commands need @ctxo/domain: run \"ctxo install domain\"'];\n const visible = [...collectModuleCommands(modules, CORE_COMMANDS).values()]\n .filter((command) => command.hidden !== true)\n .sort((a, b) => compareCodepoint(a.name, b.name));\n const lines = visible.flatMap((command) => command.help.map((line) => ` ${line}`));\n return lines.length === 0 ? [] : ['Domain commands (@ctxo/domain):', ...lines];\n}\n\n/**\n * The loaded modules for `projectRoot` in `mode`, or none when loading fails. The loader is imported here,\n * on demand, and the module host (with the zod and graph code it pulls in) only when a copy of the module\n * exists, so a run without `@ctxo/domain` (the post-commit `ctxo index --commit HEAD`, `--help`, `doctor`,\n * the hook route) never loads it.\n */\nexport async function loadModulesFor(\n projectRoot: string,\n mode: CtxoHost['mode'],\n options: { readonly cliAnchor?: string } = {},\n): Promise<readonly LoadedModule[]> {\n try {\n const [{ DOMAIN_MODULE_PACKAGE, loadCtxoModules }, { resolvePackageCandidates }] = await Promise.all([\n import('../adapters/modules/module-loader.js'),\n import('../adapters/language/plugin-discovery.js'),\n ]);\n if (resolvePackageCandidates(DOMAIN_MODULE_PACKAGE, projectRoot, options.cliAnchor).length === 0) return [];\n const { createModuleHost } = await import('./module-host.js');\n const { modules } = await loadCtxoModules(createModuleHost({ projectRoot, mode }), { cliAnchor: options.cliAnchor });\n return modules;\n } catch (err) {\n log.warn(`Modules could not be loaded: ${(err as Error).message}`);\n return [];\n }\n}\n\nexport interface FastRouteOptions {\n /** A directory standing in for the CLI's location when modules are looked up (tests); defaults to the running CLI. */\n readonly cliAnchor?: string;\n}\n\n/**\n * The fast route for hook commands (`FAST_ROUTE_COMMANDS`): finds the project root, loads the modules in\n * hook mode and runs the command, without the CLI router or the MCP stack. Fails open: resolves to 0 when\n * the module or the command is missing, or on any error (reported on stderr, never on stdout).\n */\nexport async function runFastRoute(args: readonly string[], cwd: string, options: FastRouteOptions = {}): Promise<number> {\n const command = args[0] ?? '';\n try {\n const projectRoot = locateGitRoot(cwd) ?? cwd;\n const modules = await loadModulesFor(projectRoot, 'hook', { cliAnchor: options.cliAnchor });\n const code = await runModuleCommand(command, args.slice(1), { projectRoot, modules });\n return typeof code === 'number' && Number.isInteger(code) ? code : 0;\n } catch (err) {\n console.error(`[ctxo] ${command} failed, allowing: ${(err as Error).message}`);\n return 0;\n }\n}\n"],"mappings":"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","names":[]}
|