@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,394 @@
|
|
|
1
|
+
// LEAF workflow — repo-perf engine (OpenCode port).
|
|
2
|
+
//
|
|
3
|
+
// Contract source of truth: docs/repo-review-leaf-contract.md. Canonical
|
|
4
|
+
// exemplar: workflows/repo-bughunt.js + tests/repo-bughunt.test.mjs.
|
|
5
|
+
// Port lineage (domain logic): internal Claude workflow source (adapted
|
|
6
|
+
// to OpenCode: QuickJS guest, no fs, tier-based models, pure-JS synthesis).
|
|
7
|
+
//
|
|
8
|
+
// This engine is a LEAF: it NEVER calls workflow() (the /repo-review meta
|
|
9
|
+
// invokes THIS via workflow(); nesting is one level only). It has NO
|
|
10
|
+
// Bash/fs/git and CANNOT import any module — the shared contract pieces
|
|
11
|
+
// (envelope, emptyCounts, RECON_SCHEMA, formatRecon, fingerprintOf,
|
|
12
|
+
// fitWithinBudget) are duplicated verbatim per docs/repo-review-leaf-contract.md
|
|
13
|
+
// § "How later leaves conform".
|
|
14
|
+
//
|
|
15
|
+
// Coverage-aware domain: this is a read-only-review leaf. It does NOT run any
|
|
16
|
+
// profiler, benchmark, or metrics tool (no shell), so it defaults
|
|
17
|
+
// shellCoverage="none" and emits a coverageLimitations string explaining that
|
|
18
|
+
// no runtime measurements were taken. Hotness is INFERRED from code structure
|
|
19
|
+
// (loops, entry points, data scale), never measured; findings are suspected
|
|
20
|
+
// hotspots pending profiling confirmation. The finder/skeptic prompts enforce
|
|
21
|
+
// the OBSERVED-evidence vs SUSPECTED-hot-path-risk distinction in wording and
|
|
22
|
+
// confidence (non-security domain -> critical: 0).
|
|
23
|
+
export const meta = {
|
|
24
|
+
name: "repo-perf",
|
|
25
|
+
description: "Find performance hotspots across a repo (N+1/repeated I/O, accidental quadratics, hot-path allocations, sync blocking, missing caching, inefficient data structures, redundant computation) and adversarially verify each candidate before reporting. Static read-only analysis — no profiler/benchmark/metrics are run. Report-only: returns ranked structured findings; the workflow writes no files.",
|
|
26
|
+
profile: "read-only-review",
|
|
27
|
+
maxAgents: 4096,
|
|
28
|
+
concurrency: 16,
|
|
29
|
+
phases: ["recon", "find", "verify", "synthesize"],
|
|
30
|
+
category: "repo-review-leaf",
|
|
31
|
+
notes: "Read-only report-only performance leaf. Static suspected hotspots only; no profiler, benchmark, or metrics are run. Finder lanes use fast tier, verification uses deep tier.",
|
|
32
|
+
examples: [
|
|
33
|
+
{ label: "normal perf scan", args: { depth: "normal", paths: ["src"] } },
|
|
34
|
+
{ label: "I/O and caching", args: { depth: "quick", categories: ["n-plus-one", "missing-caching"], paths: ["src"] } },
|
|
35
|
+
],
|
|
36
|
+
argsSchema: {
|
|
37
|
+
type: ["object", "string", "null"],
|
|
38
|
+
properties: {
|
|
39
|
+
paths: { type: "array", items: { type: "string" } },
|
|
40
|
+
exclude: { type: "array", items: { type: "string" } },
|
|
41
|
+
depth: { type: "string", enum: ["quick", "normal", "thorough"] },
|
|
42
|
+
categories: { type: "array", items: { type: "string", enum: ["n-plus-one", "quadratic", "hot-alloc", "sync-blocking", "missing-caching", "inefficient-structure", "redundant-compute"] } },
|
|
43
|
+
maxReturnFindings: { type: "integer", minimum: 1 },
|
|
44
|
+
recon: { type: ["object", "string"] },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ---- suite identity ----
|
|
50
|
+
const DOMAIN = "perf";
|
|
51
|
+
const SCHEMA_VERSION = 1;
|
|
52
|
+
|
|
53
|
+
// args may arrive as an object (workflow_run args) or, defensively, a JSON string.
|
|
54
|
+
let RT = args;
|
|
55
|
+
if (typeof RT === "string") { try { RT = RT.trim() ? JSON.parse(RT) : {}; } catch (error) { throw new Error(`Invalid repo-perf runtime args JSON: ${error.message}`); } }
|
|
56
|
+
if (!RT || typeof RT !== "object" || Array.isArray(RT)) RT = {};
|
|
57
|
+
|
|
58
|
+
const paths = Array.isArray(RT.paths) && RT.paths.length ? RT.paths : ["."];
|
|
59
|
+
const exclude = Array.isArray(RT.exclude) ? RT.exclude : ["node_modules", "dist", "build", ".git", "vendor", "target", "*.min.*", "*.map"];
|
|
60
|
+
const depth = ["quick", "normal", "thorough"].includes(RT.depth) ? RT.depth : "thorough";
|
|
61
|
+
|
|
62
|
+
// Model selection is intent-based: lanes declare a tier; the kernel resolves tier -> concrete model from
|
|
63
|
+
// run.modelTiers (set by the planning agent), falling back to the session-inherited default.
|
|
64
|
+
// recon + finders = bulk work (fast); skeptics = subtle correctness (deep); synth = pure JS (no model).
|
|
65
|
+
const TIER_RECON = "fast";
|
|
66
|
+
const TIER_FINDER = "fast";
|
|
67
|
+
const TIER_VERIFY = "deep";
|
|
68
|
+
|
|
69
|
+
const MAX_RETURN_FINDINGS = Number.isInteger(RT.maxReturnFindings) && RT.maxReturnFindings > 0 ? RT.maxReturnFindings : 1000000;
|
|
70
|
+
|
|
71
|
+
const ALL_CATEGORIES = ["n-plus-one", "quadratic", "hot-alloc", "sync-blocking", "missing-caching", "inefficient-structure", "redundant-compute"];
|
|
72
|
+
const requestedCategories = Array.isArray(RT.categories) ? RT.categories.filter((c) => typeof c === "string" && c.trim()) : [];
|
|
73
|
+
const categories = requestedCategories.length > 0 ? requestedCategories.filter((c) => ALL_CATEGORIES.includes(c)) : ALL_CATEGORIES;
|
|
74
|
+
if (requestedCategories.length > 0 && categories.length === 0) {
|
|
75
|
+
throw new Error(`No valid repo-perf categories supplied. Valid categories: ${ALL_CATEGORIES.join(", ")}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const scope = `Scope: paths = ${JSON.stringify(paths)}. Exclude (do not scan/report): ${JSON.stringify(exclude)}.`;
|
|
79
|
+
|
|
80
|
+
// ---- coverage-aware extension fields (docs/repo-review-leaf-contract.md §2) ----
|
|
81
|
+
// read-only-review gives the engine NO shell, so no profiler, benchmark, or
|
|
82
|
+
// runtime metrics tool is executed. We therefore default shellCoverage="none"
|
|
83
|
+
// and state why performance was NOT measured. We never claim measured hotness,
|
|
84
|
+
// speedups, or complexity we did not observe at runtime.
|
|
85
|
+
const SHELL_COVERAGE = "none";
|
|
86
|
+
const COVERAGE_LIMITATIONS = "No profiler, benchmark, or runtime metrics were executed by this read-only engine (no shell); hotness is inferred from code structure (loops, entry points, data scale), not measured. Findings are suspected hotspots pending profiling/benchmark confirmation. Do not read these as measured performance data.";
|
|
87
|
+
|
|
88
|
+
// ---- lane coverage telemetry (iui1.2) ----
|
|
89
|
+
// Tracks expected/completed/dropped lane counts per phase so a dropped finder/verifier lane
|
|
90
|
+
// (onFailure returnNull + filter(Boolean)) surfaces as degraded coverage instead of a silent
|
|
91
|
+
// null. The meta reads laneCoverage.dropped to block materialization on partial coverage.
|
|
92
|
+
const laneCoverage = { expected: 0, completed: 0, dropped: 0, byPhase: {}, droppedLabels: [] };
|
|
93
|
+
function tallyPhase(name, results, labelOf) {
|
|
94
|
+
const expected = results.length;
|
|
95
|
+
let completed = 0;
|
|
96
|
+
for (let i = 0; i < results.length; i++) {
|
|
97
|
+
if (results[i] === null || results[i] === undefined) laneCoverage.droppedLabels.push(labelOf ? labelOf(i) : `${name}:${i + 1}`);
|
|
98
|
+
else completed++;
|
|
99
|
+
}
|
|
100
|
+
const dropped = expected - completed;
|
|
101
|
+
laneCoverage.expected += expected;
|
|
102
|
+
laneCoverage.completed += completed;
|
|
103
|
+
laneCoverage.dropped += dropped;
|
|
104
|
+
const prev = laneCoverage.byPhase[name] || { expected: 0, completed: 0, dropped: 0 };
|
|
105
|
+
laneCoverage.byPhase[name] = { expected: prev.expected + expected, completed: prev.completed + completed, dropped: prev.dropped + dropped };
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- standardized return envelope ----
|
|
110
|
+
function envelope(status, extra) {
|
|
111
|
+
return { domain: DOMAIN, schemaVersion: SCHEMA_VERSION, status, abortReason: null, reportPath: null, laneCoverage, ...extra };
|
|
112
|
+
}
|
|
113
|
+
const emptyCounts = { total: 0, critical: 0, high: 0, medium: 0, low: 0 };
|
|
114
|
+
|
|
115
|
+
// ---- shared recon schema (tolerates a prose string via formatRecon) ----
|
|
116
|
+
const RECON_SCHEMA = {
|
|
117
|
+
type: "object", additionalProperties: false,
|
|
118
|
+
properties: {
|
|
119
|
+
languages: { type: "array", items: { type: "string" } },
|
|
120
|
+
frameworks: { type: "array", items: { type: "string" } },
|
|
121
|
+
packageManagers: { type: "array", items: { type: "string" } },
|
|
122
|
+
entryPoints: { type: "array", items: { type: "string" } },
|
|
123
|
+
testLayout: { type: "string" },
|
|
124
|
+
buildTooling: { type: "string" },
|
|
125
|
+
concurrencyModel: { type: "string" },
|
|
126
|
+
errorHandling: { type: "string" },
|
|
127
|
+
externalResources: { type: "array", items: { type: "string" } },
|
|
128
|
+
notes: { type: "string" },
|
|
129
|
+
},
|
|
130
|
+
required: ["languages", "notes"],
|
|
131
|
+
};
|
|
132
|
+
function formatRecon(r) {
|
|
133
|
+
if (typeof r === "string") return r;
|
|
134
|
+
if (!r || typeof r !== "object") return "No recon available.";
|
|
135
|
+
const L = (k, v) => (v && (Array.isArray(v) ? v.length : true) ? `${k}: ${Array.isArray(v) ? v.join(", ") : v}` : null);
|
|
136
|
+
return [
|
|
137
|
+
L("Languages", r.languages), L("Frameworks", r.frameworks), L("Package managers", r.packageManagers),
|
|
138
|
+
L("Entry points", r.entryPoints), L("Test layout", r.testLayout), L("Build tooling", r.buildTooling),
|
|
139
|
+
L("Concurrency model", r.concurrencyModel), L("Error handling", r.errorHandling),
|
|
140
|
+
L("External resources", r.externalResources), L("Notes", r.notes),
|
|
141
|
+
].filter(Boolean).join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---- stable content fingerprint (djb2; deterministic, no crypto) ----
|
|
145
|
+
// <suite:fingerprintOf>
|
|
146
|
+
function fingerprintOf(f) {
|
|
147
|
+
const norm = (s) => (s || "").toString().toLowerCase().replace(/\s+/g, " ").trim();
|
|
148
|
+
const basis = `${DOMAIN}|${norm(f.file)}|${norm(f.category)}|${norm(f.description).slice(0, 160)}`;
|
|
149
|
+
let h = 5381;
|
|
150
|
+
for (let i = 0; i < basis.length; i++) h = ((h * 33) ^ basis.charCodeAt(i)) >>> 0;
|
|
151
|
+
return `${DOMAIN}-${h.toString(16)}`;
|
|
152
|
+
}
|
|
153
|
+
// </suite:fingerprintOf>
|
|
154
|
+
|
|
155
|
+
// ---- schemas ----
|
|
156
|
+
const FINDINGS_SCHEMA = {
|
|
157
|
+
type: "object", additionalProperties: false,
|
|
158
|
+
properties: {
|
|
159
|
+
findings: {
|
|
160
|
+
type: "array",
|
|
161
|
+
items: {
|
|
162
|
+
type: "object", additionalProperties: false,
|
|
163
|
+
properties: {
|
|
164
|
+
category: { type: "string" },
|
|
165
|
+
file: { type: "string" },
|
|
166
|
+
line: { type: "integer" },
|
|
167
|
+
severity: { type: "string", enum: ["high", "medium", "low"] },
|
|
168
|
+
description: { type: "string" },
|
|
169
|
+
hotness: { type: "string", enum: ["hot", "warm", "cold"] },
|
|
170
|
+
estimatedImpact: { type: "string" },
|
|
171
|
+
complexityBefore: { type: "string" },
|
|
172
|
+
complexityAfter: { type: "string" },
|
|
173
|
+
proposedChange: { type: "string" },
|
|
174
|
+
confidence: { type: "integer" },
|
|
175
|
+
effort: { type: "string", enum: ["small", "medium", "large"] },
|
|
176
|
+
docImpact: { type: "string" },
|
|
177
|
+
},
|
|
178
|
+
required: ["category", "file", "line", "severity", "description", "hotness", "estimatedImpact", "complexityBefore", "complexityAfter", "proposedChange", "confidence", "effort", "docImpact"],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
required: ["findings"],
|
|
183
|
+
};
|
|
184
|
+
const VERDICT_SCHEMA = {
|
|
185
|
+
type: "object", additionalProperties: false,
|
|
186
|
+
properties: {
|
|
187
|
+
refuted: { type: "boolean" },
|
|
188
|
+
reasoning: { type: "string" },
|
|
189
|
+
adjustedConfidence: { type: "integer" },
|
|
190
|
+
},
|
|
191
|
+
required: ["refuted", "reasoning", "adjustedConfidence"],
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// ---- lenses ----
|
|
195
|
+
const LENS = {
|
|
196
|
+
"n-plus-one": "N+1 and repeated I/O: queries/requests/reads inside loops, per-item fetches that could be batched, missing eager-loading, repeated identical calls.",
|
|
197
|
+
"quadratic": "Accidental quadratic or worse: nested loops over the same large collection, repeated linear scans (indexOf/includes in a loop), array operations that rebuild each iteration.",
|
|
198
|
+
"hot-alloc": "Hot-path allocations: object/array/string allocation in tight loops, unnecessary copies/spreads, allocations that could be hoisted or pooled.",
|
|
199
|
+
"sync-blocking": "Synchronous blocking on hot paths: sync I/O, blocking calls in async/event-loop code, unawaited parallelizable work done serially.",
|
|
200
|
+
"missing-caching": "Missing caching/memoization: recomputing pure results, refetching unchanged data, no result/HTTP caching where appropriate.",
|
|
201
|
+
"inefficient-structure": "Inefficient data structures: array used where set/map needed (O(n) lookups), wrong container for the access pattern, unindexed lookups.",
|
|
202
|
+
"redundant-compute": "Redundant computation: work repeated that could be hoisted out of a loop, double serialization/parsing, recomputing invariants.",
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function finderPrompt(cat, recon, roundNote) {
|
|
206
|
+
return [
|
|
207
|
+
`You are the "${cat}" performance finder. REPORT-ONLY — do NOT modify files.`,
|
|
208
|
+
scope,
|
|
209
|
+
`Repo profile (recon):\n${formatRecon(recon)}`,
|
|
210
|
+
`Your lens: ${LENS[cat]}`,
|
|
211
|
+
roundNote || "",
|
|
212
|
+
`For EVERY finding set category to exactly "${cat}". Fill hotness (hot|warm|cold — how often the path actually runs, inferred from code), estimatedImpact (expected speedup/cost reduction), complexityBefore and complexityAfter (Big-O), and proposedChange (behavior-preserving optimization; describe, do NOT apply). Set confidence honestly. This is a STATIC READ-ONLY audit: no profiler, benchmark, or runtime metrics were run, so hotness is inferred from code structure, NOT measured. Distinguish OBSERVED evidence (the hot loop / large-data path / known entry point is directly visible in the code) from SUSPECTED hot-path risk (the path is only assumed hot without direct code evidence): state which in the description and set confidence HIGHER for observed evidence, LOWER for suspected-only risk. A premature optimization on a cold path is worse than a missed hotspot.`,
|
|
213
|
+
`Return findings via the structured output.`,
|
|
214
|
+
].filter(Boolean).join("\n\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function skepticPrompt(f) {
|
|
218
|
+
return [
|
|
219
|
+
"You are a skeptic. REFUTE the performance finding — prove it is not worth fixing.",
|
|
220
|
+
scope,
|
|
221
|
+
`Finding (${f.category}) at ${f.file}:${f.line}:`,
|
|
222
|
+
`Description: ${f.description}`,
|
|
223
|
+
`Claimed hotness: ${f.hotness}; estimatedImpact: ${f.estimatedImpact}; complexity ${f.complexityBefore} -> ${f.complexityAfter}`,
|
|
224
|
+
"Check: is this path actually hot / over large data? Is there OBSERVED code evidence (an explicit loop, a known entry point, visible data scale), or is the hot-path only SUSPECTED? Is the complexity analysis correct? Would the proposed change preserve behavior? Is this premature optimization of a cold path?",
|
|
225
|
+
"Set refuted=true if not a real, worthwhile, evidenced optimization. Default to refuted=true when the hot-path is only SUSPECTED and unsupported by code evidence.",
|
|
226
|
+
].join("\n\n");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---- 1. Recon (use injected recon if present, else profile once) ----
|
|
230
|
+
await phase("recon");
|
|
231
|
+
const recon = RT.recon
|
|
232
|
+
? RT.recon
|
|
233
|
+
: await agent(
|
|
234
|
+
["Profile this repository for performance analysis. Return the structured recon fields.", scope,
|
|
235
|
+
"Report: languages/frameworks; package managers; entry points; hot paths (request handlers, loops over large data, batch jobs, render paths); data stores & I/O; expected data scale; any existing benchmarks/profiling (note their presence but do NOT run one); concurrency model; error-handling conventions; external resources; and notes on paths that look hot or process large data.",
|
|
236
|
+
"Explore with your tools."].join("\n\n"),
|
|
237
|
+
{ label: "recon", schema: RECON_SCHEMA, tier: TIER_RECON, onFailure: "returnNull" },
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// ---- 2. Find + dedup ----
|
|
241
|
+
function dedup(findings) {
|
|
242
|
+
const seen = new Set(); const out = [];
|
|
243
|
+
for (const f of findings) {
|
|
244
|
+
if (!f) continue;
|
|
245
|
+
const key = `${f.category}::${(f.file || "").trim()}::${f.line || 0}`;
|
|
246
|
+
if (seen.has(key)) continue;
|
|
247
|
+
seen.add(key); out.push(f);
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function findRound(roundNote) {
|
|
253
|
+
await phase("find");
|
|
254
|
+
// arity-1 thunks (api) => ... so parallel() runs them CONCURRENTLY (zero-arg thunks fail unless { sequential: true } is explicit).
|
|
255
|
+
const results = await parallel(categories.map((cat) => (api) =>
|
|
256
|
+
api.agent(finderPrompt(cat, recon, roundNote), { label: `find:${cat}`, schema: FINDINGS_SCHEMA, tier: TIER_FINDER, onFailure: "returnNull" })));
|
|
257
|
+
tallyPhase("find", results, (i) => `find:${categories[i]}`);
|
|
258
|
+
return results.filter(Boolean).flatMap((r) => (r.findings || []).map((f) => ({ ...f, category: f.category || "unknown" })));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let findings = dedup(await findRound(null));
|
|
262
|
+
await log(`Round 1: ${findings.length} candidate hotspots across ${categories.length} lenses`);
|
|
263
|
+
|
|
264
|
+
if (depth === "thorough") {
|
|
265
|
+
const known = findings.map((f) => `- ${f.category} ${f.file}:${f.line} — ${f.description}`).join("\n");
|
|
266
|
+
const round2 = await findRound(`SECOND pass. Already found below — find only NEW hotspots, do not repeat:\n${known}`);
|
|
267
|
+
findings = dedup(findings.concat(round2));
|
|
268
|
+
await log(`After round 2: ${findings.length} candidates`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// positional id (in-run reference) + stable content fingerprint (cross-run dedupe key)
|
|
272
|
+
findings = findings.map((f, i) => ({ ...f, id: `${f.category}-${i + 1}`, fingerprint: fingerprintOf(f) }));
|
|
273
|
+
|
|
274
|
+
if (findings.length === 0) {
|
|
275
|
+
return envelope("empty", { summary: "No performance hotspots found.", counts: emptyCounts, findings: [], truncatedFindings: false, reportMarkdown: null, shellCoverage: SHELL_COVERAGE, coverageLimitations: COVERAGE_LIMITATIONS });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---- 3. Verify (high-FP profile) ----
|
|
279
|
+
// quick: high-severity only, 1 skeptic. normal: ALL, 1 skeptic. thorough: ALL, 3-skeptic majority (keep unless >=2 refute).
|
|
280
|
+
let toVerify, votes;
|
|
281
|
+
if (depth === "quick") { toVerify = findings.filter((f) => f.severity === "high"); votes = 1; }
|
|
282
|
+
else if (depth === "thorough") { toVerify = findings; votes = 3; }
|
|
283
|
+
else { toVerify = findings; votes = 1; }
|
|
284
|
+
const verifyIds = new Set(toVerify.map((f) => f.id));
|
|
285
|
+
const passThrough = findings.filter((f) => !verifyIds.has(f.id));
|
|
286
|
+
|
|
287
|
+
let verified = passThrough;
|
|
288
|
+
if (toVerify.length > 0) {
|
|
289
|
+
await phase("verify");
|
|
290
|
+
const checked = await parallel(toVerify.map((f) => (api) => {
|
|
291
|
+
if (votes === 1) {
|
|
292
|
+
return api.agent(skepticPrompt(f), { label: `verify:${f.id}`, schema: VERDICT_SCHEMA, tier: TIER_VERIFY, onFailure: "returnNull" })
|
|
293
|
+
.then((v) => ({ f, keep: !!(v && !v.refuted), conf: v ? v.adjustedConfidence : undefined }));
|
|
294
|
+
}
|
|
295
|
+
return api.parallel([0, 1, 2].map((n) => (inner) =>
|
|
296
|
+
inner.agent(`${skepticPrompt(f)}\n\n(Independent reviewer #${n + 1}.)`, { label: `verify:${f.id}#${n + 1}`, schema: VERDICT_SCHEMA, tier: TIER_VERIFY, onFailure: "returnNull" })))
|
|
297
|
+
.then((vs) => {
|
|
298
|
+
const ok = vs.filter(Boolean);
|
|
299
|
+
const refutedCount = ok.filter((v) => v.refuted).length;
|
|
300
|
+
const avg = ok.length ? Math.round(ok.reduce((s, v) => s + (v.adjustedConfidence || 0), 0) / ok.length) : f.confidence;
|
|
301
|
+
return { f, keep: ok.length > 0 && refutedCount < 2, conf: avg };
|
|
302
|
+
});
|
|
303
|
+
}));
|
|
304
|
+
tallyPhase("verify", checked, (i) => `verify:${toVerify[i].id}`);
|
|
305
|
+
const survivors = checked.filter(Boolean).filter((c) => c.keep)
|
|
306
|
+
.map((c) => ({ ...c.f, confidence: c.conf != null ? c.conf : c.f.confidence }));
|
|
307
|
+
verified = passThrough.concat(survivors);
|
|
308
|
+
await log(`Verified: ${survivors.length}/${toVerify.length} candidates survived; ${verified.length} total`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (verified.length === 0) {
|
|
312
|
+
return envelope("empty", { summary: "No hotspots survived verification.", counts: emptyCounts, findings: [], truncatedFindings: false, reportMarkdown: null, shellCoverage: SHELL_COVERAGE, coverageLimitations: COVERAGE_LIMITATIONS });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---- 4. Synthesize (PURE JS — dedup, rank, render; the host persists the returned object) ----
|
|
316
|
+
await phase("synthesize");
|
|
317
|
+
const SEVW = { high: 3, medium: 2, low: 1 };
|
|
318
|
+
const EFFD = { small: 1, medium: 0.8, large: 0.6 };
|
|
319
|
+
function score(f) { return (SEVW[f.severity] || 1) * ((f.confidence || 0) / 100) * (EFFD[f.effort] || 0.8); }
|
|
320
|
+
|
|
321
|
+
const ranked = verified.map((f) => ({ ...f })).sort((a, b) => score(b) - score(a));
|
|
322
|
+
ranked.forEach((f, i) => { f.rank = i + 1; });
|
|
323
|
+
|
|
324
|
+
const counts = {
|
|
325
|
+
total: ranked.length,
|
|
326
|
+
critical: 0, // non-security domain: critical tier never populated
|
|
327
|
+
high: ranked.filter((f) => f.severity === "high").length,
|
|
328
|
+
medium: ranked.filter((f) => f.severity === "medium").length,
|
|
329
|
+
low: ranked.filter((f) => f.severity === "low").length,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
function mdCell(s) { return String(s == null ? "" : s).replace(/\|/g, "\\|").replace(/\n+/g, " "); }
|
|
333
|
+
function renderMarkdown(rows, c) {
|
|
334
|
+
const lines = [];
|
|
335
|
+
lines.push(`# Performance Audit Report (${DOMAIN})`, "");
|
|
336
|
+
lines.push("> Report-only. No files were modified and nothing was applied. Static read-only analysis — no profiler/benchmark/metrics were run.", "");
|
|
337
|
+
lines.push("## Summary", `- Total: ${c.total} (high: ${c.high}, medium: ${c.medium}, low: ${c.low})`, "");
|
|
338
|
+
lines.push(`## Coverage`, `- shellCoverage: ${SHELL_COVERAGE}`, `- coverageLimitations: ${COVERAGE_LIMITATIONS}`, "");
|
|
339
|
+
lines.push("## Ranked findings", "");
|
|
340
|
+
lines.push("| Rank | Category | Severity | Hotness | Confidence | Location | Estimated impact |");
|
|
341
|
+
lines.push("| ---- | -------- | -------- | ------- | ---------- | -------- | ---------------- |");
|
|
342
|
+
for (const f of rows) {
|
|
343
|
+
lines.push(`| ${f.rank} | ${mdCell(f.category)} | ${mdCell(f.severity)} | ${mdCell(f.hotness)} | ${f.confidence} | ${mdCell(f.file)}:${f.line || 0} | ${mdCell(f.estimatedImpact).slice(0, 100)} |`);
|
|
344
|
+
}
|
|
345
|
+
lines.push("", "## Detail");
|
|
346
|
+
for (const f of rows) {
|
|
347
|
+
lines.push("", `### ${f.rank}. ${mdCell(f.category)} — ${mdCell(f.file)}:${f.line || 0} (${f.severity}, ${mdCell(f.hotness)}, conf ${f.confidence})`);
|
|
348
|
+
lines.push(`- **What:** ${f.description}`);
|
|
349
|
+
lines.push(`- **Estimated impact:** ${f.estimatedImpact}`);
|
|
350
|
+
lines.push(`- **Complexity:** ${f.complexityBefore} -> ${f.complexityAfter}`);
|
|
351
|
+
lines.push(`- **Proposed change:** ${f.proposedChange}`);
|
|
352
|
+
if (f.docImpact) lines.push(`- **Doc impact:** ${f.docImpact}`);
|
|
353
|
+
lines.push(`- **Fingerprint:** \`${f.fingerprint}\``);
|
|
354
|
+
}
|
|
355
|
+
return lines.join("\n");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Size-fit to the 256 KB host cap: drop reportMarkdown first, then halve findings, until it fits.
|
|
359
|
+
function utf8ByteLength(value) {
|
|
360
|
+
const s = String(value ?? "");
|
|
361
|
+
let bytes = 0;
|
|
362
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
363
|
+
const code = s.charCodeAt(i);
|
|
364
|
+
if (code <= 0x7f) bytes += 1;
|
|
365
|
+
else if (code <= 0x7ff) bytes += 2;
|
|
366
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < s.length) {
|
|
367
|
+
const next = s.charCodeAt(i + 1);
|
|
368
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
369
|
+
bytes += 4;
|
|
370
|
+
i += 1;
|
|
371
|
+
} else bytes += 3;
|
|
372
|
+
} else bytes += 3;
|
|
373
|
+
}
|
|
374
|
+
return bytes;
|
|
375
|
+
}
|
|
376
|
+
function jsonUtf8ByteLength(value) {
|
|
377
|
+
return utf8ByteLength(JSON.stringify(value));
|
|
378
|
+
}
|
|
379
|
+
function fitWithinBudget(status, summary) {
|
|
380
|
+
const LIMIT = 230000; // headroom under MAX_RESULT_BYTES (262144) for the {output:...} wrapper + envelope fields
|
|
381
|
+
let returned = ranked.slice(0, MAX_RETURN_FINDINGS);
|
|
382
|
+
let truncated = ranked.length > returned.length;
|
|
383
|
+
let reportMarkdown = renderMarkdown(ranked, counts);
|
|
384
|
+
const sizeOf = () => jsonUtf8ByteLength(envelope(status, { summary, counts, findings: returned, truncatedFindings: truncated, reportMarkdown, shellCoverage: SHELL_COVERAGE, coverageLimitations: COVERAGE_LIMITATIONS }));
|
|
385
|
+
if (sizeOf() > LIMIT) reportMarkdown = null;
|
|
386
|
+
while (sizeOf() > LIMIT && returned.length > 10) {
|
|
387
|
+
returned = returned.slice(0, Math.ceil(returned.length / 2));
|
|
388
|
+
truncated = true;
|
|
389
|
+
}
|
|
390
|
+
return envelope(status, { summary, counts, findings: returned, truncatedFindings: truncated, reportMarkdown, shellCoverage: SHELL_COVERAGE, coverageLimitations: COVERAGE_LIMITATIONS });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const summary = `Found ${counts.total} performance hotspot(s): ${counts.high} high, ${counts.medium} medium, ${counts.low} low. Report-only — nothing applied. Static read-only analysis (no profiler/benchmark run).`;
|
|
394
|
+
return fitWithinBudget("ok", summary);
|