@appchy/jarvis 0.1.36 → 0.1.39
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/bin/config-change.mjs +1 -1
- package/bin/jarvis.mjs +8 -12
- package/bin/pre-tool-use.mjs +2 -2
- package/bin/session-start.mjs +1 -1
- package/bin/stop.mjs +1 -1
- package/bin/user-prompt-submit.mjs +1 -1
- package/dist/bin.js +10104 -37174
- package/dist/bin.js.map +1 -1
- package/dist/data/backends.mjs +469 -0
- package/dist/data/chunk-7REP35VA.mjs +462 -0
- package/dist/data/chunk-AKQQC5IT.mjs +1224 -0
- package/dist/data/chunk-AYOJSS2F.mjs +72 -0
- package/dist/data/chunk-RRJ6KKYL.mjs +42 -0
- package/dist/data/chunk-YWSWQEJN.mjs +138 -0
- package/dist/data/embedders.mjs +80 -0
- package/dist/data/finders.mjs +35 -0
- package/dist/data/index.mjs +137 -0
- package/dist/data/linkers.mjs +83 -0
- package/dist/data/mcp.mjs +2624 -0
- package/dist/data/persistences.mjs +8 -0
- package/dist/data/rerankers.mjs +52 -0
- package/dist/data/stores.mjs +38 -0
- package/dist/hooks/config-change.js +7 -29
- package/dist/hooks/config-change.js.map +1 -1
- package/dist/hooks/pre-tool-use.js +129 -32
- package/dist/hooks/pre-tool-use.js.map +1 -1
- package/dist/hooks/session-start.js +72 -37
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop.js +140 -32
- package/dist/hooks/stop.js.map +1 -1
- package/dist/hooks/user-prompt-submit.js +7 -29
- package/dist/hooks/user-prompt-submit.js.map +1 -1
- package/harness/assets/architecture-template.md +53 -0
- package/harness/assets/design-template.md +89 -0
- package/harness/assets/domains/architecture.md +43 -0
- package/harness/assets/domains/commercial.md +42 -0
- package/harness/assets/domains/design.md +42 -0
- package/harness/assets/domains/legal.md +43 -0
- package/harness/assets/domains/operations.md +39 -0
- package/harness/assets/domains/product.md +43 -0
- package/harness/assets/domains/quality.md +43 -0
- package/harness/assets/domains/security.md +44 -0
- package/harness/assets/domains/support.md +45 -0
- package/harness/assets/epic-template.md +85 -0
- package/harness/assets/feature-template.md +86 -0
- package/harness/assets/handoff-template.md +26 -0
- package/harness/assets/task-template.md +38 -0
- package/harness/assets/version-template.md +43 -0
- package/harness/harness/__init__.py +7 -0
- package/harness/harness/align.py +582 -0
- package/harness/harness/architecture.py +254 -0
- package/harness/harness/autonomy.py +374 -0
- package/harness/harness/branches.py +408 -0
- package/harness/harness/config.py +1482 -0
- package/harness/harness/coverage.py +199 -0
- package/harness/harness/epic.py +220 -0
- package/harness/harness/events.py +153 -0
- package/harness/harness/extend.py +99 -0
- package/harness/harness/frontmatter.py +218 -0
- package/harness/harness/gate.py +591 -0
- package/harness/harness/generate.py +267 -0
- package/harness/harness/git.py +775 -0
- package/harness/harness/ids.py +140 -0
- package/harness/harness/kickoff.py +231 -0
- package/harness/harness/lint.py +505 -0
- package/harness/harness/model.py +364 -0
- package/harness/harness/peers.py +187 -0
- package/harness/harness/product.py +29 -0
- package/harness/harness/registry.py +382 -0
- package/harness/harness/report.py +227 -0
- package/harness/harness/safety.py +387 -0
- package/harness/harness/scaffold.py +129 -0
- package/harness/harness/shard.py +63 -0
- package/harness/harness/shift.py +348 -0
- package/harness/harness/task.py +507 -0
- package/harness/harness/tree.py +258 -0
- package/harness/harness/version.py +305 -0
- package/harness/harness/wrap.py +217 -0
- package/harness/hooks/guard.py +259 -0
- package/harness/presets/appchy/PRESET.md +717 -0
- package/harness/presets/appchy/references/artifacts.md +539 -0
- package/harness/presets/appchy/references/graph.md +154 -0
- package/harness/presets/appchy/references/operations.md +444 -0
- package/harness/presets/appchy/references/research.md +216 -0
- package/harness/schema/work.config.schema.json +401 -0
- package/harness/test_work.py +5002 -0
- package/harness/work.py +534 -0
- package/package.json +34 -40
- package/bin/config-change.dev.mjs +0 -17
- package/bin/jarvis-dev.mjs +0 -30
- package/bin/pre-tool-use.dev.mjs +0 -23
- package/bin/session-start.dev.mjs +0 -17
- package/bin/stop.dev.mjs +0 -17
- package/bin/user-prompt-submit.dev.mjs +0 -17
- package/dev/bin.js +0 -38718
- package/dev/bin.js.map +0 -1
- package/dev/env.json +0 -1
- package/dev/hooks/config-change.js +0 -110
- package/dev/hooks/config-change.js.map +0 -1
- package/dev/hooks/pre-tool-use.js +0 -120
- package/dev/hooks/pre-tool-use.js.map +0 -1
- package/dev/hooks/session-start.js +0 -115
- package/dev/hooks/session-start.js.map +0 -1
- package/dev/hooks/stop.js +0 -112
- package/dev/hooks/stop.js.map +0 -1
- package/dev/hooks/user-prompt-submit.js +0 -111
- package/dev/hooks/user-prompt-submit.js.map +0 -1
- package/dist/env.json +0 -1
|
@@ -0,0 +1,2624 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COVERAGE_VIA,
|
|
3
|
+
DERIVED_FROM,
|
|
4
|
+
GraphStore,
|
|
5
|
+
applyMemoryOverlay,
|
|
6
|
+
categoryOf,
|
|
7
|
+
deleteMemory,
|
|
8
|
+
edgeMatchesConfidence,
|
|
9
|
+
isPrecise,
|
|
10
|
+
memoryNode,
|
|
11
|
+
readMemoryStore,
|
|
12
|
+
runEnforcers,
|
|
13
|
+
summarize,
|
|
14
|
+
writeMemoryStore
|
|
15
|
+
} from "./chunk-AKQQC5IT.mjs";
|
|
16
|
+
import {
|
|
17
|
+
diversify,
|
|
18
|
+
isConfident,
|
|
19
|
+
isSingleTerm,
|
|
20
|
+
loadSearchRuntime,
|
|
21
|
+
rankNodes
|
|
22
|
+
} from "./chunk-7REP35VA.mjs";
|
|
23
|
+
import "./chunk-RRJ6KKYL.mjs";
|
|
24
|
+
import "./chunk-AYOJSS2F.mjs";
|
|
25
|
+
import {
|
|
26
|
+
parseFrontmatter
|
|
27
|
+
} from "./chunk-YWSWQEJN.mjs";
|
|
28
|
+
|
|
29
|
+
// ../../packages/data/src/mcp/server.ts
|
|
30
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
31
|
+
|
|
32
|
+
// ../../packages/data/src/mcp/freshness.ts
|
|
33
|
+
import { execFile } from "child_process";
|
|
34
|
+
import { stat } from "fs/promises";
|
|
35
|
+
import { resolve } from "path";
|
|
36
|
+
import { promisify } from "util";
|
|
37
|
+
|
|
38
|
+
// ../../packages/data/src/context.ts
|
|
39
|
+
function createGraphContext(config, graph, buildDiagnostics, builtAt, options = {}) {
|
|
40
|
+
return {
|
|
41
|
+
graph,
|
|
42
|
+
config,
|
|
43
|
+
backendName: config.backend.name,
|
|
44
|
+
buildDiagnostics,
|
|
45
|
+
builtAt,
|
|
46
|
+
search: memoize(options.search)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function memoize(search2) {
|
|
50
|
+
if (!search2) return () => Promise.resolve(void 0);
|
|
51
|
+
if (typeof search2 !== "function") return () => Promise.resolve(search2);
|
|
52
|
+
let pending;
|
|
53
|
+
return () => {
|
|
54
|
+
if (!pending) {
|
|
55
|
+
pending = search2().catch((err) => {
|
|
56
|
+
pending = void 0;
|
|
57
|
+
throw err;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return pending;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function isCode(ctx, node) {
|
|
64
|
+
return node.owner === ctx.backendName || node.metadata["data.federated.code"] === true;
|
|
65
|
+
}
|
|
66
|
+
function fileOf(node) {
|
|
67
|
+
const f = node.metadata.sourceFile;
|
|
68
|
+
return typeof f === "string" ? f : void 0;
|
|
69
|
+
}
|
|
70
|
+
function labelOf(node) {
|
|
71
|
+
const l = node.metadata.label ?? node.metadata.title;
|
|
72
|
+
return typeof l === "string" ? l : void 0;
|
|
73
|
+
}
|
|
74
|
+
function statusOf(node) {
|
|
75
|
+
const raw = node.metadata.status;
|
|
76
|
+
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
77
|
+
}
|
|
78
|
+
function lineOf(node) {
|
|
79
|
+
const raw = node.metadata.sourceLine;
|
|
80
|
+
if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
|
|
81
|
+
if (typeof raw !== "string") return void 0;
|
|
82
|
+
const m = raw.match(/\d+/);
|
|
83
|
+
return m ? Number(m[0]) : void 0;
|
|
84
|
+
}
|
|
85
|
+
function renderNode(ctx, node, detailed = false) {
|
|
86
|
+
if (isCode(ctx, node)) {
|
|
87
|
+
const line = lineOf(node);
|
|
88
|
+
return {
|
|
89
|
+
file: fileOf(node),
|
|
90
|
+
symbol: labelOf(node),
|
|
91
|
+
...line !== void 0 ? { line } : {},
|
|
92
|
+
...detailed ? { nodeId: node.id, type: node.type } : {}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
id: node.id,
|
|
97
|
+
type: node.type,
|
|
98
|
+
title: labelOf(node),
|
|
99
|
+
...detailed ? { nodeId: node.id } : {}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function previewOf(node) {
|
|
103
|
+
const ex = node.metadata.excerpt;
|
|
104
|
+
return typeof ex === "string" && ex.length > 0 ? ex : void 0;
|
|
105
|
+
}
|
|
106
|
+
function edgeScore(edge) {
|
|
107
|
+
const score = edge.metadata?.score;
|
|
108
|
+
return typeof score === "number" ? score : void 0;
|
|
109
|
+
}
|
|
110
|
+
function originOf(node) {
|
|
111
|
+
if (node.metadata["data.federated"] !== true) return void 0;
|
|
112
|
+
const origin = node.metadata["data.origin"];
|
|
113
|
+
return typeof origin === "string" ? origin : void 0;
|
|
114
|
+
}
|
|
115
|
+
function nodeMatchesOrigin(node, origin) {
|
|
116
|
+
if (origin === void 0) return true;
|
|
117
|
+
const o = originOf(node);
|
|
118
|
+
return o === void 0 || origin.includes(o);
|
|
119
|
+
}
|
|
120
|
+
function hopPasses(ctx, hop, confidence, origin) {
|
|
121
|
+
if (!edgeMatchesConfidence(hop, ctx.backendName, confidence)) return false;
|
|
122
|
+
const target = ctx.graph.node(hop.to);
|
|
123
|
+
return !target || nodeMatchesOrigin(target, origin);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ../../packages/data/src/mcp/freshness.ts
|
|
127
|
+
var run = promisify(execFile);
|
|
128
|
+
var HEAD_TTL_MS = 1e4;
|
|
129
|
+
var headCache = /* @__PURE__ */ new Map();
|
|
130
|
+
async function gitHeadCached(root) {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const hit = headCache.get(root);
|
|
133
|
+
if (hit && now - hit.at < HEAD_TTL_MS) return hit.head;
|
|
134
|
+
const head = await gitHead(root);
|
|
135
|
+
headCache.set(root, { at: now, head });
|
|
136
|
+
return head;
|
|
137
|
+
}
|
|
138
|
+
async function gitHead(root) {
|
|
139
|
+
try {
|
|
140
|
+
const { stdout: commit } = await run("git", ["rev-parse", "HEAD"], { cwd: root });
|
|
141
|
+
const { stdout: ts } = await run("git", ["show", "-s", "--format=%ct", "HEAD"], { cwd: root });
|
|
142
|
+
const { stdout: dirty } = await run("git", ["status", "--porcelain"], { cwd: root });
|
|
143
|
+
return {
|
|
144
|
+
commit: commit.trim(),
|
|
145
|
+
committedMs: Number(ts.trim()) * 1e3,
|
|
146
|
+
dirty: dirty.trim().length > 0
|
|
147
|
+
};
|
|
148
|
+
} catch {
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
var ORIGIN_TTL_MS = 6e4;
|
|
153
|
+
var ORIGIN_TIMEOUT_MS = 1e4;
|
|
154
|
+
var originCache = /* @__PURE__ */ new Map();
|
|
155
|
+
async function git(root, argv, timeout = 5e3) {
|
|
156
|
+
try {
|
|
157
|
+
const { stdout } = await run("git", argv, {
|
|
158
|
+
cwd: root,
|
|
159
|
+
timeout,
|
|
160
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
161
|
+
});
|
|
162
|
+
return stdout.trim();
|
|
163
|
+
} catch {
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function fetchHeadIso(root) {
|
|
168
|
+
const path = await git(root, ["rev-parse", "--git-path", "FETCH_HEAD"]);
|
|
169
|
+
if (!path) return void 0;
|
|
170
|
+
try {
|
|
171
|
+
return (await stat(resolve(root, path))).mtime.toISOString();
|
|
172
|
+
} catch {
|
|
173
|
+
return void 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function originState(root) {
|
|
177
|
+
const ref = await git(root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
|
|
178
|
+
if (!ref) return void 0;
|
|
179
|
+
const reachable = await git(root, ["fetch", "--quiet", "--no-tags"], ORIGIN_TIMEOUT_MS) !== void 0;
|
|
180
|
+
const lastSyncedIso = await fetchHeadIso(root);
|
|
181
|
+
if (!reachable) return { ref, reachable: false, ...lastSyncedIso ? { lastSyncedIso } : {} };
|
|
182
|
+
const behindBy = Number(await git(root, ["rev-list", "--count", `HEAD..${ref}`]));
|
|
183
|
+
return {
|
|
184
|
+
ref,
|
|
185
|
+
reachable: true,
|
|
186
|
+
...Number.isFinite(behindBy) ? { behindBy } : {},
|
|
187
|
+
...lastSyncedIso ? { lastSyncedIso } : {}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function originStateCached(root) {
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
const hit = originCache.get(root);
|
|
193
|
+
if (hit && now - hit.at < ORIGIN_TTL_MS) return hit.state;
|
|
194
|
+
const state = await originState(root);
|
|
195
|
+
originCache.set(root, { at: now, state });
|
|
196
|
+
return state;
|
|
197
|
+
}
|
|
198
|
+
function nativeGraphJson(ctx) {
|
|
199
|
+
const g = ctx.config.backend.native?.()?.graphJson;
|
|
200
|
+
return typeof g === "string" ? g : void 0;
|
|
201
|
+
}
|
|
202
|
+
async function freshness(ctx) {
|
|
203
|
+
const root = ctx.config.repoRoot;
|
|
204
|
+
const graphRel = nativeGraphJson(ctx) ?? ".data/graphify/graph.json";
|
|
205
|
+
const graphPath = resolve(root, graphRel);
|
|
206
|
+
let mtimeIso;
|
|
207
|
+
let mtimeMs;
|
|
208
|
+
try {
|
|
209
|
+
const s = await stat(graphPath);
|
|
210
|
+
mtimeIso = s.mtime.toISOString();
|
|
211
|
+
mtimeMs = s.mtimeMs;
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
const head = await gitHeadCached(root);
|
|
215
|
+
const builtMs = Date.parse(ctx.builtAt);
|
|
216
|
+
let behindHead;
|
|
217
|
+
if (head && mtimeMs) behindHead = mtimeMs < head.committedMs;
|
|
218
|
+
const replacedMs = Math.max(mtimeMs ?? 0, await snapshotMtimeMs(ctx) ?? 0);
|
|
219
|
+
const rebuiltOnDisk = Boolean(
|
|
220
|
+
replacedMs && Number.isFinite(builtMs) && replacedMs > builtMs + 1e3
|
|
221
|
+
);
|
|
222
|
+
const origin = await originStateCached(root);
|
|
223
|
+
const behindOrigin = Boolean(origin?.reachable && (origin.behindBy ?? 0) > 0);
|
|
224
|
+
const originUnknown = Boolean(origin && !origin.reachable);
|
|
225
|
+
let reason;
|
|
226
|
+
if (behindOrigin)
|
|
227
|
+
reason = `this checkout is ${origin?.behindBy} commit(s) behind ${origin?.ref} \u2014 the map and the board both describe an older world than the origin's. Pull, then \`jarvis build graph\`.`;
|
|
228
|
+
else if (originUnknown)
|
|
229
|
+
reason = `cannot reach ${origin?.ref}, so I cannot tell whether this is current${origin?.lastSyncedIso ? ` \u2014 last synced at ${origin.lastSyncedIso}` : " and this clone has never fetched"}.`;
|
|
230
|
+
else if (behindHead)
|
|
231
|
+
reason = `${graphRel} is older than HEAD \u2014 the code graph is stale; run \`jarvis build graph\` (the incremental update \u2014 it re-extracts in place), then traces and impact are trustworthy again. \`--rebuild\` re-extracts from scratch and relabels; it is for a graph still wrong after a plain build, not for ordinary drift.`;
|
|
232
|
+
else if (rebuiltOnDisk)
|
|
233
|
+
reason = `${graphRel} was rebuilt after this MCP server loaded it \u2014 reloading automatically.`;
|
|
234
|
+
return {
|
|
235
|
+
graphPath: graphRel,
|
|
236
|
+
...mtimeIso ? { mtimeIso } : {},
|
|
237
|
+
...head ? { head: { commit: head.commit, dirty: head.dirty } } : {},
|
|
238
|
+
...behindHead !== void 0 ? { behindHead } : {},
|
|
239
|
+
...rebuiltOnDisk ? { rebuiltOnDisk } : {},
|
|
240
|
+
...origin ? { origin } : {},
|
|
241
|
+
...behindOrigin ? { behindOrigin } : {},
|
|
242
|
+
...originUnknown ? { originUnknown } : {},
|
|
243
|
+
stale: Boolean(behindHead) || rebuiltOnDisk || behindOrigin || originUnknown,
|
|
244
|
+
...reason ? { reason } : {}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async function snapshotMtimeMs(ctx) {
|
|
248
|
+
const path = ctx.config.persistence?.locate?.({ repoRoot: ctx.config.repoRoot }, "snapshot");
|
|
249
|
+
if (!path) return void 0;
|
|
250
|
+
try {
|
|
251
|
+
return (await stat(path)).mtimeMs;
|
|
252
|
+
} catch {
|
|
253
|
+
return void 0;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function federatedFreshness(ctx) {
|
|
257
|
+
const byOrigin = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const node of ctx.graph.nodes()) {
|
|
259
|
+
const origin = originOf(node);
|
|
260
|
+
const builtAt = node.metadata["data.federated.builtAt"];
|
|
261
|
+
if (origin === void 0 || typeof builtAt !== "string" || byOrigin.has(origin)) continue;
|
|
262
|
+
const repoPath = node.metadata["data.federated.repoPath"];
|
|
263
|
+
byOrigin.set(origin, { builtAt, ...typeof repoPath === "string" ? { repoPath } : {} });
|
|
264
|
+
}
|
|
265
|
+
const out = [];
|
|
266
|
+
for (const [origin, { builtAt, repoPath }] of byOrigin) {
|
|
267
|
+
const head = repoPath ? await gitHeadCached(repoPath) : void 0;
|
|
268
|
+
const builtMs = Date.parse(builtAt);
|
|
269
|
+
const behindOwnHead = head && Number.isFinite(builtMs) ? builtMs < head.committedMs : void 0;
|
|
270
|
+
out.push({ origin, builtAt, ...behindOwnHead !== void 0 ? { behindOwnHead } : {} });
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ../../packages/data/src/mcp/provider.ts
|
|
276
|
+
function createGraphProvider(build, options = {}) {
|
|
277
|
+
const checkFreshness = options.checkFreshness ?? freshness;
|
|
278
|
+
let current = build();
|
|
279
|
+
current.catch(() => {
|
|
280
|
+
});
|
|
281
|
+
let reloading;
|
|
282
|
+
return async function getCtx() {
|
|
283
|
+
const ctx = await current;
|
|
284
|
+
if (reloading) return reloading;
|
|
285
|
+
const f = await checkFreshness(ctx);
|
|
286
|
+
if (!f.rebuiltOnDisk) return ctx;
|
|
287
|
+
if (reloading) return reloading;
|
|
288
|
+
options.onReload?.(f.reason ?? `${f.graphPath} was rebuilt on disk`);
|
|
289
|
+
const next = build();
|
|
290
|
+
next.catch(() => {
|
|
291
|
+
});
|
|
292
|
+
reloading = next.then((fresh) => {
|
|
293
|
+
current = next;
|
|
294
|
+
return fresh;
|
|
295
|
+
}).finally(() => {
|
|
296
|
+
reloading = void 0;
|
|
297
|
+
});
|
|
298
|
+
return reloading;
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function notReadyMs() {
|
|
302
|
+
const raw = Number(process.env.JARVIS_DATA_MCP_NOT_READY_MS);
|
|
303
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : 8e3;
|
|
304
|
+
}
|
|
305
|
+
var PENDING = /* @__PURE__ */ Symbol("pending");
|
|
306
|
+
async function readyWithin(pending) {
|
|
307
|
+
let timer;
|
|
308
|
+
const timeout = new Promise((resolve4) => {
|
|
309
|
+
timer = setTimeout(() => resolve4(PENDING), notReadyMs());
|
|
310
|
+
timer.unref?.();
|
|
311
|
+
});
|
|
312
|
+
try {
|
|
313
|
+
const won = await Promise.race([pending, timeout]);
|
|
314
|
+
return won === PENDING ? void 0 : won;
|
|
315
|
+
} finally {
|
|
316
|
+
if (timer) clearTimeout(timer);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
var STILL_LOADING = "The graph is still loading \u2014 retry this call in a few seconds.";
|
|
320
|
+
|
|
321
|
+
// ../../packages/data/src/data.tools.ts
|
|
322
|
+
var CONFIDENCE = {
|
|
323
|
+
type: "string",
|
|
324
|
+
enum: ["precise", "fuzzy", "all"],
|
|
325
|
+
description: "Edge confidence to include. Default 'precise' (deterministic linker/citation edges); 'fuzzy' = discovery-only structural/semantic edges; 'all' = both."
|
|
326
|
+
};
|
|
327
|
+
function limit(description) {
|
|
328
|
+
return { type: "integer", minimum: 1, maximum: 200, description };
|
|
329
|
+
}
|
|
330
|
+
function dataTools(handlers) {
|
|
331
|
+
return [
|
|
332
|
+
briefTool(handlers),
|
|
333
|
+
scopeTool(handlers),
|
|
334
|
+
searchTool(handlers),
|
|
335
|
+
readTool(handlers),
|
|
336
|
+
graphTool(handlers),
|
|
337
|
+
connectionsTool(handlers)
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
function briefTool(handlers) {
|
|
341
|
+
return {
|
|
342
|
+
name: "map_brief",
|
|
343
|
+
title: "Repo briefing",
|
|
344
|
+
description: `THE SESSION-START CALL. Returns the top-down state of the repo in one bounded response: what work is in flight (with status, priority, dependencies, and which version/milestone it belongs to), the most-cited decisions/docs, governance health, code areas, and graph freshness. Call it FIRST in a new session \u2014 before planning or searching \u2014 so every later call starts oriented; then drill down with map_read / map_connections / map_scope (each section names its follow-up). Freshness + governance health ride every briefing; pass include:['diagnostics'] for the full lint list (formerly lint) or include:['shape'] for the node/edge breakdown + graph.json freshness (formerly status). Use format:"detailed" for longer lists. Examples: {} \xB7 {"format":"detailed"} \xB7 {"include":["diagnostics"],"severity":"error"}.`,
|
|
345
|
+
metadata: { readOnly: true },
|
|
346
|
+
input: {
|
|
347
|
+
type: "object",
|
|
348
|
+
properties: {
|
|
349
|
+
format: {
|
|
350
|
+
type: "string",
|
|
351
|
+
enum: ["summary", "detailed"],
|
|
352
|
+
description: "'detailed' lengthens the lists. Default summary."
|
|
353
|
+
},
|
|
354
|
+
include: {
|
|
355
|
+
type: "array",
|
|
356
|
+
items: { type: "string", enum: ["diagnostics", "shape"] },
|
|
357
|
+
description: "Opt into heavy sections: 'diagnostics' (the full lint list, formerly lint) or 'shape' (node/edge breakdown + graph.json freshness, formerly status). Default: neither."
|
|
358
|
+
},
|
|
359
|
+
severity: {
|
|
360
|
+
type: "string",
|
|
361
|
+
enum: ["error", "warn", "info"],
|
|
362
|
+
description: "When include has 'diagnostics', keep only this severity."
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
call: (ctx, input) => handlers.brief(ctx, input)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function scopeTool(handlers) {
|
|
370
|
+
return {
|
|
371
|
+
name: "map_scope",
|
|
372
|
+
title: "Scope a change, or check one",
|
|
373
|
+
description: 'THE PLANNING FAST PATH, and the check before you commit \u2014 one question at two moments. Pass `task` (prose) to PLAN a change: a plan-shaped As-found in one call \u2014 the ranked EDIT SET, the GOVERNANCE it is under IN FULL (top rules complete, the rest as ids, with a count of each, because a summary is not the rule), the TESTS COVERING it, and its precise BLAST RADIUS. Pass `edits` (repo-relative paths) to VERIFY one: typed findings grounded in the graph \u2014 governance the edits precisely touch that `cites` does not name, what the change ripples into, and any lint finding landing on the edit set \u2014 plus a verdict of \'approve\' | \'revise\' | \'unassessable\' (\'unassessable\' iff no edit is in the graph yet, so \'approve\' never means could not check). Deterministic, not an opinion \u2014 contest a finding you disagree with. Pass NEITHER and it refuses saying which to give. A parameter for the other door is ignored rather than refused: `format`/`confidence` shape planning, `cites`/`coverage` shape verifying. WHEN TO USE: for MODIFYING existing code. For a BUILD-NEW task the code does not exist yet \u2014 prefer map_search + map_read. Examples: {"task":"rework camera framing"} \xB7 {"edits":["src/server.ts"],"cites":["S-F"]}.',
|
|
374
|
+
metadata: { readOnly: true },
|
|
375
|
+
input: {
|
|
376
|
+
type: "object",
|
|
377
|
+
properties: {
|
|
378
|
+
task: {
|
|
379
|
+
type: "string",
|
|
380
|
+
description: 'PLANNING door. The change in natural language, e.g. "rework camera framing".'
|
|
381
|
+
},
|
|
382
|
+
edits: {
|
|
383
|
+
type: "array",
|
|
384
|
+
items: { type: "string" },
|
|
385
|
+
minItems: 1,
|
|
386
|
+
description: "VERIFYING door. Repo-relative file paths the change edits."
|
|
387
|
+
},
|
|
388
|
+
cites: {
|
|
389
|
+
type: "array",
|
|
390
|
+
items: { type: "string" },
|
|
391
|
+
description: 'With `edits`: the decision/standard ids the plan honors (e.g. "D87"). Governance the edits touch that is not listed is flagged.'
|
|
392
|
+
},
|
|
393
|
+
rules: {
|
|
394
|
+
type: "string",
|
|
395
|
+
enum: ["full", "ids"],
|
|
396
|
+
description: "With `task`: 'ids' skips the rule text."
|
|
397
|
+
},
|
|
398
|
+
coverage: {
|
|
399
|
+
type: "boolean",
|
|
400
|
+
description: "With `edits`: also report files no test exercises. Off by default \u2014 it fires on every file of a layer with no colocated tests."
|
|
401
|
+
},
|
|
402
|
+
limit: limit(
|
|
403
|
+
"How many top hits / edit-set entries / findings. Default 5 planning, 50 verifying."
|
|
404
|
+
),
|
|
405
|
+
format: {
|
|
406
|
+
type: "string",
|
|
407
|
+
enum: ["summary", "detailed"],
|
|
408
|
+
description: "'detailed' deepens each hit's trace, adds an impact sample and a fuzzy reachability view. Default summary."
|
|
409
|
+
},
|
|
410
|
+
confidence: CONFIDENCE
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
call: (ctx, input) => handlers.scope(ctx, input)
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function searchTool(handlers) {
|
|
417
|
+
return {
|
|
418
|
+
name: "map_search",
|
|
419
|
+
title: "Find or browse the map",
|
|
420
|
+
description: 'Finds nodes across BOTH governance artifacts and code \u2014 and, with no `query`, LISTS what the map holds at all. One question through two doors, and the answer\'s shape follows what you pass; a parameter meant for the other door is ignored rather than refused. WITH a query: ranked matches, each a semantic id (artifact id / file:line) with a short preview, ready to feed into map_connections, map_read or map_graph. Its edge over plain text search is graph + artifact awareness, so prefer it for CONCEPTUAL queries ("where is framing decided") and use native grep for literal strings. Multi-word queries return one row per file and come back empty with guidance when nothing clears the relevance floor; a single word is an identifier lookup with symbol-level results. Reach for this (+ map_read) FIRST when planning a BUILD-NEW task, where there is no existing code for map_scope or map_connections to work from. WITHOUT a query: one page of everything, in a stable order (type then id, so paging never skips or repeats), each thing saying what it sits inside and, where it declares one, its own status \u2014 plus `types`, every type in the WHOLE map with its count, so a caller can offer a filter without a second call. Ranking is right for FINDING something and wrong for SEEING everything, so use this door to open the map and look at it, to learn what types a repo maps, or to check what one actually holds. Examples: {} \xB7 {"type":"decision"} \xB7 {"limit":50,"offset":50} \xB7 {"query":"camera framing"} \xB7 {"query":"where the graph ends and its overlay begins","deep":true}.',
|
|
421
|
+
metadata: { readOnly: true },
|
|
422
|
+
input: {
|
|
423
|
+
type: "object",
|
|
424
|
+
properties: {
|
|
425
|
+
query: {
|
|
426
|
+
type: "string",
|
|
427
|
+
description: "Natural-language search terms. ABSENT (or blank) IS the browse: everything, unranked, in a stable order."
|
|
428
|
+
},
|
|
429
|
+
type: {
|
|
430
|
+
type: "string",
|
|
431
|
+
description: "Restrict to one node type (e.g. 'decision'). Valid at either door; omit for every type."
|
|
432
|
+
},
|
|
433
|
+
limit: limit("Max results. Default 10 ranked \xB7 100 browsing."),
|
|
434
|
+
offset: {
|
|
435
|
+
type: "integer",
|
|
436
|
+
minimum: 0,
|
|
437
|
+
description: "BROWSING ONLY \u2014 skip this many, for paging past `limit`. A ranked list has no stable page to resume from. Default 0."
|
|
438
|
+
},
|
|
439
|
+
fields: {
|
|
440
|
+
type: "array",
|
|
441
|
+
items: { type: "string" },
|
|
442
|
+
description: "BROWSING ONLY \u2014 extra keys this repo's own records carry, returned untouched under `fields`. Nothing here is interpreted: ask for what a repo writes (e.g. ['met'], ['priority']) when you want it beside the thing rather than after a read."
|
|
443
|
+
},
|
|
444
|
+
expand: {
|
|
445
|
+
type: "boolean",
|
|
446
|
+
description: "QUERY ONLY \u2014 broaden recall: pull a 1-hop citation/community frontier around the top semantic matches before ranking (needs semantic search configured; best with a reranker). Default false."
|
|
447
|
+
},
|
|
448
|
+
deep: {
|
|
449
|
+
type: "boolean",
|
|
450
|
+
description: 'QUERY ONLY \u2014 re-rank the head with a cross-encoder precision pass (needs a configured reranker; slower). Use ONLY for ABSTRACT governance questions \u2014 "why/where is X decided or governed" \u2014 that a plain search buries; it mis-ranks code and identifier lookups. Default false.'
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
call: (ctx, input) => handlers.search(ctx, input)
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function readTool(handlers) {
|
|
458
|
+
return {
|
|
459
|
+
name: "map_read",
|
|
460
|
+
title: "Read an artifact",
|
|
461
|
+
description: `Reads the FULL text of a governance artifact (decision, standard, spec, \u2026) by id \u2014 what map_scope or map_connections surfaced as governing some code. Those tell you D87 applies; this tells you what D87 actually SAYS. Returns the artifact's source markdown (frontmatter included), truncated to maxChars. For artifacts only \u2014 read code files directly. Examples: {"id":"D87"} \xB7 {"id":"D87","maxChars":40000}.`,
|
|
462
|
+
metadata: { readOnly: true },
|
|
463
|
+
input: {
|
|
464
|
+
type: "object",
|
|
465
|
+
properties: {
|
|
466
|
+
id: {
|
|
467
|
+
type: "string",
|
|
468
|
+
description: 'An artifact id, e.g. "D87" (discover ids with map_search/map_connections).'
|
|
469
|
+
},
|
|
470
|
+
maxChars: {
|
|
471
|
+
type: "integer",
|
|
472
|
+
minimum: 200,
|
|
473
|
+
maximum: 1e5,
|
|
474
|
+
description: "Truncate the body to this many chars. Default 20000."
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
required: ["id"]
|
|
478
|
+
},
|
|
479
|
+
call: (ctx, input) => handlers.read(ctx, input)
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function graphTool(handlers) {
|
|
483
|
+
return {
|
|
484
|
+
name: "map_graph",
|
|
485
|
+
title: "Explore graph connections",
|
|
486
|
+
description: 'Explores how the graph connects. action:\'impact\' returns everything reachable within `depth` hops (the blast radius of a change \u2014 run it BEFORE editing), summarised by type; action:\'neighbors\' lists direct links ONE hop out \u2014 every edge, settled or not, in one flat list. When the question is what open WORK touches something, use map_connections instead: a file usually carries no edge to anything tracked because it sits INSIDE something that does, and only that tool takes the second hop; action:\'path\' finds the shortest connection between `node` and `to`; action:\'hubs\' ranks the most-connected nodes (coupling hotspots / god-nodes); action:\'community\' returns the cluster a node belongs to, or \u2014 with no `node` \u2014 the clusters by size. neighbors/impact/path need `node` (a file path or artifact id); hubs/community can scan the whole graph. Outputs are bounded \u2014 page through a large neighbors/impact result with `offset` (offset:0,limit:25 then offset:25,... while `truncated` is true; `total` says when to stop). Examples: {"action":"impact","node":"packages/art/src/camera.ts"} \xB7 {"action":"hubs","type":"code"}.',
|
|
487
|
+
metadata: { readOnly: true },
|
|
488
|
+
input: {
|
|
489
|
+
type: "object",
|
|
490
|
+
properties: {
|
|
491
|
+
action: {
|
|
492
|
+
type: "string",
|
|
493
|
+
enum: ["neighbors", "impact", "path", "hubs", "community"],
|
|
494
|
+
description: "neighbors | impact (blast radius) | path | hubs (coupling hotspots) | community (clusters)."
|
|
495
|
+
},
|
|
496
|
+
node: {
|
|
497
|
+
type: "string",
|
|
498
|
+
description: "Start node (file path or artifact id). Required for neighbors/impact/path; optional for hubs/community."
|
|
499
|
+
},
|
|
500
|
+
to: { type: "string", description: "End node for action:'path'." },
|
|
501
|
+
relation: {
|
|
502
|
+
type: "string",
|
|
503
|
+
description: "Restrict traversal to one relation (e.g. 'enforces'). See map_brief {include:[.shape.]}."
|
|
504
|
+
},
|
|
505
|
+
depth: {
|
|
506
|
+
type: "integer",
|
|
507
|
+
minimum: 1,
|
|
508
|
+
maximum: 5,
|
|
509
|
+
description: "Hops for neighbors/impact. Default 1 / 2."
|
|
510
|
+
},
|
|
511
|
+
type: {
|
|
512
|
+
type: "string",
|
|
513
|
+
description: "Restrict hubs/community to one node type (e.g. 'code')."
|
|
514
|
+
},
|
|
515
|
+
limit: limit("Max results. Default 25."),
|
|
516
|
+
offset: {
|
|
517
|
+
type: "integer",
|
|
518
|
+
minimum: 0,
|
|
519
|
+
description: "Skip this many neighbors/impact results (paging past `limit`). Default 0."
|
|
520
|
+
},
|
|
521
|
+
confidence: CONFIDENCE
|
|
522
|
+
},
|
|
523
|
+
required: ["action"]
|
|
524
|
+
},
|
|
525
|
+
call: (ctx, input) => handlers.graph(ctx, input)
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function connectionsTool(handlers) {
|
|
529
|
+
return {
|
|
530
|
+
name: "map_connections",
|
|
531
|
+
title: "What one thing connects to",
|
|
532
|
+
description: 'What ONE thing connects to \u2014 both the work touching it and the governance it is read against. For when you already have a starting point, where map_search with no query is for when you have none. Point it at anything: a rule, a criterion, a file, a work item, a machine. Returns `items` (connected things carrying a status, filtered to those still OPEN by default \u2014 status:\'all\' includes settled ones, whose count is always reported as `settled`) and `context` (things carrying no status \u2014 what this is read against rather than what is being done to it). So one call answers both directions: ask about a RULE and get the open work touching it plus the code enforcing it; ask about a FILE and get why it exists \u2014 the artifacts it cites. Use it BEFORE changing unfamiliar existing code; a build-new task has nothing to connect yet, so prefer map_search + map_read. Direction is inferred from what you pass, and `direction` narrows it to one side. Where nothing carrying a status connects directly \u2014 usually a code file, which sits INSIDE something tracked rather than being tracked itself \u2014 it steps through one statusless neighbour and each result names that neighbour as `via`, so a regional answer is never mistaken for a file-precise one. Two hops and no further. Walks every edge by default because a circumstantial edge is a poor gate and a good lead; pass confidence:\'precise\' to ask the citation question narrowly. Artifact hits carry a short preview; map_read gives the full text. Prefer this over map_graph{action:\'neighbors\'} for what is LIVE; reach for map_graph for raw structure \u2014 blast radius, a shortest path, the most-coupled nodes. Examples: {"node":"D-9"} \xB7 {"node":"src/camera.ts"} \xB7 {"node":"D-9","status":"all"}.',
|
|
533
|
+
metadata: { readOnly: true },
|
|
534
|
+
input: {
|
|
535
|
+
type: "object",
|
|
536
|
+
properties: {
|
|
537
|
+
node: {
|
|
538
|
+
type: "string",
|
|
539
|
+
description: "The thing to ask about \u2014 an artifact id, or a repo-relative file path."
|
|
540
|
+
},
|
|
541
|
+
status: {
|
|
542
|
+
type: "string",
|
|
543
|
+
enum: ["open", "all"],
|
|
544
|
+
description: "Which connected things to return. 'open' (default) drops the ones already settled (done/dropped) and reports how many; 'all' keeps them."
|
|
545
|
+
},
|
|
546
|
+
direction: {
|
|
547
|
+
type: "string",
|
|
548
|
+
enum: ["auto", "to-code", "to-artifacts"],
|
|
549
|
+
description: "Narrow `context` to one side. Default auto \u2014 everything one hop out."
|
|
550
|
+
},
|
|
551
|
+
confidence: CONFIDENCE,
|
|
552
|
+
limit: limit("Max connected things returned. Default 25.")
|
|
553
|
+
},
|
|
554
|
+
required: ["node"]
|
|
555
|
+
},
|
|
556
|
+
call: (ctx, input) => handlers.connections(ctx, input)
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function memoryTool(handler) {
|
|
560
|
+
return {
|
|
561
|
+
name: "map_memory",
|
|
562
|
+
title: "Record or delete an agent memory",
|
|
563
|
+
description: `RECORD (op:"write", the default) or DELETE (op:"delete") a generic agent memory \u2014 a fact/connection the extractors can't derive, held as an overlay node + edges in the graph. One tool, one operation param (mirrors map_graph). Two-step by design: call WITHOUT confirm for a preview (writes nothing), then confirm:true to commit (append-only, audited, persisted via the storage seam). Write links a memory to existing files (by path) or artifacts (by id); a link that doesn't resolve is reported, not silently dropped. Reconciled on rebuild \u2014 a memory whose links no longer resolve is kept and flagged, never silently dropped. Delete needs the memory's id. Not for repo task management. Examples: {"text":"Prod DB pool caps at 20 \u2014 tune via DATABASE_POOL_MAX","links":[{"to":"src/config.ts"}]} \xB7 {"op":"write","text":"D87 supersedes the old cache key format","links":[{"to":"D87"}],"confirm":true} \xB7 {"op":"delete","id":"mem-...","confirm":true}.`,
|
|
564
|
+
metadata: { readOnly: false },
|
|
565
|
+
input: {
|
|
566
|
+
type: "object",
|
|
567
|
+
properties: {
|
|
568
|
+
op: {
|
|
569
|
+
type: "string",
|
|
570
|
+
enum: ["write", "delete"],
|
|
571
|
+
description: 'The operation. Default "write".'
|
|
572
|
+
},
|
|
573
|
+
text: {
|
|
574
|
+
type: "string",
|
|
575
|
+
minLength: 1,
|
|
576
|
+
description: "The memory text (write only \u2014 its title/body)."
|
|
577
|
+
},
|
|
578
|
+
type: {
|
|
579
|
+
type: "string",
|
|
580
|
+
description: 'The memory node type (data \u2014 note/insight/decision/task/\u2026). Default "note".'
|
|
581
|
+
},
|
|
582
|
+
links: {
|
|
583
|
+
type: "array",
|
|
584
|
+
items: {
|
|
585
|
+
type: "object",
|
|
586
|
+
properties: { to: { type: "string" }, relation: { type: "string" } },
|
|
587
|
+
required: ["to"]
|
|
588
|
+
},
|
|
589
|
+
description: "Connections to existing graph nodes (files by path, artifacts by id). Write only."
|
|
590
|
+
},
|
|
591
|
+
id: {
|
|
592
|
+
type: "string",
|
|
593
|
+
description: "Stable id. Required for op:delete; for write, derived from text if omitted (re-write updates in place)."
|
|
594
|
+
},
|
|
595
|
+
rationale: { type: "string", description: "Rationale recorded in the append-only audit." },
|
|
596
|
+
confirm: {
|
|
597
|
+
type: "boolean",
|
|
598
|
+
description: "COMMIT the operation. Omitted/false \u2192 a dry-run preview, nothing persisted."
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
call: (ctx, input) => handler(ctx, input)
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ../../packages/data/src/ladder.ts
|
|
607
|
+
function workLadder(graph) {
|
|
608
|
+
const items = [];
|
|
609
|
+
for (const node of graph.nodes()) {
|
|
610
|
+
if (typeof node.metadata.statusCategory === "string") items.push(node);
|
|
611
|
+
}
|
|
612
|
+
const isItem = new Set(items.map((item) => item.id));
|
|
613
|
+
const containers = /* @__PURE__ */ new Set();
|
|
614
|
+
const holders = /* @__PURE__ */ new Map();
|
|
615
|
+
for (const item of items) {
|
|
616
|
+
for (const edge of graph.edges({ from: item.id, relation: "belongs_to" })) {
|
|
617
|
+
if (!isItem.has(edge.to)) continue;
|
|
618
|
+
containers.add(edge.to);
|
|
619
|
+
holders.set(item.id, edge.to);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const children = /* @__PURE__ */ new Map();
|
|
623
|
+
const roots = [];
|
|
624
|
+
for (const item of items) {
|
|
625
|
+
const home = holders.get(item.id);
|
|
626
|
+
if (home === void 0) {
|
|
627
|
+
roots.push(item);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const bucket = children.get(home) ?? { containers: [], leaves: [] };
|
|
631
|
+
(containers.has(item.id) ? bucket.containers : bucket.leaves).push(item);
|
|
632
|
+
children.set(home, bucket);
|
|
633
|
+
}
|
|
634
|
+
const members = /* @__PURE__ */ new Map();
|
|
635
|
+
for (const item of items) {
|
|
636
|
+
if (containers.has(item.id)) continue;
|
|
637
|
+
const category = String(item.metadata.statusCategory);
|
|
638
|
+
const seen = /* @__PURE__ */ new Set([item.id]);
|
|
639
|
+
let home = holders.get(item.id);
|
|
640
|
+
while (home !== void 0 && !seen.has(home)) {
|
|
641
|
+
seen.add(home);
|
|
642
|
+
const rollup = members.get(home) ?? {};
|
|
643
|
+
rollup[category] = (rollup[category] ?? 0) + 1;
|
|
644
|
+
members.set(home, rollup);
|
|
645
|
+
home = holders.get(home);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return { items, containers, holders, children, members, roots };
|
|
649
|
+
}
|
|
650
|
+
function leavesOf(ladder) {
|
|
651
|
+
return ladder.items.filter((item) => !ladder.containers.has(item.id));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ../../packages/data/src/mcp/tools/cache.ts
|
|
655
|
+
import { statSync } from "fs";
|
|
656
|
+
import { resolve as resolve2 } from "path";
|
|
657
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
658
|
+
async function cachedEnforcerDiags(ctx, freshPaths) {
|
|
659
|
+
const hit = cache.get(ctx);
|
|
660
|
+
if (hit && isFresh(hit, ctx.config.repoRoot, freshPaths)) {
|
|
661
|
+
process.stderr.write("[data] lint: reused snapshot result\n");
|
|
662
|
+
return hit.diags;
|
|
663
|
+
}
|
|
664
|
+
const snapshot = {
|
|
665
|
+
at: Date.now(),
|
|
666
|
+
diags: runEnforcers(ctx.config, ctx.graph)
|
|
667
|
+
};
|
|
668
|
+
cache.set(ctx, snapshot);
|
|
669
|
+
return snapshot.diags;
|
|
670
|
+
}
|
|
671
|
+
function isFresh(snapshot, repoRoot, freshPaths) {
|
|
672
|
+
if (!freshPaths || freshPaths.length === 0) return true;
|
|
673
|
+
for (const path of freshPaths) {
|
|
674
|
+
let mtimeMs;
|
|
675
|
+
try {
|
|
676
|
+
mtimeMs = statSync(resolve2(repoRoot, path)).mtimeMs;
|
|
677
|
+
} catch {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
if (mtimeMs > snapshot.at) return false;
|
|
681
|
+
}
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ../../packages/data/src/mcp/tools/shared.ts
|
|
686
|
+
function hops(ctx, id, relation, edgeOk) {
|
|
687
|
+
let list = ctx.graph.hops(id);
|
|
688
|
+
if (relation) list = list.filter((h) => h.relation === relation);
|
|
689
|
+
if (edgeOk) list = list.filter(edgeOk);
|
|
690
|
+
return list;
|
|
691
|
+
}
|
|
692
|
+
function bfs(ctx, startIds, depth, relation, edgeOk) {
|
|
693
|
+
const seen = new Set(startIds);
|
|
694
|
+
let frontier = [...startIds];
|
|
695
|
+
const out = [];
|
|
696
|
+
for (let d = 0; d < depth && frontier.length; d++) {
|
|
697
|
+
const next = [];
|
|
698
|
+
for (const id of frontier) {
|
|
699
|
+
for (const hop of hops(ctx, id, relation, edgeOk)) {
|
|
700
|
+
if (seen.has(hop.to)) continue;
|
|
701
|
+
seen.add(hop.to);
|
|
702
|
+
out.push(hop.to);
|
|
703
|
+
next.push(hop.to);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
frontier = next;
|
|
707
|
+
}
|
|
708
|
+
return out;
|
|
709
|
+
}
|
|
710
|
+
function shortestPath(ctx, startIds, endIds, relation, edgeOk) {
|
|
711
|
+
const prev = /* @__PURE__ */ new Map();
|
|
712
|
+
const seen = new Set(startIds);
|
|
713
|
+
let frontier = [...startIds];
|
|
714
|
+
let hit = startIds.find((id) => endIds.has(id));
|
|
715
|
+
while (!hit && frontier.length) {
|
|
716
|
+
const next = [];
|
|
717
|
+
for (const id of frontier) {
|
|
718
|
+
for (const hop of hops(ctx, id, relation, edgeOk)) {
|
|
719
|
+
if (seen.has(hop.to)) continue;
|
|
720
|
+
seen.add(hop.to);
|
|
721
|
+
prev.set(hop.to, { from: id, relation: hop.relation });
|
|
722
|
+
if (endIds.has(hop.to)) {
|
|
723
|
+
hit = hop.to;
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
next.push(hop.to);
|
|
727
|
+
}
|
|
728
|
+
if (hit) break;
|
|
729
|
+
}
|
|
730
|
+
frontier = next;
|
|
731
|
+
}
|
|
732
|
+
if (!hit) return null;
|
|
733
|
+
const chain = [];
|
|
734
|
+
let cur = hit;
|
|
735
|
+
while (cur) {
|
|
736
|
+
const node = ctx.graph.node(cur);
|
|
737
|
+
const step = prev.get(cur);
|
|
738
|
+
if (node) chain.unshift({ node, ...step ? { relation: step.relation } : {} });
|
|
739
|
+
cur = step?.from;
|
|
740
|
+
}
|
|
741
|
+
return chain;
|
|
742
|
+
}
|
|
743
|
+
var MAX_LIMIT = 200;
|
|
744
|
+
function clampLimit(value, fallback) {
|
|
745
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
746
|
+
return Math.max(1, Math.min(MAX_LIMIT, Math.floor(value)));
|
|
747
|
+
}
|
|
748
|
+
function clampDepth(value, fallback) {
|
|
749
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
750
|
+
return Math.max(1, Math.min(5, Math.floor(value)));
|
|
751
|
+
}
|
|
752
|
+
function clampOffset(value, fallback) {
|
|
753
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
754
|
+
return Math.max(0, Math.floor(value));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ../../packages/data/src/mcp/tools/lint.ts
|
|
758
|
+
async function lint(ctx, req) {
|
|
759
|
+
const limit2 = clampLimit(req.limit, 50);
|
|
760
|
+
const enforcerDiags = await cachedEnforcerDiags(ctx);
|
|
761
|
+
const all = [...ctx.buildDiagnostics, ...enforcerDiags];
|
|
762
|
+
const filtered = all.filter(
|
|
763
|
+
(d) => (!req.severity || d.severity === req.severity) && (!req.rule || d.code === req.rule || d.plugin === req.rule)
|
|
764
|
+
);
|
|
765
|
+
const counts = { error: 0, warn: 0, info: 0 };
|
|
766
|
+
const byRule = {};
|
|
767
|
+
for (const d of filtered) {
|
|
768
|
+
counts[d.severity] += 1;
|
|
769
|
+
byRule[d.code] = (byRule[d.code] ?? 0) + 1;
|
|
770
|
+
}
|
|
771
|
+
const diagnostics = filtered.slice(0, limit2).map((d) => ({
|
|
772
|
+
severity: d.severity,
|
|
773
|
+
rule: d.code,
|
|
774
|
+
message: d.message,
|
|
775
|
+
...d.nodeId !== void 0 ? { nodeId: d.nodeId } : {},
|
|
776
|
+
...d.file !== void 0 ? { file: d.file } : {},
|
|
777
|
+
...d.line !== void 0 ? { line: d.line } : {},
|
|
778
|
+
...d.data !== void 0 ? { data: d.data } : {}
|
|
779
|
+
}));
|
|
780
|
+
return {
|
|
781
|
+
summary: `${counts.error} error(s), ${counts.warn} warning(s), ${counts.info} info across ${Object.keys(byRule).length} rule(s).`,
|
|
782
|
+
counts,
|
|
783
|
+
byRule,
|
|
784
|
+
diagnostics,
|
|
785
|
+
total: filtered.length,
|
|
786
|
+
truncated: filtered.length > diagnostics.length
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ../../packages/data/src/mcp/tools/brief.ts
|
|
791
|
+
async function brief(ctx, req) {
|
|
792
|
+
const detailed = req.format === "detailed";
|
|
793
|
+
const itemCap = detailed ? 20 : 8;
|
|
794
|
+
const listCap = detailed ? 10 : 5;
|
|
795
|
+
const want = new Set(req.include ?? []);
|
|
796
|
+
const wantDiag = want.has("diagnostics");
|
|
797
|
+
const wantShape = want.has("shape");
|
|
798
|
+
const f = await freshness(ctx);
|
|
799
|
+
const originsFresh = await federatedFreshness(ctx);
|
|
800
|
+
const staleOrigins = originsFresh.filter((o) => o.behindOwnHead);
|
|
801
|
+
let artifacts = 0;
|
|
802
|
+
const communityCounts = /* @__PURE__ */ new Map();
|
|
803
|
+
const nodesByType = {};
|
|
804
|
+
for (const n of ctx.graph.nodes()) {
|
|
805
|
+
if (!isCode(ctx, n)) artifacts += 1;
|
|
806
|
+
const community2 = n.metadata.community;
|
|
807
|
+
if (typeof community2 === "string" && community2) {
|
|
808
|
+
communityCounts.set(community2, (communityCounts.get(community2) ?? 0) + 1);
|
|
809
|
+
}
|
|
810
|
+
if (wantShape) nodesByType[n.type] = (nodesByType[n.type] ?? 0) + 1;
|
|
811
|
+
}
|
|
812
|
+
const ladder = workLadder(ctx.graph);
|
|
813
|
+
const leaves = leavesOf(ladder);
|
|
814
|
+
const byCategory = {};
|
|
815
|
+
const inFlight = [];
|
|
816
|
+
for (const item of leaves) {
|
|
817
|
+
const category = String(item.metadata.statusCategory);
|
|
818
|
+
byCategory[category] = (byCategory[category] ?? 0) + 1;
|
|
819
|
+
if (category === "in-progress")
|
|
820
|
+
inFlight.push(renderWorkItem(ctx, item, ladder.holders.get(item.id)));
|
|
821
|
+
}
|
|
822
|
+
const openItems = leaves.filter((i) => i.metadata.statusCategory === "open");
|
|
823
|
+
for (const item of openItems.slice(0, Math.max(0, itemCap - inFlight.length))) {
|
|
824
|
+
inFlight.push(renderWorkItem(ctx, item, ladder.holders.get(item.id)));
|
|
825
|
+
}
|
|
826
|
+
const shown = inFlight.slice(0, itemCap);
|
|
827
|
+
const allContainers = [...ladder.containers].map((id) => ctx.graph.node(id)).filter((n) => n !== void 0).map((n) => ({
|
|
828
|
+
id: n.id,
|
|
829
|
+
...labelOf(n) ? { title: labelOf(n) } : {},
|
|
830
|
+
type: n.type,
|
|
831
|
+
statusCategory: String(n.metadata.statusCategory ?? "unknown"),
|
|
832
|
+
members: ladder.members.get(n.id) ?? {},
|
|
833
|
+
...ladder.holders.get(n.id) ? { belongsTo: ladder.holders.get(n.id) } : {}
|
|
834
|
+
})).sort(byContainerRelevance);
|
|
835
|
+
const containers = allContainers.slice(0, listCap);
|
|
836
|
+
const cited = [];
|
|
837
|
+
for (const n of ctx.graph.nodes()) {
|
|
838
|
+
if (isCode(ctx, n) || typeof n.metadata.statusCategory === "string") continue;
|
|
839
|
+
const count = ctx.graph.edges({ to: n.id }).filter((e) => isPrecise(e, ctx.backendName)).length;
|
|
840
|
+
if (count > 0) cited.push({ node: n, citedBy: count });
|
|
841
|
+
}
|
|
842
|
+
cited.sort((a, b) => b.citedBy - a.citedBy);
|
|
843
|
+
const knowledge = cited.slice(0, listCap).map(({ node, citedBy }) => ({
|
|
844
|
+
id: node.id,
|
|
845
|
+
...labelOf(node) ? { title: labelOf(node) } : {},
|
|
846
|
+
type: node.type,
|
|
847
|
+
citedBy
|
|
848
|
+
}));
|
|
849
|
+
const diagCap = wantDiag ? detailed ? 50 : 20 : 1;
|
|
850
|
+
const health = await lint(ctx, { limit: diagCap });
|
|
851
|
+
const diagnostics = wantDiag ? {
|
|
852
|
+
lints: health.diagnostics.filter((d) => !req.severity || d.severity === req.severity),
|
|
853
|
+
truncated: health.truncated
|
|
854
|
+
} : void 0;
|
|
855
|
+
let shape;
|
|
856
|
+
if (wantShape) {
|
|
857
|
+
const edgesByRelation = {};
|
|
858
|
+
let preciseEdges = 0;
|
|
859
|
+
for (const e of ctx.graph.edges()) {
|
|
860
|
+
edgesByRelation[e.relation] = (edgesByRelation[e.relation] ?? 0) + 1;
|
|
861
|
+
if (isPrecise(e, ctx.backendName)) preciseEdges += 1;
|
|
862
|
+
}
|
|
863
|
+
shape = {
|
|
864
|
+
graphJson: {
|
|
865
|
+
path: f.graphPath,
|
|
866
|
+
...f.mtimeIso ? { mtime: f.mtimeIso } : {},
|
|
867
|
+
...f.behindHead !== void 0 ? { stale: f.behindHead } : {}
|
|
868
|
+
},
|
|
869
|
+
preciseEdges,
|
|
870
|
+
nodesByType,
|
|
871
|
+
edgesByRelation
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
const areas = [...communityCounts.entries()].map(([name, size]) => ({ name, size })).sort((a, b) => b.size - a.size).slice(0, listCap);
|
|
875
|
+
const inProgressCount = byCategory["in-progress"] ?? 0;
|
|
876
|
+
return {
|
|
877
|
+
summary: `${leaves.length} work item(s) \u2014 ${inProgressCount} in flight, ${byCategory["open"] ?? 0} open, ${byCategory["done"] ?? 0} done. ${artifacts} knowledge artifact(s); health: ${health.counts.error} error(s), ${health.counts.warn} warning(s)${f.stale ? " \u2014 GRAPH IS STALE, run `jarvis build graph` (incremental) before trusting details" : ""}${staleOrigins.length ? ` \u2014 ${staleOrigins.map((o) => o.origin).join(", ")} behind their own HEAD (re-run \`jarvis build graph\` there, then re-merge)` : ""}.`,
|
|
878
|
+
freshness: {
|
|
879
|
+
builtAt: ctx.builtAt,
|
|
880
|
+
...f.head ? { head: { commit: f.head.commit, dirty: f.head.dirty } } : {},
|
|
881
|
+
...f.stale !== void 0 ? { stale: f.stale } : {},
|
|
882
|
+
...originsFresh.length ? { federated: originsFresh } : {}
|
|
883
|
+
},
|
|
884
|
+
graph: { nodes: ctx.graph.nodes().length, artifacts, edges: ctx.graph.edges().length },
|
|
885
|
+
work: {
|
|
886
|
+
total: leaves.length,
|
|
887
|
+
byCategory,
|
|
888
|
+
containers,
|
|
889
|
+
inFlight: shown,
|
|
890
|
+
truncated: inFlight.length > shown.length || allContainers.length > containers.length
|
|
891
|
+
},
|
|
892
|
+
knowledge,
|
|
893
|
+
health: { ...health.counts, byRule: health.byRule },
|
|
894
|
+
...diagnostics ? { diagnostics } : {},
|
|
895
|
+
...wantShape ? { shape } : {},
|
|
896
|
+
areas,
|
|
897
|
+
nextSteps: [
|
|
898
|
+
'scope {"task":"\u2026"} \u2014 edit set + governing decisions + blast radius before editing',
|
|
899
|
+
'read {"id":"D-nn"} \xB7 trace {"target":"\u2026"} \u2014 the full rule \xB7 why a file exists (a summary is not the rule)',
|
|
900
|
+
'review {"edits":[\u2026]} \u2014 verify the edit-set against governance before you commit'
|
|
901
|
+
]
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function byContainerRelevance(a, b) {
|
|
905
|
+
const flight = (c) => c.members["in-progress"] ?? 0;
|
|
906
|
+
const size = (c) => Object.values(c.members).reduce((sum, n) => sum + n, 0);
|
|
907
|
+
return flight(b) - flight(a) || size(b) - size(a) || a.id.localeCompare(b.id);
|
|
908
|
+
}
|
|
909
|
+
function renderWorkItem(ctx, item, home) {
|
|
910
|
+
const priority = item.metadata.priority;
|
|
911
|
+
const dependsOn = ctx.graph.edges({ from: item.id, relation: "depends_on" }).map((e) => e.to);
|
|
912
|
+
return {
|
|
913
|
+
id: item.id,
|
|
914
|
+
...labelOf(item) ? { title: labelOf(item) } : {},
|
|
915
|
+
type: item.type,
|
|
916
|
+
...typeof item.metadata.status === "string" ? { status: item.metadata.status } : {},
|
|
917
|
+
statusCategory: String(item.metadata.statusCategory),
|
|
918
|
+
...typeof priority === "string" ? { priority } : {},
|
|
919
|
+
...dependsOn.length ? { dependsOn } : {},
|
|
920
|
+
...home !== void 0 ? { belongsTo: home } : {}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ../../packages/data/src/resolve.ts
|
|
925
|
+
function resolveTarget(ctx, input) {
|
|
926
|
+
const direct = ctx.graph.node(input);
|
|
927
|
+
if (direct && !isCode(ctx, direct)) return { type: "artifact", node: direct };
|
|
928
|
+
const exact = [];
|
|
929
|
+
const suffix = [];
|
|
930
|
+
for (const n of ctx.graph.nodes()) {
|
|
931
|
+
const f = fileOf(n);
|
|
932
|
+
if (!f) continue;
|
|
933
|
+
if (f === input) exact.push(n);
|
|
934
|
+
else if (f.endsWith(`/${input}`) || f.endsWith(input)) suffix.push(n);
|
|
935
|
+
}
|
|
936
|
+
const matched = exact.length ? exact : suffix;
|
|
937
|
+
const first = matched[0];
|
|
938
|
+
if (first) return { type: "file", path: fileOf(first) ?? input, nodes: matched };
|
|
939
|
+
return { type: "unknown", input };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ../../packages/data/src/mcp/tools/connections.ts
|
|
943
|
+
var SETTLED = /* @__PURE__ */ new Set(["done", "dropped"]);
|
|
944
|
+
function render(ctx, node, via) {
|
|
945
|
+
const preview = previewOf(node);
|
|
946
|
+
const status2 = statusOf(node);
|
|
947
|
+
return {
|
|
948
|
+
...renderNode(ctx, node),
|
|
949
|
+
...preview ? { preview } : {},
|
|
950
|
+
...status2 ? { status: status2, statusCategory: categoryOf(status2) } : {},
|
|
951
|
+
...via ? { via } : {}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
function wants(ctx, node, direction) {
|
|
955
|
+
if (!direction || direction === "auto") return true;
|
|
956
|
+
return isCode(ctx, node) === (direction === "to-code");
|
|
957
|
+
}
|
|
958
|
+
function listConnections(ctx, req) {
|
|
959
|
+
const limit2 = clampLimit(req.limit, 25);
|
|
960
|
+
const keepSettled = req.status === "all";
|
|
961
|
+
const edgeOk = (hop) => hopPasses(ctx, hop, req.confidence ?? "all", req.origin);
|
|
962
|
+
const resolved = resolveTarget(ctx, req.node);
|
|
963
|
+
if (resolved.type === "unknown") {
|
|
964
|
+
return {
|
|
965
|
+
summary: `Nothing in the map matched "${req.node}".`,
|
|
966
|
+
items: [],
|
|
967
|
+
settled: 0,
|
|
968
|
+
context: [],
|
|
969
|
+
total: 0,
|
|
970
|
+
truncated: false
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
const startIds = resolved.type === "artifact" ? [resolved.node.id] : resolved.nodes.map((n) => n.id);
|
|
974
|
+
const start = ctx.graph.node(startIds[0] ?? "");
|
|
975
|
+
const statusOf0 = start ? statusOf(start) : void 0;
|
|
976
|
+
const seen = new Set(startIds);
|
|
977
|
+
const withStatus = /* @__PURE__ */ new Map();
|
|
978
|
+
const context = /* @__PURE__ */ new Map();
|
|
979
|
+
let settled = 0;
|
|
980
|
+
const take = (id, via) => {
|
|
981
|
+
if (seen.has(id)) return false;
|
|
982
|
+
seen.add(id);
|
|
983
|
+
const node = ctx.graph.node(id);
|
|
984
|
+
if (!node) return false;
|
|
985
|
+
const status2 = statusOf(node);
|
|
986
|
+
if (status2 === void 0) {
|
|
987
|
+
if (via === void 0 && wants(ctx, node, req.direction)) {
|
|
988
|
+
const thing = render(ctx, node);
|
|
989
|
+
const key = thing.id ?? thing.file ?? id;
|
|
990
|
+
if (!context.has(key)) context.set(key, thing);
|
|
991
|
+
}
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
if (!keepSettled && SETTLED.has(categoryOf(status2))) {
|
|
995
|
+
settled += 1;
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
withStatus.set(id, render(ctx, node, via));
|
|
999
|
+
return false;
|
|
1000
|
+
};
|
|
1001
|
+
const stepThrough = [];
|
|
1002
|
+
for (const id of startIds) {
|
|
1003
|
+
for (const hop of ctx.graph.hops(id)) {
|
|
1004
|
+
if (edgeOk(hop) && take(hop.to)) stepThrough.push(hop.to);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (statusOf0 === void 0) {
|
|
1008
|
+
for (const step of stepThrough) {
|
|
1009
|
+
for (const hop of ctx.graph.hops(step)) {
|
|
1010
|
+
if (edgeOk(hop)) take(hop.to, step);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const ordered = [...withStatus.values()].sort(
|
|
1015
|
+
(a, b) => Number(Boolean(a.via)) - Number(Boolean(b.via)) || (a.type ?? "").localeCompare(b.type ?? "") || (a.id ?? a.file ?? "").localeCompare(b.id ?? b.file ?? "")
|
|
1016
|
+
);
|
|
1017
|
+
const page = ordered.slice(0, limit2);
|
|
1018
|
+
const label = start ? labelOf(start) ?? start.id : req.node;
|
|
1019
|
+
const read2 = [...context.values()];
|
|
1020
|
+
const nothing = ordered.length === 0 && read2.length === 0;
|
|
1021
|
+
return {
|
|
1022
|
+
summary: nothing ? `Nothing in the map connects to ${label}.` : `${label} connects to ${ordered.length} thing(s) still open${settled > 0 ? ` (and ${settled} settled)` : ""}${read2.length > 0 ? `, plus ${read2.length} it is read against` : ""}.${ordered.length === 0 ? " Nothing open is on it." : ""}`,
|
|
1023
|
+
...start ? { node: renderNode(ctx, start) } : {},
|
|
1024
|
+
items: page,
|
|
1025
|
+
settled,
|
|
1026
|
+
context: read2,
|
|
1027
|
+
total: ordered.length,
|
|
1028
|
+
truncated: page.length < ordered.length
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// ../../packages/data/src/mcp/tools/graph.ts
|
|
1033
|
+
function graphExplore(ctx, req) {
|
|
1034
|
+
const limit2 = clampLimit(req.limit, 25);
|
|
1035
|
+
const confidence = req.confidence ?? "precise";
|
|
1036
|
+
const edgeOk = (h) => hopPasses(ctx, h, confidence, req.origin);
|
|
1037
|
+
if (req.action === "hubs") return hubs(ctx, req, limit2);
|
|
1038
|
+
if (req.action === "community") return community(ctx, req, limit2);
|
|
1039
|
+
if (!req.node) {
|
|
1040
|
+
return {
|
|
1041
|
+
summary: `\`${req.action}\` requires a \`node\` (an artifact id or a file path).`,
|
|
1042
|
+
action: req.action
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
const resolved = resolveTarget(ctx, req.node);
|
|
1046
|
+
if (resolved.type === "unknown") {
|
|
1047
|
+
return {
|
|
1048
|
+
summary: `No artifact or file matched "${req.node}".`,
|
|
1049
|
+
action: req.action
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
const startIds = resolved.type === "artifact" ? [resolved.node.id] : resolved.nodes.map((n) => n.id);
|
|
1053
|
+
if (req.action === "path") {
|
|
1054
|
+
if (!req.to)
|
|
1055
|
+
return {
|
|
1056
|
+
summary: "`path` requires a `to` target.",
|
|
1057
|
+
action: "path",
|
|
1058
|
+
path: null
|
|
1059
|
+
};
|
|
1060
|
+
const end = resolveTarget(ctx, req.to);
|
|
1061
|
+
if (end.type === "unknown")
|
|
1062
|
+
return {
|
|
1063
|
+
summary: `No artifact or file matched "${req.to}".`,
|
|
1064
|
+
action: "path",
|
|
1065
|
+
path: null
|
|
1066
|
+
};
|
|
1067
|
+
const endIds = new Set(end.type === "artifact" ? [end.node.id] : end.nodes.map((n) => n.id));
|
|
1068
|
+
const pathEdgeOk = (h) => endIds.has(h.to) ? edgeMatchesConfidence(h, ctx.backendName, confidence) : hopPasses(ctx, h, confidence, req.origin);
|
|
1069
|
+
const found = shortestPath(ctx, startIds, endIds, req.relation, pathEdgeOk);
|
|
1070
|
+
if (!found) {
|
|
1071
|
+
return {
|
|
1072
|
+
summary: `No path from ${req.node} to ${req.to}.`,
|
|
1073
|
+
action: "path",
|
|
1074
|
+
path: null
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
summary: `Path of ${found.length - 1} hop(s) from ${req.node} to ${req.to}.`,
|
|
1079
|
+
action: "path",
|
|
1080
|
+
path: found.map((s) => ({
|
|
1081
|
+
node: renderNode(ctx, s.node),
|
|
1082
|
+
...s.relation ? { relation: s.relation } : {}
|
|
1083
|
+
}))
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
const depth = req.action === "impact" ? clampDepth(req.depth, 2) : clampDepth(req.depth, 1);
|
|
1087
|
+
const offset = clampOffset(req.offset, 0);
|
|
1088
|
+
const reached = bfs(ctx, startIds, depth, req.relation, edgeOk);
|
|
1089
|
+
const nodes = reached.map((id) => ctx.graph.node(id)).filter((n) => Boolean(n));
|
|
1090
|
+
const byType = {};
|
|
1091
|
+
for (const n of nodes) byType[n.type] = (byType[n.type] ?? 0) + 1;
|
|
1092
|
+
const page = nodes.slice(offset, offset + limit2);
|
|
1093
|
+
const rendered = page.map((n) => renderNode(ctx, n));
|
|
1094
|
+
return {
|
|
1095
|
+
summary: req.action === "impact" ? `${req.node} reaches ${nodes.length} node(s) within ${depth} hop(s).` : `${req.node} has ${nodes.length} neighbor(s).`,
|
|
1096
|
+
action: req.action,
|
|
1097
|
+
nodes: rendered,
|
|
1098
|
+
byType,
|
|
1099
|
+
total: nodes.length,
|
|
1100
|
+
truncated: offset + rendered.length < nodes.length
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function hubs(ctx, req, limit2) {
|
|
1104
|
+
const pool = (req.type ? ctx.graph.nodes(req.type) : ctx.graph.nodes()).filter(
|
|
1105
|
+
(n) => nodeMatchesOrigin(n, req.origin)
|
|
1106
|
+
);
|
|
1107
|
+
const ranked = pool.map((n) => ({ node: n, degree: ctx.graph.hops(n.id).length })).filter((r) => r.degree > 0).sort((a, b) => b.degree - a.degree);
|
|
1108
|
+
const top = ranked.slice(0, limit2);
|
|
1109
|
+
const busiest = top[0];
|
|
1110
|
+
return {
|
|
1111
|
+
summary: `Top ${top.length} most-connected node(s)${req.type ? ` of type ${req.type}` : ""} (coupling hotspots)${busiest ? ` \u2014 busiest: ${labelOf(busiest.node) ?? busiest.node.id} (${busiest.degree} edges)` : ""}.`,
|
|
1112
|
+
action: "hubs",
|
|
1113
|
+
hubs: top.map((r) => ({ ...renderNode(ctx, r.node), degree: r.degree })),
|
|
1114
|
+
total: ranked.length,
|
|
1115
|
+
truncated: ranked.length > top.length
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function community(ctx, req, limit2) {
|
|
1119
|
+
if (req.node) {
|
|
1120
|
+
const resolved = resolveTarget(ctx, req.node);
|
|
1121
|
+
if (resolved.type === "unknown") {
|
|
1122
|
+
return {
|
|
1123
|
+
summary: `No artifact or file matched "${req.node}".`,
|
|
1124
|
+
action: "community"
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
const seed = resolved.type === "artifact" ? resolved.node : resolved.nodes[0];
|
|
1128
|
+
const name = seed ? str(seed.metadata.community) : "";
|
|
1129
|
+
if (!name) {
|
|
1130
|
+
return {
|
|
1131
|
+
summary: `${req.node} has no community assigned.`,
|
|
1132
|
+
action: "community"
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
const members = ctx.graph.nodes().filter((n) => str(n.metadata.community) === name && nodeMatchesOrigin(n, req.origin));
|
|
1136
|
+
const rendered = members.slice(0, limit2).map((n) => renderNode(ctx, n));
|
|
1137
|
+
return {
|
|
1138
|
+
summary: `Community "${name}" has ${members.length} member(s) (the cluster ${req.node} belongs to).`,
|
|
1139
|
+
action: "community",
|
|
1140
|
+
nodes: rendered,
|
|
1141
|
+
total: members.length,
|
|
1142
|
+
truncated: members.length > rendered.length
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
1146
|
+
for (const n of req.type ? ctx.graph.nodes(req.type) : ctx.graph.nodes()) {
|
|
1147
|
+
if (!nodeMatchesOrigin(n, req.origin)) continue;
|
|
1148
|
+
const name = str(n.metadata.community);
|
|
1149
|
+
if (name) sizes.set(name, (sizes.get(name) ?? 0) + 1);
|
|
1150
|
+
}
|
|
1151
|
+
const ranked = [...sizes.entries()].map(([name, size]) => ({ name, size })).sort((a, b) => b.size - a.size);
|
|
1152
|
+
const top = ranked.slice(0, limit2);
|
|
1153
|
+
const largest = top[0];
|
|
1154
|
+
return {
|
|
1155
|
+
summary: `${ranked.length} communit${ranked.length === 1 ? "y" : "ies"}${largest ? ` \u2014 largest: "${largest.name}" (${largest.size})` : ""}.`,
|
|
1156
|
+
action: "community",
|
|
1157
|
+
communities: top,
|
|
1158
|
+
total: ranked.length,
|
|
1159
|
+
truncated: ranked.length > top.length
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function str(value) {
|
|
1163
|
+
return typeof value === "string" ? value : "";
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// ../../packages/data/src/mcp/tools/memory.ts
|
|
1167
|
+
async function memory(ctx, req, options = {}) {
|
|
1168
|
+
return (req.op ?? "write") === "delete" ? deleteOp(ctx, req, options) : writeOp(ctx, req, options);
|
|
1169
|
+
}
|
|
1170
|
+
async function writeOp(ctx, req, options) {
|
|
1171
|
+
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
1172
|
+
const actor = options.actor ?? "agent";
|
|
1173
|
+
const type = req.type ?? "note";
|
|
1174
|
+
if (req.text === void 0 || req.text.trim() === "") {
|
|
1175
|
+
return errorResponse("write", 'op:"write" needs `text` (the memory to record).');
|
|
1176
|
+
}
|
|
1177
|
+
const id = req.id ?? memoryIdFromText(req.text);
|
|
1178
|
+
const resolved = [];
|
|
1179
|
+
const unresolvedLinks = [];
|
|
1180
|
+
for (const link of req.links ?? []) {
|
|
1181
|
+
const relation = link.relation ?? "about";
|
|
1182
|
+
const target = resolveTarget(ctx, link.to);
|
|
1183
|
+
if (target.type === "artifact") {
|
|
1184
|
+
resolved.push({ to: link.to, targetId: target.node.id, relation });
|
|
1185
|
+
} else if (target.type === "file") {
|
|
1186
|
+
const rep = target.nodes[0];
|
|
1187
|
+
if (rep) resolved.push({ to: link.to, targetId: rep.id, relation });
|
|
1188
|
+
else unresolvedLinks.push(link.to);
|
|
1189
|
+
} else {
|
|
1190
|
+
unresolvedLinks.push(link.to);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const edges = resolved.map((r) => ({ from: id, to: r.targetId, relation: r.relation }));
|
|
1194
|
+
const preview = {
|
|
1195
|
+
node: { id, type, title: req.text },
|
|
1196
|
+
edges,
|
|
1197
|
+
unresolvedLinks
|
|
1198
|
+
};
|
|
1199
|
+
if (!req.confirm) {
|
|
1200
|
+
const unresolvedNote = unresolvedLinks.length > 0 ? `, ${unresolvedLinks.length} unresolved` : "";
|
|
1201
|
+
return {
|
|
1202
|
+
summary: `Preview: would remember "${req.text}" (${edges.length} link(s)${unresolvedNote}). Pass confirm:true to write.`,
|
|
1203
|
+
op: "write",
|
|
1204
|
+
persisted: false,
|
|
1205
|
+
preview
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
const store = await readMemoryStore(ctx.config);
|
|
1209
|
+
const links = resolved.map((r) => ({ to: r.to, relation: r.relation }));
|
|
1210
|
+
const existingIndex = store.memories.findIndex((m) => m.id === id);
|
|
1211
|
+
const existing = existingIndex >= 0 ? store.memories[existingIndex] : void 0;
|
|
1212
|
+
const rec = {
|
|
1213
|
+
id,
|
|
1214
|
+
type,
|
|
1215
|
+
text: req.text,
|
|
1216
|
+
links,
|
|
1217
|
+
origin: "agent",
|
|
1218
|
+
createdAt: existing?.createdAt ?? now(),
|
|
1219
|
+
updatedAt: now(),
|
|
1220
|
+
...existing?.orphaned !== void 0 ? { orphaned: existing.orphaned } : {}
|
|
1221
|
+
};
|
|
1222
|
+
if (existingIndex >= 0) store.memories[existingIndex] = rec;
|
|
1223
|
+
else store.memories.push(rec);
|
|
1224
|
+
const audit = {
|
|
1225
|
+
at: now(),
|
|
1226
|
+
actor,
|
|
1227
|
+
action: "write",
|
|
1228
|
+
id,
|
|
1229
|
+
...req.rationale !== void 0 ? { rationale: req.rationale } : {}
|
|
1230
|
+
};
|
|
1231
|
+
store.audit.push(audit);
|
|
1232
|
+
await writeMemoryStore(ctx.config, store);
|
|
1233
|
+
ctx.graph.addNode(memoryNode(rec), "memory");
|
|
1234
|
+
for (const r of resolved)
|
|
1235
|
+
ctx.graph.addEdge({ from: id, to: r.targetId, relation: r.relation }, "memory");
|
|
1236
|
+
ctx.graph.stitch();
|
|
1237
|
+
return {
|
|
1238
|
+
summary: `Remembered "${req.text}" as ${id} (${edges.length} link(s)).`,
|
|
1239
|
+
op: "write",
|
|
1240
|
+
persisted: true,
|
|
1241
|
+
preview,
|
|
1242
|
+
written: { id },
|
|
1243
|
+
audit
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
async function deleteOp(ctx, req, options) {
|
|
1247
|
+
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
1248
|
+
const actor = options.actor ?? "agent";
|
|
1249
|
+
if (req.id === void 0 || req.id.trim() === "") {
|
|
1250
|
+
return errorResponse("delete", 'op:"delete" needs `id` (the memory to remove).');
|
|
1251
|
+
}
|
|
1252
|
+
const id = req.id;
|
|
1253
|
+
const store = await readMemoryStore(ctx.config);
|
|
1254
|
+
const rec = store.memories.find((m) => m.id === id);
|
|
1255
|
+
if (!rec) {
|
|
1256
|
+
return {
|
|
1257
|
+
summary: `No memory "${id}" to delete.`,
|
|
1258
|
+
op: "delete",
|
|
1259
|
+
persisted: false,
|
|
1260
|
+
preview: { node: { id, type: "", title: "" }, edges: [], unresolvedLinks: [] }
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
const edges = [];
|
|
1264
|
+
const unresolvedLinks = [];
|
|
1265
|
+
for (const link of rec.links) {
|
|
1266
|
+
const target = resolveTarget(ctx, link.to);
|
|
1267
|
+
if (target.type === "artifact")
|
|
1268
|
+
edges.push({ from: id, to: target.node.id, relation: link.relation });
|
|
1269
|
+
else if (target.type === "file" && target.nodes[0])
|
|
1270
|
+
edges.push({ from: id, to: target.nodes[0].id, relation: link.relation });
|
|
1271
|
+
else unresolvedLinks.push(link.to);
|
|
1272
|
+
}
|
|
1273
|
+
const preview = {
|
|
1274
|
+
node: { id, type: rec.type, title: rec.text },
|
|
1275
|
+
edges,
|
|
1276
|
+
unresolvedLinks
|
|
1277
|
+
};
|
|
1278
|
+
if (!req.confirm) {
|
|
1279
|
+
return {
|
|
1280
|
+
summary: `Preview: would delete "${rec.text}" (${id}). Pass confirm:true to remove.`,
|
|
1281
|
+
op: "delete",
|
|
1282
|
+
persisted: false,
|
|
1283
|
+
preview
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
deleteMemory(store, id);
|
|
1287
|
+
const audit = {
|
|
1288
|
+
at: now(),
|
|
1289
|
+
actor,
|
|
1290
|
+
action: "delete",
|
|
1291
|
+
id,
|
|
1292
|
+
...req.rationale !== void 0 ? { rationale: req.rationale } : {}
|
|
1293
|
+
};
|
|
1294
|
+
store.audit.push(audit);
|
|
1295
|
+
await writeMemoryStore(ctx.config, store);
|
|
1296
|
+
ctx.graph.removeNode(id);
|
|
1297
|
+
return {
|
|
1298
|
+
summary: `Deleted memory "${rec.text}" (${id}).`,
|
|
1299
|
+
op: "delete",
|
|
1300
|
+
persisted: true,
|
|
1301
|
+
preview,
|
|
1302
|
+
removed: { id },
|
|
1303
|
+
audit
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
function errorResponse(op, summary) {
|
|
1307
|
+
return {
|
|
1308
|
+
summary,
|
|
1309
|
+
op,
|
|
1310
|
+
persisted: false,
|
|
1311
|
+
preview: { node: { id: "", type: "", title: "" }, edges: [], unresolvedLinks: [] }
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
function memoryIdFromText(text) {
|
|
1315
|
+
const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "memory";
|
|
1316
|
+
return `mem-${slug}-${hashText(text)}`;
|
|
1317
|
+
}
|
|
1318
|
+
function hashText(text) {
|
|
1319
|
+
let h = 5381;
|
|
1320
|
+
for (let i = 0; i < text.length; i++) h = (h << 5) + h + text.charCodeAt(i) | 0;
|
|
1321
|
+
return (h >>> 0).toString(36);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ../../packages/data/src/mcp/tools/read.ts
|
|
1325
|
+
import { readFile } from "fs/promises";
|
|
1326
|
+
import { resolve as resolve3 } from "path";
|
|
1327
|
+
|
|
1328
|
+
// ../../packages/data/src/meta.ts
|
|
1329
|
+
function stringMeta(metadata, path) {
|
|
1330
|
+
const value = metaValue(metadata, path);
|
|
1331
|
+
return typeof value === "string" ? value : void 0;
|
|
1332
|
+
}
|
|
1333
|
+
function metaValue(metadata, path) {
|
|
1334
|
+
if (!metadata) return void 0;
|
|
1335
|
+
if (Object.prototype.hasOwnProperty.call(metadata, path)) return metadata[path];
|
|
1336
|
+
let current = metadata;
|
|
1337
|
+
for (const part of path.split(".")) {
|
|
1338
|
+
if (!current || typeof current !== "object" || !Object.prototype.hasOwnProperty.call(current, part))
|
|
1339
|
+
return void 0;
|
|
1340
|
+
current = current[part];
|
|
1341
|
+
}
|
|
1342
|
+
return current;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// ../../packages/data/src/mcp/tools/read.ts
|
|
1346
|
+
async function read(ctx, req) {
|
|
1347
|
+
const max = clampChars(req.maxChars, 2e4);
|
|
1348
|
+
const resolved = resolveTarget(ctx, req.id);
|
|
1349
|
+
if (resolved.type === "unknown") {
|
|
1350
|
+
return {
|
|
1351
|
+
summary: `No artifact matched "${req.id}". Pass an artifact id like D87 (find one with search).`,
|
|
1352
|
+
found: false
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
if (resolved.type === "file") {
|
|
1356
|
+
return {
|
|
1357
|
+
summary: `"${req.id}" is code, not a governance artifact \u2014 read the file directly. read is for artifacts (decisions, standards, \u2026).`,
|
|
1358
|
+
found: false
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
const node = resolved.node;
|
|
1362
|
+
if (isCode(ctx, node)) {
|
|
1363
|
+
return {
|
|
1364
|
+
summary: `"${req.id}" is a code node, not an artifact.`,
|
|
1365
|
+
found: false
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
const rel = fileOf(node);
|
|
1369
|
+
const title = labelOf(node);
|
|
1370
|
+
const url = stringMeta(node.metadata, "data.url");
|
|
1371
|
+
const preview = stringMeta(node.metadata, "data.preview");
|
|
1372
|
+
const base = {
|
|
1373
|
+
found: true,
|
|
1374
|
+
id: node.id,
|
|
1375
|
+
type: node.type,
|
|
1376
|
+
...title ? { title } : {},
|
|
1377
|
+
...rel ? { file: rel } : {},
|
|
1378
|
+
...url ? { href: url } : {},
|
|
1379
|
+
...preview ? { preview } : {}
|
|
1380
|
+
};
|
|
1381
|
+
if (!rel) {
|
|
1382
|
+
const excerpt = previewOf(node);
|
|
1383
|
+
return {
|
|
1384
|
+
...base,
|
|
1385
|
+
summary: `${node.id}${title ? ` (${title})` : ""} has no source file; returning its excerpt.`,
|
|
1386
|
+
...excerpt ? { body: excerpt } : {}
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
const full = await readFile(resolve3(ctx.config.repoRoot, rel), "utf8");
|
|
1391
|
+
const span = sectionSpan(node);
|
|
1392
|
+
const stripped = span ? parseFrontmatter(full).body : full;
|
|
1393
|
+
const text = span ? reanchorSection(stripped, node.id) ?? stripped.slice(span[0], span[1]) : full;
|
|
1394
|
+
const truncated = text.length > max;
|
|
1395
|
+
return {
|
|
1396
|
+
...base,
|
|
1397
|
+
summary: `${node.id}${title ? ` (${title})` : ""} \u2014 ${text.length} chars from ${rel}${span ? " (section)" : ""}${truncated ? ` (truncated to ${max})` : ""}.`,
|
|
1398
|
+
body: truncated ? `${text.slice(0, max)}
|
|
1399
|
+
|
|
1400
|
+
\u2026[truncated; ${text.length - max} more chars \u2014 raise maxChars to read the rest]` : text,
|
|
1401
|
+
truncated
|
|
1402
|
+
};
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
return {
|
|
1405
|
+
...base,
|
|
1406
|
+
summary: `${node.id} maps to ${rel} but it could not be read: ${err instanceof Error ? err.message : String(err)}.`
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
function reanchorSection(body, id) {
|
|
1411
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1412
|
+
const heading = new RegExp(`^(#{1,6})[ \\t]+${escaped}\\b.*$`, "m").exec(body);
|
|
1413
|
+
if (!heading || heading[1] === void 0) return void 0;
|
|
1414
|
+
const level = heading[1].length;
|
|
1415
|
+
const bodyStart = heading.index + heading[0].length;
|
|
1416
|
+
const boundary = new RegExp(`^#{1,${level}}[ \\t]`, "m").exec(body.slice(bodyStart));
|
|
1417
|
+
const end = boundary ? bodyStart + boundary.index : body.length;
|
|
1418
|
+
return body.slice(heading.index, end);
|
|
1419
|
+
}
|
|
1420
|
+
function sectionSpan(node) {
|
|
1421
|
+
const span = node.metadata.sourceSpan;
|
|
1422
|
+
if (Array.isArray(span) && span.length === 2) {
|
|
1423
|
+
const [start, end] = span;
|
|
1424
|
+
if (typeof start === "number" && typeof end === "number") return [start, end];
|
|
1425
|
+
}
|
|
1426
|
+
return void 0;
|
|
1427
|
+
}
|
|
1428
|
+
function clampChars(value, fallback) {
|
|
1429
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
1430
|
+
return Math.max(200, Math.min(1e5, Math.floor(value)));
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// ../../packages/data/src/scope.ts
|
|
1434
|
+
function resolveScopeGovernance(ctx, members, codeNodes) {
|
|
1435
|
+
const out = /* @__PURE__ */ new Map();
|
|
1436
|
+
for (const member of members) {
|
|
1437
|
+
if (!isCode(ctx, member)) out.set(member.id, member);
|
|
1438
|
+
}
|
|
1439
|
+
for (const code of codeNodes) {
|
|
1440
|
+
for (const hop of ctx.graph.hops(code.id)) {
|
|
1441
|
+
const other = ctx.graph.node(hop.to);
|
|
1442
|
+
if (!other || isCode(ctx, other)) continue;
|
|
1443
|
+
if (isPrecise(hop, ctx.backendName)) out.set(other.id, other);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return [...out.values()];
|
|
1447
|
+
}
|
|
1448
|
+
function resolveDirectoryGovernance(ctx, path) {
|
|
1449
|
+
let dir = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
|
|
1450
|
+
for (; ; ) {
|
|
1451
|
+
const siblings = codeNodesInDir(ctx, dir);
|
|
1452
|
+
if (siblings.length > 0) return resolveScopeGovernance(ctx, [], siblings);
|
|
1453
|
+
if (dir === "") return [];
|
|
1454
|
+
const slash = dir.lastIndexOf("/");
|
|
1455
|
+
dir = slash === -1 ? "" : dir.slice(0, slash);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
function codeNodesInDir(ctx, dir) {
|
|
1459
|
+
const out = [];
|
|
1460
|
+
for (const node of ctx.graph.nodes()) {
|
|
1461
|
+
if (!isCode(ctx, node)) continue;
|
|
1462
|
+
const file = fileOf(node);
|
|
1463
|
+
if (file === void 0) continue;
|
|
1464
|
+
const fileDir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
|
|
1465
|
+
if (fileDir === dir) out.push(node);
|
|
1466
|
+
}
|
|
1467
|
+
return out;
|
|
1468
|
+
}
|
|
1469
|
+
function resolveCoveringCode(ctx, codeNodes) {
|
|
1470
|
+
const ids = new Set(codeNodes.map((n) => n.id));
|
|
1471
|
+
const out = /* @__PURE__ */ new Map();
|
|
1472
|
+
for (const code of codeNodes) {
|
|
1473
|
+
for (const hop of ctx.graph.hops(code.id)) {
|
|
1474
|
+
if (ids.has(hop.to)) continue;
|
|
1475
|
+
const other = ctx.graph.node(hop.to);
|
|
1476
|
+
if (!other || !isCode(ctx, other)) continue;
|
|
1477
|
+
if (!isPrecise(hop, ctx.backendName)) continue;
|
|
1478
|
+
const bucket = out.get(other.id) ?? { node: other, relations: /* @__PURE__ */ new Set() };
|
|
1479
|
+
bucket.relations.add(hop.relation);
|
|
1480
|
+
out.set(other.id, bucket);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return [...out.values()].map((b) => ({ node: b.node, relations: [...b.relations] }));
|
|
1484
|
+
}
|
|
1485
|
+
var COVERAGE_VIA_MARKERS = new Set(Object.values(COVERAGE_VIA));
|
|
1486
|
+
function coverageViaOf(ctx, codeId, hop) {
|
|
1487
|
+
const query = hop.dir === "out" ? { from: codeId, to: hop.to } : { from: hop.to, to: codeId };
|
|
1488
|
+
for (const edge of ctx.graph.edges(query)) {
|
|
1489
|
+
const via = edge.metadata?.via;
|
|
1490
|
+
if (typeof via === "string" && COVERAGE_VIA_MARKERS.has(via)) return via;
|
|
1491
|
+
}
|
|
1492
|
+
return void 0;
|
|
1493
|
+
}
|
|
1494
|
+
function hasTestCoverage(ctx, codeNodes) {
|
|
1495
|
+
if (resolveCoveringCode(ctx, codeNodes).length > 0) return true;
|
|
1496
|
+
const ids = new Set(codeNodes.map((n) => n.id));
|
|
1497
|
+
for (const code of codeNodes) {
|
|
1498
|
+
for (const hop of ctx.graph.hops(code.id)) {
|
|
1499
|
+
if (ids.has(hop.to)) continue;
|
|
1500
|
+
const other = ctx.graph.node(hop.to);
|
|
1501
|
+
if (!other || !isCode(ctx, other)) continue;
|
|
1502
|
+
if (coverageViaOf(ctx, code.id, hop) !== void 0) return true;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// ../../packages/data/src/mcp/tools/trace.ts
|
|
1509
|
+
function trace(ctx, req) {
|
|
1510
|
+
const res = traceOnce(ctx, req);
|
|
1511
|
+
if (res.total > 0 || (req.confidence ?? "precise") !== "precise") return res;
|
|
1512
|
+
const loose = traceOnce(ctx, { ...req, confidence: "all" });
|
|
1513
|
+
if (loose.total === 0) return res;
|
|
1514
|
+
return {
|
|
1515
|
+
...res,
|
|
1516
|
+
summary: `${res.summary} ${loose.total} discovery-only link(s) exist \u2014 pass confidence:"all" to see them.`
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
function traceOnce(ctx, req) {
|
|
1520
|
+
const limit2 = clampLimit(req.limit, 20);
|
|
1521
|
+
const detailed = req.format === "detailed";
|
|
1522
|
+
const confidence = req.confidence ?? "precise";
|
|
1523
|
+
const resolved = resolveTarget(ctx, req.target);
|
|
1524
|
+
if (resolved.type === "unknown") {
|
|
1525
|
+
return {
|
|
1526
|
+
summary: `No artifact or file matched "${req.target}". Pass an artifact id (e.g. D87) or a repo-relative file path (e.g. packages/art/src/camera.ts).`,
|
|
1527
|
+
target: { id: req.target },
|
|
1528
|
+
direction: "to-artifacts",
|
|
1529
|
+
results: [],
|
|
1530
|
+
total: 0,
|
|
1531
|
+
truncated: false
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
if (resolved.type === "artifact") {
|
|
1535
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1536
|
+
for (const e of ctx.graph.edges({ to: resolved.node.id })) {
|
|
1537
|
+
if (!edgeMatchesConfidence(e, ctx.backendName, confidence)) continue;
|
|
1538
|
+
const from = ctx.graph.node(e.from);
|
|
1539
|
+
if (!from || !isCode(ctx, from) || !nodeMatchesOrigin(from, req.origin)) continue;
|
|
1540
|
+
const file = fileOf(from) ?? from.id;
|
|
1541
|
+
const bucket = byFile.get(file) ?? {
|
|
1542
|
+
relations: /* @__PURE__ */ new Set(),
|
|
1543
|
+
symbols: /* @__PURE__ */ new Set(),
|
|
1544
|
+
nodeIds: [],
|
|
1545
|
+
count: 0
|
|
1546
|
+
};
|
|
1547
|
+
bucket.relations.add(e.relation);
|
|
1548
|
+
const sym = labelOf(from);
|
|
1549
|
+
if (sym) bucket.symbols.add(sym);
|
|
1550
|
+
bucket.nodeIds.push(from.id);
|
|
1551
|
+
bucket.count += occurrencesOf(e);
|
|
1552
|
+
byFile.set(file, bucket);
|
|
1553
|
+
}
|
|
1554
|
+
const all2 = [...byFile.entries()].map(([file, b]) => ({
|
|
1555
|
+
file,
|
|
1556
|
+
relations: [...b.relations],
|
|
1557
|
+
count: b.count,
|
|
1558
|
+
...b.symbols.size ? { symbols: [...b.symbols].slice(0, 5) } : {},
|
|
1559
|
+
...detailed ? { nodeIds: b.nodeIds } : {}
|
|
1560
|
+
}));
|
|
1561
|
+
all2.sort((a, b) => b.count - a.count);
|
|
1562
|
+
const results2 = all2.slice(0, limit2);
|
|
1563
|
+
return {
|
|
1564
|
+
summary: `${resolved.node.id} (${labelOf(resolved.node) ?? resolved.node.type}) is referenced by ${all2.length} file(s) across ${countRefs(all2)} citation(s). NEXT: read ${resolved.node.id} for the full text; open a listed file to see the citation in context.`,
|
|
1565
|
+
target: renderNode(ctx, resolved.node, detailed),
|
|
1566
|
+
direction: "to-code",
|
|
1567
|
+
results: results2,
|
|
1568
|
+
total: all2.length,
|
|
1569
|
+
truncated: all2.length > results2.length
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const byArtifact = /* @__PURE__ */ new Map();
|
|
1573
|
+
for (const cn of resolved.nodes) {
|
|
1574
|
+
for (const e of ctx.graph.edges({ from: cn.id })) {
|
|
1575
|
+
if (!edgeMatchesConfidence(e, ctx.backendName, confidence)) continue;
|
|
1576
|
+
const to = ctx.graph.node(e.to);
|
|
1577
|
+
if (!to || isCode(ctx, to) || !nodeMatchesOrigin(to, req.origin)) continue;
|
|
1578
|
+
const bucket = byArtifact.get(to.id) ?? {
|
|
1579
|
+
node: to,
|
|
1580
|
+
relations: /* @__PURE__ */ new Set(),
|
|
1581
|
+
count: 0
|
|
1582
|
+
};
|
|
1583
|
+
bucket.relations.add(e.relation);
|
|
1584
|
+
bucket.count += occurrencesOf(e);
|
|
1585
|
+
byArtifact.set(to.id, bucket);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
const all = [...byArtifact.values()].map((b) => ({
|
|
1589
|
+
id: b.node.id,
|
|
1590
|
+
title: labelOf(b.node),
|
|
1591
|
+
type: b.node.type,
|
|
1592
|
+
relations: [...b.relations],
|
|
1593
|
+
count: b.count,
|
|
1594
|
+
...previewOf(b.node) ? { preview: previewOf(b.node) } : {},
|
|
1595
|
+
...detailed ? { nodeIds: [b.node.id] } : {}
|
|
1596
|
+
}));
|
|
1597
|
+
all.sort((a, b) => b.count - a.count);
|
|
1598
|
+
const results = all.slice(0, limit2);
|
|
1599
|
+
return {
|
|
1600
|
+
summary: `${resolved.path} cites ${all.length} artifact(s)${all.length ? `: ${all.slice(0, 5).map((r) => r.id).join(", ")} \u2014 NEXT: read one for the full rule before you change this file` : ""}.`,
|
|
1601
|
+
target: {
|
|
1602
|
+
file: resolved.path,
|
|
1603
|
+
...detailed ? { nodeIds: resolved.nodes.map((n) => n.id) } : {}
|
|
1604
|
+
},
|
|
1605
|
+
direction: "to-artifacts",
|
|
1606
|
+
results,
|
|
1607
|
+
total: all.length,
|
|
1608
|
+
truncated: all.length > results.length
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function countRefs(hits) {
|
|
1612
|
+
return hits.reduce((sum, h) => sum + h.count, 0);
|
|
1613
|
+
}
|
|
1614
|
+
function occurrencesOf(edge) {
|
|
1615
|
+
const n = edge.metadata?.occurrences;
|
|
1616
|
+
return typeof n === "number" && n > 0 ? n : 1;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// ../../packages/data/src/mcp/tools/scope/plan.ts
|
|
1620
|
+
function planScope(ctx, req) {
|
|
1621
|
+
const task = req.task ?? "";
|
|
1622
|
+
const symbols = buildSymbolIndex(ctx);
|
|
1623
|
+
const namedGovIds = namedGovernanceIds(task, req.found);
|
|
1624
|
+
const { ranked, conceptDerived } = buildEditSet(
|
|
1625
|
+
ctx,
|
|
1626
|
+
req.found,
|
|
1627
|
+
req.limit,
|
|
1628
|
+
namedGovIds,
|
|
1629
|
+
req.origin
|
|
1630
|
+
);
|
|
1631
|
+
const definitions = definitionEdits(ctx, symbols, task);
|
|
1632
|
+
const definitionFiles = new Set(definitions.map((e) => e.file));
|
|
1633
|
+
const rankedWithDefs = mergeNamedEdits(definitions, ranked, req.limit);
|
|
1634
|
+
const hasEvidence = req.found.some((h) => h.confident !== void 0);
|
|
1635
|
+
const weakCodeFiles = new Set(
|
|
1636
|
+
req.found.filter((h) => h.confident === false && isCodeHit(h)).map((h) => h.file).filter((f) => f !== void 0)
|
|
1637
|
+
);
|
|
1638
|
+
const isLead = (e) => !definitionFiles.has(e.file) && (weakCodeFiles.has(e.file) || conceptDerived.has(e.file));
|
|
1639
|
+
const editSet = hasEvidence ? rankedWithDefs.filter((e) => !isLead(e)) : rankedWithDefs;
|
|
1640
|
+
const leads = hasEvidence ? rankedWithDefs.filter(isLead) : [];
|
|
1641
|
+
const lowConfidence = hasEvidence && editSet.length === 0;
|
|
1642
|
+
const anticipated = [...leads, ...areaDiverseLeads(ctx, req.found, editSet, leads)];
|
|
1643
|
+
const editCodeNodes = codeNodesOf(ctx, editSet);
|
|
1644
|
+
const precise = buildGovernance(ctx, editCodeNodes, req.detailed, req.origin);
|
|
1645
|
+
const governance = [...precise, ...conceptGovernance(ctx, req.found, precise)];
|
|
1646
|
+
const references = buildReferenceSet(ctx, editCodeNodes, governance, req.origin);
|
|
1647
|
+
const testsCovering = buildTestsCovering(ctx, editCodeNodes, req.detailed, req.origin);
|
|
1648
|
+
const blastRadius = buildBlastRadius(ctx, editCodeNodes, req.confidence, req.origin);
|
|
1649
|
+
return {
|
|
1650
|
+
editSet,
|
|
1651
|
+
anticipated,
|
|
1652
|
+
editCodeNodes,
|
|
1653
|
+
governance,
|
|
1654
|
+
references,
|
|
1655
|
+
testsCovering,
|
|
1656
|
+
blastRadius,
|
|
1657
|
+
lowConfidence
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
function isCodeHit(hit) {
|
|
1661
|
+
return hit.file !== void 0 && hit.id === void 0;
|
|
1662
|
+
}
|
|
1663
|
+
var AREA_LEAD_CAP = 3;
|
|
1664
|
+
function areaDiverseLeads(ctx, found, editSet, leads) {
|
|
1665
|
+
const taken = new Set([...editSet, ...leads].map((e) => e.file));
|
|
1666
|
+
const editAreas = /* @__PURE__ */ new Set();
|
|
1667
|
+
for (const e of editSet) {
|
|
1668
|
+
const area = communityOfFile(ctx, e.file);
|
|
1669
|
+
if (area) editAreas.add(area);
|
|
1670
|
+
}
|
|
1671
|
+
const byArea = /* @__PURE__ */ new Map();
|
|
1672
|
+
for (const hit of found) {
|
|
1673
|
+
if (!hit.file || taken.has(hit.file)) continue;
|
|
1674
|
+
const area = communityOfFile(ctx, hit.file);
|
|
1675
|
+
if (!area || editAreas.has(area)) continue;
|
|
1676
|
+
const existing = byArea.get(area);
|
|
1677
|
+
if (!existing || hit.score > (existing.score ?? -Infinity)) {
|
|
1678
|
+
byArea.set(area, {
|
|
1679
|
+
file: hit.file,
|
|
1680
|
+
...hit.line !== void 0 ? { line: hit.line } : {},
|
|
1681
|
+
score: hit.score,
|
|
1682
|
+
why: `area: ${area}`
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return [...byArea.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).slice(0, AREA_LEAD_CAP);
|
|
1687
|
+
}
|
|
1688
|
+
function communityOfFile(ctx, file) {
|
|
1689
|
+
const resolved = resolveTarget(ctx, file);
|
|
1690
|
+
if (resolved.type !== "file") return void 0;
|
|
1691
|
+
for (const n of resolved.nodes) {
|
|
1692
|
+
const c = n.metadata.community;
|
|
1693
|
+
if (typeof c === "string" && c.length > 0) return c;
|
|
1694
|
+
}
|
|
1695
|
+
return void 0;
|
|
1696
|
+
}
|
|
1697
|
+
function namedGovernanceIds(task, found) {
|
|
1698
|
+
const words = new Set(
|
|
1699
|
+
task.toLowerCase().split(/[^a-z0-9-]+/).filter(Boolean)
|
|
1700
|
+
);
|
|
1701
|
+
const out = /* @__PURE__ */ new Set();
|
|
1702
|
+
for (const hit of found) if (hit.id && words.has(hit.id.toLowerCase())) out.add(hit.id);
|
|
1703
|
+
return out;
|
|
1704
|
+
}
|
|
1705
|
+
var NAMED_SYMBOL_CAP = 4;
|
|
1706
|
+
function buildSymbolIndex(ctx) {
|
|
1707
|
+
const index = /* @__PURE__ */ new Map();
|
|
1708
|
+
for (const node of ctx.graph.nodes()) {
|
|
1709
|
+
if (!isCode(ctx, node)) continue;
|
|
1710
|
+
const label = labelOf(node);
|
|
1711
|
+
if (!label) continue;
|
|
1712
|
+
const key = label.toLowerCase();
|
|
1713
|
+
const list = index.get(key);
|
|
1714
|
+
if (list) list.push(node);
|
|
1715
|
+
else index.set(key, [node]);
|
|
1716
|
+
}
|
|
1717
|
+
return index;
|
|
1718
|
+
}
|
|
1719
|
+
function identifierTokens(task) {
|
|
1720
|
+
return task.split(/[^A-Za-z0-9]+/).filter((t) => t.length >= 3 && /[a-z][A-Z]/.test(t));
|
|
1721
|
+
}
|
|
1722
|
+
function definitionEdits(ctx, symbols, task) {
|
|
1723
|
+
const out = /* @__PURE__ */ new Map();
|
|
1724
|
+
for (const token of identifierTokens(task)) {
|
|
1725
|
+
for (const node of symbols.get(token.toLowerCase()) ?? []) {
|
|
1726
|
+
const file = fileOf(node);
|
|
1727
|
+
if (!file || out.has(file)) continue;
|
|
1728
|
+
const line = lineOf(node);
|
|
1729
|
+
out.set(file, {
|
|
1730
|
+
file,
|
|
1731
|
+
...line !== void 0 ? { line } : {},
|
|
1732
|
+
why: `defines ${labelOf(node) ?? token}`
|
|
1733
|
+
});
|
|
1734
|
+
if (out.size >= NAMED_SYMBOL_CAP) return [...out.values()];
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return [...out.values()];
|
|
1738
|
+
}
|
|
1739
|
+
function mergeNamedEdits(front, rest, limit2) {
|
|
1740
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1741
|
+
for (const e of front) byFile.set(e.file, { ...e });
|
|
1742
|
+
for (const e of rest) {
|
|
1743
|
+
const existing = byFile.get(e.file);
|
|
1744
|
+
if (!existing) {
|
|
1745
|
+
byFile.set(e.file, { ...e });
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
if (e.why && existing.why && !existing.why.includes(e.why))
|
|
1749
|
+
existing.why = `${existing.why}; ${e.why}`;
|
|
1750
|
+
if (existing.score === void 0 && e.score !== void 0) existing.score = e.score;
|
|
1751
|
+
if (existing.line === void 0 && e.line !== void 0) existing.line = e.line;
|
|
1752
|
+
}
|
|
1753
|
+
return [...byFile.values()].slice(0, limit2);
|
|
1754
|
+
}
|
|
1755
|
+
function conceptGovernance(ctx, found, precise) {
|
|
1756
|
+
const seen = new Set(precise.map((g) => g.id));
|
|
1757
|
+
const out = [];
|
|
1758
|
+
for (const hit of found) {
|
|
1759
|
+
if (!hit.id || seen.has(hit.id)) continue;
|
|
1760
|
+
const node = ctx.graph.node(hit.id);
|
|
1761
|
+
if (!node || isCode(ctx, node)) continue;
|
|
1762
|
+
seen.add(node.id);
|
|
1763
|
+
const title = labelOf(node);
|
|
1764
|
+
out.push({
|
|
1765
|
+
id: node.id,
|
|
1766
|
+
type: node.type,
|
|
1767
|
+
...title ? { title } : {},
|
|
1768
|
+
relations: ["concept-match"],
|
|
1769
|
+
score: hit.score
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
return out.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
1773
|
+
}
|
|
1774
|
+
var REFERENCE_CAP = 8;
|
|
1775
|
+
function buildReferenceSet(ctx, editCodeNodes, governance, origin) {
|
|
1776
|
+
const govIds = new Set(governance.map((g) => g.id));
|
|
1777
|
+
const out = /* @__PURE__ */ new Map();
|
|
1778
|
+
const preciseHops = (id) => ctx.graph.hops(id).filter((h) => h.dir === "out" && edgeMatchesConfidence(h, ctx.backendName, "precise"));
|
|
1779
|
+
const add = (node, why) => {
|
|
1780
|
+
if (!node || govIds.has(node.id) || out.has(node.id) || isCode(ctx, node) || !nodeMatchesOrigin(node, origin))
|
|
1781
|
+
return;
|
|
1782
|
+
const title = labelOf(node);
|
|
1783
|
+
out.set(node.id, { id: node.id, type: node.type, ...title ? { title } : {}, why });
|
|
1784
|
+
};
|
|
1785
|
+
for (const code of editCodeNodes) {
|
|
1786
|
+
for (const h1 of preciseHops(code.id)) {
|
|
1787
|
+
const via = ctx.graph.node(h1.to);
|
|
1788
|
+
if (!via) continue;
|
|
1789
|
+
add(via, "linked from the edit-set");
|
|
1790
|
+
for (const h2 of preciseHops(via.id)) add(ctx.graph.node(h2.to), `via ${via.id}`);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
for (const g of governance) {
|
|
1794
|
+
for (const h of preciseHops(g.id)) add(ctx.graph.node(h.to), `cited by ${g.id}`);
|
|
1795
|
+
}
|
|
1796
|
+
return [...out.values()].slice(0, REFERENCE_CAP);
|
|
1797
|
+
}
|
|
1798
|
+
function codeNodesOf(ctx, editSet) {
|
|
1799
|
+
const out = [];
|
|
1800
|
+
for (const target of editSet) {
|
|
1801
|
+
const resolved = resolveTarget(ctx, target.file);
|
|
1802
|
+
if (resolved.type === "file") out.push(...resolved.nodes);
|
|
1803
|
+
}
|
|
1804
|
+
return out;
|
|
1805
|
+
}
|
|
1806
|
+
function buildEditSet(ctx, found, limit2, namedGovIds, origin) {
|
|
1807
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1808
|
+
const directFiles = /* @__PURE__ */ new Set();
|
|
1809
|
+
const namedGovFiles = /* @__PURE__ */ new Set();
|
|
1810
|
+
for (const hit of found) {
|
|
1811
|
+
if (!hit.file) continue;
|
|
1812
|
+
directFiles.add(hit.file);
|
|
1813
|
+
const existing = byFile.get(hit.file);
|
|
1814
|
+
if (existing) {
|
|
1815
|
+
if (existing.score === void 0 || hit.score > existing.score) existing.score = hit.score;
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
byFile.set(hit.file, {
|
|
1819
|
+
file: hit.file,
|
|
1820
|
+
...hit.line !== void 0 ? { line: hit.line } : {},
|
|
1821
|
+
score: hit.score,
|
|
1822
|
+
why: "direct match"
|
|
1823
|
+
// provenance: the query hit this file itself (vs "enforces X" below)
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
for (const hit of found) {
|
|
1827
|
+
if (hit.file || !hit.id) continue;
|
|
1828
|
+
const enforcing = trace(ctx, {
|
|
1829
|
+
target: hit.id,
|
|
1830
|
+
confidence: "precise",
|
|
1831
|
+
...origin ? { origin } : {}
|
|
1832
|
+
});
|
|
1833
|
+
const named = namedGovIds.has(hit.id);
|
|
1834
|
+
for (const r of enforcing.results) {
|
|
1835
|
+
if (!r.file) continue;
|
|
1836
|
+
if (named) namedGovFiles.add(r.file);
|
|
1837
|
+
const why = `enforces ${hit.id}`;
|
|
1838
|
+
const existing = byFile.get(r.file);
|
|
1839
|
+
if (existing) existing.why = existing.why ? `${existing.why}; ${why}` : why;
|
|
1840
|
+
else byFile.set(r.file, { file: r.file, why });
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
const ranked = [...byFile.values()];
|
|
1844
|
+
ranked.sort((a, b) => {
|
|
1845
|
+
if (a.score !== void 0 && b.score !== void 0) return b.score - a.score;
|
|
1846
|
+
if (a.score !== void 0) return -1;
|
|
1847
|
+
if (b.score !== void 0) return 1;
|
|
1848
|
+
return a.file.localeCompare(b.file);
|
|
1849
|
+
});
|
|
1850
|
+
const capped = ranked.slice(0, limit2);
|
|
1851
|
+
const conceptDerived = new Set(
|
|
1852
|
+
capped.filter((e) => !directFiles.has(e.file) && !namedGovFiles.has(e.file)).map((e) => e.file)
|
|
1853
|
+
);
|
|
1854
|
+
return { ranked: capped, conceptDerived };
|
|
1855
|
+
}
|
|
1856
|
+
function buildGovernance(ctx, editCodeNodes, detailed, origin) {
|
|
1857
|
+
const governanceNodes = resolveScopeGovernance(ctx, [], editCodeNodes).filter(
|
|
1858
|
+
(n) => nodeMatchesOrigin(n, origin)
|
|
1859
|
+
);
|
|
1860
|
+
const ranked = governanceNodes.map((node) => {
|
|
1861
|
+
const relations = /* @__PURE__ */ new Set();
|
|
1862
|
+
let linkCount = 0;
|
|
1863
|
+
let score;
|
|
1864
|
+
for (const code of editCodeNodes) {
|
|
1865
|
+
for (const hop of ctx.graph.hops(code.id)) {
|
|
1866
|
+
if (hop.to !== node.id) continue;
|
|
1867
|
+
if (!edgeMatchesConfidence(hop, ctx.backendName, "precise")) continue;
|
|
1868
|
+
relations.add(hop.relation);
|
|
1869
|
+
linkCount++;
|
|
1870
|
+
if (detailed && score === void 0) score = scoreOfHop(ctx, code.id, hop);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
const title = labelOf(node);
|
|
1874
|
+
const ref = {
|
|
1875
|
+
id: node.id,
|
|
1876
|
+
type: node.type,
|
|
1877
|
+
...title ? { title } : {},
|
|
1878
|
+
relations: [...relations],
|
|
1879
|
+
...score !== void 0 ? { score } : {}
|
|
1880
|
+
};
|
|
1881
|
+
return { ref, linkCount };
|
|
1882
|
+
});
|
|
1883
|
+
ranked.sort((a, b) => b.linkCount - a.linkCount);
|
|
1884
|
+
return ranked.map((r) => r.ref);
|
|
1885
|
+
}
|
|
1886
|
+
var FUZZY_COVERAGE_CAP = 8;
|
|
1887
|
+
function buildTestsCovering(ctx, editCodeNodes, detailed, origin) {
|
|
1888
|
+
const editIds = new Set(editCodeNodes.map((n) => n.id));
|
|
1889
|
+
const precise = /* @__PURE__ */ new Map();
|
|
1890
|
+
for (const entry of resolveCoveringCode(ctx, editCodeNodes)) {
|
|
1891
|
+
if (!nodeMatchesOrigin(entry.node, origin)) continue;
|
|
1892
|
+
precise.set(entry.node.id, { node: entry.node, relations: new Set(entry.relations) });
|
|
1893
|
+
}
|
|
1894
|
+
const fuzzy = /* @__PURE__ */ new Map();
|
|
1895
|
+
if (detailed) {
|
|
1896
|
+
for (const code of editCodeNodes) {
|
|
1897
|
+
for (const hop of ctx.graph.hops(code.id)) {
|
|
1898
|
+
if (editIds.has(hop.to)) continue;
|
|
1899
|
+
const other = ctx.graph.node(hop.to);
|
|
1900
|
+
if (!other || !isCode(ctx, other) || !nodeMatchesOrigin(other, origin)) continue;
|
|
1901
|
+
if (coverageViaOf(ctx, code.id, hop) === void 0) continue;
|
|
1902
|
+
const existing = precise.get(other.id);
|
|
1903
|
+
if (existing) {
|
|
1904
|
+
existing.relations.add(hop.relation);
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
const bucket = fuzzy.get(other.id) ?? { node: other, relations: /* @__PURE__ */ new Set() };
|
|
1908
|
+
bucket.relations.add(hop.relation);
|
|
1909
|
+
fuzzy.set(other.id, bucket);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
const toRef = (b) => {
|
|
1914
|
+
const line = lineOf(b.node);
|
|
1915
|
+
return {
|
|
1916
|
+
file: fileOf(b.node) ?? b.node.id,
|
|
1917
|
+
...line !== void 0 ? { line } : {},
|
|
1918
|
+
relations: [...b.relations]
|
|
1919
|
+
};
|
|
1920
|
+
};
|
|
1921
|
+
return [...precise.values()].map(toRef).concat([...fuzzy.values()].slice(0, FUZZY_COVERAGE_CAP).map(toRef));
|
|
1922
|
+
}
|
|
1923
|
+
function buildBlastRadius(ctx, editCodeNodes, confidence, origin) {
|
|
1924
|
+
const startIds = editCodeNodes.map((n) => n.id);
|
|
1925
|
+
const edgeOk = (h) => hopPasses(ctx, h, confidence, origin);
|
|
1926
|
+
const reached = bfs(ctx, startIds, 2, void 0, edgeOk);
|
|
1927
|
+
const byType = {};
|
|
1928
|
+
for (const id of reached) {
|
|
1929
|
+
const node = ctx.graph.node(id);
|
|
1930
|
+
if (node) byType[node.type] = (byType[node.type] ?? 0) + 1;
|
|
1931
|
+
}
|
|
1932
|
+
return { total: reached.length, byType };
|
|
1933
|
+
}
|
|
1934
|
+
function scoreOfHop(ctx, fromId, hop) {
|
|
1935
|
+
const query = hop.dir === "out" ? { from: fromId, to: hop.to, relation: hop.relation } : { from: hop.to, to: fromId, relation: hop.relation };
|
|
1936
|
+
for (const edge of ctx.graph.edges(query)) {
|
|
1937
|
+
const score = edgeScore(edge);
|
|
1938
|
+
if (score !== void 0) return score;
|
|
1939
|
+
}
|
|
1940
|
+
return void 0;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// ../../packages/data/src/mcp/tools/review.ts
|
|
1944
|
+
function isWorkItem(node) {
|
|
1945
|
+
return node?.metadata.statusCategory !== void 0;
|
|
1946
|
+
}
|
|
1947
|
+
async function review(ctx, req) {
|
|
1948
|
+
const limit2 = clampLimit(req.limit, 50);
|
|
1949
|
+
const cites = new Set(req.cites ?? []);
|
|
1950
|
+
const findings = [];
|
|
1951
|
+
const resolvedFiles = [];
|
|
1952
|
+
const unresolved = [];
|
|
1953
|
+
for (const input of req.edits) {
|
|
1954
|
+
const r = resolveTarget(ctx, input);
|
|
1955
|
+
if (r.type === "file") resolvedFiles.push(r.path);
|
|
1956
|
+
else unresolved.push(input);
|
|
1957
|
+
}
|
|
1958
|
+
const found = resolvedFiles.map((file) => ({ file, score: 1 }));
|
|
1959
|
+
const plan = planScope(ctx, {
|
|
1960
|
+
found,
|
|
1961
|
+
limit: Math.max(found.length, 1),
|
|
1962
|
+
confidence: "precise",
|
|
1963
|
+
detailed: false
|
|
1964
|
+
});
|
|
1965
|
+
for (const g of plan.governance) {
|
|
1966
|
+
if (cites.has(g.id)) continue;
|
|
1967
|
+
if (isWorkItem(ctx.graph.node(g.id))) continue;
|
|
1968
|
+
const rel = g.relations.join("/");
|
|
1969
|
+
findings.push({
|
|
1970
|
+
code: "uncited-governance",
|
|
1971
|
+
severity: "warn",
|
|
1972
|
+
confidence: "precise",
|
|
1973
|
+
subject: g.id,
|
|
1974
|
+
message: `Edit touches code governed by ${g.id}${g.title ? ` (${g.title})` : ""}${rel ? ` via ${rel}` : ""}, but the plan doesn't cite it.`,
|
|
1975
|
+
cites: [g.id]
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1979
|
+
for (const n of plan.editCodeNodes) {
|
|
1980
|
+
if (!isCode(ctx, n)) continue;
|
|
1981
|
+
const f = fileOf(n) ?? n.id;
|
|
1982
|
+
const list = byFile.get(f);
|
|
1983
|
+
if (list) list.push(n);
|
|
1984
|
+
else byFile.set(f, [n]);
|
|
1985
|
+
}
|
|
1986
|
+
for (const [file, nodes] of req.coverage ? byFile : []) {
|
|
1987
|
+
if (hasTestCoverage(ctx, nodes)) continue;
|
|
1988
|
+
findings.push({
|
|
1989
|
+
code: "uncovered-code",
|
|
1990
|
+
severity: "info",
|
|
1991
|
+
confidence: "precise",
|
|
1992
|
+
subject: file,
|
|
1993
|
+
message: `${file} has no covering test (no colocated test, and no test imports it).`,
|
|
1994
|
+
cites: []
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
const resolvedFileSet = new Set(resolvedFiles);
|
|
1998
|
+
const enforcerDiags = resolvedFileSet.size > 0 ? await cachedEnforcerDiags(ctx, [...resolvedFileSet]) : [];
|
|
1999
|
+
for (const d of enforcerDiags) {
|
|
2000
|
+
if (!d.file || !resolvedFileSet.has(d.file)) continue;
|
|
2001
|
+
findings.push({
|
|
2002
|
+
code: d.code,
|
|
2003
|
+
severity: d.severity,
|
|
2004
|
+
confidence: d.severity === "error" ? "precise" : "fuzzy",
|
|
2005
|
+
subject: d.file,
|
|
2006
|
+
message: d.message,
|
|
2007
|
+
cites: []
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
for (const path of unresolved) {
|
|
2011
|
+
const dirGov = resolveDirectoryGovernance(ctx, path).filter((n) => !isWorkItem(n)).map((g) => g.id).filter((id) => !cites.has(id)).slice(0, 3);
|
|
2012
|
+
const hint = dirGov.length ? ` Files in its directory are governed by ${dirGov.join(", ")} \u2014 cite if it applies.` : "";
|
|
2013
|
+
findings.push({
|
|
2014
|
+
code: "unresolved-edit",
|
|
2015
|
+
severity: "info",
|
|
2016
|
+
confidence: "precise",
|
|
2017
|
+
subject: path,
|
|
2018
|
+
message: `${path} isn't in the graph yet (a new file, or a path that doesn't match).${hint || " No governed siblings to assess it by."}`,
|
|
2019
|
+
cites: dirGov
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
const rank = { error: 0, warn: 1, info: 2 };
|
|
2023
|
+
findings.sort((a, b) => rank[a.severity] - rank[b.severity]);
|
|
2024
|
+
const truncated = findings.length > limit2;
|
|
2025
|
+
const shown = truncated ? findings.slice(0, limit2) : findings;
|
|
2026
|
+
const coverage = { resolved: resolvedFiles.length, unresolved: unresolved.length };
|
|
2027
|
+
const verdict = resolvedFiles.length === 0 && req.edits.length > 0 ? "unassessable" : shown.some(
|
|
2028
|
+
(f) => f.confidence === "precise" && (f.severity === "warn" || f.severity === "error")
|
|
2029
|
+
) ? "revise" : "approve";
|
|
2030
|
+
const label = req.task ? ` "${req.task}"` : "";
|
|
2031
|
+
const coverageNote = unresolved.length > 0 ? ` ${unresolved.length} of ${req.edits.length} edit(s) new/unresolved.` : "";
|
|
2032
|
+
const refList = plan.references.slice(0, 5).map((r) => r.id).join(", ");
|
|
2033
|
+
const audit = plan.references.length > 0 ? ` Before finalizing: READ ${refList}${plan.references.length > 5 ? " \u2026" : ""}, then self-audit the diff against them + your repo's smell rules (layering, DRY, hardcoded knobs, task-refs).` : "";
|
|
2034
|
+
const summary = verdict === "unassessable" ? `Reviewed${label} (${req.edits.length} edit(s)): unassessable \u2014 none are in the graph yet (all new files / wrong paths), so governance and coverage can't be checked.${audit}` : shown.length === 0 ? `Reviewed${label} (${req.edits.length} edit(s)): clean \u2014 no governance or coverage gaps. approve.${coverageNote}${audit}` : `Reviewed${label} (${req.edits.length} edit(s)): ${shown.length} finding(s). ${verdict}.${coverageNote}${audit}`;
|
|
2035
|
+
return { summary, verdict, coverage, findings: shown, references: plan.references, truncated };
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// ../../packages/data/src/mcp/tools/browse.ts
|
|
2039
|
+
function browseMap(ctx, req) {
|
|
2040
|
+
const limit2 = clampLimit(req.limit, 100);
|
|
2041
|
+
const offset = Math.max(0, req.offset ?? 0);
|
|
2042
|
+
const ladder = workLadder(ctx.graph);
|
|
2043
|
+
const matched = ctx.graph.nodes(req.type).filter((node) => nodeMatchesOrigin(node, req.origin));
|
|
2044
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2045
|
+
for (const node of ctx.graph.nodes()) {
|
|
2046
|
+
if (!nodeMatchesOrigin(node, req.origin)) continue;
|
|
2047
|
+
counts.set(node.type, (counts.get(node.type) ?? 0) + 1);
|
|
2048
|
+
}
|
|
2049
|
+
const ordered = [...matched].sort(
|
|
2050
|
+
(a, b) => a.type.localeCompare(b.type) || a.id.localeCompare(b.id)
|
|
2051
|
+
);
|
|
2052
|
+
const page = ordered.slice(offset, offset + limit2);
|
|
2053
|
+
return {
|
|
2054
|
+
summary: `${ordered.length} thing(s) in the map${req.type ? ` of type '${req.type}'` : ""}${ordered.length > page.length ? `, showing ${page.length} from ${offset}` : ""}.${page.length > 0 ? " NEXT: read an artifact id for its full text, or trace a file for why it exists." : ""}`,
|
|
2055
|
+
items: page.map((node) => render2(ctx, node, ladder.holders.get(node.id), req.fields)),
|
|
2056
|
+
total: ordered.length,
|
|
2057
|
+
offset,
|
|
2058
|
+
limit: limit2,
|
|
2059
|
+
hasMore: offset + page.length < ordered.length,
|
|
2060
|
+
types: [...counts.entries()].map(([type, total]) => ({ type, total })).sort((a, b) => b.total - a.total || a.type.localeCompare(b.type))
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
function render2(ctx, node, holder, fields) {
|
|
2064
|
+
const preview = previewOf(node);
|
|
2065
|
+
const status2 = statusOf(node);
|
|
2066
|
+
const derived = node.metadata[DERIVED_FROM];
|
|
2067
|
+
const belongsTo = typeof derived === "string" ? derived : holder;
|
|
2068
|
+
const free = pick(node, fields);
|
|
2069
|
+
return {
|
|
2070
|
+
...renderNode(ctx, node),
|
|
2071
|
+
...preview ? { preview } : {},
|
|
2072
|
+
...belongsTo !== void 0 ? { belongsTo } : {},
|
|
2073
|
+
...status2 ? { status: status2, statusCategory: categoryOf(status2) } : {},
|
|
2074
|
+
...free ? { fields: free } : {}
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
function pick(node, fields) {
|
|
2078
|
+
if (fields === void 0 || fields.length === 0) return void 0;
|
|
2079
|
+
const free = {};
|
|
2080
|
+
for (const key of fields) {
|
|
2081
|
+
const value = node.metadata[key];
|
|
2082
|
+
if (value !== void 0) free[key] = value;
|
|
2083
|
+
}
|
|
2084
|
+
return Object.keys(free).length > 0 ? free : void 0;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// ../../packages/data/src/mcp/tools/search.ts
|
|
2088
|
+
async function search(ctx, req) {
|
|
2089
|
+
const limit2 = clampLimit(req.limit, 10);
|
|
2090
|
+
if (req.query.trim().length === 0)
|
|
2091
|
+
return { summary: "Empty query.", results: [], total: 0, truncated: false };
|
|
2092
|
+
const runtime = await ctx.search();
|
|
2093
|
+
const candidates = ctx.graph.nodes(req.type).filter((n) => nodeMatchesOrigin(n, req.origin));
|
|
2094
|
+
let ranked = await rankNodes({
|
|
2095
|
+
...runtime ? { runtime } : {},
|
|
2096
|
+
candidates,
|
|
2097
|
+
query: req.query,
|
|
2098
|
+
graph: ctx.graph,
|
|
2099
|
+
...req.expand !== void 0 ? { expand: req.expand } : {},
|
|
2100
|
+
...req.deep !== void 0 ? { deep: req.deep } : {}
|
|
2101
|
+
});
|
|
2102
|
+
if (req.origin !== void 0)
|
|
2103
|
+
ranked = ranked.filter((r) => nodeMatchesOrigin(r.node, req.origin));
|
|
2104
|
+
const conceptual = !isSingleTerm(req.query);
|
|
2105
|
+
if (conceptual) {
|
|
2106
|
+
ranked = diversify(ranked, (node) => isCode(ctx, node) ? fileOf(node) ?? node.id : node.id);
|
|
2107
|
+
}
|
|
2108
|
+
const top = ranked.slice(0, limit2);
|
|
2109
|
+
if (conceptual && top.length > 0 && !top.some((r) => isConfident(r, runtime?.confidence))) {
|
|
2110
|
+
return {
|
|
2111
|
+
summary: `No confident match for "${req.query}"${req.type ? ` (type=${req.type})` : ""} \u2014 the graph has nothing that matches these concepts well. Try more specific words or an identifier (symbol, file path, artifact id), restrict with type, or drop the query entirely to see what this map holds.`,
|
|
2112
|
+
results: [],
|
|
2113
|
+
total: 0,
|
|
2114
|
+
truncated: false
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
const expanding = Boolean(runtime && (req.expand ?? runtime.expand));
|
|
2118
|
+
const mode = `${runtime ? runtime.hybrid ? "hybrid" : "semantic" : "lexical"}${expanding ? "+expand" : ""}`;
|
|
2119
|
+
return {
|
|
2120
|
+
summary: `${ranked.length} match(es) for "${req.query}"${req.type ? ` (type=${req.type})` : ""} [${mode}].${ranked.length > 0 ? " NEXT: read an artifact id for the full rule, or trace a file for why it exists." : ""}`,
|
|
2121
|
+
results: top.map((r) => {
|
|
2122
|
+
const { node, score, collapsed } = r;
|
|
2123
|
+
const preview = previewOf(node);
|
|
2124
|
+
return {
|
|
2125
|
+
...renderNode(ctx, node),
|
|
2126
|
+
...preview ? { preview } : {},
|
|
2127
|
+
// Scores are rank-scale hints; full float precision is noise in an agent's context.
|
|
2128
|
+
score: Math.round(score * 1e3) / 1e3,
|
|
2129
|
+
// Evidence-based: did this hit clear the relevance floor (not just rank high)? Lets a
|
|
2130
|
+
// caller (scope) tell a strongly-located match from rank-noise.
|
|
2131
|
+
confident: isConfident(r, runtime?.confidence),
|
|
2132
|
+
...collapsed ? { matches: collapsed + 1 } : {}
|
|
2133
|
+
};
|
|
2134
|
+
}),
|
|
2135
|
+
total: ranked.length,
|
|
2136
|
+
truncated: ranked.length > top.length
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
async function searchMap(ctx, req) {
|
|
2140
|
+
const query = req.query?.trim();
|
|
2141
|
+
return query ? search(ctx, { ...req, query }) : browseMap(ctx, req);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// ../../packages/data/src/mcp/tools/scope/scope.ts
|
|
2145
|
+
var RULE_BUDGET = 12e3;
|
|
2146
|
+
async function readRules(ctx, governance, mode) {
|
|
2147
|
+
if (mode === "ids" || governance.length === 0) {
|
|
2148
|
+
return { full: 0, ids: governance.length };
|
|
2149
|
+
}
|
|
2150
|
+
let spent = 0;
|
|
2151
|
+
let full = 0;
|
|
2152
|
+
for (const ref of governance) {
|
|
2153
|
+
if (spent >= RULE_BUDGET) break;
|
|
2154
|
+
const node = ctx.graph.node(ref.id);
|
|
2155
|
+
if (node && statusOf(node) !== void 0) continue;
|
|
2156
|
+
try {
|
|
2157
|
+
const artifact = await read(ctx, { id: ref.id, maxChars: RULE_BUDGET - spent });
|
|
2158
|
+
if (!artifact.body) continue;
|
|
2159
|
+
if (spent + artifact.body.length > RULE_BUDGET) continue;
|
|
2160
|
+
ref.body = artifact.body;
|
|
2161
|
+
spent += artifact.body.length;
|
|
2162
|
+
full += 1;
|
|
2163
|
+
} catch {
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return { full, ids: governance.length - full };
|
|
2167
|
+
}
|
|
2168
|
+
async function scope(ctx, req) {
|
|
2169
|
+
const limit2 = clampLimit(req.limit, 5);
|
|
2170
|
+
const detailed = req.format === "detailed";
|
|
2171
|
+
const confidence = req.confidence ?? "precise";
|
|
2172
|
+
const emptyPlan = {
|
|
2173
|
+
editSet: [],
|
|
2174
|
+
anticipatedIntegrationPoints: [],
|
|
2175
|
+
governance: [],
|
|
2176
|
+
references: [],
|
|
2177
|
+
testsCovering: [],
|
|
2178
|
+
blastRadius: { total: 0, byType: {} },
|
|
2179
|
+
lowConfidence: false,
|
|
2180
|
+
rules: { full: 0, ids: 0 }
|
|
2181
|
+
};
|
|
2182
|
+
if (req.task.trim().length === 0)
|
|
2183
|
+
return {
|
|
2184
|
+
summary: "Empty task.",
|
|
2185
|
+
task: req.task,
|
|
2186
|
+
...emptyPlan,
|
|
2187
|
+
hits: [],
|
|
2188
|
+
total: 0,
|
|
2189
|
+
truncated: false
|
|
2190
|
+
};
|
|
2191
|
+
const found = await search(ctx, {
|
|
2192
|
+
query: req.task,
|
|
2193
|
+
limit: limit2 * 2,
|
|
2194
|
+
...req.origin ? { origin: req.origin } : {}
|
|
2195
|
+
});
|
|
2196
|
+
if (found.results.length === 0)
|
|
2197
|
+
return {
|
|
2198
|
+
summary: `No nodes matched "${req.task}". Try different wording, or search with a broader query.`,
|
|
2199
|
+
task: req.task,
|
|
2200
|
+
...emptyPlan,
|
|
2201
|
+
hits: [],
|
|
2202
|
+
total: found.total,
|
|
2203
|
+
truncated: false
|
|
2204
|
+
};
|
|
2205
|
+
const plan = planScope(ctx, {
|
|
2206
|
+
found: found.results,
|
|
2207
|
+
task: req.task,
|
|
2208
|
+
limit: limit2,
|
|
2209
|
+
confidence,
|
|
2210
|
+
detailed,
|
|
2211
|
+
...req.origin ? { origin: req.origin } : {}
|
|
2212
|
+
});
|
|
2213
|
+
const traceLimit = detailed ? 8 : 4;
|
|
2214
|
+
const impactDepth = 2;
|
|
2215
|
+
const impactSample = detailed ? 6 : 0;
|
|
2216
|
+
const knownIds = /* @__PURE__ */ new Set([
|
|
2217
|
+
...plan.governance.map((g) => g.id),
|
|
2218
|
+
...plan.references.map((r) => r.id),
|
|
2219
|
+
...found.results.slice(0, limit2).map((n) => n.id).filter((id) => id !== void 0)
|
|
2220
|
+
]);
|
|
2221
|
+
const renderedAbove = /* @__PURE__ */ new Set([
|
|
2222
|
+
...plan.governance.map((g) => g.id),
|
|
2223
|
+
...plan.references.map((r) => r.id)
|
|
2224
|
+
]);
|
|
2225
|
+
const hits = found.results.slice(0, limit2).map((node) => {
|
|
2226
|
+
const target = node.file ?? node.id;
|
|
2227
|
+
if (!target) {
|
|
2228
|
+
return {
|
|
2229
|
+
target: "",
|
|
2230
|
+
node,
|
|
2231
|
+
ok: false,
|
|
2232
|
+
reason: "search hit had no file path or id to chain from."
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
try {
|
|
2236
|
+
const tr = trace(ctx, {
|
|
2237
|
+
target,
|
|
2238
|
+
limit: traceLimit,
|
|
2239
|
+
confidence,
|
|
2240
|
+
...detailed ? { format: "detailed" } : {},
|
|
2241
|
+
...req.origin ? { origin: req.origin } : {}
|
|
2242
|
+
});
|
|
2243
|
+
const impact = detailed ? graphExplore(ctx, {
|
|
2244
|
+
action: "impact",
|
|
2245
|
+
node: target,
|
|
2246
|
+
depth: impactDepth,
|
|
2247
|
+
...req.origin ? { origin: req.origin } : {},
|
|
2248
|
+
// As wide a pool as graphExplore itself ever returns (its own `limit` ceiling) —
|
|
2249
|
+
// pickImpactSample below ranks the 6 most informative out of it; total/byType are
|
|
2250
|
+
// unaffected (computed pre-slice). A narrower, formula-derived cap regressed to the
|
|
2251
|
+
// original bug for any hub with more low-value edges than the cap (oss-review):
|
|
2252
|
+
// this is bounded, not unbounded, but a hub would need MORE non-code edges than
|
|
2253
|
+
// graphExplore's own max page size before its first code edge to still come back
|
|
2254
|
+
// thin — at that point `graph`'s own `offset` pagination is the real escape
|
|
2255
|
+
// hatch, not a bigger magic number here.
|
|
2256
|
+
limit: MAX_LIMIT,
|
|
2257
|
+
confidence
|
|
2258
|
+
}) : void 0;
|
|
2259
|
+
return {
|
|
2260
|
+
target,
|
|
2261
|
+
node: idOnlyForKnown([node], renderedAbove)[0] ?? node,
|
|
2262
|
+
ok: true,
|
|
2263
|
+
...tr.results.length ? { why: idOnlyForKnown(tr.results, knownIds) } : {},
|
|
2264
|
+
...impact ? {
|
|
2265
|
+
impact: {
|
|
2266
|
+
total: impact.total ?? 0,
|
|
2267
|
+
byType: impact.byType ?? {},
|
|
2268
|
+
...impactSample && impact.nodes?.length ? {
|
|
2269
|
+
sample: idOnlyForKnown(
|
|
2270
|
+
pickImpactSample(impact.nodes, impactSample),
|
|
2271
|
+
knownIds
|
|
2272
|
+
)
|
|
2273
|
+
} : {}
|
|
2274
|
+
}
|
|
2275
|
+
} : {}
|
|
2276
|
+
};
|
|
2277
|
+
} catch (err) {
|
|
2278
|
+
return { target, node, ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
2279
|
+
}
|
|
2280
|
+
});
|
|
2281
|
+
const rules = await readRules(ctx, plan.governance, req.rules);
|
|
2282
|
+
const topFiles = plan.editSet.slice(0, 3).map((e) => e.file).join(", ");
|
|
2283
|
+
const topGov = plan.governance.filter((g) => g.body === void 0).slice(0, 3).map((g) => g.id).join(", ");
|
|
2284
|
+
const summary = plan.lowConfidence ? `Low-confidence (build-new) scope for "${req.task}": no code strongly matched \u2014 ${plan.anticipated.length} integration point(s) to READ, not edit; ${plan.governance.length} concept-matched governance. Lead with search + read.` : `Scoped "${req.task}": ${plan.editSet.length} edit target(s), ${plan.governance.length} governance, ${plan.testsCovering.length} covering test(s), blast radius ${plan.blastRadius.total} (${confidence})${topFiles ? ` \u2014 ${topFiles}` : ""}.${rules.ids > 0 && topGov ? ` ${rules.full} rule(s) came back in full; NEXT: read ${topGov} for the ${rules.ids} that did not,` : rules.full > 0 ? ` All ${rules.full} governing rule(s) came back in FULL \u2014 read them here, not with map_read;` : " NEXT:"} then check the edit-set with map_scope {edits} before you commit.`;
|
|
2285
|
+
return {
|
|
2286
|
+
summary,
|
|
2287
|
+
task: req.task,
|
|
2288
|
+
editSet: plan.editSet,
|
|
2289
|
+
anticipatedIntegrationPoints: plan.anticipated,
|
|
2290
|
+
governance: plan.governance,
|
|
2291
|
+
rules,
|
|
2292
|
+
references: plan.references,
|
|
2293
|
+
testsCovering: plan.testsCovering,
|
|
2294
|
+
blastRadius: plan.blastRadius,
|
|
2295
|
+
lowConfidence: plan.lowConfidence,
|
|
2296
|
+
hits,
|
|
2297
|
+
total: found.total,
|
|
2298
|
+
truncated: found.truncated
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
function pickImpactSample(nodes, count) {
|
|
2302
|
+
const preferred = [];
|
|
2303
|
+
const rest = [];
|
|
2304
|
+
for (const n of nodes) (n.file !== void 0 ? preferred : rest).push(n);
|
|
2305
|
+
return [...preferred, ...rest].slice(0, count);
|
|
2306
|
+
}
|
|
2307
|
+
function idOnlyForKnown(entries, knownIds) {
|
|
2308
|
+
return entries.map((e) => {
|
|
2309
|
+
const known = e.id !== void 0 && knownIds.has(e.id);
|
|
2310
|
+
const redundantNodeId = e.nodeId !== void 0 && e.nodeId === e.id;
|
|
2311
|
+
const redundantNodeIds = e.nodeIds !== void 0 && e.nodeIds.length === 1 && e.nodeIds[0] === e.id;
|
|
2312
|
+
if (!known && !redundantNodeId && !redundantNodeIds) return e;
|
|
2313
|
+
const trimmed = { ...e };
|
|
2314
|
+
if (known) {
|
|
2315
|
+
delete trimmed.title;
|
|
2316
|
+
delete trimmed.preview;
|
|
2317
|
+
}
|
|
2318
|
+
if (redundantNodeId) delete trimmed.nodeId;
|
|
2319
|
+
if (redundantNodeIds) delete trimmed.nodeIds;
|
|
2320
|
+
return trimmed;
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
async function scopeOrReview(ctx, req) {
|
|
2324
|
+
if (req.edits && req.edits.length > 0) return review(ctx, req);
|
|
2325
|
+
if (req.task) return scope(ctx, req);
|
|
2326
|
+
throw new Error(
|
|
2327
|
+
"map_scope needs either `task` (planning a change) or `edits` (verifying one); it received neither."
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// ../../packages/data/src/mcp/tools/status.ts
|
|
2332
|
+
async function status(ctx) {
|
|
2333
|
+
const f = await freshness(ctx);
|
|
2334
|
+
const nodesByType = {};
|
|
2335
|
+
let artifacts = 0;
|
|
2336
|
+
for (const n of ctx.graph.nodes()) {
|
|
2337
|
+
nodesByType[n.type] = (nodesByType[n.type] ?? 0) + 1;
|
|
2338
|
+
if (!isCode(ctx, n)) artifacts += 1;
|
|
2339
|
+
}
|
|
2340
|
+
const edges = ctx.graph.edges();
|
|
2341
|
+
const edgesByRelation = {};
|
|
2342
|
+
let preciseEdges = 0;
|
|
2343
|
+
for (const e of edges) {
|
|
2344
|
+
edgesByRelation[e.relation] = (edgesByRelation[e.relation] ?? 0) + 1;
|
|
2345
|
+
if (isPrecise(e, ctx.backendName)) preciseEdges += 1;
|
|
2346
|
+
}
|
|
2347
|
+
return {
|
|
2348
|
+
summary: `${ctx.graph.nodes().length} nodes (${artifacts} artifacts), ${edges.length} edges (${preciseEdges} precise). Built ${ctx.builtAt}${f.head ? `, HEAD ${f.head.commit.slice(0, 8)}${f.head.dirty ? "-dirty" : ""}` : ""}${f.stale ? ` \u2014 ${f.reason ?? "graph is stale \u2014 run `jarvis build graph` (incremental)"}` : ""}.`,
|
|
2349
|
+
builtAt: ctx.builtAt,
|
|
2350
|
+
...f.head ? { head: { commit: f.head.commit, dirty: f.head.dirty } } : {},
|
|
2351
|
+
graphJson: {
|
|
2352
|
+
path: f.graphPath,
|
|
2353
|
+
...f.mtimeIso ? { mtime: f.mtimeIso } : {},
|
|
2354
|
+
...f.behindHead !== void 0 ? { stale: f.behindHead } : {}
|
|
2355
|
+
},
|
|
2356
|
+
...f.origin ? { origin: f.origin } : {},
|
|
2357
|
+
counts: {
|
|
2358
|
+
nodes: ctx.graph.nodes().length,
|
|
2359
|
+
artifacts,
|
|
2360
|
+
edges: edges.length,
|
|
2361
|
+
preciseEdges
|
|
2362
|
+
},
|
|
2363
|
+
nodesByType,
|
|
2364
|
+
edgesByRelation
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// ../../packages/data/src/mcp/tools/index.ts
|
|
2369
|
+
var HANDLERS = {
|
|
2370
|
+
brief: (ctx, req) => brief(ctx, req),
|
|
2371
|
+
scope: (ctx, req) => scopeOrReview(ctx, req),
|
|
2372
|
+
search: (ctx, req) => searchMap(ctx, req),
|
|
2373
|
+
read: (ctx, req) => read(ctx, req),
|
|
2374
|
+
graph: async (ctx, req) => graphExplore(ctx, req),
|
|
2375
|
+
connections: async (ctx, req) => listConnections(ctx, req)
|
|
2376
|
+
};
|
|
2377
|
+
var TOOLS = [
|
|
2378
|
+
...dataTools(HANDLERS),
|
|
2379
|
+
memoryTool((ctx, req) => memory(ctx, req))
|
|
2380
|
+
];
|
|
2381
|
+
|
|
2382
|
+
// ../../packages/data/src/mcp/tools/schema.ts
|
|
2383
|
+
import { z } from "zod";
|
|
2384
|
+
function toShape(schema) {
|
|
2385
|
+
if (schema.type !== "object")
|
|
2386
|
+
throw new Error(`a tool's input must be an object, got ${String(schema.type)}`);
|
|
2387
|
+
const properties = schema.properties ?? {};
|
|
2388
|
+
const required = new Set(schema.required ?? []);
|
|
2389
|
+
const shape = {};
|
|
2390
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
2391
|
+
const field = toZod(property);
|
|
2392
|
+
shape[name] = required.has(name) ? field : field.optional();
|
|
2393
|
+
}
|
|
2394
|
+
return shape;
|
|
2395
|
+
}
|
|
2396
|
+
function toZod(schema) {
|
|
2397
|
+
const described = (type) => typeof schema.description === "string" ? type.describe(schema.description) : type;
|
|
2398
|
+
if (Array.isArray(schema.enum)) {
|
|
2399
|
+
const values = schema.enum;
|
|
2400
|
+
if (values.length > 0 && values.every((each) => typeof each === "number")) {
|
|
2401
|
+
const literals = values.map((each) => z.literal(each));
|
|
2402
|
+
return described(
|
|
2403
|
+
literals.length === 1 ? literals[0] : z.union(literals)
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
return described(z.enum(values));
|
|
2407
|
+
}
|
|
2408
|
+
switch (schema.type) {
|
|
2409
|
+
case "string": {
|
|
2410
|
+
let type = z.string();
|
|
2411
|
+
if (typeof schema.minLength === "number") type = type.min(schema.minLength);
|
|
2412
|
+
if (typeof schema.maxLength === "number") type = type.max(schema.maxLength);
|
|
2413
|
+
return described(type);
|
|
2414
|
+
}
|
|
2415
|
+
case "integer":
|
|
2416
|
+
case "number": {
|
|
2417
|
+
let type = schema.type === "integer" ? z.number().int() : z.number();
|
|
2418
|
+
if (typeof schema.minimum === "number") type = type.min(schema.minimum);
|
|
2419
|
+
if (typeof schema.maximum === "number") type = type.max(schema.maximum);
|
|
2420
|
+
return described(type);
|
|
2421
|
+
}
|
|
2422
|
+
case "boolean":
|
|
2423
|
+
return described(z.boolean());
|
|
2424
|
+
case "array": {
|
|
2425
|
+
let type = z.array(toZod(schema.items ?? { type: "string" }));
|
|
2426
|
+
if (typeof schema.minItems === "number") type = type.min(schema.minItems);
|
|
2427
|
+
if (typeof schema.maxItems === "number") type = type.max(schema.maxItems);
|
|
2428
|
+
return described(type);
|
|
2429
|
+
}
|
|
2430
|
+
case "object":
|
|
2431
|
+
return described(z.object(toShape(schema)));
|
|
2432
|
+
default:
|
|
2433
|
+
throw new Error(
|
|
2434
|
+
`unsupported schema type in a tool declaration: ${JSON.stringify(schema.type)}`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// ../../packages/data/src/mcp/vocabulary.ts
|
|
2440
|
+
function vocabulary(ctx) {
|
|
2441
|
+
const types = /* @__PURE__ */ new Map();
|
|
2442
|
+
const artifactIds = [];
|
|
2443
|
+
for (const n of ctx.graph.nodes()) {
|
|
2444
|
+
types.set(n.type, (types.get(n.type) ?? 0) + 1);
|
|
2445
|
+
if (!isCode(ctx, n) && artifactIds.length < 8) artifactIds.push(n.id);
|
|
2446
|
+
}
|
|
2447
|
+
const relations = /* @__PURE__ */ new Set();
|
|
2448
|
+
for (const e of ctx.graph.edges()) relations.add(e.relation);
|
|
2449
|
+
return {
|
|
2450
|
+
types: [...types.entries()].sort((a, b) => b[1] - a[1]),
|
|
2451
|
+
relations: [...relations],
|
|
2452
|
+
artifactIds
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
function vocabularyLine(ctx) {
|
|
2456
|
+
const v = vocabulary(ctx);
|
|
2457
|
+
return `This repo's graph \u2014 node types: ${v.types.map(([t]) => t).join(", ")}; relations: ${v.relations.join(", ")}. Use these exact strings for the type/relation params (counts + example ids: brief {include:['shape']}).`;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// ../../packages/data/src/mcp/server.ts
|
|
2461
|
+
var READ_ONLY = {
|
|
2462
|
+
readOnlyHint: true,
|
|
2463
|
+
openWorldHint: false
|
|
2464
|
+
};
|
|
2465
|
+
var WRITES = {
|
|
2466
|
+
readOnlyHint: false,
|
|
2467
|
+
destructiveHint: true
|
|
2468
|
+
};
|
|
2469
|
+
var NOT_READY = {
|
|
2470
|
+
content: [{ type: "text", text: STILL_LOADING }],
|
|
2471
|
+
structuredContent: { summary: STILL_LOADING, ready: false }
|
|
2472
|
+
};
|
|
2473
|
+
function createServer(ctx, hosted = [], prompts = []) {
|
|
2474
|
+
const getCtx = typeof ctx === "function" ? ctx : () => Promise.resolve(ctx);
|
|
2475
|
+
const server = new McpServer(
|
|
2476
|
+
{ name: "jarvis-data", version: "0.1.0" },
|
|
2477
|
+
// Live vocabulary needs the built graph; on the deferred (provider) path it isn't available at
|
|
2478
|
+
// connect time, so instructions stay static and send the agent to `brief` for the vocabulary.
|
|
2479
|
+
{
|
|
2480
|
+
instructions: buildInstructions({
|
|
2481
|
+
...typeof ctx === "function" ? {} : { ctx },
|
|
2482
|
+
served: [...TOOLS.map((tool) => tool.name), ...hosted.map((tool) => tool.name)]
|
|
2483
|
+
})
|
|
2484
|
+
}
|
|
2485
|
+
);
|
|
2486
|
+
for (const t of TOOLS) {
|
|
2487
|
+
server.registerTool(
|
|
2488
|
+
t.name,
|
|
2489
|
+
{
|
|
2490
|
+
title: t.title,
|
|
2491
|
+
description: t.description,
|
|
2492
|
+
// Converted HERE, not declared here. A tool is declared in JSON Schema, which the
|
|
2493
|
+
// hosted surface serves directly from inside a bundler; this SDK takes a zod raw
|
|
2494
|
+
// shape and nothing else. The conversion belongs at the boundary that needs it,
|
|
2495
|
+
// so the declaration carries no validator for one server to suit the other.
|
|
2496
|
+
inputSchema: toShape(t.input),
|
|
2497
|
+
annotations: t.metadata.readOnly ? READ_ONLY : WRITES
|
|
2498
|
+
},
|
|
2499
|
+
async (input) => {
|
|
2500
|
+
const ready = await readyWithin(getCtx());
|
|
2501
|
+
if (!ready) return NOT_READY;
|
|
2502
|
+
return result(ready, await t.call(ready, input));
|
|
2503
|
+
}
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
for (const t of hosted) {
|
|
2507
|
+
server.registerTool(
|
|
2508
|
+
t.name,
|
|
2509
|
+
{
|
|
2510
|
+
title: t.title,
|
|
2511
|
+
description: t.description,
|
|
2512
|
+
inputSchema: t.inputSchema,
|
|
2513
|
+
...t.annotations ? { annotations: t.annotations } : {}
|
|
2514
|
+
},
|
|
2515
|
+
async (input) => {
|
|
2516
|
+
const structured = await t.call(input);
|
|
2517
|
+
return {
|
|
2518
|
+
content: [{ type: "text", text: structured.summary }],
|
|
2519
|
+
structuredContent: structured
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
for (const p of prompts) {
|
|
2525
|
+
server.registerPrompt(
|
|
2526
|
+
p.name,
|
|
2527
|
+
{ title: p.title, description: p.description },
|
|
2528
|
+
async () => ({
|
|
2529
|
+
messages: [
|
|
2530
|
+
{ role: "user", content: { type: "text", text: await p.read() } }
|
|
2531
|
+
]
|
|
2532
|
+
})
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
return server;
|
|
2536
|
+
}
|
|
2537
|
+
function buildInstructions(req = {}) {
|
|
2538
|
+
const served = req.served ?? TOOLS.map((tool) => tool.name);
|
|
2539
|
+
const has = (prefix) => served.some((name) => name.startsWith(prefix));
|
|
2540
|
+
const lines = [
|
|
2541
|
+
"This repo, through one connection: the work it has to do, and a map of what it knows."
|
|
2542
|
+
];
|
|
2543
|
+
if (has("work_")) {
|
|
2544
|
+
lines.push(
|
|
2545
|
+
"work_* \u2014 THE BOARD, and what this workspace has to do. Take an item and finish it: work_list, work_take, work_verify (it runs this repo's gates and does not block), work_complete. Hit a decision that is not yours \u2014 architecture, product, design \u2014 and park it with work_update rather than choosing it."
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
if (has("map_")) {
|
|
2549
|
+
lines.push(
|
|
2550
|
+
"map_* \u2014 THE MAP: decisions, standards and specs linked to the code that enforces them. PREFER IT OVER GREP for planning any non-trivial change; one map_scope replaces a dozen greps. The loop: map_brief to orient, map_scope {task} for the edit set and what governs it, map_read every rule it named because a summary is not the rule, then map_scope {edits} before you commit."
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
if (served.includes("work_start")) {
|
|
2554
|
+
lines.push("work_start \u2014 how work is done here. Once, before planning or editing anything.");
|
|
2555
|
+
}
|
|
2556
|
+
lines.push(
|
|
2557
|
+
"Name things semantically: artifacts by id (S-13), code by repo-relative path. Answers carry file:line, and _meta.data.stale when the map has drifted from HEAD.",
|
|
2558
|
+
req.ctx ? vocabularyLine(req.ctx) : "map_brief {include:['shape']} returns this repo's node types and relations \u2014 the exact strings the type/relation params take. Call it once rather than guessing."
|
|
2559
|
+
);
|
|
2560
|
+
return lines.join("\n");
|
|
2561
|
+
}
|
|
2562
|
+
async function result(ctx, structured) {
|
|
2563
|
+
const f = await freshness(ctx);
|
|
2564
|
+
const summary = f.stale && !/stale/i.test(structured.summary) ? `${STALE_PREFIX}${structured.summary}` : structured.summary;
|
|
2565
|
+
return {
|
|
2566
|
+
content: [{ type: "text", text: summary }],
|
|
2567
|
+
structuredContent: { ...structured, summary },
|
|
2568
|
+
...f.stale ? { _meta: { data: { stale: true, ...f.reason ? { reason: f.reason } : {} } } } : {}
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
var STALE_PREFIX = "\u26A0 STALE GRAPH \u2014 results may be wrong; run `jarvis build graph` (the incremental update \u2014 not --rebuild). ";
|
|
2572
|
+
|
|
2573
|
+
// ../../packages/data/src/mcp/snapshot.ts
|
|
2574
|
+
async function loadSnapshot(config) {
|
|
2575
|
+
const raw = await config.persistence.load({ repoRoot: config.repoRoot }, "snapshot");
|
|
2576
|
+
if (!raw) {
|
|
2577
|
+
throw new Error(
|
|
2578
|
+
"no built graph found \u2014 run `jarvis build graph` first (the incremental update, not a full re-extract). Read surfaces present the graph a build produced; they never produce one themselves."
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
let snapshot;
|
|
2582
|
+
try {
|
|
2583
|
+
snapshot = JSON.parse(raw);
|
|
2584
|
+
} catch (err) {
|
|
2585
|
+
throw new Error(
|
|
2586
|
+
`built graph is unreadable (${err instanceof Error ? err.message : String(err)}) \u2014 run \`jarvis build graph\` to rewrite it.`
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
const graph = new GraphStore();
|
|
2590
|
+
graph.load({ nodes: snapshot.nodes, edges: snapshot.edges });
|
|
2591
|
+
return { graph, diagnostics: snapshot.diagnostics, builtAt: snapshot.builtAt };
|
|
2592
|
+
}
|
|
2593
|
+
async function loadQueryContext(config) {
|
|
2594
|
+
const { graph, diagnostics, builtAt } = await loadSnapshot(config);
|
|
2595
|
+
const mem = await applyMemoryOverlay(config, graph);
|
|
2596
|
+
const { errors, warns } = summarize(diagnostics);
|
|
2597
|
+
process.stderr.write(
|
|
2598
|
+
`[data] graph loaded \u2014 ${graph.nodes().length} nodes, ${graph.edges().length} edges (${errors} error(s), ${warns} warning(s)); memory: ${mem.loaded} loaded (${mem.orphaned} orphaned)
|
|
2599
|
+
`
|
|
2600
|
+
);
|
|
2601
|
+
return createGraphContext(config, graph, diagnostics, builtAt, {
|
|
2602
|
+
search: () => loadSearchRuntime(config, graph)
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
export {
|
|
2606
|
+
HANDLERS,
|
|
2607
|
+
STILL_LOADING,
|
|
2608
|
+
TOOLS,
|
|
2609
|
+
createGraphContext,
|
|
2610
|
+
createGraphProvider,
|
|
2611
|
+
createServer,
|
|
2612
|
+
graphExplore,
|
|
2613
|
+
lint,
|
|
2614
|
+
loadQueryContext,
|
|
2615
|
+
loadSnapshot,
|
|
2616
|
+
memory,
|
|
2617
|
+
read,
|
|
2618
|
+
readyWithin,
|
|
2619
|
+
scope,
|
|
2620
|
+
search,
|
|
2621
|
+
status,
|
|
2622
|
+
toShape,
|
|
2623
|
+
trace
|
|
2624
|
+
};
|