@mmerterden/multi-agent-pipeline 20.21.0 → 20.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/docs/adr/0010-own-code-graph.md +3 -2
- package/docs/adr/0017-change-impact-from-the-graph.md +69 -0
- package/docs/adr/README.md +1 -0
- package/docs/ecosystem.md +6 -5
- package/docs/facts.json +5 -4
- package/docs/features.md +2 -2
- package/install/_usage-lifecycle.mjs +44 -0
- package/install/catalog-history.json +1 -1
- package/install/index.mjs +2 -0
- package/install/templates/copilot-instructions.md +1 -0
- package/manifest.json +72 -65
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/review/SKILL.md +2 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +9 -18
- package/pipeline/commands/multi-agent/update/SKILL.md +3 -3
- package/pipeline/lib/json-file-lock.mjs +24 -2
- package/pipeline/multi-agent-refs/_dev-context.md +15 -0
- package/pipeline/multi-agent-refs/analysis/evidence.md +6 -4
- package/pipeline/multi-agent-refs/analysis/locked.md +1 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +2 -0
- package/pipeline/multi-agent-refs/features/analysis-jira.md +5 -0
- package/pipeline/multi-agent-refs/features/code-graph.md +66 -2
- package/pipeline/multi-agent-refs/features/product-context.md +238 -0
- package/pipeline/multi-agent-refs/features/usage-reporting.md +40 -23
- package/pipeline/multi-agent-refs/generate-issue.md +11 -1
- package/pipeline/multi-agent-refs/knowledge.md +20 -0
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +9 -14
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +7 -6
- package/pipeline/multi-agent-refs/platform-parity.md +23 -9
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +24 -0
- package/pipeline/schemas/agent-state.schema.json +31 -0
- package/pipeline/schemas/analysis-spec.schema.json +14 -0
- package/pipeline/schemas/code-graph.schema.json +13 -2
- package/pipeline/schemas/diff-risk.schema.json +2 -1
- package/pipeline/schemas/prefs.schema.json +84 -4
- package/pipeline/scripts/_code-graph.mjs +188 -6
- package/pipeline/scripts/_diff-impact.mjs +319 -0
- package/pipeline/scripts/_supertypes.mjs +133 -0
- package/pipeline/scripts/analysis-story-body.mjs +24 -8
- package/pipeline/scripts/capture-flush.sh +62 -4
- package/pipeline/scripts/code-graph-rules/android.json +1 -0
- package/pipeline/scripts/code-graph-rules/ios.json +5 -0
- package/pipeline/scripts/code-graph-rules/node.json +1 -0
- package/pipeline/scripts/code-graph-rules/python.json +1 -0
- package/pipeline/scripts/diff-risk-score.mjs +80 -9
- package/pipeline/scripts/git-path.mjs +21 -0
- package/pipeline/scripts/graph-affected.mjs +168 -36
- package/pipeline/scripts/graph-confirm.mjs +140 -0
- package/pipeline/scripts/graph-query.mjs +16 -4
- package/pipeline/scripts/learnings-ledger.mjs +3 -2
- package/pipeline/scripts/product-context.mjs +550 -0
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +39 -1
- package/pipeline/scripts/symbol-existence-gate.mjs +21 -9
- package/pipeline/scripts/test-gap-scan.mjs +4 -1
- package/pipeline/scripts/test-strength.mjs +7 -8
- package/pipeline/scripts/usage-identity.mjs +1 -1
- package/pipeline/scripts/usage-register.mjs +181 -15
- package/pipeline/scripts/validate-code-graph.mjs +3 -2
- package/pipeline/scripts/validate-diff-risk.mjs +1 -0
- package/pipeline/scripts/valven-gate.mjs +9 -1
- package/pipeline/scripts/valven-port.mjs +16 -5
- package/pipeline/scripts/valven-split.mjs +6 -4
- package/pipeline/skills/.skill-manifest.json +4 -4
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -1
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +11 -9
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +3 -3
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
* radius of a change rather than its neighbourhood.
|
|
10
10
|
*
|
|
11
11
|
* Inputs:
|
|
12
|
-
* "<symbol>" Symbol name, exact or case-insensitive. Required
|
|
12
|
+
* "<symbol>" Symbol name, exact or case-insensitive. Required unless
|
|
13
|
+
* --diff or --diff-file is given.
|
|
14
|
+
* --diff <spec> Seed from a change instead: `<base>` (working tree against
|
|
15
|
+
* base) or `<base>..<head>`. Hunks map onto the innermost
|
|
16
|
+
* declarations they touch, read on the side of the diff
|
|
17
|
+
* the graph was built from (_diff-impact.mjs graphSide).
|
|
18
|
+
* --diff-file <p> The same, from a saved unified diff.
|
|
19
|
+
* --root <path> Repo for --diff. Default: the root recorded in the graph
|
|
13
20
|
* --graph <path> Graph file. Default: ~/.claude/knowledge/<cwd name>/code-graph.json
|
|
14
21
|
* --depth N Reverse traversal depth. Default: 2
|
|
15
22
|
* --kind K Restrict to one edge kind (repeatable via comma)
|
|
@@ -24,8 +31,16 @@
|
|
|
24
31
|
*/
|
|
25
32
|
|
|
26
33
|
import { resolve } from "node:path";
|
|
34
|
+
import { readFileSync } from "node:fs";
|
|
27
35
|
import { parseFlags } from "./graph-build.mjs";
|
|
28
36
|
import { loadGraph, defaultGraphPath } from "./graph-query.mjs";
|
|
37
|
+
import {
|
|
38
|
+
parseHunkRanges,
|
|
39
|
+
changedSymbols,
|
|
40
|
+
graphFreshness,
|
|
41
|
+
graphSide,
|
|
42
|
+
diffFor,
|
|
43
|
+
} from "./_diff-impact.mjs";
|
|
29
44
|
|
|
30
45
|
/**
|
|
31
46
|
* Nodes whose name matches, preferring an exact hit.
|
|
@@ -62,37 +77,156 @@ export function incoming(graph, kinds = null) {
|
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
/**
|
|
65
|
-
* Walk backwards from each seed.
|
|
80
|
+
* Walk backwards from each seed, over files.
|
|
81
|
+
*
|
|
82
|
+
* Dependents are files: a file depends on a target when it references a
|
|
83
|
+
* symbol or imports a file. The walk continues through what a dependent file
|
|
84
|
+
* itself declares, because the next ring depends on those declarations, not on
|
|
85
|
+
* the file node - references point from files to symbols, so a walk that only
|
|
86
|
+
* followed a file's own incoming edges would stop after one ring. `defines`
|
|
87
|
+
* is never a dependency, and the files that define the seeds are never
|
|
88
|
+
* reported as their own dependents.
|
|
66
89
|
*
|
|
67
90
|
* @param {object} params
|
|
91
|
+
* @param {object} params.graph
|
|
92
|
+
* @param {object[]} params.seeds - symbol or file nodes
|
|
93
|
+
* @param {number} params.depth
|
|
94
|
+
* @param {Set<string>|null} [params.kinds] - dependency edge kinds to follow
|
|
68
95
|
* @returns {Array<{id: string, dist: number, kind: string, node: object}>}
|
|
69
96
|
*/
|
|
70
|
-
export function affected({ graph, seeds, depth, kinds }) {
|
|
97
|
+
export function affected({ graph, seeds, depth, kinds = null }) {
|
|
71
98
|
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
72
99
|
const adj = incoming(graph, kinds);
|
|
73
|
-
const
|
|
100
|
+
const declares = new Map();
|
|
101
|
+
const definer = new Map();
|
|
102
|
+
for (const e of graph.edges) {
|
|
103
|
+
if (e.kind !== "defines") continue;
|
|
104
|
+
if (!declares.has(e.from)) declares.set(e.from, []);
|
|
105
|
+
declares.get(e.from).push(e.to);
|
|
106
|
+
definer.set(e.to, e.from);
|
|
107
|
+
}
|
|
108
|
+
const expand = (node) =>
|
|
109
|
+
node.kind === "file"
|
|
110
|
+
? [node.id, ...(declares.get(node.id) || [])]
|
|
111
|
+
: [node.id, ...(definer.has(node.id) ? [definer.get(node.id)] : [])];
|
|
112
|
+
|
|
113
|
+
const seedFiles = new Set(
|
|
114
|
+
seeds.map((n) => (n.kind === "file" ? n.id : definer.get(n.id))).filter(Boolean),
|
|
115
|
+
);
|
|
116
|
+
const walked = new Set();
|
|
117
|
+
let frontier = [];
|
|
118
|
+
for (const seed of seeds)
|
|
119
|
+
for (const id of expand(seed)) if (!walked.has(id)) walked.add(id) && frontier.push(id);
|
|
120
|
+
|
|
121
|
+
const reported = new Set();
|
|
74
122
|
const out = [];
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
123
|
+
for (let dist = 1; dist <= depth && frontier.length; dist++) {
|
|
124
|
+
const next = [];
|
|
125
|
+
for (const target of frontier) {
|
|
126
|
+
for (const dep of adj.get(target) || []) {
|
|
127
|
+
if (dep.kind === "defines" || seedFiles.has(dep.id) || reported.has(dep.id)) continue;
|
|
128
|
+
const node = byId.get(dep.id);
|
|
129
|
+
if (!node) continue;
|
|
130
|
+
reported.add(dep.id);
|
|
131
|
+
out.push({ id: dep.id, dist, kind: dep.kind, node });
|
|
132
|
+
for (const id of expand(node)) if (!walked.has(id)) walked.add(id) && next.push(id);
|
|
133
|
+
}
|
|
85
134
|
}
|
|
135
|
+
frontier = next;
|
|
86
136
|
}
|
|
87
137
|
return out;
|
|
88
138
|
}
|
|
89
139
|
|
|
140
|
+
function printJson(obj) {
|
|
141
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hitRows(hits) {
|
|
145
|
+
return hits.map((h) => ({
|
|
146
|
+
id: h.id,
|
|
147
|
+
dist: h.dist,
|
|
148
|
+
edgeKind: h.kind,
|
|
149
|
+
path: h.node?.path || null,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function freshnessLine(f) {
|
|
154
|
+
if (!f.stale) return "graph: current";
|
|
155
|
+
const parts = [];
|
|
156
|
+
if (f.commitMoved)
|
|
157
|
+
parts.push(
|
|
158
|
+
`built at ${String(f.baseCommit).slice(0, 12)}, HEAD is ${String(f.head).slice(0, 12)}`,
|
|
159
|
+
);
|
|
160
|
+
if (f.changedFiles.length) parts.push(`${f.changedFiles.length} file(s) edited since the build`);
|
|
161
|
+
if (f.removedFiles.length) parts.push(`${f.removedFiles.length} file(s) removed`);
|
|
162
|
+
return `graph: STALE (${parts.join("; ")}) - rebuild with graph-build.mjs for a current answer`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function runDiff(flags, graph, depth, kinds) {
|
|
166
|
+
const root = flags.root && flags.root !== true ? resolve(flags.root) : graph.root;
|
|
167
|
+
let text;
|
|
168
|
+
if (flags["diff-file"] && flags["diff-file"] !== true) {
|
|
169
|
+
text = readFileSync(resolve(flags["diff-file"]), "utf8");
|
|
170
|
+
} else {
|
|
171
|
+
text = diffFor(root, flags.diff);
|
|
172
|
+
}
|
|
173
|
+
const freshness = graphFreshness(graph, { root });
|
|
174
|
+
const range = flags["diff-file"] && flags["diff-file"] !== true ? undefined : flags.diff;
|
|
175
|
+
const side = graphSide(graph, { root, range });
|
|
176
|
+
const mapped = changedSymbols(graph, parseHunkRanges(text, side));
|
|
177
|
+
const hits = affected({ graph, seeds: mapped.seeds, depth, kinds });
|
|
178
|
+
const result = {
|
|
179
|
+
mode: "diff",
|
|
180
|
+
diff: flags["diff-file"] && flags["diff-file"] !== true ? flags["diff-file"] : flags.diff,
|
|
181
|
+
changedSymbols: mapped.changed.map((c) => ({
|
|
182
|
+
name: c.name,
|
|
183
|
+
path: c.path,
|
|
184
|
+
line: c.line,
|
|
185
|
+
endLine: c.endLine,
|
|
186
|
+
})),
|
|
187
|
+
seeds: mapped.seeds.map((s) => s.id),
|
|
188
|
+
fileLevel: mapped.fileLevel,
|
|
189
|
+
notIndexed: mapped.notIndexed,
|
|
190
|
+
affected: hitRows(hits),
|
|
191
|
+
side,
|
|
192
|
+
freshness,
|
|
193
|
+
};
|
|
194
|
+
if (flags.json === true) return printJson(result);
|
|
195
|
+
|
|
196
|
+
process.stdout.write(`# affected by diff ${result.diff}\n\n`);
|
|
197
|
+
for (const c of result.changedSymbols)
|
|
198
|
+
process.stdout.write(`* ${c.name} ${c.path}:${c.line}-${c.endLine}\n`);
|
|
199
|
+
for (const p of result.fileLevel)
|
|
200
|
+
process.stdout.write(`* ${p} (whole file: graph has no spans, rebuild for symbol level)\n`);
|
|
201
|
+
const files = new Set();
|
|
202
|
+
process.stdout.write("\n");
|
|
203
|
+
for (const hit of hits) {
|
|
204
|
+
if (hit.node?.path) files.add(hit.node.path);
|
|
205
|
+
process.stdout.write(
|
|
206
|
+
`${" ".repeat(hit.dist)}- ${hit.node?.name || hit.id} [${hit.kind}] ${hit.node?.path || "-"}\n`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
process.stdout.write(`\n${hits.length} dependents across ${files.size} files\n`);
|
|
210
|
+
if (result.notIndexed.length)
|
|
211
|
+
process.stdout.write(`not in the graph: ${result.notIndexed.join(", ")}\n`);
|
|
212
|
+
process.stdout.write(`${freshnessLine(freshness)}\n`);
|
|
213
|
+
}
|
|
214
|
+
|
|
90
215
|
function main() {
|
|
91
216
|
const { flags, positional } = parseFlags(process.argv.slice(2));
|
|
92
217
|
const name = positional.join(" ").trim();
|
|
93
|
-
|
|
218
|
+
const diffMode = flags.diff !== undefined || flags["diff-file"] !== undefined;
|
|
219
|
+
if (diffMode) {
|
|
220
|
+
const spec = flags["diff-file"] !== undefined ? flags["diff-file"] : flags.diff;
|
|
221
|
+
if (spec === true || String(spec).startsWith("-")) {
|
|
222
|
+
process.stderr.write(
|
|
223
|
+
"graph-affected: --diff takes <base> or <base>..<head>; --diff-file takes a path\n",
|
|
224
|
+
);
|
|
225
|
+
process.exit(64);
|
|
226
|
+
}
|
|
227
|
+
} else if (!name) {
|
|
94
228
|
process.stderr.write(
|
|
95
|
-
'graph-affected: a symbol name is required, e.g. graph-affected "Flight"
|
|
229
|
+
'graph-affected: a symbol name is required, e.g. graph-affected "Flight", or --diff <base>\n',
|
|
96
230
|
);
|
|
97
231
|
process.exit(64);
|
|
98
232
|
}
|
|
@@ -109,12 +243,20 @@ function main() {
|
|
|
109
243
|
const depth = Number(flags.depth) > 0 ? Number(flags.depth) : 2;
|
|
110
244
|
const kinds = flags.kind && flags.kind !== true ? new Set(String(flags.kind).split(",")) : null;
|
|
111
245
|
|
|
246
|
+
if (diffMode) {
|
|
247
|
+
try {
|
|
248
|
+
return runDiff(flags, graph, depth, kinds);
|
|
249
|
+
} catch (e) {
|
|
250
|
+
process.stderr.write(`graph-affected: ${e.message}\n`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
112
255
|
const seeds = findByName(graph, name);
|
|
256
|
+
const freshness = graphFreshness(graph);
|
|
113
257
|
if (seeds.length === 0) {
|
|
114
258
|
if (flags.json === true) {
|
|
115
|
-
|
|
116
|
-
JSON.stringify({ symbol: name, seeds: [], affected: [] }, null, 2) + "\n",
|
|
117
|
-
);
|
|
259
|
+
printJson({ symbol: name, seeds: [], affected: [], freshness });
|
|
118
260
|
} else {
|
|
119
261
|
process.stdout.write(`no node named '${name}'\n`);
|
|
120
262
|
}
|
|
@@ -124,23 +266,12 @@ function main() {
|
|
|
124
266
|
const hits = affected({ graph, seeds, depth, kinds });
|
|
125
267
|
|
|
126
268
|
if (flags.json === true) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
id: h.id,
|
|
134
|
-
dist: h.dist,
|
|
135
|
-
edgeKind: h.kind,
|
|
136
|
-
path: h.node?.path || null,
|
|
137
|
-
})),
|
|
138
|
-
},
|
|
139
|
-
null,
|
|
140
|
-
2,
|
|
141
|
-
) + "\n",
|
|
142
|
-
);
|
|
143
|
-
return;
|
|
269
|
+
return printJson({
|
|
270
|
+
symbol: name,
|
|
271
|
+
seeds: seeds.map((s) => s.id),
|
|
272
|
+
affected: hitRows(hits),
|
|
273
|
+
freshness,
|
|
274
|
+
});
|
|
144
275
|
}
|
|
145
276
|
|
|
146
277
|
process.stdout.write(`# affected by ${name}\n\n`);
|
|
@@ -156,6 +287,7 @@ function main() {
|
|
|
156
287
|
);
|
|
157
288
|
}
|
|
158
289
|
process.stdout.write(`\n${hits.length} dependents across ${files.size} files\n`);
|
|
290
|
+
process.stdout.write(`${freshnessLine(freshness)}\n`);
|
|
159
291
|
}
|
|
160
292
|
|
|
161
293
|
if (process.argv[1] && process.argv[1].endsWith("graph-affected.mjs")) main();
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file graph-confirm.mjs - a name-based blast radius, checked by a language server.
|
|
5
|
+
*
|
|
6
|
+
* The code graph joins files to declarations by name, so a dependent it lists
|
|
7
|
+
* may only share a name with the changed type, and a dependent behind an
|
|
8
|
+
* ambiguous name is missing. The toolkit's `code_references` answers the same
|
|
9
|
+
* question semantically when the project has an index. This merges the two
|
|
10
|
+
* without trusting either blindly:
|
|
11
|
+
*
|
|
12
|
+
* confirmed a direct dependent the server also found
|
|
13
|
+
* name-only a direct dependent a ready semantic index did not find
|
|
14
|
+
* unchecked a deeper ring the server did not name (it depends on an
|
|
15
|
+
* intermediate declaration), or no answer could check it
|
|
16
|
+
*
|
|
17
|
+
* A deeper ring the server names directly is confirmed too.
|
|
18
|
+
*
|
|
19
|
+
* `lspOnly` lists files the server found that the graph did not. Only a result
|
|
20
|
+
* with `semantic: true` and `indexReady: true` confirms or demotes anything; a
|
|
21
|
+
* fallback answer or an index still building leaves every file unchecked,
|
|
22
|
+
* because "not found" from an unready index is not evidence.
|
|
23
|
+
*
|
|
24
|
+
* Inputs:
|
|
25
|
+
* --affected <path> graph-affected --json output
|
|
26
|
+
* --references <path> JSON array of code_references results, one per changed type
|
|
27
|
+
* --root <path> repository root the server's absolute paths are under
|
|
28
|
+
*
|
|
29
|
+
* Exit codes: 0 answered, 64 usage error.
|
|
30
|
+
*
|
|
31
|
+
* Zero runtime dependencies (ADR-0004).
|
|
32
|
+
*
|
|
33
|
+
* @module pipeline/scripts/graph-confirm
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
37
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
38
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {{affected: Array<{path: string|null, dist: number}>}} affectedDoc
|
|
42
|
+
* @param {Array<{semantic?: boolean, indexReady?: boolean, results?: Array<{file: string}>}>} references
|
|
43
|
+
* @param {{root: string}} opts
|
|
44
|
+
*/
|
|
45
|
+
export function confirmImpact(affectedDoc, references, { root }) {
|
|
46
|
+
const usable = (references || []).filter(
|
|
47
|
+
(r) => r && r.semantic === true && r.indexReady === true,
|
|
48
|
+
);
|
|
49
|
+
const checked = usable.length > 0 && usable.length === (references || []).length;
|
|
50
|
+
const real = (p) => {
|
|
51
|
+
const abs = resolve(p);
|
|
52
|
+
try {
|
|
53
|
+
return realpathSync(abs);
|
|
54
|
+
} catch {
|
|
55
|
+
const parent = dirname(abs);
|
|
56
|
+
return parent === abs ? abs : join(real(parent), basename(abs));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const repo = real(root);
|
|
60
|
+
// A server answers with paths relative to its own workspace root (the
|
|
61
|
+
// nearest package), absolute ones through whatever spelling of a symlinked
|
|
62
|
+
// directory it was given, and files outside the repository (SDKs).
|
|
63
|
+
const rel = (file, serverRoot) => {
|
|
64
|
+
const abs = isAbsolute(file) ? file : resolve(serverRoot || root, file);
|
|
65
|
+
const r = relative(repo, real(abs));
|
|
66
|
+
return r && !r.startsWith("..") && !isAbsolute(r) ? r : null;
|
|
67
|
+
};
|
|
68
|
+
const found = new Set();
|
|
69
|
+
for (const r of usable)
|
|
70
|
+
for (const loc of r.results || []) {
|
|
71
|
+
const f = loc?.file ? rel(loc.file, r.root) : null;
|
|
72
|
+
if (f) found.add(f);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const seedPaths = new Set((affectedDoc.changedSymbols || []).map((c) => c.path).filter(Boolean));
|
|
76
|
+
for (const id of affectedDoc.seeds || []) {
|
|
77
|
+
const s = String(id);
|
|
78
|
+
if (s.startsWith("sym:")) seedPaths.add(s.slice(4, s.lastIndexOf("#")));
|
|
79
|
+
else if (s.startsWith("file:")) seedPaths.add(s.slice(5));
|
|
80
|
+
}
|
|
81
|
+
const graphPaths = new Set();
|
|
82
|
+
const affected = (affectedDoc.affected || []).map((row) => {
|
|
83
|
+
if (row.path) graphPaths.add(row.path);
|
|
84
|
+
let confirmation = "unchecked";
|
|
85
|
+
if (checked && row.path && found.has(row.path)) confirmation = "confirmed";
|
|
86
|
+
else if (checked && row.dist === 1 && row.path) confirmation = "name-only";
|
|
87
|
+
return { ...row, confirmation };
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const lspOnly = checked
|
|
91
|
+
? [...found].filter((f) => !graphPaths.has(f) && !seedPaths.has(f)).sort()
|
|
92
|
+
: [];
|
|
93
|
+
const byPath = new Map();
|
|
94
|
+
for (const a of affected) if (a.path && !byPath.has(a.path)) byPath.set(a.path, a.confirmation);
|
|
95
|
+
const count = (c) => [...byPath.values()].filter((v) => v === c).length;
|
|
96
|
+
return {
|
|
97
|
+
affected,
|
|
98
|
+
lspOnly,
|
|
99
|
+
summary: {
|
|
100
|
+
confirmed: count("confirmed"),
|
|
101
|
+
nameOnly: count("name-only"),
|
|
102
|
+
unchecked: count("unchecked"),
|
|
103
|
+
lspOnly: lspOnly.length,
|
|
104
|
+
checked,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function flag(argv, name) {
|
|
110
|
+
const i = argv.indexOf(name);
|
|
111
|
+
const v = i >= 0 ? argv[i + 1] : undefined;
|
|
112
|
+
return v && !v.startsWith("--") ? v : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main(argv) {
|
|
116
|
+
const affectedPath = flag(argv, "--affected");
|
|
117
|
+
const referencesPath = flag(argv, "--references");
|
|
118
|
+
const root = flag(argv, "--root");
|
|
119
|
+
if (!affectedPath || !referencesPath || !root) {
|
|
120
|
+
process.stderr.write(
|
|
121
|
+
"usage: graph-confirm.mjs --affected <json> --references <json> --root <path>\n",
|
|
122
|
+
);
|
|
123
|
+
process.exitCode = 64;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let affectedDoc;
|
|
127
|
+
let references;
|
|
128
|
+
try {
|
|
129
|
+
affectedDoc = JSON.parse(readFileSync(affectedPath, "utf8"));
|
|
130
|
+
references = JSON.parse(readFileSync(referencesPath, "utf8"));
|
|
131
|
+
} catch (e) {
|
|
132
|
+
process.stderr.write(`graph-confirm: ${e.message}\n`);
|
|
133
|
+
process.exitCode = 64;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!Array.isArray(references)) references = [references];
|
|
137
|
+
process.stdout.write(`${JSON.stringify(confirmImpact(affectedDoc, references, { root }))}\n`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (invokedDirectly(import.meta.url)) main(process.argv.slice(2));
|
|
@@ -99,6 +99,20 @@ function renderNode(node) {
|
|
|
99
99
|
return `${node.name} (${kind}) ${where}`;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The line main() prints for a hit. The budget is charged for this, not for
|
|
104
|
+
* the bare node text, so the token count it reports is the one the caller pays.
|
|
105
|
+
*
|
|
106
|
+
* @param {number} dist
|
|
107
|
+
* @param {string} line
|
|
108
|
+
* @param {string|null} via
|
|
109
|
+
* @returns {string}
|
|
110
|
+
*/
|
|
111
|
+
export function printedLine(dist, line, via) {
|
|
112
|
+
const prefix = dist === 0 ? "*" : " ".repeat(dist) + "-";
|
|
113
|
+
return `${prefix} ${line}${via ? ` [${via}]` : ""}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
102
116
|
/**
|
|
103
117
|
* Seed, then breadth-first expand until the budget is spent.
|
|
104
118
|
*
|
|
@@ -119,7 +133,7 @@ export function traverse({ graph, question, budget, depth, seedCount }) {
|
|
|
119
133
|
const queue = seeds.map((n) => ({ node: n, dist: 0, via: null }));
|
|
120
134
|
while (queue.length) {
|
|
121
135
|
const { node, dist, via } = queue.shift();
|
|
122
|
-
const cost = Math.ceil(renderNode(node).length / CHARS_PER_TOKEN);
|
|
136
|
+
const cost = Math.ceil(printedLine(dist, renderNode(node), via).length / CHARS_PER_TOKEN);
|
|
123
137
|
if (tokens + cost > budget) {
|
|
124
138
|
truncated = true;
|
|
125
139
|
break;
|
|
@@ -177,9 +191,7 @@ function main() {
|
|
|
177
191
|
return;
|
|
178
192
|
}
|
|
179
193
|
for (const hit of result.hits) {
|
|
180
|
-
|
|
181
|
-
const via = hit.via ? ` [${hit.via}]` : "";
|
|
182
|
-
process.stdout.write(`${prefix} ${hit.line}${via}\n`);
|
|
194
|
+
process.stdout.write(`${printedLine(hit.dist, hit.line, hit.via)}\n`);
|
|
183
195
|
}
|
|
184
196
|
process.stdout.write(
|
|
185
197
|
`\n${result.hits.length} nodes, ~${result.tokens} tokens` +
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
* Distill the triage's rejected[] findings into rejected-preference
|
|
42
42
|
* entries so reviewers stop re-raising them. SAFETY: blocking-
|
|
43
43
|
* severity rejections are NOT distilled - a wrong rejection of a
|
|
44
|
-
* blocking issue must never permanently suppress that class.
|
|
44
|
+
* blocking issue must never permanently suppress that class. Exit 2
|
|
45
|
+
* when nothing new was distilled (a repeat of the same triage).
|
|
45
46
|
* forget [--statement "..."] [--kind <kind>] [--repo-slug <slug>]
|
|
46
47
|
* Remove ledger entries matching the statement substring and/or
|
|
47
48
|
* kind (rewrites the file). Use to clear a bad/stale learning.
|
|
@@ -311,7 +312,7 @@ function cmdFromTriage() {
|
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
314
|
process.stdout.write(JSON.stringify({ ok: true, slug, added: written, skippedBlocking }) + "\n");
|
|
314
|
-
process.exitCode = 0;
|
|
315
|
+
process.exitCode = written ? 0 : 2;
|
|
315
316
|
return;
|
|
316
317
|
}
|
|
317
318
|
|