@azure-id/orc 1.9.0 → 1.9.2
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 +202 -0
- package/README-id.md +57 -59
- package/README.md +53 -54
- package/bin/build-agents.js +109 -109
- package/bin/cli.js +262 -58
- package/bin/graph-audit.js +235 -0
- package/bin/graph-extract.js +308 -3
- package/bin/graph-gain.js +23 -3
- package/bin/graph-map.js +9 -1
- package/bin/graph-query.js +152 -10
- package/bin/graph-shard.js +6 -1
- package/bin/graph-signals.js +287 -4
- package/bin/graph.js +717 -658
- package/bin/onboarding-content.js +4 -4
- package/bin/pricing.json +207 -200
- package/bin/verify-contracts.js +117 -2
- package/bin/verify-package.js +11 -7
- package/bin/webui/css/04-motion.css +162 -152
- package/bin/webui/css/panels/knowledge.css +563 -116
- package/bin/webui/fixtures/extra.js +2036 -2036
- package/bin/webui/fixtures/hookui.js +5 -5
- package/bin/webui/fixtures/knowledge.js +21 -4
- package/bin/webui/fixtures/settings.js +305 -305
- package/bin/webui/i18n/en/knowledge.json +122 -4
- package/bin/webui/i18n/id/knowledge.json +122 -4
- package/bin/webui/js/panels/knowledge.js +573 -9
- package/bin/webui/js/panels/overview.js +13 -4
- package/mock-run/context-combiner.md +100 -100
- package/mock-run/orc-budget.md +534 -534
- package/mock-run/orc-challenge-council.md +262 -262
- package/mock-run/orc-ultra.md +103 -103
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +43 -43
- package/templates/agents/orc-advisor-opus-5-xhigh.md +56 -56
- package/templates/agents/orc-analyze-mini-opus-5-med.md +60 -60
- package/templates/agents/orc-analyze-mini-sonnet-5-high.md +58 -58
- package/templates/agents/orc-challenge-advisor-opus-5-med.md +75 -75
- package/templates/agents/orc-challenge-contrarian-opus-5-high.md +110 -110
- package/templates/agents/orc-challenge-executor-opus-5-med.md +114 -114
- package/templates/agents/orc-challenge-expansionist-opus-5-med.md +112 -112
- package/templates/agents/orc-challenge-judge-opus-5-high.md +132 -132
- package/templates/agents/orc-challenge-outsider-opus-5-low.md +109 -109
- package/templates/agents/orc-challenge-principles-opus-5-high.md +109 -109
- package/templates/agents/orc-challenge-reader-opus-5-low.md +90 -90
- package/templates/agents/orc-claude-writer-opus-5-med.md +55 -55
- package/templates/agents/orc-context-combiner-opus-5-high.md +88 -88
- package/templates/agents/orc-doc-checker-opus-5-low.md +108 -108
- package/templates/agents/orc-doc-writer-opus-5-med.md +134 -134
- package/templates/agents/orc-executor-haiku-4-5.md +2 -1
- package/templates/agents/orc-executor-opus-4-7-high.md +2 -1
- package/templates/agents/orc-executor-opus-4-7-med.md +2 -1
- package/templates/agents/orc-executor-opus-4-8-high.md +2 -1
- package/templates/agents/orc-executor-opus-5-high.md +4 -3
- package/templates/agents/orc-executor-opus-5-low.md +4 -3
- package/templates/agents/orc-executor-opus-5-med.md +4 -3
- package/templates/agents/orc-executor-sonnet-4-6-high.md +2 -1
- package/templates/agents/orc-executor-sonnet-4-6-med.md +2 -1
- package/templates/agents/orc-executor-sonnet-5-high.md +2 -1
- package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -1
- package/templates/agents/orc-judge-opus-5-xhigh.md +85 -85
- package/templates/agents/orc-learn-writer-opus-5-low.md +73 -73
- package/templates/agents/orc-pattern-codifier-opus-5-med.md +65 -65
- package/templates/agents/orc-planner-mini-opus-5-med.md +4 -4
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +1 -1
- package/templates/agents/orc-planner-opus-5-med.md +160 -160
- package/templates/agents/orc-recon-opus-5-low.md +18 -8
- package/templates/agents/orc-recon-sonnet-4-6-med.md +15 -5
- package/templates/agents/orc-retro-opus-5-med.md +3 -3
- package/templates/agents/orc-reviewer-opus-5-med.md +60 -60
- package/templates/agents/orc-scout-opus-5-low.md +40 -40
- package/templates/agents/orc-system-analyst-opus-5-high.md +120 -120
- package/templates/agents/orc-test-author-opus-5-med.md +71 -71
- package/templates/agents/orc-test-designer-opus-5-high.md +158 -158
- package/templates/agents/orc-test-interpreter-opus-5-low.md +130 -130
- package/templates/agents/orc-verifier-opus-5-med.md +69 -69
- package/templates/agents/orc-wiki-scanner-opus-5-med.md +81 -81
- package/templates/commands/orc-ultra.md +17 -17
- package/templates/commands/orc-verify.md +11 -11
- package/templates/hooks/README.md +13 -3
- package/templates/hooks/orc-effort-guard.js +178 -178
- package/templates/hooks/orc-graph-hook.js +53 -7
- package/templates/hooks/orc-statusline.js +6 -6
- package/templates/skills/_shared/code-graph.md +57 -18
- package/templates/skills/_shared/config-precedence.md +198 -198
- package/templates/skills/_shared/extra-dispatch.md +1346 -1346
- package/templates/skills/_shared/opus5-only.md +8 -8
- package/templates/skills/_shared/phases/analyst-gates.md +136 -136
- package/templates/skills/_shared/phases/execution.md +4 -4
- package/templates/skills/_shared/phases/planning.md +3 -3
- package/templates/skills/_shared/phases/preflight.md +1 -1
- package/templates/skills/_shared/phases/review.md +4 -2
- package/templates/skills/_shared/phases/ship.md +1 -1
- package/templates/skills/_shared/phases/trace.md +3 -2
- package/templates/skills/_shared/read-ladder.md +14 -1
- package/templates/skills/_shared/return-validation.md +1 -1
- package/templates/skills/context-combiner/SKILL.md +1 -1
- package/templates/skills/context-combiner/schemas/combined-report.md +78 -78
- package/templates/skills/orc/README.md +2 -2
- package/templates/skills/orc/SKILL.md +8 -8
- package/templates/skills/orc/config.md +9 -9
- package/templates/skills/orc/examples/full-run-mock.md +73 -73
- package/templates/skills/orc/references/effort-and-mode.md +222 -222
- package/templates/skills/orc/references/preflight-report.md +2 -2
- package/templates/skills/orc/references/ultra-mode.md +3 -3
- package/templates/skills/orc/subskills/orc-planner/SKILL.md +2 -2
- package/templates/skills/orc/subskills/orc-planner-mini/SKILL.md +121 -121
- package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +76 -76
- package/templates/skills/orc/subskills/orc-testgen/SKILL.md +45 -45
- package/templates/skills/orc-analyze/SKILL.md +2 -2
- package/templates/skills/orc-analyze/examples/analyze-mock.md +42 -42
- package/templates/skills/orc-analyze/schemas/report-audit.md +83 -83
- package/templates/skills/orc-analyze/schemas/report-prose.md +63 -63
- package/templates/skills/orc-analyze/schemas/report-requirement.md +78 -78
- package/templates/skills/orc-challenge/README.md +142 -142
- package/templates/skills/orc-challenge/examples/council-full-roster.md +273 -273
- package/templates/skills/orc-challenge/references/council.md +315 -315
- package/templates/skills/orc-challenge/references/intake.md +171 -171
- package/templates/skills/orc-claude/SKILL.md +14 -14
- package/templates/skills/orc-diy/README.md +143 -143
- package/templates/skills/orc-diy/references/flow-schema.md +1 -1
- package/templates/skills/orc-doc/README.md +229 -229
- package/templates/skills/orc-doc/examples/orc-doc-prd-run.md +325 -325
- package/templates/skills/orc-doc/references/chunking.md +527 -527
- package/templates/skills/orc-fast/SKILL.md +4 -4
- package/templates/skills/orc-learn/SKILL.md +14 -14
- package/templates/skills/orc-learn/examples/learn-run-mock.md +61 -61
- package/templates/skills/orc-mini/SKILL.md +16 -20
- package/templates/skills/orc-mini/references/complexity.md +25 -11
- package/templates/skills/orc-quick/SKILL.md +12 -13
- package/templates/skills/orc-quick/references/gh-mode.md +1 -1
- package/templates/skills/orc-quick/references/look.md +5 -5
- package/templates/skills/orc-verify/SKILL.md +15 -15
- package/templates/skills/orc-verify/examples/verify-mock.md +33 -33
- package/templates/skills/orc-wiki/references/extra.md +1 -1
package/bin/graph-signals.js
CHANGED
|
@@ -35,6 +35,51 @@ const COCHANGE_MIN = 3;
|
|
|
35
35
|
const COCHANGE_SINCE = "6.months";
|
|
36
36
|
const COCHANGE_ROWS = 12;
|
|
37
37
|
|
|
38
|
+
// ── the complexity thresholds (v1.9.1 V2, DE-12) ────────────────────────────
|
|
39
|
+
// They live HERE, with their reasons, because the lane used to hold them in
|
|
40
|
+
// prose and count the rows itself. A threshold in two places is a threshold
|
|
41
|
+
// that drifts. `orc-mini/references/complexity.md` quotes these by NAME and
|
|
42
|
+
// VALUE, and `bin/verify-contracts.js` fails if one side is renamed alone.
|
|
43
|
+
//
|
|
44
|
+
// COMPLEXITY_CALLER_FILES callers in this many files OUTSIDE the plan mean the
|
|
45
|
+
// change reaches further than one executor can see.
|
|
46
|
+
// COMPLEXITY_CALLERS this many confident callers outside the plan mean the
|
|
47
|
+
// same, by weight rather than by spread.
|
|
48
|
+
// COMPLEXITY_COCHANGE_MIN a file that changed WITH an operand this many times
|
|
49
|
+
// and is not in the plan is a file the plan forgot.
|
|
50
|
+
// It is COCHANGE_MIN by construction — the same
|
|
51
|
+
// evidence, read for a different question.
|
|
52
|
+
const COMPLEXITY_CALLER_FILES = 4;
|
|
53
|
+
const COMPLEXITY_CALLERS = 8;
|
|
54
|
+
const COMPLEXITY_COCHANGE_MIN = COCHANGE_MIN;
|
|
55
|
+
|
|
56
|
+
// ── the THIN thresholds (v1.9.1 A1, DE-4) ───────────────────────────────────
|
|
57
|
+
// A graph is THIN when it holds so few named symbols that most questions asked
|
|
58
|
+
// of it answer "not found" — and nothing, until now, said so. All three must
|
|
59
|
+
// hold, because each one alone is a false alarm:
|
|
60
|
+
//
|
|
61
|
+
// THIN_MIN_FILES a small repository has few symbols because it is small.
|
|
62
|
+
// THIN_PER_FILE under three named symbols per file, a `ctx` on a name the
|
|
63
|
+
// request mentions is more likely to miss than to hit.
|
|
64
|
+
// THIN_ZERO_SHARE the decisive one: files the parser read as EMPTY. Measured
|
|
65
|
+
// at 0.97 for `.vue` on a real front-end project, where the
|
|
66
|
+
// graph@5 parser did not read Options API object members
|
|
67
|
+
// (graph@6, W5b, does — THIN stays for what it still misses).
|
|
68
|
+
const THIN_MIN_FILES = 30;
|
|
69
|
+
const THIN_PER_FILE = 3;
|
|
70
|
+
const THIN_ZERO_SHARE = 0.4;
|
|
71
|
+
|
|
72
|
+
function isThin(density, files) {
|
|
73
|
+
if (!density) return false;
|
|
74
|
+
return (
|
|
75
|
+
Number(files) >= THIN_MIN_FILES &&
|
|
76
|
+
Number(density.symbols_per_file) < THIN_PER_FILE &&
|
|
77
|
+
Number(density.zero_share) >= THIN_ZERO_SHARE
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const plural = (n, word, many) => `${n} ${n === 1 ? word : many || word + "s"}`;
|
|
82
|
+
|
|
38
83
|
function git(root, args) {
|
|
39
84
|
return spawnSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: GIT_MAX_BUFFER });
|
|
40
85
|
}
|
|
@@ -142,12 +187,27 @@ function graphChanges(claudeDir, root, opts) {
|
|
|
142
187
|
}
|
|
143
188
|
}
|
|
144
189
|
|
|
190
|
+
// V1 (v1.9.1) `--files=`: the lane used to take the whole answer and keep the
|
|
191
|
+
// rows whose file it had just written. The CLI restricts them instead, so the
|
|
192
|
+
// rows it never wanted never enter its context.
|
|
193
|
+
const only = Array.isArray(opts.files) && opts.files.length ? new Set(opts.files.map(norm)) : null;
|
|
194
|
+
if (only) for (const k of [...hunks.keys()]) if (!only.has(k)) hunks.delete(k);
|
|
195
|
+
|
|
145
196
|
const rows = [];
|
|
146
197
|
const files = [...hunks.keys()].filter((f) => model.fileSet.has(f)).sort();
|
|
147
198
|
const indexable = new Set(Object.keys(G.LANG_BY_EXT));
|
|
148
199
|
const absent = [...hunks.keys()]
|
|
149
200
|
.filter((f) => !model.fileSet.has(f) && indexable.has(path.posix.extname(f).toLowerCase()))
|
|
150
201
|
.sort();
|
|
202
|
+
// Which KIND of edge reaches each test file: a route beats a call, a call
|
|
203
|
+
// beats a name. A file that reaches two ways is counted once, under the
|
|
204
|
+
// strongest — otherwise the three numbers do not add up to the total.
|
|
205
|
+
const TEST_RANK = { route: 3, call: 2, name: 1 };
|
|
206
|
+
const testKind = new Map();
|
|
207
|
+
const noteTest = (file, kind) => {
|
|
208
|
+
const had = testKind.get(file);
|
|
209
|
+
if (!had || TEST_RANK[kind] > TEST_RANK[had]) testKind.set(file, kind);
|
|
210
|
+
};
|
|
151
211
|
for (const rel of files) {
|
|
152
212
|
const ranges = hunks.get(rel);
|
|
153
213
|
for (const sym of (model.byFile[rel] || {}).symbols || []) {
|
|
@@ -157,6 +217,8 @@ function graphChanges(claudeDir, root, opts) {
|
|
|
157
217
|
const c = Q.callersOf(model, sym);
|
|
158
218
|
const callerFiles = [...new Set(c.confident.map((x) => x.sym.file))];
|
|
159
219
|
const tests = [...new Set([...callerFiles.filter((f) => Q.TEST_FILE.test(f)), ...testsByName(model, rel)])];
|
|
220
|
+
for (const x of c.confident) if (Q.TEST_FILE.test(x.sym.file)) noteTest(x.sym.file, x.state === "ROUTE" ? "route" : "call");
|
|
221
|
+
for (const f of testsByName(model, rel)) noteTest(f, "name");
|
|
160
222
|
// The reason is part of the answer. "high" with nothing behind it is a
|
|
161
223
|
// number nobody can check, and nobody should act on.
|
|
162
224
|
const why = [];
|
|
@@ -185,6 +247,39 @@ function graphChanges(claudeDir, root, opts) {
|
|
|
185
247
|
rows.sort((a, b) => order[a.risk] - order[b.risk] || b.fan_in - a.fan_in || a.file.localeCompare(b.file) || a.lines[0] - b.lines[0]);
|
|
186
248
|
const counts = { high: 0, medium: 0, low: 0 };
|
|
187
249
|
for (const r of rows) counts[r.risk]++;
|
|
250
|
+
|
|
251
|
+
// V1: the CLI computes the line. Both lines used to be assembled by the
|
|
252
|
+
// orchestrator from `symbols[]`, which is why `symbols[]` had to travel into
|
|
253
|
+
// the main session to be counted there. Now the numbers travel and the rows
|
|
254
|
+
// do not.
|
|
255
|
+
const testFiles = new Set(rows.flatMap((r) => r.tests));
|
|
256
|
+
const byKind = { route: 0, call: 0, name: 0 };
|
|
257
|
+
for (const f of testFiles) byKind[testKind.get(f) || "name"]++;
|
|
258
|
+
const totals = {
|
|
259
|
+
symbols: rows.length,
|
|
260
|
+
callers: rows.reduce((a, r) => a + r.fan_in, 0),
|
|
261
|
+
caller_files: new Set(rows.flatMap((r) => r.caller_files)).size,
|
|
262
|
+
maybe: rows.reduce((a, r) => a + r.maybe_callers, 0),
|
|
263
|
+
tests: testFiles.size,
|
|
264
|
+
tests_by_kind: byKind,
|
|
265
|
+
high: counts.high,
|
|
266
|
+
medium: counts.medium,
|
|
267
|
+
low: counts.low,
|
|
268
|
+
not_in_graph: absent.length,
|
|
269
|
+
};
|
|
270
|
+
// The PATHS behind `totals.tests`. A count alone cannot be run, and the lane
|
|
271
|
+
// that used to read them out of `symbols[]` no longer receives that array.
|
|
272
|
+
const tests = [...testFiles].sort();
|
|
273
|
+
const worst = rows[0];
|
|
274
|
+
const blast_line = rows.length
|
|
275
|
+
? `blast radius ${plural(totals.symbols, "symbol")} · callers ${totals.callers} in ${plural(totals.caller_files, "file")}` +
|
|
276
|
+
(totals.maybe ? ` · maybe ${totals.maybe}` : "") +
|
|
277
|
+
` · tests reach ${totals.tests} · risk ${worst.risk}: ${worst.qname} (${worst.why.join(", ")})`
|
|
278
|
+
: `blast radius none indexed${absent.length ? ` (${plural(absent.length, "file")} not in the graph)` : ""}`;
|
|
279
|
+
const tests_line = totals.tests
|
|
280
|
+
? `tests reached ${plural(totals.tests, "file")} (ROUTE ${byKind.route} · call ${byKind.call}${byKind.name ? ` · name ${byKind.name}` : ""})`
|
|
281
|
+
: "tests reached none";
|
|
282
|
+
|
|
188
283
|
return {
|
|
189
284
|
ok: true,
|
|
190
285
|
state: "found",
|
|
@@ -193,6 +288,10 @@ function graphChanges(claudeDir, root, opts) {
|
|
|
193
288
|
not_in_graph: absent,
|
|
194
289
|
symbols: rows,
|
|
195
290
|
counts,
|
|
291
|
+
totals,
|
|
292
|
+
tests,
|
|
293
|
+
tests_line,
|
|
294
|
+
blast_line,
|
|
196
295
|
generation: model.meta.generation || 0,
|
|
197
296
|
exit: 0,
|
|
198
297
|
};
|
|
@@ -231,9 +330,15 @@ function buildCochange(root) {
|
|
|
231
330
|
function graphCochange(claudeDir, root, opts) {
|
|
232
331
|
const rel = norm(opts.file || "");
|
|
233
332
|
if (!rel) return { ok: false, reason: "missing-operand", exit: 1 };
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
333
|
+
// `opts.at` is the HEAD a caller has already asked git for. V2 asks this
|
|
334
|
+
// question once per DECLARED FILE, and one `rev-parse` per file is one git
|
|
335
|
+
// process per file for an answer that cannot change between them.
|
|
336
|
+
let at = opts.at;
|
|
337
|
+
if (!at) {
|
|
338
|
+
const head = git(root, ["rev-parse", "HEAD"]);
|
|
339
|
+
if (head.status !== 0) return { ok: false, state: "unavailable", reason: "not-git", exit: 1 };
|
|
340
|
+
at = head.stdout.trim();
|
|
341
|
+
}
|
|
237
342
|
|
|
238
343
|
let cache = null;
|
|
239
344
|
try {
|
|
@@ -274,4 +379,182 @@ function graphCochange(claudeDir, root, opts) {
|
|
|
274
379
|
};
|
|
275
380
|
}
|
|
276
381
|
|
|
277
|
-
|
|
382
|
+
// ── complexity (v1.9.1 V2) ──────────────────────────────────────────────────
|
|
383
|
+
// exit codes are `impact`'s — this is an ADDITION to that answer, never a
|
|
384
|
+
// command of its own.
|
|
385
|
+
//
|
|
386
|
+
// The question is mini's: is this change small enough for one executor? It used
|
|
387
|
+
// to be answered by the orchestrator, which ran `impact` and one `cochange` per
|
|
388
|
+
// declared file, read `callers[]` and `rows[]` in its own context, and applied
|
|
389
|
+
// four thresholds written in prose. Now ONE call answers it, the four
|
|
390
|
+
// thresholds are the constants at the top of this file, and the lane prints the
|
|
391
|
+
// line it is handed.
|
|
392
|
+
//
|
|
393
|
+
// The verdict is never a decision. `recommend-orc` is a recommendation the
|
|
394
|
+
// lane OFFERS; the user chooses.
|
|
395
|
+
|
|
396
|
+
// `--risk=<class>[@<file:line>],…`. An unparsable item is DROPPED and named,
|
|
397
|
+
// never guessed at: a risk class the reader cannot check is worse than none.
|
|
398
|
+
function parseRisk(spec) {
|
|
399
|
+
const risk = [];
|
|
400
|
+
const ignored = [];
|
|
401
|
+
for (const raw of String(spec || "").split(",")) {
|
|
402
|
+
const item = raw.trim();
|
|
403
|
+
if (!item) continue;
|
|
404
|
+
const m = /^([a-z][a-z0-9-]*)(?:@(.+))?$/.exec(item);
|
|
405
|
+
if (!m) {
|
|
406
|
+
ignored.push(item);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
risk.push({ class: m[1], ...(m[2] ? { at: m[2] } : {}) });
|
|
410
|
+
}
|
|
411
|
+
return { risk, ignored };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const CONFIDENT_STATES = new Set(["LOCAL", "IMPORT", "UNIQUE", "ROUTE"]);
|
|
415
|
+
|
|
416
|
+
function complexityOf(claudeDir, root, model, rels, opts) {
|
|
417
|
+
const Q = require("./graph-query.js");
|
|
418
|
+
const o = opts || {};
|
|
419
|
+
const operands = new Set(rels.map(norm));
|
|
420
|
+
const targets = rels.flatMap((r) => ((model.byFile[r] || {}).symbols || []).filter((s) => s.kind !== "module"));
|
|
421
|
+
|
|
422
|
+
const callerFiles = new Set();
|
|
423
|
+
let callersOutside = 0;
|
|
424
|
+
let maybe = 0;
|
|
425
|
+
const testFiles = new Set();
|
|
426
|
+
for (const t of targets) {
|
|
427
|
+
const c = Q.callersOf(model, t);
|
|
428
|
+
for (const x of c.confident) {
|
|
429
|
+
const f = x.sym.file;
|
|
430
|
+
if (Q.TEST_FILE.test(f)) testFiles.add(f);
|
|
431
|
+
if (operands.has(f) || !CONFIDENT_STATES.has(x.state)) continue;
|
|
432
|
+
callersOutside++;
|
|
433
|
+
callerFiles.add(f);
|
|
434
|
+
}
|
|
435
|
+
// The stored AMBIGUOUS map knows WHICH file each unresolved call came from,
|
|
436
|
+
// so a caller inside the plan is not counted against it. Without the cache
|
|
437
|
+
// only the total exists, and the total is what is used.
|
|
438
|
+
const m = model.maybeCache;
|
|
439
|
+
if (m && Object.prototype.hasOwnProperty.call(m, t.id)) {
|
|
440
|
+
for (const [file, n] of m[t.id]) if (!operands.has(norm(file))) maybe += n;
|
|
441
|
+
} else maybe += c.maybe;
|
|
442
|
+
}
|
|
443
|
+
// Tests are counted the two ways `changes` counts them: by CALL (an edge into
|
|
444
|
+
// an operand's symbol) and by NAME (`x.test.js` beside `x.js`). When the
|
|
445
|
+
// `impact` answer is handed in, ITS list wins: it followed the call chain to
|
|
446
|
+
// the configured depth and through routes, and a second, shallower count of
|
|
447
|
+
// the same thing beside it would be two answers to one question.
|
|
448
|
+
for (const rel of rels) for (const f of testsByName(model, rel)) testFiles.add(f);
|
|
449
|
+
const testsReaching = Array.isArray(o.tests) ? new Set(o.tests).size : testFiles.size;
|
|
450
|
+
|
|
451
|
+
// The partners history says change WITH an operand and the plan does not
|
|
452
|
+
// name. One process: `graphCochange` reads the per-HEAD cache, which is built
|
|
453
|
+
// at most once for the whole call.
|
|
454
|
+
const cochange = [];
|
|
455
|
+
const seenPartner = new Set();
|
|
456
|
+
const perFile = [];
|
|
457
|
+
const head = git(root, ["rev-parse", "HEAD"]);
|
|
458
|
+
const at = head.status === 0 ? head.stdout.trim() : null;
|
|
459
|
+
for (const rel of rels) {
|
|
460
|
+
let r = null;
|
|
461
|
+
try {
|
|
462
|
+
r = at ? graphCochange(claudeDir, root, { file: rel, at }) : null;
|
|
463
|
+
} catch (_) {
|
|
464
|
+
r = null;
|
|
465
|
+
}
|
|
466
|
+
const partners = r && r.ok ? r.rows.filter((x) => x.together >= COMPLEXITY_COCHANGE_MIN) : [];
|
|
467
|
+
perFile.push({ file: rel, partners: partners.map((x) => ({ path: x.file, count: x.together })) });
|
|
468
|
+
for (const x of partners) {
|
|
469
|
+
const p = norm(x.file);
|
|
470
|
+
if (operands.has(p) || seenPartner.has(p)) continue;
|
|
471
|
+
seenPartner.add(p);
|
|
472
|
+
cochange.push({ path: p, count: x.together, of: x.of });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
cochange.sort((a, b) => b.count - a.count || a.path.localeCompare(b.path));
|
|
476
|
+
|
|
477
|
+
const { risk, ignored } = parseRisk(o.risk);
|
|
478
|
+
const riskGiven = typeof o.risk === "string";
|
|
479
|
+
|
|
480
|
+
const why = [];
|
|
481
|
+
if (callerFiles.size >= COMPLEXITY_CALLER_FILES) why.push(`callers in ${callerFiles.size} files (≥ ${COMPLEXITY_CALLER_FILES})`);
|
|
482
|
+
if (callersOutside >= COMPLEXITY_CALLERS) why.push(`callers ${callersOutside} (≥ ${COMPLEXITY_CALLERS})`);
|
|
483
|
+
for (const x of risk) why.push(`risk ${x.class}`);
|
|
484
|
+
for (const x of cochange) why.push(`cochange ${x.path} x${x.count} not in plan`);
|
|
485
|
+
for (const x of ignored) why.push(`risk item ignored: ${x}`);
|
|
486
|
+
if (!riskGiven) why.push("risk: not given");
|
|
487
|
+
|
|
488
|
+
const verdict =
|
|
489
|
+
callerFiles.size >= COMPLEXITY_CALLER_FILES || callersOutside >= COMPLEXITY_CALLERS || risk.length > 0 || cochange.length > 0 ? "recommend-orc" : "mini-ok";
|
|
490
|
+
|
|
491
|
+
const riskText = risk.length ? risk.map((x) => `${x.class}${x.at ? ` (${x.at})` : ""}`).join(", ") : riskGiven ? "none" : "not given";
|
|
492
|
+
const cochangeText = cochange.length ? cochange.map((x) => `${x.path} x${x.count} not in plan`).join(", ") : "none";
|
|
493
|
+
const maybeText = maybe ? ` · maybe ${maybe}` : "";
|
|
494
|
+
const line =
|
|
495
|
+
verdict === "mini-ok"
|
|
496
|
+
? `complexity: mini-ok — ${rels.length} files · confident callers ${callersOutside} in ${callerFiles.size} files${maybeText} · tests reach ${testsReaching} · risk ${riskText} · cochange ${cochangeText}`
|
|
497
|
+
: `complexity: recommend /orc — ${rels.length} files · callers ${callersOutside} in ${callerFiles.size} files${maybeText} · tests reach ${testsReaching} · risk ${riskText} · cochange ${cochangeText}`;
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
verdict,
|
|
501
|
+
why,
|
|
502
|
+
numbers: {
|
|
503
|
+
files: rels.length,
|
|
504
|
+
callers_outside: callersOutside,
|
|
505
|
+
caller_files_outside: callerFiles.size,
|
|
506
|
+
maybe,
|
|
507
|
+
tests_reaching: testsReaching,
|
|
508
|
+
cochange_missing: cochange,
|
|
509
|
+
risk,
|
|
510
|
+
risk_given: riskGiven,
|
|
511
|
+
},
|
|
512
|
+
line,
|
|
513
|
+
thresholds: { caller_files: COMPLEXITY_CALLER_FILES, callers: COMPLEXITY_CALLERS, cochange_min: COMPLEXITY_COCHANGE_MIN },
|
|
514
|
+
// V3: `graph_facts` in the shape the lane pastes. `map` is null — the lane
|
|
515
|
+
// already holds the `map --focus` card and fills it there, so the same card
|
|
516
|
+
// is never computed twice.
|
|
517
|
+
facts: {
|
|
518
|
+
map: null,
|
|
519
|
+
impact: rels.map((rel) => {
|
|
520
|
+
const syms = ((model.byFile[rel] || {}).symbols || []).filter((s) => s.kind !== "module");
|
|
521
|
+
const cf = new Set();
|
|
522
|
+
let n = 0;
|
|
523
|
+
const tf = new Set();
|
|
524
|
+
for (const t of syms) {
|
|
525
|
+
for (const x of Q.callersOf(model, t).confident) {
|
|
526
|
+
if (Q.TEST_FILE.test(x.sym.file)) tf.add(x.sym.file);
|
|
527
|
+
if (!CONFIDENT_STATES.has(x.state)) continue;
|
|
528
|
+
n++;
|
|
529
|
+
cf.add(x.sym.file);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
for (const f of testsByName(model, rel)) tf.add(f);
|
|
533
|
+
return { file: rel, confident_callers: n, caller_files: cf.size, tests_reaching: tf.size };
|
|
534
|
+
}),
|
|
535
|
+
cochange: perFile,
|
|
536
|
+
generation: model.meta.generation || 0,
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
module.exports = {
|
|
542
|
+
importance,
|
|
543
|
+
testsByName,
|
|
544
|
+
parseDiff,
|
|
545
|
+
changedPaths,
|
|
546
|
+
graphChanges,
|
|
547
|
+
graphCochange,
|
|
548
|
+
cochangePath,
|
|
549
|
+
complexityOf,
|
|
550
|
+
parseRisk,
|
|
551
|
+
COCHANGE_MIN,
|
|
552
|
+
COCHANGE_MAX_FILES,
|
|
553
|
+
isThin,
|
|
554
|
+
THIN_MIN_FILES,
|
|
555
|
+
THIN_PER_FILE,
|
|
556
|
+
THIN_ZERO_SHARE,
|
|
557
|
+
COMPLEXITY_CALLER_FILES,
|
|
558
|
+
COMPLEXITY_CALLERS,
|
|
559
|
+
COMPLEXITY_COCHANGE_MIN,
|
|
560
|
+
};
|