@mcrescenzo/opencode-workflows 0.1.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 +14 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/CONTRIBUTING.md +95 -0
- package/LICENSE +21 -0
- package/README.md +746 -0
- package/SECURITY.md +38 -0
- package/commands/repo-bughunt.md +94 -0
- package/commands/repo-review.md +148 -0
- package/commands/workflow-live-gates-release-check.md +143 -0
- package/docs/workflow-plugin.md +400 -0
- package/opencode-workflows.js +5 -0
- package/package.json +86 -0
- package/skills/opencode-workflow-authoring/SKILL.md +180 -0
- package/skills/repo-review-command-protocol/SKILL.md +56 -0
- package/skills/workflow-model-tiering/SKILL.md +57 -0
- package/skills/workflow-plan-review/SKILL.md +91 -0
- package/workflow-kernel/approval-hashing.js +39 -0
- package/workflow-kernel/async-util.js +33 -0
- package/workflow-kernel/audited-shell-policy.js +200 -0
- package/workflow-kernel/authority-policy.js +670 -0
- package/workflow-kernel/budget-accounting.js +142 -0
- package/workflow-kernel/capability-adapter.js +753 -0
- package/workflow-kernel/child-agent-runner.js +1264 -0
- package/workflow-kernel/constants.js +117 -0
- package/workflow-kernel/diagnostics.js +152 -0
- package/workflow-kernel/drain-runtime.js +421 -0
- package/workflow-kernel/errors.js +181 -0
- package/workflow-kernel/event-journal.js +487 -0
- package/workflow-kernel/extension-registry.js +144 -0
- package/workflow-kernel/free-text-redactor.js +91 -0
- package/workflow-kernel/gate-shapes.js +82 -0
- package/workflow-kernel/git-util.js +45 -0
- package/workflow-kernel/index.js +72 -0
- package/workflow-kernel/integration-mode.js +155 -0
- package/workflow-kernel/lane-effort-policy.js +134 -0
- package/workflow-kernel/lifecycle-control.js +608 -0
- package/workflow-kernel/live-gate-probes.js +916 -0
- package/workflow-kernel/notification-toast-cards.js +393 -0
- package/workflow-kernel/notification-toast-policy.js +179 -0
- package/workflow-kernel/notification-toast-scope.js +100 -0
- package/workflow-kernel/notification-toast.js +287 -0
- package/workflow-kernel/path-policy.js +219 -0
- package/workflow-kernel/result-readback.js +106 -0
- package/workflow-kernel/role-template-loading.js +606 -0
- package/workflow-kernel/run-context.js +139 -0
- package/workflow-kernel/run-observability.js +43 -0
- package/workflow-kernel/run-store-fs.js +231 -0
- package/workflow-kernel/run-store-locks.js +180 -0
- package/workflow-kernel/run-store-projections.js +421 -0
- package/workflow-kernel/run-store-rehydrate.js +68 -0
- package/workflow-kernel/run-store-state.js +147 -0
- package/workflow-kernel/run-store-status-format.js +1154 -0
- package/workflow-kernel/run-store-status.js +107 -0
- package/workflow-kernel/sandbox-executor.js +1131 -0
- package/workflow-kernel/session-access.js +56 -0
- package/workflow-kernel/structured-output.js +143 -0
- package/workflow-kernel/test-fix-drain-adapter.js +119 -0
- package/workflow-kernel/text-json.js +136 -0
- package/workflow-kernel/workflow-plugin.js +3017 -0
- package/workflow-kernel/workflow-source.js +444 -0
- package/workflow-kernel/worktree-adapter.js +256 -0
- package/workflow-kernel/worktree-git.js +147 -0
- package/workflows/repo-bughunt.js +362 -0
- package/workflows/repo-cleanup.js +383 -0
- package/workflows/repo-complexity.js +404 -0
- package/workflows/repo-deps.js +457 -0
- package/workflows/repo-modernize.js +395 -0
- package/workflows/repo-perf.js +394 -0
- package/workflows/repo-review.js +831 -0
- package/workflows/repo-security-audit.js +466 -0
- package/workflows/repo-test-gaps.js +377 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// LEAF workflow — repo-complexity engine. Conforms to
|
|
2
|
+
// docs/repo-review-leaf-contract.md (the shared leaf contract). Mirrors the
|
|
3
|
+
// repo-bughunt template + the contract; the shared pieces (envelope(),
|
|
4
|
+
// RECON_SCHEMA, formatRecon(), fingerprintOf(), the arg-coercion preamble, and
|
|
5
|
+
// fitWithinBudget()) are duplicated verbatim because the QuickJS guest cannot
|
|
6
|
+
// `import` any module. This is a LEAF: it NEVER calls workflow() (the
|
|
7
|
+
// /repo-review meta invokes THIS via workflow(); nesting is one level only).
|
|
8
|
+
export const meta = {
|
|
9
|
+
name: "repo-complexity",
|
|
10
|
+
description: "Rank refactor hotspots across a repo by complexity x churn (god-object, long-function, deep-nesting, tangled-module, high-churn-hotspot). Fans out one scorer per source directory, adversarially verifies each refactor suggestion, and ranks by hotspot score. Report-only: returns ranked structured findings; the workflow writes no files.",
|
|
11
|
+
profile: "read-only-review",
|
|
12
|
+
maxAgents: 4096,
|
|
13
|
+
concurrency: 16,
|
|
14
|
+
phases: ["recon", "find", "verify", "synthesize"],
|
|
15
|
+
category: "repo-review-leaf",
|
|
16
|
+
notes: "Read-only report-only complexity leaf. Ranks refactor hotspots by static complexity/churn signals; maxDirs bounds scorer fan-out. Finder lanes use fast tier, verification uses deep tier.",
|
|
17
|
+
examples: [
|
|
18
|
+
{ label: "normal complexity scan", args: { depth: "normal", paths: ["src"], maxDirs: 40 } },
|
|
19
|
+
{ label: "focused hotspots", args: { depth: "quick", categories: ["long-function", "tangled-module"], paths: ["src"], maxDirs: 20 } },
|
|
20
|
+
],
|
|
21
|
+
argsSchema: {
|
|
22
|
+
type: ["object", "string", "null"],
|
|
23
|
+
properties: {
|
|
24
|
+
paths: { type: "array", items: { type: "string" } },
|
|
25
|
+
exclude: { type: "array", items: { type: "string" } },
|
|
26
|
+
depth: { type: "string", enum: ["quick", "normal", "thorough"] },
|
|
27
|
+
categories: { type: "array", items: { type: "string", enum: ["god-object", "long-function", "deep-nesting", "tangled-module", "high-churn-hotspot"] } },
|
|
28
|
+
maxDirs: { type: "integer", minimum: 1 },
|
|
29
|
+
maxReturnFindings: { type: "integer", minimum: 1 },
|
|
30
|
+
recon: { type: ["object", "string"] },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ---- suite identity ----
|
|
36
|
+
const DOMAIN = "complexity";
|
|
37
|
+
const SCHEMA_VERSION = 1;
|
|
38
|
+
|
|
39
|
+
// ---- arg coercion preamble (duplicated verbatim per docs/repo-review-leaf-contract.md §8/§11) ----
|
|
40
|
+
// args may arrive as an object (workflow_run args) or, defensively, a JSON string.
|
|
41
|
+
let RT = args;
|
|
42
|
+
if (typeof RT === "string") { try { RT = RT.trim() ? JSON.parse(RT) : {}; } catch (error) { throw new Error(`Invalid repo-complexity runtime args JSON: ${error.message}`); } }
|
|
43
|
+
if (!RT || typeof RT !== "object" || Array.isArray(RT)) RT = {};
|
|
44
|
+
|
|
45
|
+
const paths = Array.isArray(RT.paths) && RT.paths.length ? RT.paths : ["."];
|
|
46
|
+
const exclude = Array.isArray(RT.exclude) ? RT.exclude : ["node_modules", "dist", "build", ".git", "vendor", "target", "*.min.*", "*.map"];
|
|
47
|
+
const depth = ["quick", "normal", "thorough"].includes(RT.depth) ? RT.depth : "thorough";
|
|
48
|
+
|
|
49
|
+
// Complexity is churn-dependent: accept a maxDirs clamp arg (SUITE-CONTRACT per-domain carve-out;
|
|
50
|
+
// cited in docs/repo-review-leaf-contract.md §2). Positive integer; bounds the per-directory scorer
|
|
51
|
+
// fan-out so agent count stays predictable. 0/neg would drop the whole tail, so default to 40.
|
|
52
|
+
const maxDirs = Number.isInteger(RT.maxDirs) && RT.maxDirs > 0 ? RT.maxDirs : 1000000;
|
|
53
|
+
|
|
54
|
+
// Model selection is intent-based: lanes declare a tier; the kernel resolves tier -> concrete model
|
|
55
|
+
// from run.modelTiers (set by the planning agent). recon + scorers = bulk work (fast); skeptics =
|
|
56
|
+
// subtle correctness (deep); synth = pure JS (no model).
|
|
57
|
+
const TIER_RECON = "fast";
|
|
58
|
+
const TIER_FINDER = "fast";
|
|
59
|
+
const TIER_VERIFY = "deep";
|
|
60
|
+
|
|
61
|
+
const MAX_RETURN_FINDINGS = Number.isInteger(RT.maxReturnFindings) && RT.maxReturnFindings > 0 ? RT.maxReturnFindings : 1000000;
|
|
62
|
+
|
|
63
|
+
const ALL_CATEGORIES = ["god-object", "long-function", "deep-nesting", "tangled-module", "high-churn-hotspot"];
|
|
64
|
+
const requestedCategories = Array.isArray(RT.categories) ? RT.categories.filter((c) => typeof c === "string" && c.trim()) : [];
|
|
65
|
+
const categories = requestedCategories.length > 0 ? requestedCategories.filter((c) => ALL_CATEGORIES.includes(c)) : ALL_CATEGORIES;
|
|
66
|
+
if (requestedCategories.length > 0 && categories.length === 0) {
|
|
67
|
+
throw new Error(`No valid repo-complexity categories supplied. Valid categories: ${ALL_CATEGORIES.join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const scope = `Scope: paths = ${JSON.stringify(paths)}. Exclude (do not scan/report): ${JSON.stringify(exclude)}.`;
|
|
71
|
+
|
|
72
|
+
// ---- shell/churn lens (DECISION: deferred under read-only-review) ----
|
|
73
|
+
//
|
|
74
|
+
// Complexity is the ONE domain whose hotspot score depends on git churn (the
|
|
75
|
+
// "high-churn-hotspot" category), which needs a shell (`git log`). The OPTIONAL
|
|
76
|
+
// shell lens maps to the `inspect-with-shell` authority profile
|
|
77
|
+
// (workflow-kernel/authority-policy.js:21-24; requiredGates:
|
|
78
|
+
// ["permissionEnforcement","commandScopedBash"]). Those gates are currently
|
|
79
|
+
// UNVERIFIED in this runtime, so this engine ships under `read-only-review`
|
|
80
|
+
// (authority-policy.js:17-20; requiredGates: [], authority readOnly:true) as the
|
|
81
|
+
// SAFE DEFAULT: no Bash, no `git`, no fs writes. Churn is therefore best-effort
|
|
82
|
+
// (inferred from read-only tree signals, or 0 when unknown) and hotspotScore
|
|
83
|
+
// approximates complexityScore. Enabling the shell/churn lens is a future,
|
|
84
|
+
// separately-approved change gated on VERIFIED inspect-with-shell gates; until
|
|
85
|
+
// then shellCoverage stays "none" (emitted on every exit path below). This engine
|
|
86
|
+
// does NOT enable the shell lens.
|
|
87
|
+
const SHELL_COVERAGE = "none";
|
|
88
|
+
const COVERAGE_LIMITATIONS =
|
|
89
|
+
"Shell/git churn not measured: read-only-review denies Bash, so `git log` churn is unavailable. churn is best-effort (0 when unknown) and hotspotScore approximates complexityScore. Enable inspect-with-shell (verified permissionEnforcement + commandScopedBash gates) for real git-log churn.";
|
|
90
|
+
|
|
91
|
+
// ---- lane coverage telemetry (iui1.2) ----
|
|
92
|
+
// Tracks expected/completed/dropped lane counts per phase so a dropped scorer/verifier lane
|
|
93
|
+
// (onFailure returnNull + filter(Boolean)) surfaces as degraded coverage instead of a silent
|
|
94
|
+
// null. The meta reads laneCoverage.dropped to block materialization on partial coverage.
|
|
95
|
+
const laneCoverage = { expected: 0, completed: 0, dropped: 0, byPhase: {}, droppedLabels: [] };
|
|
96
|
+
function tallyPhase(name, results, labelOf) {
|
|
97
|
+
const expected = results.length;
|
|
98
|
+
let completed = 0;
|
|
99
|
+
for (let i = 0; i < results.length; i++) {
|
|
100
|
+
if (results[i] === null || results[i] === undefined) laneCoverage.droppedLabels.push(labelOf ? labelOf(i) : `${name}:${i + 1}`);
|
|
101
|
+
else completed++;
|
|
102
|
+
}
|
|
103
|
+
const dropped = expected - completed;
|
|
104
|
+
laneCoverage.expected += expected;
|
|
105
|
+
laneCoverage.completed += completed;
|
|
106
|
+
laneCoverage.dropped += dropped;
|
|
107
|
+
const prev = laneCoverage.byPhase[name] || { expected: 0, completed: 0, dropped: 0 };
|
|
108
|
+
laneCoverage.byPhase[name] = { expected: prev.expected + expected, completed: prev.completed + completed, dropped: prev.dropped + dropped };
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- standardized return envelope ----
|
|
113
|
+
// Complexity always emits the shellCoverage/coverageLimitations extension fields (per-domain carve-out,
|
|
114
|
+
// docs/repo-review-leaf-contract.md §2); they are added to the base envelope here so every exit path carries them.
|
|
115
|
+
function envelope(status, extra) {
|
|
116
|
+
return { domain: DOMAIN, schemaVersion: SCHEMA_VERSION, status, abortReason: null, reportPath: null, laneCoverage, shellCoverage: SHELL_COVERAGE, coverageLimitations: COVERAGE_LIMITATIONS, ...extra };
|
|
117
|
+
}
|
|
118
|
+
const emptyCounts = { total: 0, critical: 0, high: 0, medium: 0, low: 0 };
|
|
119
|
+
|
|
120
|
+
// ---- shared recon schema (tolerates a prose string via formatRecon) ----
|
|
121
|
+
const RECON_SCHEMA = {
|
|
122
|
+
type: "object", additionalProperties: false,
|
|
123
|
+
properties: {
|
|
124
|
+
languages: { type: "array", items: { type: "string" } },
|
|
125
|
+
frameworks: { type: "array", items: { type: "string" } },
|
|
126
|
+
packageManagers: { type: "array", items: { type: "string" } },
|
|
127
|
+
entryPoints: { type: "array", items: { type: "string" } },
|
|
128
|
+
testLayout: { type: "string" },
|
|
129
|
+
buildTooling: { type: "string" },
|
|
130
|
+
concurrencyModel: { type: "string" },
|
|
131
|
+
errorHandling: { type: "string" },
|
|
132
|
+
externalResources: { type: "array", items: { type: "string" } },
|
|
133
|
+
notes: { type: "string" },
|
|
134
|
+
},
|
|
135
|
+
required: ["languages", "notes"],
|
|
136
|
+
};
|
|
137
|
+
function formatRecon(r) {
|
|
138
|
+
if (typeof r === "string") return r;
|
|
139
|
+
if (!r || typeof r !== "object") return "No recon available.";
|
|
140
|
+
const L = (k, v) => (v && (Array.isArray(v) ? v.length : true) ? `${k}: ${Array.isArray(v) ? v.join(", ") : v}` : null);
|
|
141
|
+
return [
|
|
142
|
+
L("Languages", r.languages), L("Frameworks", r.frameworks), L("Package managers", r.packageManagers),
|
|
143
|
+
L("Entry points", r.entryPoints), L("Test layout", r.testLayout), L("Build tooling", r.buildTooling),
|
|
144
|
+
L("Concurrency model", r.concurrencyModel), L("Error handling", r.errorHandling),
|
|
145
|
+
L("External resources", r.externalResources), L("Notes", r.notes),
|
|
146
|
+
].filter(Boolean).join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- stable content fingerprint (djb2; deterministic, no crypto) ----
|
|
150
|
+
// <suite:fingerprintOf>
|
|
151
|
+
function fingerprintOf(f) {
|
|
152
|
+
const norm = (s) => (s || "").toString().toLowerCase().replace(/\s+/g, " ").trim();
|
|
153
|
+
const basis = `${DOMAIN}|${norm(f.file)}|${norm(f.category)}|${norm(f.description).slice(0, 160)}`;
|
|
154
|
+
let h = 5381;
|
|
155
|
+
for (let i = 0; i < basis.length; i++) h = ((h * 33) ^ basis.charCodeAt(i)) >>> 0;
|
|
156
|
+
return `${DOMAIN}-${h.toString(16)}`;
|
|
157
|
+
}
|
|
158
|
+
// </suite:fingerprintOf>
|
|
159
|
+
|
|
160
|
+
// ---- domain-specific recon schema (source dirs + git availability) ----
|
|
161
|
+
const COMPLEXITY_RECON_SCHEMA = {
|
|
162
|
+
type: "object", additionalProperties: false,
|
|
163
|
+
properties: {
|
|
164
|
+
profile: { type: "string" },
|
|
165
|
+
dirs: { type: "array", items: { type: "string" } },
|
|
166
|
+
gitAvailable: { type: "boolean" },
|
|
167
|
+
},
|
|
168
|
+
required: ["profile", "dirs", "gitAvailable"],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// ---- schemas ----
|
|
172
|
+
const FINDINGS_SCHEMA = {
|
|
173
|
+
type: "object", additionalProperties: false,
|
|
174
|
+
properties: {
|
|
175
|
+
findings: {
|
|
176
|
+
type: "array",
|
|
177
|
+
items: {
|
|
178
|
+
type: "object", additionalProperties: false,
|
|
179
|
+
properties: {
|
|
180
|
+
category: { type: "string" },
|
|
181
|
+
file: { type: "string" },
|
|
182
|
+
line: { type: "integer" },
|
|
183
|
+
severity: { type: "string", enum: ["high", "medium", "low"] },
|
|
184
|
+
description: { type: "string" },
|
|
185
|
+
churn: { type: "integer" },
|
|
186
|
+
complexityScore: { type: "integer" },
|
|
187
|
+
hotspotScore: { type: "integer" },
|
|
188
|
+
refactorSuggestion: { type: "string" },
|
|
189
|
+
proposedChange: { type: "string" },
|
|
190
|
+
confidence: { type: "integer" },
|
|
191
|
+
effort: { type: "string", enum: ["small", "medium", "large"] },
|
|
192
|
+
docImpact: { type: "string" },
|
|
193
|
+
},
|
|
194
|
+
required: ["category", "file", "line", "severity", "description", "churn", "complexityScore", "hotspotScore", "refactorSuggestion", "proposedChange", "confidence", "effort", "docImpact"],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
required: ["findings"],
|
|
199
|
+
};
|
|
200
|
+
const VERDICT_SCHEMA = {
|
|
201
|
+
type: "object", additionalProperties: false,
|
|
202
|
+
properties: {
|
|
203
|
+
refuted: { type: "boolean" },
|
|
204
|
+
reasoning: { type: "string" },
|
|
205
|
+
adjustedConfidence: { type: "integer" },
|
|
206
|
+
},
|
|
207
|
+
required: ["refuted", "reasoning", "adjustedConfidence"],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// ---- lenses / prompts ----
|
|
211
|
+
function scorerPrompt(dir, recon, complexityProfile, gitAvailable) {
|
|
212
|
+
return [
|
|
213
|
+
`You are a complexity scorer for the directory "${dir}". REPORT-ONLY — do NOT modify files.`,
|
|
214
|
+
scope,
|
|
215
|
+
`Repo profile (recon):\n${formatRecon(recon)}`,
|
|
216
|
+
`Domain profile:\n${complexityProfile || "(no profile)"}`,
|
|
217
|
+
"You have READ-ONLY tools only (no shell, no git). Compute a complexity heuristic (0-100) from file size, max nesting depth, function length, and parameter counts by inspecting the files in this directory.",
|
|
218
|
+
gitAvailable
|
|
219
|
+
? "This is a git repo, but you CANNOT run git under read-only-review. Infer churn signals only from working-tree evidence visible to read-only tools (recent WIP/TODO markers, obvious churn hints); otherwise set churn to 0."
|
|
220
|
+
: "Not a git repo: set churn to 0 for every finding.",
|
|
221
|
+
`Compute hotspotScore as a 0-100 composite of churn x complexity. With churn unavailable, hotspotScore approximates complexityScore. Emit a finding for each genuinely notable hotspot — category one of: ${categories.join(" | ")}. Provide a concrete refactorSuggestion and proposedChange. Only flag notable files, not every file.`,
|
|
222
|
+
"Return findings via the structured output.",
|
|
223
|
+
].join("\n\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function skepticPrompt(f) {
|
|
227
|
+
return [
|
|
228
|
+
"You are a skeptic. Try to REFUTE the refactor suggestion below — argue it is unsound, risky, or not worth the effort.",
|
|
229
|
+
scope,
|
|
230
|
+
`Hotspot (${f.category}) at ${f.file}:${f.line}:`,
|
|
231
|
+
`Description: ${f.description}`,
|
|
232
|
+
`Suggested refactor: ${f.refactorSuggestion}`,
|
|
233
|
+
"Consider: would the refactor preserve behavior? is the file genuinely a maintenance burden (complexity x churn)? is the suggestion concrete and safe?",
|
|
234
|
+
"Set refuted=true if the suggestion is poor. Default to refuted=true when genuinely uncertain.",
|
|
235
|
+
].join("\n\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---- 1. Recon (use injected recon if present, else profile once) + domain recon ----
|
|
239
|
+
await phase("recon");
|
|
240
|
+
const recon = RT.recon
|
|
241
|
+
? RT.recon
|
|
242
|
+
: await agent(
|
|
243
|
+
["Profile this repository for review. Return the structured recon fields.", scope,
|
|
244
|
+
"Report: languages/frameworks; package managers; entry points; test layout; build tooling; concurrency model (threads/async/event loop); error-handling conventions; external resources (DB, files, network); and notes on anything that makes a code path notable for complexity or high churn.",
|
|
245
|
+
"Explore with your tools."].join("\n\n"),
|
|
246
|
+
{ label: "recon", schema: RECON_SCHEMA, tier: TIER_RECON, onFailure: "returnNull" },
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Domain recon always computed locally (shared recon does not carry dirs/gitAvailable).
|
|
250
|
+
const complexityRecon = await agent(
|
|
251
|
+
["Profile this repository for a complexity / refactor-hotspot analysis.", scope,
|
|
252
|
+
"Determine: whether it is a git repo (so git-based churn would be relevant); and the list of real SOURCE directories worth analyzing (repo-relative paths, excluding vendored/build/test-fixture dirs). Keep the dir list focused (merge tiny ones). Also write a compact prose profile of the codebase.",
|
|
253
|
+
"Explore with your tools. Return the structured output."].join("\n\n"),
|
|
254
|
+
{ label: "complexity-recon", schema: COMPLEXITY_RECON_SCHEMA, tier: TIER_RECON, onFailure: "returnNull" },
|
|
255
|
+
);
|
|
256
|
+
if (!complexityRecon || typeof complexityRecon !== "object") {
|
|
257
|
+
return envelope("aborted", { abortReason: "complexity-recon agent returned null; cannot score directories.", summary: "Aborted: complexity recon failed.", counts: emptyCounts, findings: [], truncatedFindings: false, reportMarkdown: null });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Apply the maxDirs clamp so the per-directory fan-out stays bounded.
|
|
261
|
+
const rawDirs = (Array.isArray(complexityRecon.dirs) && complexityRecon.dirs.length) ? complexityRecon.dirs : paths;
|
|
262
|
+
const dirs = rawDirs.slice(0, maxDirs);
|
|
263
|
+
if (rawDirs.length > maxDirs) {
|
|
264
|
+
await log(`maxDirs clamp applied: ${rawDirs.length} dirs -> ${dirs.length} (maxDirs=${maxDirs})`);
|
|
265
|
+
}
|
|
266
|
+
const gitAvailable = !!complexityRecon.gitAvailable;
|
|
267
|
+
|
|
268
|
+
// ---- 2. Find + dedup (one scorer per directory) ----
|
|
269
|
+
function dedup(findings) {
|
|
270
|
+
const seen = new Set(); const out = [];
|
|
271
|
+
for (const f of findings) {
|
|
272
|
+
if (!f) continue;
|
|
273
|
+
const key = `${f.category}::${(f.file || "").trim()}::${f.line || 0}`;
|
|
274
|
+
if (seen.has(key)) continue;
|
|
275
|
+
seen.add(key); out.push(f);
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await phase("find");
|
|
281
|
+
// arity-1 thunks (api) => ... so parallel() runs them CONCURRENTLY (zero-arg thunks fail unless { sequential: true } is explicit).
|
|
282
|
+
const perDir = await parallel(dirs.map((dir) => (api) =>
|
|
283
|
+
api.agent(scorerPrompt(dir, recon, complexityRecon.profile, gitAvailable), { label: `score:${dir}`, schema: FINDINGS_SCHEMA, tier: TIER_FINDER, onFailure: "returnNull" })));
|
|
284
|
+
|
|
285
|
+
tallyPhase("score", perDir, (i) => `score:${dirs[i]}`);
|
|
286
|
+
let findings = dedup(perDir.filter(Boolean).flatMap((r) => (r.findings || []).map((f) => ({ ...f, category: f.category || "unknown" }))));
|
|
287
|
+
await log(`Scored ${dirs.length} directories: ${findings.length} hotspots`);
|
|
288
|
+
|
|
289
|
+
// positional id (in-run reference) + stable content fingerprint (cross-run dedupe key)
|
|
290
|
+
findings = findings.map((f, i) => ({ ...f, id: `${f.category}-${i + 1}`, fingerprint: fingerprintOf(f) }));
|
|
291
|
+
|
|
292
|
+
if (findings.length === 0) {
|
|
293
|
+
return envelope("empty", { summary: "No notable complexity hotspots found.", counts: emptyCounts, findings: [], truncatedFindings: false, reportMarkdown: null });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---- 3. Verify (light: refactor soundness) ----
|
|
297
|
+
// quick: none. normal: verify high-severity. thorough: verify all. Single skeptic each (no majority vote).
|
|
298
|
+
let toVerify;
|
|
299
|
+
if (depth === "quick") toVerify = [];
|
|
300
|
+
else if (depth === "thorough") toVerify = findings;
|
|
301
|
+
else toVerify = findings.filter((f) => f.severity === "high");
|
|
302
|
+
const verifyIds = new Set(toVerify.map((f) => f.id));
|
|
303
|
+
const passThrough = findings.filter((f) => !verifyIds.has(f.id));
|
|
304
|
+
|
|
305
|
+
let verified = passThrough;
|
|
306
|
+
if (toVerify.length > 0) {
|
|
307
|
+
await phase("verify");
|
|
308
|
+
const checked = await parallel(toVerify.map((f) => (api) =>
|
|
309
|
+
api.agent(skepticPrompt(f), { label: `verify:${f.id}`, schema: VERDICT_SCHEMA, tier: TIER_VERIFY, onFailure: "returnNull" })
|
|
310
|
+
.then((v) => ({ f, keep: !!(v && !v.refuted), conf: v ? v.adjustedConfidence : undefined }))));
|
|
311
|
+
tallyPhase("verify", checked, (i) => `verify:${toVerify[i].id}`);
|
|
312
|
+
const survivors = checked.filter(Boolean).filter((c) => c.keep)
|
|
313
|
+
.map((c) => ({ ...c.f, confidence: c.conf != null ? c.conf : c.f.confidence }));
|
|
314
|
+
verified = passThrough.concat(survivors);
|
|
315
|
+
await log(`Verified: ${survivors.length}/${toVerify.length} suggestions held; ${verified.length} total`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (verified.length === 0) {
|
|
319
|
+
return envelope("empty", { summary: "No hotspots survived verification.", counts: emptyCounts, findings: [], truncatedFindings: false, reportMarkdown: null });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---- 4. Synthesize (PURE JS — rank by hotspotScore, then severity, then confidence; render markdown) ----
|
|
323
|
+
await phase("synthesize");
|
|
324
|
+
const SEVW = { high: 3, medium: 2, low: 1 };
|
|
325
|
+
function rankKey(f) {
|
|
326
|
+
return [Number(f.hotspotScore) || 0, SEVW[f.severity] || 0, Number(f.confidence) || 0];
|
|
327
|
+
}
|
|
328
|
+
const ranked = verified.map((f) => ({ ...f })).sort((a, b) => {
|
|
329
|
+
const ka = rankKey(a), kb = rankKey(b);
|
|
330
|
+
if (ka[0] !== kb[0]) return kb[0] - ka[0];
|
|
331
|
+
if (ka[1] !== kb[1]) return kb[1] - ka[1];
|
|
332
|
+
return kb[2] - ka[2];
|
|
333
|
+
});
|
|
334
|
+
ranked.forEach((f, i) => { f.rank = i + 1; });
|
|
335
|
+
|
|
336
|
+
const counts = {
|
|
337
|
+
total: ranked.length,
|
|
338
|
+
critical: 0, // non-security domain: never populates critical (docs/repo-review-leaf-contract.md §4/§6)
|
|
339
|
+
high: ranked.filter((f) => f.severity === "high").length,
|
|
340
|
+
medium: ranked.filter((f) => f.severity === "medium").length,
|
|
341
|
+
low: ranked.filter((f) => f.severity === "low").length,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
function mdCell(s) { return String(s == null ? "" : s).replace(/\|/g, "\\|").replace(/\n+/g, " "); }
|
|
345
|
+
function renderMarkdown(rows, c) {
|
|
346
|
+
const lines = [];
|
|
347
|
+
lines.push(`# Complexity Hotspot Report (${DOMAIN})`, "");
|
|
348
|
+
lines.push("> Report-only. No files were modified and nothing was applied.", "");
|
|
349
|
+
lines.push("## Summary", `- Total: ${c.total} (high: ${c.high}, medium: ${c.medium}, low: ${c.low})`, "");
|
|
350
|
+
lines.push("## Ranked findings", "");
|
|
351
|
+
lines.push("| Rank | Category | Severity | Hotspot | Churn | Complexity | Confidence | Location | Description |");
|
|
352
|
+
lines.push("| ---- | -------- | -------- | ------- | ----- | ---------- | ---------- | -------- | ----------- |");
|
|
353
|
+
for (const f of rows) {
|
|
354
|
+
lines.push(`| ${f.rank} | ${mdCell(f.category)} | ${mdCell(f.severity)} | ${f.hotspotScore} | ${f.churn} | ${f.complexityScore} | ${f.confidence} | ${mdCell(f.file)}:${f.line || 0} | ${mdCell(f.description).slice(0, 140)} |`);
|
|
355
|
+
}
|
|
356
|
+
lines.push("", "## Detail");
|
|
357
|
+
for (const f of rows) {
|
|
358
|
+
lines.push("", `### ${f.rank}. ${mdCell(f.category)} — ${mdCell(f.file)}:${f.line || 0} (${f.severity}, hotspot ${f.hotspotScore}, churn ${f.churn}, complexity ${f.complexityScore})`);
|
|
359
|
+
lines.push(`- **What:** ${f.description}`);
|
|
360
|
+
lines.push(`- **Refactor:** ${f.refactorSuggestion}`);
|
|
361
|
+
lines.push(`- **Proposed change:** ${f.proposedChange}`);
|
|
362
|
+
if (f.docImpact) lines.push(`- **Doc impact:** ${f.docImpact}`);
|
|
363
|
+
lines.push(`- **Fingerprint:** \`${f.fingerprint}\``);
|
|
364
|
+
}
|
|
365
|
+
return lines.join("\n");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Size-fit to the 256 KB host cap: drop reportMarkdown first, then halve findings, until it fits.
|
|
369
|
+
function utf8ByteLength(value) {
|
|
370
|
+
const s = String(value ?? "");
|
|
371
|
+
let bytes = 0;
|
|
372
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
373
|
+
const code = s.charCodeAt(i);
|
|
374
|
+
if (code <= 0x7f) bytes += 1;
|
|
375
|
+
else if (code <= 0x7ff) bytes += 2;
|
|
376
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < s.length) {
|
|
377
|
+
const next = s.charCodeAt(i + 1);
|
|
378
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
379
|
+
bytes += 4;
|
|
380
|
+
i += 1;
|
|
381
|
+
} else bytes += 3;
|
|
382
|
+
} else bytes += 3;
|
|
383
|
+
}
|
|
384
|
+
return bytes;
|
|
385
|
+
}
|
|
386
|
+
function jsonUtf8ByteLength(value) {
|
|
387
|
+
return utf8ByteLength(JSON.stringify(value));
|
|
388
|
+
}
|
|
389
|
+
function fitWithinBudget(status, summary) {
|
|
390
|
+
const LIMIT = 230000; // headroom under MAX_RESULT_BYTES (262144) for the {output:...} wrapper + envelope fields
|
|
391
|
+
let returned = ranked.slice(0, MAX_RETURN_FINDINGS);
|
|
392
|
+
let truncated = ranked.length > returned.length;
|
|
393
|
+
let reportMarkdown = renderMarkdown(ranked, counts);
|
|
394
|
+
const sizeOf = () => jsonUtf8ByteLength(envelope(status, { summary, counts, findings: returned, truncatedFindings: truncated, reportMarkdown }));
|
|
395
|
+
if (sizeOf() > LIMIT) reportMarkdown = null;
|
|
396
|
+
while (sizeOf() > LIMIT && returned.length > 10) {
|
|
397
|
+
returned = returned.slice(0, Math.ceil(returned.length / 2));
|
|
398
|
+
truncated = true;
|
|
399
|
+
}
|
|
400
|
+
return envelope(status, { summary, counts, findings: returned, truncatedFindings: truncated, reportMarkdown });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const summary = `Found ${counts.total} complexity hotspot(s): ${counts.high} high, ${counts.medium} medium, ${counts.low} low. Report-only — nothing applied.`;
|
|
404
|
+
return fitWithinBudget("ok", summary);
|