@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,831 @@
|
|
|
1
|
+
// META orchestrator workflow — /repo-review. Runs the eight repo-* LEAF engines via
|
|
2
|
+
// static-literal workflow() calls (one nesting level — legal because each engine is a
|
|
3
|
+
// leaf that never nests), computes shared recon ONCE, then normalizes + conservatively
|
|
4
|
+
// merges + ranks their findings into ONE cross-domain report. Report-only.
|
|
5
|
+
//
|
|
6
|
+
// Contract: docs/repo-review-leaf-contract.md §14 (Meta-to-leaf arg contract).
|
|
7
|
+
// Recon is computed ONCE and the SAME recon+paths+exclude+depth object reference is
|
|
8
|
+
// injected into every literal one-level workflow("repo-X", leafArgs) call, so all eight
|
|
9
|
+
// domains analyze one coherent file inventory and cross-domain dedupe stays consistent.
|
|
10
|
+
//
|
|
11
|
+
// Authority: profile "read-only-review" (readOnly, requiredGates:[]). The meta NEVER
|
|
12
|
+
// calls materialize, drain, workflow_apply, git, or Beads mutation, and NEVER writes a
|
|
13
|
+
// file — the QuickJS guest cannot. reportPath is always null; the command wrapper
|
|
14
|
+
// persists the report (engine-vs-wrapper reversal, contract §1).
|
|
15
|
+
//
|
|
16
|
+
// Budget: nested workflow() lanes run INSIDE this run and share its maxAgents/concurrency;
|
|
17
|
+
// a nested leaf's own declared maxAgents/concurrency is IGNORED at runtime. Parent
|
|
18
|
+
// maxAgents=100000 intentionally over-provisions (effectively unlimited) one recon lane, the coverage auditor, plus
|
|
19
|
+
// the cumulative cold-run fan-out of all eight leaves under batched parallel()
|
|
20
|
+
// orchestration (≈50 finder lanes for an empty run, ≈150+ for a thorough-depth exhaustive
|
|
21
|
+
// run with skeptics, and potentially far more in large repos). concurrency=16 is the
|
|
22
|
+
// kernel's hard cap and bounds peak concurrent child sessions across the whole tree. An
|
|
23
|
+
// exhaustive thorough run may still budget-stop gracefully if a caller overrides a smaller
|
|
24
|
+
// parent budget: a stopped lane returns null (onFailure-equivalent try/catch), is dropped
|
|
25
|
+
// by .filter(Boolean), and surfaces as partial coverage + a materialization blocker rather
|
|
26
|
+
// than a crash.
|
|
27
|
+
//
|
|
28
|
+
// EXHAUSTIVE-BY-DEFAULT (rrev.27): empty args default to mode:"exhaustive", which selects
|
|
29
|
+
// depth:"thorough", a higher maxReturnFindings, and a coverage-auditor lane. Pass
|
|
30
|
+
// mode:"bounded" for the legacy normal-depth behavior. The read-only boundary is unchanged:
|
|
31
|
+
// the meta NEVER mutates Beads, applies diffs, or writes files. A separate, explicitly-
|
|
32
|
+
// approved review-materialize flow consumes the report ONLY when materializationReady is true.
|
|
33
|
+
export const meta = {
|
|
34
|
+
name: "repo-review",
|
|
35
|
+
description: "Comprehensive repo review meta: runs all eight repo-* domain engines (bughunt, security, test-gaps, cleanup, modernize, perf, complexity, deps) with ONE shared recon, then normalizes + conservatively merges + ranks findings cross-domain into a single report. Exhaustive by default (thorough depth + coverage auditor); report-only, nothing is applied.",
|
|
36
|
+
profile: "read-only-review",
|
|
37
|
+
maxAgents: 100000,
|
|
38
|
+
concurrency: 16,
|
|
39
|
+
phases: ["recon", "domains", "merge", "audit", "synthesize"],
|
|
40
|
+
category: "repo-review-meta",
|
|
41
|
+
notes: "Read-only whole-repo review. Defaults to EXHAUSTIVE (thorough depth, high maxReturnFindings, coverage auditor); pass mode:'bounded' for the legacy normal-depth pass. Emits materializationReady/materializationBlockers so a separately-approved materialize flow can refuse incomplete reports. Model tier labels (fast/deep) are lane INTENT only — the command maps both to the same deep model so no fast model is ever selected.",
|
|
42
|
+
examples: [
|
|
43
|
+
{ label: "exhaustive full review (default)", args: { mode: "exhaustive", paths: ["src"] } },
|
|
44
|
+
{ label: "bounded legacy review", args: { mode: "bounded", depth: "normal", paths: ["src"] } },
|
|
45
|
+
{ label: "focused security and bugs (exhaustive)", args: { mode: "exhaustive", domains: ["bughunt", "security"], paths: ["src", "tests"], batchSize: 2 } },
|
|
46
|
+
],
|
|
47
|
+
argsSchema: {
|
|
48
|
+
type: ["object", "string", "null"],
|
|
49
|
+
properties: {
|
|
50
|
+
mode: { type: "string", enum: ["exhaustive", "bounded"] },
|
|
51
|
+
paths: { type: "array", items: { type: "string" } },
|
|
52
|
+
exclude: { type: "array", items: { type: "string" } },
|
|
53
|
+
depth: { type: "string", enum: ["quick", "normal", "thorough"] },
|
|
54
|
+
domains: { type: "array", items: { type: "string", enum: ["bughunt", "security", "test-gaps", "cleanup", "modernize", "perf", "complexity", "deps", "repo-bughunt", "repo-security-audit", "repo-test-gaps", "repo-cleanup", "repo-modernize", "repo-perf", "repo-complexity", "repo-deps"] } },
|
|
55
|
+
batchSize: { type: "integer", minimum: 1 },
|
|
56
|
+
maxReturnFindings: { type: "integer", minimum: 1 },
|
|
57
|
+
maxDirs: { type: "integer", minimum: 1 },
|
|
58
|
+
deepMode: { type: "string", enum: ["static", "audited-shell", "network-advisory"] },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ---- suite identity ----
|
|
64
|
+
const DOMAIN = "repo-review";
|
|
65
|
+
const SCHEMA_VERSION = 1;
|
|
66
|
+
|
|
67
|
+
// args may arrive as an object (workflow_run args) or, defensively, a JSON string.
|
|
68
|
+
let RT = args;
|
|
69
|
+
if (typeof RT === "string") { try { RT = RT.trim() ? JSON.parse(RT) : {}; } catch (error) { throw new Error(`Invalid repo-review runtime args JSON: ${error.message}`); } }
|
|
70
|
+
if (!RT || typeof RT !== "object" || Array.isArray(RT)) RT = {};
|
|
71
|
+
|
|
72
|
+
// ---- args ----
|
|
73
|
+
const paths = Array.isArray(RT.paths) && RT.paths.length ? RT.paths : ["."];
|
|
74
|
+
const exclude = Array.isArray(RT.exclude) ? RT.exclude : ["node_modules", "dist", "build", ".git", "vendor", "target", "*.min.*", "*.map"];
|
|
75
|
+
// EXHAUSTIVE-BY-DEFAULT: empty args select exhaustive mode, which drives thorough depth and a
|
|
76
|
+
// high maxReturnFindings. mode:"bounded" preserves the legacy normal-depth behavior.
|
|
77
|
+
const mode = RT.mode === "bounded" ? "bounded" : "exhaustive";
|
|
78
|
+
const exhaustive = mode === "exhaustive";
|
|
79
|
+
const depth = ["quick", "normal", "thorough"].includes(RT.depth) ? RT.depth : (exhaustive ? "thorough" : "normal");
|
|
80
|
+
// batchSize bounds how many leaves run per parallel() batch (peak-concurrency control),
|
|
81
|
+
// mirroring the Claude meta. Positive integer only (0/neg would infinite-loop the batcher).
|
|
82
|
+
// Default = all active domains in ONE batch (scale: serialize the eight leaves into a single
|
|
83
|
+
// batched parallel() call rather than four batches of two).
|
|
84
|
+
const maxReturnFindings = Number.isInteger(RT.maxReturnFindings) && RT.maxReturnFindings > 0 ? RT.maxReturnFindings : 1000000;
|
|
85
|
+
// maxDirs (the complexity per-domain carve-out) is forwarded to repo-complexity so the meta's
|
|
86
|
+
// scale decision reaches the leaf instead of it silently clamping to a low local default.
|
|
87
|
+
const maxDirs = Number.isInteger(RT.maxDirs) && RT.maxDirs > 0 ? RT.maxDirs : 1000000;
|
|
88
|
+
|
|
89
|
+
// Domain filter: the eight leaf domains. Accept either the bare domain ("bughunt") or the
|
|
90
|
+
// leaf name ("repo-bughunt"); normalize by stripping a leading "repo-". Default = all eight.
|
|
91
|
+
const ALL_DOMAINS = ["bughunt", "security", "test-gaps", "cleanup", "modernize", "perf", "complexity", "deps"];
|
|
92
|
+
function normDomain(d) { return typeof d === "string" ? d.replace(/^repo-/, "").trim() : ""; }
|
|
93
|
+
const suppliedDomains = Array.isArray(RT.domains) && RT.domains.length ? RT.domains.map(normDomain) : [];
|
|
94
|
+
const requestedDomains = suppliedDomains.filter((d) => ALL_DOMAINS.includes(d));
|
|
95
|
+
// Reject unknown-only domains with a clear error instead of silently falling back to ALL_DOMAINS.
|
|
96
|
+
// A mix of known+unknown is tolerated (unknown ones are dropped); only an ALL-unknown request is fatal.
|
|
97
|
+
if (suppliedDomains.length && requestedDomains.length === 0) {
|
|
98
|
+
throw new Error(`repo-review: all requested domains are unknown. Supplied: ${JSON.stringify(RT.domains)}. Known domains: ${JSON.stringify(ALL_DOMAINS)}.`);
|
|
99
|
+
}
|
|
100
|
+
const want = new Set(requestedDomains.length ? requestedDomains : ALL_DOMAINS);
|
|
101
|
+
const activeDomains = ALL_DOMAINS.filter((d) => want.has(d));
|
|
102
|
+
const batchSize = Number.isInteger(RT.batchSize) && RT.batchSize > 0 ? RT.batchSize : activeDomains.length;
|
|
103
|
+
|
|
104
|
+
const scope = `Scope: paths = ${JSON.stringify(paths)}. Exclude (do not scan/report): ${JSON.stringify(exclude)}.`;
|
|
105
|
+
|
|
106
|
+
// DEEP MODE (iui1.7): repo-review is static + read-only by default. audited-shell and
|
|
107
|
+
// network-advisory are OPTIONAL, explicitly opt-in deep modes that require VERIFIED gates
|
|
108
|
+
// (enforced at the kernel/command level when a caller passes profile:"inspect-with-shell" etc.).
|
|
109
|
+
// The QuickJS guest itself NEVER requests shell or network — it stays static. This field reports
|
|
110
|
+
// what was REQUESTED so the envelope + report disclose the (intentionally static) coverage limit.
|
|
111
|
+
const deepModeRequested = ["audited-shell", "network-advisory"].includes(RT.deepMode) ? RT.deepMode : "static";
|
|
112
|
+
const deepMode = {
|
|
113
|
+
requested: deepModeRequested,
|
|
114
|
+
active: "static",
|
|
115
|
+
shellCoverage: "none",
|
|
116
|
+
coverageLimitations: deepModeRequested === "static"
|
|
117
|
+
? "Static, read-only analysis (no shell/git-churn, no network advisory lookups). Optional audited-shell / network-advisory modes require verified gates."
|
|
118
|
+
: `${deepModeRequested} requested but the review engine runs static in-guest; the deep mode requires verified gates enforced at the kernel/command level.`,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// ---- standardized return envelope ----
|
|
122
|
+
function envelope(status, extra) {
|
|
123
|
+
return { domain: DOMAIN, schemaVersion: SCHEMA_VERSION, status, abortReason: null, reportPath: null, ...extra };
|
|
124
|
+
}
|
|
125
|
+
const emptyCounts = { total: 0, critical: 0, high: 0, medium: 0, low: 0 };
|
|
126
|
+
|
|
127
|
+
// ---- shared recon schema (tolerates a prose string via formatRecon) ----
|
|
128
|
+
const RECON_SCHEMA = {
|
|
129
|
+
type: "object", additionalProperties: false,
|
|
130
|
+
properties: {
|
|
131
|
+
languages: { type: "array", items: { type: "string" } },
|
|
132
|
+
frameworks: { type: "array", items: { type: "string" } },
|
|
133
|
+
packageManagers: { type: "array", items: { type: "string" } },
|
|
134
|
+
entryPoints: { type: "array", items: { type: "string" } },
|
|
135
|
+
testLayout: { type: "string" },
|
|
136
|
+
buildTooling: { type: "string" },
|
|
137
|
+
concurrencyModel: { type: "string" },
|
|
138
|
+
errorHandling: { type: "string" },
|
|
139
|
+
externalResources: { type: "array", items: { type: "string" } },
|
|
140
|
+
notes: { type: "string" },
|
|
141
|
+
},
|
|
142
|
+
required: ["languages", "notes"],
|
|
143
|
+
};
|
|
144
|
+
function formatRecon(r) {
|
|
145
|
+
if (typeof r === "string") return r;
|
|
146
|
+
if (!r || typeof r !== "object") return "No recon available.";
|
|
147
|
+
const L = (k, v) => (v && (Array.isArray(v) ? v.length : true) ? `${k}: ${Array.isArray(v) ? v.join(", ") : v}` : null);
|
|
148
|
+
return [
|
|
149
|
+
L("Languages", r.languages), L("Frameworks", r.frameworks), L("Package managers", r.packageManagers),
|
|
150
|
+
L("Entry points", r.entryPoints), L("Test layout", r.testLayout), L("Build tooling", r.buildTooling),
|
|
151
|
+
L("Concurrency model", r.concurrencyModel), L("Error handling", r.errorHandling),
|
|
152
|
+
L("External resources", r.externalResources), L("Notes", r.notes),
|
|
153
|
+
].filter(Boolean).join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- 1. Shared recon (computed ONCE for all domains; or accept an injected args.recon) ----
|
|
157
|
+
await phase("recon");
|
|
158
|
+
// Deterministic file inventory + sharding (iui1.5): a bounded, sorted fs walk produces a real
|
|
159
|
+
// file manifest so coverage is NOT agent-discovered ("Explore with your tools"). Large repos are
|
|
160
|
+
// partitioned into shards (by source root, capped at shardSize files each); a shard ledger tracks
|
|
161
|
+
// coverage so a missed/failed shard blocks materialization. The QuickJS guest has no fs, so
|
|
162
|
+
// inventoryFiles is a kernel host op. inventoryFailed or any shardMissed is a materialization blocker.
|
|
163
|
+
let inventory = null;
|
|
164
|
+
try {
|
|
165
|
+
inventory = await inventoryFiles({ paths, exclude, shardSize: 2000 });
|
|
166
|
+
} catch (e) {
|
|
167
|
+
inventory = { ok: false, error: String((e && e.message) || e), manifest: null, shards: [], partial: false };
|
|
168
|
+
}
|
|
169
|
+
const inventoryReady = !!(inventory && inventory.ok);
|
|
170
|
+
const manifest = (inventory && inventory.manifest) || null;
|
|
171
|
+
const inventoryShards = (inventory && inventory.shards) || [];
|
|
172
|
+
// Shard ledger: one entry per shard. Status is finalized after the domain phase (a shard is
|
|
173
|
+
// "completed" when at least one domain leaf ran; "missed" when every domain failed so the shard
|
|
174
|
+
// received zero review coverage). A partial inventory (hit the file cap) leaves a coverage gap.
|
|
175
|
+
const shardLedger = inventoryShards.map((s) => ({ id: s.id, root: s.root, fileCount: s.fileCount, languages: s.languages, status: "expected" }));
|
|
176
|
+
if (inventory && inventory.ok) {
|
|
177
|
+
await log(`Inventory: ${manifest.totalFiles} files, ${inventoryShards.length} shard(s) across ${manifest.sourceRoots.length} source root(s)${inventory.partial ? " (PARTIAL — hit file cap)" : ""}.`);
|
|
178
|
+
} else {
|
|
179
|
+
await log(`WARNING: file inventory failed — ${inventory && inventory.error}. Coverage accounting degraded.`);
|
|
180
|
+
}
|
|
181
|
+
const manifestBrief = manifest
|
|
182
|
+
? `Deterministic file inventory (grounding, not a suggestion): ${manifest.totalFiles} files across ${manifest.sourceRoots.length} source root(s) (${manifest.sourceRoots.slice(0, 12).join(", ")}${manifest.sourceRoots.length > 12 ? ", ..." : ""}); languages: ${Object.entries(manifest.byLanguage).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}(${v})`).join(", ") || "none"}; file roles: ${Object.entries(manifest.byRole).map(([k, v]) => `${k}(${v})`).join(", ") || "none"}; sharded into ${inventoryShards.length} shard(s)${inventory.partial ? " (PARTIAL inventory — some files beyond the cap were not enumerated)" : ""}.`
|
|
183
|
+
: "No deterministic file inventory available; profile from what you can observe.";
|
|
184
|
+
|
|
185
|
+
let recon;
|
|
186
|
+
if (RT.recon !== undefined && RT.recon !== null) {
|
|
187
|
+
recon = RT.recon;
|
|
188
|
+
await log("Using injected shared recon (args.recon present) — every leaf skips self-profiling.");
|
|
189
|
+
} else {
|
|
190
|
+
recon = await agent(
|
|
191
|
+
["Profile this repository once for a comprehensive multi-domain review (bugs, security, test gaps, cleanliness, complexity, deps, modernization, performance). Return the structured recon fields.",
|
|
192
|
+
scope,
|
|
193
|
+
manifestBrief,
|
|
194
|
+
"Report: languages/frameworks; package managers; entry points; test layout; build tooling; concurrency model; error-handling conventions; external resources (DB, files, network); and notes on anything relevant to reviewing this repo.",
|
|
195
|
+
"Use the deterministic inventory above as the file-set ground truth; only explore further for semantics the file list cannot reveal."].join("\n\n"),
|
|
196
|
+
{ label: "recon", schema: RECON_SCHEMA, tier: "fast", onFailure: "returnNull" },
|
|
197
|
+
);
|
|
198
|
+
if (!recon || typeof recon !== "object") {
|
|
199
|
+
await log("WARNING: shared recon returned null/invalid — each leaf will self-profile (~8x recon cost).");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ONE shared args object: the SAME reference is injected into every literal one-level
|
|
204
|
+
// workflow("repo-X", leafArgs) call (contract §14.2). Leaves skip self-profiling because
|
|
205
|
+
// recon is present. maxReturnFindings (effectively-unlimited ceiling) and maxDirs (the
|
|
206
|
+
// complexity per-domain carve-out) are forwarded so the meta's scale decisions reach every
|
|
207
|
+
// leaf instead of each leaf silently defaulting to a low local cap.
|
|
208
|
+
const leafArgs = { recon, paths, exclude, depth, maxReturnFindings, maxDirs };
|
|
209
|
+
|
|
210
|
+
// ---- 2. Run the eight leaf engines (static literals only, batched to bound peak concurrency) ----
|
|
211
|
+
// Each leaf is a LEAF (never nests), so this is one nesting level. workflow() does not take
|
|
212
|
+
// an onFailure option, so each literal call is wrapped in try/catch: a failed/budget-stopped
|
|
213
|
+
// leaf resolves to null and is dropped by .filter(Boolean) — it never aborts the meta.
|
|
214
|
+
async function runBughunt(a, w) { if (!w.has("bughunt")) return null; try { return await workflow("repo-bughunt", a); } catch (e) { return { domain: "bughunt", __error: String((e && e.message) || e) }; } }
|
|
215
|
+
async function runSecurity(a, w) { if (!w.has("security")) return null; try { return await workflow("repo-security-audit", a); } catch (e) { return { domain: "security", __error: String((e && e.message) || e) }; } }
|
|
216
|
+
async function runTestGaps(a, w) { if (!w.has("test-gaps")) return null; try { return await workflow("repo-test-gaps", a); } catch (e) { return { domain: "test-gaps", __error: String((e && e.message) || e) }; } }
|
|
217
|
+
async function runCleanup(a, w) { if (!w.has("cleanup")) return null; try { return await workflow("repo-cleanup", a); } catch (e) { return { domain: "cleanup", __error: String((e && e.message) || e) }; } }
|
|
218
|
+
async function runModernize(a, w) { if (!w.has("modernize")) return null; try { return await workflow("repo-modernize", a); } catch (e) { return { domain: "modernize", __error: String((e && e.message) || e) }; } }
|
|
219
|
+
async function runPerf(a, w) { if (!w.has("perf")) return null; try { return await workflow("repo-perf", a); } catch (e) { return { domain: "perf", __error: String((e && e.message) || e) }; } }
|
|
220
|
+
async function runComplexity(a, w) { if (!w.has("complexity")) return null; try { return await workflow("repo-complexity", a); } catch (e) { return { domain: "complexity", __error: String((e && e.message) || e) }; } }
|
|
221
|
+
async function runDeps(a, w) { if (!w.has("deps")) return null; try { return await workflow("repo-deps", a); } catch (e) { return { domain: "deps", __error: String((e && e.message) || e) }; } }
|
|
222
|
+
|
|
223
|
+
// Ordered runner list (one entry per static-literal leaf). Slicing this into batchSize
|
|
224
|
+
// groups keeps the workflow() call sites static literals while bounding peak concurrency.
|
|
225
|
+
const LEAF_RUNNERS = [
|
|
226
|
+
runBughunt, runSecurity, runTestGaps, runCleanup, runModernize, runPerf, runComplexity, runDeps,
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
await phase("domains");
|
|
230
|
+
const rawCoverage = [];
|
|
231
|
+
for (let i = 0; i < LEAF_RUNNERS.length; i += batchSize) {
|
|
232
|
+
const batchRunners = LEAF_RUNNERS.slice(i, i + batchSize);
|
|
233
|
+
await log(`Domains batch ${Math.floor(i / batchSize) + 1}: ${batchRunners.map((fn) => fn.name.replace(/^run/, "").toLowerCase()).join(", ")}`);
|
|
234
|
+
// arity-1 thunks (api) => ... so parallel() runs them CONCURRENTLY (zero-arg thunks fail unless { sequential: true } is explicit).
|
|
235
|
+
const got = await parallel(batchRunners.map((fn) => (api) => fn(leafArgs, want)));
|
|
236
|
+
for (const res of got) if (res) rawCoverage.push(res);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Normalize raw leaf outputs into a DomainOutcome ledger (defensive against malformed leaves).
|
|
240
|
+
const EXTRAS_KEYS = ["staleDocs", "migrationPlan", "upgradePlan", "shellCoverage", "coverageLimitations"];
|
|
241
|
+
const leafOutcomes = [];
|
|
242
|
+
const domainExtras = {};
|
|
243
|
+
for (const env of rawCoverage) {
|
|
244
|
+
if (!env || typeof env !== "object") continue;
|
|
245
|
+
const dom = typeof env.domain === "string" ? env.domain.replace(/^repo-/, "") : "unknown";
|
|
246
|
+
if (env.__error) {
|
|
247
|
+
leafOutcomes.push({ domain: dom, status: "failed", counts: emptyCounts, error: env.__error });
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const status = env.status === "ok" || env.status === "empty" || env.status === "aborted" ? env.status : (Array.isArray(env.findings) && env.findings.length ? "ok" : "empty");
|
|
251
|
+
const counts = (env.counts && typeof env.counts === "object") ? env.counts : emptyCounts;
|
|
252
|
+
const outcome = { domain: dom, status, counts };
|
|
253
|
+
// Propagate leaf-level truncation into the ledger so the materialization gate can refuse
|
|
254
|
+
// a report whose per-domain finding set was size-capped (materializing from a truncated
|
|
255
|
+
// leaf would silently drop issues).
|
|
256
|
+
if (env.truncatedFindings === true) outcome.truncatedFindings = true;
|
|
257
|
+
// Propagate per-leaf lane coverage telemetry (iui1.2). A dropped finder/verifier/scorer lane
|
|
258
|
+
// means the leaf's coverage is degraded even if it returned status ok/empty; the meta surfaces
|
|
259
|
+
// it as partialCoverage + a materialization blocker so a silent per-lane drop can never produce
|
|
260
|
+
// a report that looks complete but is missing whole issue classes.
|
|
261
|
+
if (env.laneCoverage && typeof env.laneCoverage === "object") {
|
|
262
|
+
outcome.laneCoverage = env.laneCoverage;
|
|
263
|
+
if (env.laneCoverage.dropped > 0) outcome.laneDropped = env.laneCoverage.dropped;
|
|
264
|
+
}
|
|
265
|
+
leafOutcomes.push(outcome);
|
|
266
|
+
// Preserve per-domain top-level extras (carve-outs) surfaced, not merged.
|
|
267
|
+
const extras = {};
|
|
268
|
+
for (const k of EXTRAS_KEYS) if (env[k] !== undefined) extras[k] = env[k];
|
|
269
|
+
if (Object.keys(extras).length) domainExtras[dom] = extras;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const ran = leafOutcomes.filter((o) => o.status === "ok" || o.status === "empty");
|
|
273
|
+
const failed = leafOutcomes.filter((o) => o.status === "failed");
|
|
274
|
+
await log(`Domains complete: ${ran.length} ran (ok/empty), ${failed.length} failed, of ${activeDomains.length} active.`);
|
|
275
|
+
|
|
276
|
+
// Finalize shard coverage (iui1.5): leaves run over the whole repo, so a shard is "completed"
|
|
277
|
+
// when at least one domain leaf produced a usable envelope. A shard is "missed" only when EVERY
|
|
278
|
+
// domain leaf failed (the shard's files received zero review). A partial inventory leaves any
|
|
279
|
+
// un-enumerated files as an inherent coverage gap (recorded on the ledger, not a per-shard miss).
|
|
280
|
+
const shardAnyCoverage = ran.length > 0;
|
|
281
|
+
for (const s of shardLedger) s.status = (s.fileCount > 0 && !shardAnyCoverage) ? "missed" : "completed";
|
|
282
|
+
const shardMissedCount = shardLedger.filter((s) => s.status === "missed").length;
|
|
283
|
+
if (shardMissedCount > 0) await log(`Shard coverage: ${shardMissedCount} shard(s) missed (no domain produced coverage).`);
|
|
284
|
+
|
|
285
|
+
// Map domain -> raw findings (only from leaves that returned an envelope with findings).
|
|
286
|
+
const findingsByDomain = {};
|
|
287
|
+
for (const env of rawCoverage) {
|
|
288
|
+
if (!env || typeof env !== "object" || env.__error) continue;
|
|
289
|
+
const dom = typeof env.domain === "string" ? env.domain.replace(/^repo-/, "") : "unknown";
|
|
290
|
+
findingsByDomain[dom] = Array.isArray(env.findings) ? env.findings : [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---- 3. Normalize + conservatively merge cross-domain (pure JS — no tokens) ----
|
|
294
|
+
await phase("merge");
|
|
295
|
+
const SEVW = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
296
|
+
const EFFW = { small: 1, medium: 2, large: 3 };
|
|
297
|
+
const EFFD = { small: 1, medium: 0.8, large: 0.6 };
|
|
298
|
+
const normFile = (s) => (s || "").toString().replace(/^\.\//, "").trim();
|
|
299
|
+
const sevRank = (s) => SEVW[s] || 1;
|
|
300
|
+
const maxEffort = (a, b) => ((EFFW[a] || 2) >= (EFFW[b] || 2) ? a : b);
|
|
301
|
+
const weight = (f) => sevRank(f.severity) * ((f.confidence || 0) / 100);
|
|
302
|
+
|
|
303
|
+
// corroborationKey (iui1.6): a CROSS-DOMAIN "same root cause" key DISTINCT from the domain-
|
|
304
|
+
// prefixed materialization fingerprint. Two findings with the same file+category corroborate
|
|
305
|
+
// each other even across domains (fingerprints never match cross-domain because each leaf's
|
|
306
|
+
// fingerprintOf prefixes with its DOMAIN). Used ONLY to LINK (relatesTo) and to BOOST priority
|
|
307
|
+
// for multi-domain corroboration — never to MERGE (the materialization fingerprint stays
|
|
308
|
+
// domain-stable so a fix closes coverage for exactly the issue it fixes).
|
|
309
|
+
function corroborationKeyOf(file, category) {
|
|
310
|
+
return `${normFile(file)}::${String(category || "").toLowerCase().replace(/\s+/g, " ").trim()}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Curated per-domain fields preserved for Beads materialization. The meta keeps these under
|
|
314
|
+
// domainDetails in findings.full.json so /review-materialize can build native Beads design and
|
|
315
|
+
// acceptance fields instead of losing implementation-relevant context during normalization.
|
|
316
|
+
const DOMAIN_DETAIL_KEYS = {
|
|
317
|
+
bughunt: ["reproSketch", "fixSketch", "docImpact"],
|
|
318
|
+
security: ["cwe", "attackVector", "exploitability", "docImpact"],
|
|
319
|
+
"test-gaps": ["targetUnderTest", "suggestedTest", "docImpact"],
|
|
320
|
+
cleanup: ["docImpact"],
|
|
321
|
+
modernize: ["deprecatedSince", "replacement", "targetVersion", "docImpact"],
|
|
322
|
+
perf: ["hotness", "estimatedImpact", "complexityBefore", "complexityAfter", "docImpact"],
|
|
323
|
+
complexity: ["churn", "complexityScore", "hotspotScore", "refactorSuggestion", "docImpact"],
|
|
324
|
+
deps: ["package", "currentVersion", "targetVersion", "breaking", "cve", "advisory", "docImpact"],
|
|
325
|
+
};
|
|
326
|
+
function domainDetailsFor(dom, finding) {
|
|
327
|
+
const details = {};
|
|
328
|
+
for (const key of DOMAIN_DETAIL_KEYS[dom] || []) {
|
|
329
|
+
if (finding && Object.prototype.hasOwnProperty.call(finding, key)) details[key] = finding[key];
|
|
330
|
+
}
|
|
331
|
+
return details;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildUnified() {
|
|
335
|
+
// Flatten every domain's findings into the UnifiedFinding shape. The lead `domain` and
|
|
336
|
+
// `sourceDomain` start as the originating leaf domain; a fingerprint merge unions tags.
|
|
337
|
+
const all = [];
|
|
338
|
+
const domainsInOrder = ALL_DOMAINS.filter((d) => findingsByDomain[d]);
|
|
339
|
+
for (const dom of domainsInOrder) {
|
|
340
|
+
for (const f of findingsByDomain[dom]) {
|
|
341
|
+
if (!f || typeof f !== "object") continue;
|
|
342
|
+
all.push({
|
|
343
|
+
domain: dom,
|
|
344
|
+
sourceDomain: dom,
|
|
345
|
+
sourceDomains: [dom],
|
|
346
|
+
fingerprints: [f.fingerprint].filter(Boolean),
|
|
347
|
+
category: f.category || dom,
|
|
348
|
+
file: normFile(f.file),
|
|
349
|
+
line: Number.isInteger(f.line) && f.line > 0 ? f.line : 0,
|
|
350
|
+
severity: f.severity || "low",
|
|
351
|
+
description: f.description || "",
|
|
352
|
+
proposedChange: f.proposedChange || "",
|
|
353
|
+
confidence: Number.isInteger(f.confidence) ? f.confidence : 60,
|
|
354
|
+
effort: f.effort || "medium",
|
|
355
|
+
domainDetails: domainDetailsFor(dom, f),
|
|
356
|
+
relatesTo: [],
|
|
357
|
+
__corr: corroborationKeyOf(f.file, f.category || dom),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Corroboration domain sets (iui1.6): corroborationKey -> Set<domain>. Drives the multi-domain
|
|
363
|
+
// priority boost that was previously dead code (sourceDomains.length was always 1 because
|
|
364
|
+
// cross-domain findings never fingerprint-merge). Computed from the pre-merge `all` set so a
|
|
365
|
+
// corroborating finding boosts even when it is NOT merged into the lead.
|
|
366
|
+
const corroborationDomains = new Map();
|
|
367
|
+
for (const f of all) {
|
|
368
|
+
if (!f.file) continue;
|
|
369
|
+
const set = corroborationDomains.get(f.__corr);
|
|
370
|
+
if (set) set.add(f.sourceDomain);
|
|
371
|
+
else corroborationDomains.set(f.__corr, new Set([f.sourceDomain]));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// CONSERVATIVE MERGE — only on identical fingerprint (intra-domain, since fingerprints are
|
|
375
|
+
// domain-prefixed). O(1) Map lookup (iui1.6) instead of O(n^2) unified.find(). Adoption is
|
|
376
|
+
// decided on ORIGINAL severities first, then upgraded, so the weight compare is never skewed by
|
|
377
|
+
// an in-place severity upgrade. Proximity/corroboration is NEVER a merge (it is a LINK below) so
|
|
378
|
+
// two distinct nearby issues never collapse into one (which would let a fix close coverage for
|
|
379
|
+
// an unfixed bug).
|
|
380
|
+
const byFingerprint = new Map();
|
|
381
|
+
const unified = [];
|
|
382
|
+
for (const f of all) {
|
|
383
|
+
let hit = null;
|
|
384
|
+
for (const fp of f.fingerprints) { const g = byFingerprint.get(fp); if (g) { hit = g; break; } }
|
|
385
|
+
if (!hit) {
|
|
386
|
+
unified.push(f);
|
|
387
|
+
for (const fp of f.fingerprints) byFingerprint.set(fp, f);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
// Adoption picks which contributor's LEAD fields the merged finding carries. Higher weight
|
|
391
|
+
// wins; on an EXACT weight tie, break deterministically by (description, proposedChange) so the
|
|
392
|
+
// result NEVER depends on lane/insertion order (rrev.25). `line` tracks the lead so the
|
|
393
|
+
// reported location (and the relatesTo links derived from it) are stable too.
|
|
394
|
+
const wf = weight(f);
|
|
395
|
+
const wh = weight(hit);
|
|
396
|
+
const adopt = wf > wh
|
|
397
|
+
|| (wf === wh && (String(f.description) < String(hit.description)
|
|
398
|
+
|| (String(f.description) === String(hit.description) && String(f.proposedChange) < String(hit.proposedChange))));
|
|
399
|
+
if (sevRank(f.severity) > sevRank(hit.severity)) hit.severity = f.severity;
|
|
400
|
+
if (adopt) { hit.description = f.description; hit.proposedChange = f.proposedChange; hit.domain = f.domain; hit.sourceDomain = f.sourceDomain; hit.line = f.line; hit.domainDetails = f.domainDetails; hit.__corr = f.__corr; }
|
|
401
|
+
hit.confidence = Math.max(hit.confidence, f.confidence);
|
|
402
|
+
hit.effort = maxEffort(hit.effort, f.effort);
|
|
403
|
+
for (const d of f.sourceDomains) if (!hit.sourceDomains.includes(d)) hit.sourceDomains.push(d);
|
|
404
|
+
for (const fp of f.fingerprints) { if (!hit.fingerprints.includes(fp)) hit.fingerprints.push(fp); byFingerprint.set(fp, hit); }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Singular lead fingerprint (first contributor) so every unified finding carries one.
|
|
408
|
+
for (const f of unified) f.fingerprint = f.fingerprints[0] || null;
|
|
409
|
+
|
|
410
|
+
// Rank FIRST (stable, deterministic) so relatesTo can reference stable ranks. Multi-domain
|
|
411
|
+
// corroboration gets a small, capped boost keyed on corroborationKey (iui1.6) — this previously
|
|
412
|
+
// used sourceDomains.length, which was dead code (always 1) because cross-domain findings never
|
|
413
|
+
// fingerprint-merge. Deterministic tie-break: severity weight, then domain name, then fingerprint.
|
|
414
|
+
for (const f of unified) {
|
|
415
|
+
const corrCount = f.file ? (corroborationDomains.get(f.__corr)?.size || 1) : 1;
|
|
416
|
+
const boost = Math.min(1.25, 1 + 0.1 * (corrCount - 1));
|
|
417
|
+
f.priorityScore = Math.round(sevRank(f.severity) * (f.confidence / 100) * (EFFD[f.effort] || 0.8) * boost * 100) / 100;
|
|
418
|
+
f.corroborationCount = corrCount;
|
|
419
|
+
}
|
|
420
|
+
unified.sort((a, b) => {
|
|
421
|
+
if (b.priorityScore !== a.priorityScore) return b.priorityScore - a.priorityScore;
|
|
422
|
+
if (sevRank(b.severity) !== sevRank(a.severity)) return sevRank(b.severity) - sevRank(a.severity);
|
|
423
|
+
const da = a.sourceDomains.slice().sort().join(",") || a.domain;
|
|
424
|
+
const db = b.sourceDomains.slice().sort().join(",") || b.domain;
|
|
425
|
+
if (da !== db) return da < db ? -1 : 1;
|
|
426
|
+
const fa = a.fingerprint || "";
|
|
427
|
+
const fb = b.fingerprint || "";
|
|
428
|
+
return fa < fb ? -1 : fa > fb ? 1 : 0;
|
|
429
|
+
});
|
|
430
|
+
unified.forEach((f, i) => { f.rank = i + 1; });
|
|
431
|
+
|
|
432
|
+
// relatesTo AFTER the sort, storing the related finding's stable RANK. Bucketed by
|
|
433
|
+
// corroborationKey (iui1.6) for O(n)-average lookup instead of O(n^2). Two link rules, both
|
|
434
|
+
// confined to the SAME file+category bucket (so the inner loop is over a small bucket, not the
|
|
435
|
+
// whole set): (a) PROXIMITY — same file+category within ±3 lines (both lines non-zero);
|
|
436
|
+
// (b) CROSS-DOMAIN CORROBORATION — same file+category from DIFFERENT source domains (no line
|
|
437
|
+
// requirement; the cross-domain "same root cause" link fingerprint merging could never express).
|
|
438
|
+
// Both are LINKs, never merges.
|
|
439
|
+
const byBucket = new Map();
|
|
440
|
+
for (const f of unified) {
|
|
441
|
+
if (!f.file) continue;
|
|
442
|
+
const bucket = byBucket.get(f.__corr);
|
|
443
|
+
if (bucket) bucket.push(f); else byBucket.set(f.__corr, [f]);
|
|
444
|
+
}
|
|
445
|
+
for (const bucket of byBucket.values()) {
|
|
446
|
+
for (let i = 0; i < bucket.length; i++) {
|
|
447
|
+
const a = bucket[i];
|
|
448
|
+
for (let j = i + 1; j < bucket.length; j++) {
|
|
449
|
+
const b = bucket[j];
|
|
450
|
+
const proximity = a.line !== 0 && b.line !== 0 && Math.abs(a.line - b.line) <= 3;
|
|
451
|
+
const crossDomain = a.sourceDomain !== b.sourceDomain;
|
|
452
|
+
if (proximity || crossDomain) {
|
|
453
|
+
if (!a.relatesTo.includes(b.rank)) a.relatesTo.push(b.rank);
|
|
454
|
+
if (!b.relatesTo.includes(a.rank)) b.relatesTo.push(a.rank);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
for (const f of unified) { f.relatesTo = [...new Set(f.relatesTo)].sort((x, y) => x - y); delete f.__corr; }
|
|
460
|
+
return unified;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const unified = buildUnified();
|
|
464
|
+
const counts = {
|
|
465
|
+
total: unified.length,
|
|
466
|
+
critical: unified.filter((f) => f.severity === "critical").length,
|
|
467
|
+
high: unified.filter((f) => f.severity === "high").length,
|
|
468
|
+
medium: unified.filter((f) => f.severity === "medium").length,
|
|
469
|
+
low: unified.filter((f) => f.severity === "low").length,
|
|
470
|
+
};
|
|
471
|
+
await log(`Merged into ${unified.length} unified findings across ${ran.length} domain(s).`);
|
|
472
|
+
|
|
473
|
+
// partialCoverage is true when a whole leaf failed/missing OR any leaf dropped a lane
|
|
474
|
+
// (finder/verifier/scorer). A per-lane drop degrades coverage as seriously as a failed leaf —
|
|
475
|
+
// the leaf can return "ok" while a whole finder lens silently returned null (iui1.2).
|
|
476
|
+
const partialCoverage = failed.length > 0 || ran.length < activeDomains.length || leafOutcomes.some((o) => o.laneDropped > 0);
|
|
477
|
+
const totalLaneDropped = leafOutcomes.reduce((s, o) => s + (o.laneDropped || 0), 0);
|
|
478
|
+
if (totalLaneDropped > 0) await log(`Lane coverage degraded: ${totalLaneDropped} lane(s) dropped across leaf(es).`);
|
|
479
|
+
|
|
480
|
+
// ---- materialization readiness gate (pure JS) ----
|
|
481
|
+
// A report is safe to materialize into Beads ONLY when OBJECTIVE coverage is complete and the
|
|
482
|
+
// finding set was not size-capped or lost. materializationBlockers is the authoritative list of
|
|
483
|
+
// such objective signals; the command refuses to OFFER materialize when materializationReady is
|
|
484
|
+
// false, and the separate review-materialize flow re-checks it. This is the review→materialize
|
|
485
|
+
// boundary gate.
|
|
486
|
+
//
|
|
487
|
+
// The gate is OBJECTIVE ONLY. The coverage auditor is a meta-judgment lane, not a finding producer:
|
|
488
|
+
// its subjective assessment (coverageAssessment / confidence / gaps) can NEVER hard-block
|
|
489
|
+
// materialization. Instead the auditor's concerns are surfaced as coverageAdvisories, which the
|
|
490
|
+
// command reports separately ("mechanically safe to materialize; auditor recommends follow-up
|
|
491
|
+
// review areas"). This prevents a conservative or under-informed auditor from vetoing a complete,
|
|
492
|
+
// artifact-backed review (see the goals-4bd5a7fd incident: all 8 domains ok/empty, artifacts ready,
|
|
493
|
+
// 127 findings, yet auditor partial/medium falsely blocked materialization).
|
|
494
|
+
const AUDITOR_SCHEMA = {
|
|
495
|
+
type: "object", additionalProperties: false,
|
|
496
|
+
required: ["coverageAssessment", "confidence", "gaps", "missedAreas"],
|
|
497
|
+
properties: {
|
|
498
|
+
coverageAssessment: { type: "string", enum: ["complete", "partial", "degraded"] },
|
|
499
|
+
confidence: { type: "string", enum: ["high", "medium", "low"] },
|
|
500
|
+
gaps: { type: "array", items: { type: "string" } },
|
|
501
|
+
missedAreas: { type: "array", items: { type: "string" } },
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
// coverageGrade is the OBJECTIVE coverage grade derived only from materializationBlockers (the
|
|
505
|
+
// auditor never contributes). "degraded" is a reserved/legacy enum value kept for compatibility;
|
|
506
|
+
// it is not currently produced because auditor-only concerns are coverageAdvisories, not blockers.
|
|
507
|
+
function gradeFor(blockers) {
|
|
508
|
+
if (blockers.length === 0) return "complete";
|
|
509
|
+
if (blockers.includes("truncatedFindings") || blockers.includes("reportMarkdownDropped") || blockers.some((b) => b.startsWith("leafTruncated:"))) return "truncated";
|
|
510
|
+
return "partial";
|
|
511
|
+
}
|
|
512
|
+
// Build the gate from the observable coverage state. `auditor` is null in bounded mode or
|
|
513
|
+
// when the auditor lane failed (onFailure returnNull); both are handled distinctly.
|
|
514
|
+
//
|
|
515
|
+
// Artifact awareness (iui1.4): when the full ranked set + full markdown were spilled to host-owned
|
|
516
|
+
// artifacts (artifactsReady), truncation of the RETURNED preview array and the dropped in-envelope
|
|
517
|
+
// reportMarkdown are intentional compaction, NOT data loss — so they are no longer blockers. Only
|
|
518
|
+
// when artifactization FAILED (artifactFailed) do we block with artifactPersistenceFailed, because
|
|
519
|
+
// then the full data truly was lost to the size cap.
|
|
520
|
+
function materializationGate({ truncated, reportMarkdown, unifiedCount, auditor, artifactsReady, artifactFailed }) {
|
|
521
|
+
const blockers = [];
|
|
522
|
+
if (partialCoverage) blockers.push("partialCoverage");
|
|
523
|
+
// Inventory + shard coverage (iui1.5): a failed inventory means coverage accounting is unknown,
|
|
524
|
+
// and a missed shard means a whole file group received zero review. Either blocks materialization.
|
|
525
|
+
if (!inventoryReady) blockers.push("inventoryFailed");
|
|
526
|
+
if (inventory && inventory.partial) blockers.push("inventoryPartial");
|
|
527
|
+
for (const s of shardLedger) if (s.status === "missed") blockers.push(`shardMissed:${s.id}`);
|
|
528
|
+
// Truncation/reportMarkdown-drop only block when the full data is NOT preserved in artifacts.
|
|
529
|
+
if (!artifactsReady) {
|
|
530
|
+
if (truncated) blockers.push("truncatedFindings");
|
|
531
|
+
// reportMarkdown is legitimately null when there are zero findings; it is only a blocker
|
|
532
|
+
// when findings EXISTED but the report was dropped to fit the 256 KiB cap AND no artifact
|
|
533
|
+
// holds the full markdown (the full ranked detail would be lost).
|
|
534
|
+
if (unifiedCount > 0 && reportMarkdown === null) blockers.push("reportMarkdownDropped");
|
|
535
|
+
}
|
|
536
|
+
if (artifactFailed && unifiedCount > 0) blockers.push("artifactPersistenceFailed");
|
|
537
|
+
for (const o of leafOutcomes) if (o.truncatedFindings) blockers.push(`leafTruncated:${o.domain}`);
|
|
538
|
+
// Per-lane drops (iui1.2): a leaf that dropped finder/verifier/scorer lanes has degraded
|
|
539
|
+
// coverage. Emit one blocker per domain plus finer-grained per-phase blockers so the operator
|
|
540
|
+
// sees WHICH lens class was lost, not just that something dropped.
|
|
541
|
+
for (const o of leafOutcomes) {
|
|
542
|
+
if (!o.laneCoverage || o.laneCoverage.dropped <= 0) continue;
|
|
543
|
+
blockers.push(`leafLaneDrops:${o.domain}`);
|
|
544
|
+
const bp = o.laneCoverage.byPhase || {};
|
|
545
|
+
if (bp.find && bp.find.dropped > 0) blockers.push(`leafFinderDrops:${o.domain}`);
|
|
546
|
+
if (bp.verify && bp.verify.dropped > 0) blockers.push(`leafVerifierDrops:${o.domain}`);
|
|
547
|
+
if (bp.score && bp.score.dropped > 0) blockers.push(`leafScorerDrops:${o.domain}`);
|
|
548
|
+
}
|
|
549
|
+
// Coverage auditor (ADVISORY ONLY). A meta-judgment lane must not veto a complete, artifact-
|
|
550
|
+
// backed review on subjective grounds. The auditor's assessment is surfaced as coverageAdvisories
|
|
551
|
+
// for the command to report separately from the objective blockers. `auditor` is null in bounded
|
|
552
|
+
// mode (no auditor expected) or when the auditor lane failed in exhaustive mode (onFailure
|
|
553
|
+
// returnNull); a failed auditor lane is an advisory gap, not data loss — exhaustive mode promised
|
|
554
|
+
// an auditor and did not get one, but every finding-producing leaf still completed.
|
|
555
|
+
const advisories = [];
|
|
556
|
+
if (exhaustive) {
|
|
557
|
+
if (auditor) {
|
|
558
|
+
if (auditor.coverageAssessment !== "complete") advisories.push(`auditorCoverage:${auditor.coverageAssessment}`);
|
|
559
|
+
if (auditor.confidence === "low") advisories.push("auditorLowConfidence");
|
|
560
|
+
} else {
|
|
561
|
+
advisories.push("auditorUnavailable");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return { materializationReady: blockers.length === 0, materializationBlockers: blockers, coverageAdvisories: advisories, coverageGrade: gradeFor(blockers) };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ---- coverage auditor (EXHAUSTIVE only) ----
|
|
568
|
+
// A final deep-tier lane that reviews the merged coverage for blind spots before the report
|
|
569
|
+
// is finalized. It cannot add findings; it can only flag gaps that become coverageAdvisories
|
|
570
|
+
// (ADVISORY ONLY — never a materializationBlocker). It runs BEFORE the empty-check so "we found
|
|
571
|
+
// nothing" is audited too (a clean bill of health is most valuable precisely when findings are
|
|
572
|
+
// sparse). onFailure returnNull keeps a failed auditor from crashing the meta (it becomes an
|
|
573
|
+
// "auditorUnavailable" advisory instead). Bounded mode skips it entirely.
|
|
574
|
+
//
|
|
575
|
+
// The auditor is fed OBJECTIVE telemetry (lane completion, inventory/shard status, per-domain
|
|
576
|
+
// empty-vs-failed distinction) so it cannot mistake a clean empty domain for a coverage gap. An
|
|
577
|
+
// empty domain that ran to completion (all lanes completed, zero dropped) is a clean result, not
|
|
578
|
+
// a missed-area: the domain produced no findings after full analysis.
|
|
579
|
+
let coverageAudit = null;
|
|
580
|
+
if (exhaustive) {
|
|
581
|
+
await phase("audit");
|
|
582
|
+
const auditLedger = leafOutcomes.map((o) => {
|
|
583
|
+
const lc = o.laneCoverage;
|
|
584
|
+
const laneStr = lc ? ` [lanes ${lc.completed}/${lc.expected}${lc.dropped ? `, DROPPED ${lc.dropped}` : ""}]` : "";
|
|
585
|
+
const emptyNote = o.status === "empty" && lc && lc.dropped === 0 ? " (ran to completion; no findings survived)" : "";
|
|
586
|
+
return `- ${o.domain}: ${o.status}${o.error ? ` (${o.error})` : ""}${emptyNote} — ${(o.counts && o.counts.total) || 0} findings${o.truncatedFindings ? " (TRUNCATED)" : ""}${laneStr}`;
|
|
587
|
+
}).join("\n");
|
|
588
|
+
coverageAudit = await agent(
|
|
589
|
+
["You are a coverage auditor for an exhaustive, read-only repo review. Assess whether the review likely missed important issue classes given the repo profile and the coverage ledger below.",
|
|
590
|
+
scope,
|
|
591
|
+
`Recon:\n${formatRecon(recon)}`,
|
|
592
|
+
`Active domains: ${activeDomains.join(", ")}.`,
|
|
593
|
+
`Objective coverage state: partialCoverage=${partialCoverage}; inventory=${inventoryReady ? "ok" : "FAILED"}${inventory && inventory.partial ? " (PARTIAL)" : ""}; shards=${shardLedger.length} (${shardMissedCount} missed); total lane drops=${totalLaneDropped}.`,
|
|
594
|
+
`Coverage ledger:\n${auditLedger}`,
|
|
595
|
+
`Merged findings: ${unified.length} (critical ${counts.critical}, high ${counts.high}, medium ${counts.medium}, low ${counts.low}).`,
|
|
596
|
+
"Consider: did any domain fail or return partial results? Are there repo areas the scope excludes (config, CI, scripts, generated code, secrets, infra) that might hide issues? Is thorough depth sufficient for this repo's size and language mix? Are there obvious issue classes (e.g. license compliance, accessibility, i18n, error-handling paths) no domain covers?",
|
|
597
|
+
"IMPORTANT: a domain listed as 'empty (ran to completion; no findings survived)' is a CLEAN result — the domain analyzed its scope fully and produced no findings. An empty-but-complete domain is NOT a coverage gap. Distinguish 'the domain ran and found nothing' from 'the domain failed or was not run'.",
|
|
598
|
+
"Return coverageAssessment (complete if you are confident the review found the materially important issues; partial if there are real gaps; degraded if coverage is seriously incomplete), your confidence, specific gaps (short strings), and missedAreas (short strings). Be conservative: if unsure, say partial."].join("\n\n"),
|
|
599
|
+
{ label: "coverage-auditor", schema: AUDITOR_SCHEMA, tier: "deep", onFailure: "returnNull" },
|
|
600
|
+
);
|
|
601
|
+
if (coverageAudit && typeof coverageAudit === "object") {
|
|
602
|
+
await log(`Coverage auditor: ${coverageAudit.coverageAssessment} (confidence ${coverageAudit.confidence})${coverageAudit.gaps?.length ? ` — ${coverageAudit.gaps.length} gap(s)` : ""}.`);
|
|
603
|
+
} else {
|
|
604
|
+
await log("WARNING: coverage auditor returned null — recording as auditorUnavailable advisory.");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ---- 4. Synthesize ONE cross-domain report (PURE JS render; no model tokens) ----
|
|
609
|
+
await phase("synthesize");
|
|
610
|
+
function mdCell(s) { return String(s == null ? "" : s).replace(/\|/g, "\\|").replace(/\n+/g, " "); }
|
|
611
|
+
function renderMarkdown(rows, c) {
|
|
612
|
+
const ledger = leafOutcomes.map((o) => `- ${o.domain}: ${o.status}${o.error ? ` (${o.error})` : ""} — ${(o.counts && o.counts.total) || 0} findings`).join("\n");
|
|
613
|
+
const lines = [];
|
|
614
|
+
lines.push("# Comprehensive Repo Review Report", "");
|
|
615
|
+
lines.push("> Report-only. No files were modified and nothing was applied.", "");
|
|
616
|
+
// ---- Scale + coverage telemetry (iui1.8) ----
|
|
617
|
+
lines.push("## Scope summary", `- Paths: ${JSON.stringify(paths)}`, `- Exclude: ${JSON.stringify(exclude)}`, `- Active domains: ${activeDomains.join(", ")}`, `- Depth: ${depth} (mode: ${mode})`, "");
|
|
618
|
+
if (scaleProfile) {
|
|
619
|
+
lines.push("## Inventory summary",
|
|
620
|
+
`- Files enumerated: ${scaleProfile.totalFiles}`,
|
|
621
|
+
`- Source roots: ${inventorySummary.sourceRoots}`,
|
|
622
|
+
`- Shards: ${scaleProfile.shards} (${scaleProfile.shardMissed} missed)${scaleProfile.inventoryPartial ? " — PARTIAL inventory (file cap hit)" : ""}`,
|
|
623
|
+
`- Files reviewed: ${scaleProfile.reviewedFiles}${scaleProfile.skippedFiles ? ` (skipped: ${scaleProfile.skippedFiles})` : ""}`,
|
|
624
|
+
"");
|
|
625
|
+
// Lane coverage per domain.
|
|
626
|
+
const laneLines = leafOutcomes.map((o) => {
|
|
627
|
+
const lc = o.laneCoverage;
|
|
628
|
+
if (!lc) return `- ${o.domain}: ${o.status} (no lane telemetry)`;
|
|
629
|
+
return `- ${o.domain}: ${o.status} — lanes ${lc.completed}/${lc.expected}${lc.dropped ? ` (DROPPED ${lc.dropped}: ${mdCell((lc.droppedLabels || []).join(", "))})` : ""}`;
|
|
630
|
+
});
|
|
631
|
+
lines.push("## Lane coverage", ...laneLines, "");
|
|
632
|
+
// Dropped / failed lanes named explicitly so they cannot be missed.
|
|
633
|
+
if (scaleProfile.droppedLanes > 0 || scaleProfile.failedDomains.length) {
|
|
634
|
+
lines.push("## Dropped / failed lanes");
|
|
635
|
+
for (const lbl of scaleProfile.droppedLaneLabels) lines.push(`- dropped: ${mdCell(lbl)}`);
|
|
636
|
+
for (const dom of scaleProfile.failedDomains) lines.push(`- failed domain: ${mdCell(dom)}`);
|
|
637
|
+
lines.push("");
|
|
638
|
+
}
|
|
639
|
+
lines.push("## Artifact status",
|
|
640
|
+
`- Full ranked findings + this report persisted as host-owned artifacts (see \`artifactPaths\`).`,
|
|
641
|
+
"");
|
|
642
|
+
// What this review did NOT do — prevents misreading incomplete coverage as exhaustive.
|
|
643
|
+
const gaps = [];
|
|
644
|
+
if (scaleProfile.droppedLanes > 0) gaps.push(`${scaleProfile.droppedLanes} finder/verifier lane(s) dropped — some issue classes may be under-counted.`);
|
|
645
|
+
if (scaleProfile.shardMissed > 0) gaps.push(`${scaleProfile.shardMissed} shard(s) received zero domain coverage.`);
|
|
646
|
+
if (scaleProfile.inventoryPartial) gaps.push("File inventory hit its cap — some files were not enumerated.");
|
|
647
|
+
if (scaleProfile.failedDomains.length) gaps.push(`Domain(s) failed: ${scaleProfile.failedDomains.join(", ")}.`);
|
|
648
|
+
gaps.push("Static, read-only analysis (no shell/git-churn, no network advisory lookups) — optional deep modes were not enabled.");
|
|
649
|
+
lines.push("## What this review did not do", ...gaps.map((g) => `- ${g}`), "");
|
|
650
|
+
}
|
|
651
|
+
lines.push("## Coverage ledger", ledger, "");
|
|
652
|
+
lines.push("## Summary", `- Total: ${c.total} (critical: ${c.critical}, high: ${c.high}, medium: ${c.medium}, low: ${c.low})`, "");
|
|
653
|
+
lines.push("## Ranked cross-domain findings", "");
|
|
654
|
+
lines.push("| Rank | Severity | Score | Domain(s) | Location | Category | Description |");
|
|
655
|
+
lines.push("| ---- | -------- | ----- | --------- | -------- | -------- | ----------- |");
|
|
656
|
+
for (const f of rows) {
|
|
657
|
+
lines.push(`| ${f.rank} | ${mdCell(f.severity)} | ${f.priorityScore} | ${mdCell(f.sourceDomains.join("+"))} | ${mdCell(f.file)}:${f.line || 0} | ${mdCell(f.category)} | ${mdCell(f.description).slice(0, 140)} |`);
|
|
658
|
+
}
|
|
659
|
+
lines.push("", "## Detail");
|
|
660
|
+
for (const f of rows) {
|
|
661
|
+
lines.push("", `### ${f.rank}. ${mdCell(f.category)} — ${mdCell(f.file)}:${f.line || 0} (${f.severity}, conf ${f.confidence})`);
|
|
662
|
+
lines.push(`- **Domain(s):** ${f.sourceDomains.join(", ")}`);
|
|
663
|
+
lines.push(`- **What:** ${f.description}`);
|
|
664
|
+
if (f.proposedChange) lines.push(`- **Proposed change:** ${f.proposedChange}`);
|
|
665
|
+
if (f.relatesTo.length) lines.push(`- **Related (ranks):** ${f.relatesTo.join(", ")}`);
|
|
666
|
+
lines.push(`- **Fingerprint:** \`${f.fingerprint}\``);
|
|
667
|
+
}
|
|
668
|
+
return lines.join("\n");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ---- artifactize full output (iui1.4) ----
|
|
672
|
+
// The workflow return is capped at MAX_RESULT_BYTES (256 KiB); a large exhaustive report would
|
|
673
|
+
// otherwise be truncated (reportMarkdown dropped, findings halved) and the materialization gate
|
|
674
|
+
// would block. Instead, spill the FULL ranked findings + coverage ledger + full markdown to
|
|
675
|
+
// host-owned artifacts under the run directory. NO data is lost to size fitting: the return
|
|
676
|
+
// envelope carries compact artifactPaths + counts + a top-N preview, and review-materialize reads
|
|
677
|
+
// the full set from findings.full.json via its findingsPath handoff. The QuickJS guest cannot
|
|
678
|
+
// write files; persistArtifacts is a kernel host op rooted under run.dir/artifacts/ (controller-
|
|
679
|
+
// owned, gitignored), preserving the read-only-review contract (no workspace/source writes).
|
|
680
|
+
// Compact inventory summary for the envelope (the full manifest + per-shard file lists live in the
|
|
681
|
+
// shard-ledger.json artifact). shardLedger carries per-shard coverage status for observability.
|
|
682
|
+
const inventorySummary = {
|
|
683
|
+
ready: inventoryReady,
|
|
684
|
+
partial: !!(inventory && inventory.partial),
|
|
685
|
+
totalFiles: manifest ? manifest.totalFiles : 0,
|
|
686
|
+
sourceRoots: manifest ? manifest.sourceRoots.length : 0,
|
|
687
|
+
shards: shardLedger.length,
|
|
688
|
+
shardMissed: shardMissedCount,
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// scaleProfile (iui1.8): structured scale + coverage telemetry aggregated from the inventory,
|
|
692
|
+
// shard ledger, and per-leaf lane coverage. Makes dropped lanes, missed shards, and skipped files
|
|
693
|
+
// visible at the envelope + report level so an operator cannot misread incomplete coverage as
|
|
694
|
+
// exhaustive. artifactsReady/truncatedFindings are mutated after persistence / size-fitting.
|
|
695
|
+
const scaleProfile = (() => {
|
|
696
|
+
const totalFiles = manifest ? manifest.totalFiles : 0;
|
|
697
|
+
const missedFiles = shardLedger.filter((s) => s.status === "missed").reduce((s2, sh) => s2 + (sh.fileCount || 0), 0);
|
|
698
|
+
let totalLanes = 0, droppedLanes = 0;
|
|
699
|
+
const droppedLaneLabels = [];
|
|
700
|
+
for (const o of leafOutcomes) {
|
|
701
|
+
if (o.laneCoverage && typeof o.laneCoverage === "object") {
|
|
702
|
+
totalLanes += o.laneCoverage.expected || 0;
|
|
703
|
+
droppedLanes += o.laneCoverage.dropped || 0;
|
|
704
|
+
for (const lbl of (o.laneCoverage.droppedLabels || [])) droppedLaneLabels.push(`${o.domain}:${lbl}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
shards: shardLedger.length,
|
|
709
|
+
shardMissed: shardMissedCount,
|
|
710
|
+
totalFiles,
|
|
711
|
+
reviewedFiles: Math.max(0, totalFiles - missedFiles),
|
|
712
|
+
skippedFiles: missedFiles,
|
|
713
|
+
inventoryPartial: !!(inventory && inventory.partial),
|
|
714
|
+
totalLanes,
|
|
715
|
+
droppedLanes,
|
|
716
|
+
droppedLaneLabels,
|
|
717
|
+
failedDomains: failed.map((o) => o.domain),
|
|
718
|
+
artifactsReady: false, // mutated after artifact persistence completes
|
|
719
|
+
truncatedFindings: false, // mutated in fitWithinBudget for the non-empty path
|
|
720
|
+
};
|
|
721
|
+
})();
|
|
722
|
+
|
|
723
|
+
const fullMarkdown = unified.length ? renderMarkdown(unified, counts) : null;
|
|
724
|
+
const findingsJsonl = unified.map((f) => JSON.stringify(f)).join("\n");
|
|
725
|
+
let artifactResult = null;
|
|
726
|
+
try {
|
|
727
|
+
artifactResult = await persistArtifacts({
|
|
728
|
+
namespace: "repo-review",
|
|
729
|
+
files: [
|
|
730
|
+
{ name: "findings.full.json", content: unified },
|
|
731
|
+
{ name: "findings.jsonl", content: findingsJsonl || "" },
|
|
732
|
+
{ name: "coverage-ledger.json", content: { counts, leafOutcomes, domainExtras, partialCoverage, coverageAudit, shardLedger } },
|
|
733
|
+
{ name: "leaf-outcomes.json", content: leafOutcomes },
|
|
734
|
+
{ name: "shard-ledger.json", content: { inventory: manifest, shards: inventoryShards, shardLedger } },
|
|
735
|
+
...(fullMarkdown ? [{ name: "report-markdown.md", content: fullMarkdown }] : []),
|
|
736
|
+
],
|
|
737
|
+
});
|
|
738
|
+
} catch (e) {
|
|
739
|
+
artifactResult = { ok: false, error: String((e && e.message) || e), dir: null, files: [] };
|
|
740
|
+
}
|
|
741
|
+
const artifactsReady = !!(artifactResult && artifactResult.ok);
|
|
742
|
+
const artifactFailed = !artifactsReady;
|
|
743
|
+
scaleProfile.artifactsReady = artifactsReady;
|
|
744
|
+
if (artifactsReady) {
|
|
745
|
+
await log(`Artifacts persisted: ${unified.length} findings + coverage ledger under ${artifactResult.dir}.`);
|
|
746
|
+
} else {
|
|
747
|
+
await log(`WARNING: artifact persistence failed — ${artifactResult && artifactResult.error}. Full findings will be subject to the size cap.`);
|
|
748
|
+
}
|
|
749
|
+
// artifactPaths maps a stable logical key -> absolute host path. findingsJson (findings.full.json)
|
|
750
|
+
// is the canonical handoff to review-materialize (its findingsPath expects a JSON array).
|
|
751
|
+
const ARTIFACT_KEY = {
|
|
752
|
+
"findings.full.json": "findingsJson",
|
|
753
|
+
"findings.jsonl": "findingsJsonl",
|
|
754
|
+
"coverage-ledger.json": "coverageLedgerJson",
|
|
755
|
+
"leaf-outcomes.json": "leafOutcomesJson",
|
|
756
|
+
"shard-ledger.json": "shardLedgerJson",
|
|
757
|
+
"report-markdown.md": "reportMarkdownPath",
|
|
758
|
+
};
|
|
759
|
+
const artifactPaths = {};
|
|
760
|
+
if (artifactResult && Array.isArray(artifactResult.files)) {
|
|
761
|
+
for (const af of artifactResult.files) {
|
|
762
|
+
const key = ARTIFACT_KEY[af.name] || af.name.replace(/[^A-Za-z0-9]+/g, "_");
|
|
763
|
+
artifactPaths[key] = af.path;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (unified.length === 0) {
|
|
767
|
+
const summary = `No findings across ${ran.length} of ${activeDomains.length} active domain(s).${failed.length ? ` ${failed.length} domain(s) failed — partial coverage.` : ""}`;
|
|
768
|
+
const gate = materializationGate({ truncated: false, reportMarkdown: null, unifiedCount: 0, auditor: coverageAudit, artifactsReady, artifactFailed });
|
|
769
|
+
return envelope(ran.length ? "empty" : "aborted", {
|
|
770
|
+
abortReason: ran.length ? null : "All active domains failed; no findings available.",
|
|
771
|
+
summary,
|
|
772
|
+
counts,
|
|
773
|
+
findings: [],
|
|
774
|
+
truncatedFindings: false,
|
|
775
|
+
reportMarkdown: null,
|
|
776
|
+
leafOutcomes,
|
|
777
|
+
domainExtras,
|
|
778
|
+
partialCoverage,
|
|
779
|
+
coverageAudit,
|
|
780
|
+
artifactPaths,
|
|
781
|
+
artifactsReady,
|
|
782
|
+
artifactCounts: { findings: unified.length },
|
|
783
|
+
inventorySummary,
|
|
784
|
+
scaleProfile,
|
|
785
|
+
shardLedger,
|
|
786
|
+
deepMode,
|
|
787
|
+
...gate,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Size-fit the RETURNED preview to the 256 KiB host cap. Because the FULL ranked set + markdown
|
|
792
|
+
// were spilled to artifacts above, this compaction is lossless: counts.total ALWAYS reflects the
|
|
793
|
+
// full ranked set, and the full data lives in artifactPaths. The envelope keeps a top-N preview +
|
|
794
|
+
// reportMarkdown when they fit (dropped only to fit the cap, never losing the artifact copy).
|
|
795
|
+
function utf8ByteLength(value) {
|
|
796
|
+
const s = String(value ?? "");
|
|
797
|
+
let bytes = 0;
|
|
798
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
799
|
+
const code = s.charCodeAt(i);
|
|
800
|
+
if (code <= 0x7f) bytes += 1;
|
|
801
|
+
else if (code <= 0x7ff) bytes += 2;
|
|
802
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < s.length) {
|
|
803
|
+
const next = s.charCodeAt(i + 1);
|
|
804
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
805
|
+
bytes += 4;
|
|
806
|
+
i += 1;
|
|
807
|
+
} else bytes += 3;
|
|
808
|
+
} else bytes += 3;
|
|
809
|
+
}
|
|
810
|
+
return bytes;
|
|
811
|
+
}
|
|
812
|
+
function jsonUtf8ByteLength(value) {
|
|
813
|
+
return utf8ByteLength(JSON.stringify(value));
|
|
814
|
+
}
|
|
815
|
+
function fitWithinBudget() {
|
|
816
|
+
const LIMIT = 230000; // headroom under MAX_RESULT_BYTES (262144) for the {output:...} wrapper + envelope fields
|
|
817
|
+
let returned = unified.slice(0, maxReturnFindings);
|
|
818
|
+
let truncated = unified.length > returned.length;
|
|
819
|
+
let reportMarkdown = fullMarkdown;
|
|
820
|
+
const summary = `Reviewed ${ran.length} of ${activeDomains.length} domain(s): ${counts.total} findings (${counts.critical} critical, ${counts.high} high). Report-only — nothing applied.${partialCoverage ? " PARTIAL coverage." : ""} Full findings in artifacts.`;
|
|
821
|
+
const gate0 = () => materializationGate({ truncated, reportMarkdown, unifiedCount: unified.length, auditor: coverageAudit, artifactsReady, artifactFailed });
|
|
822
|
+
const sizeOf = () => jsonUtf8ByteLength(envelope("ok", { summary, counts, findings: returned, truncatedFindings: truncated, reportMarkdown, leafOutcomes, domainExtras, partialCoverage, artifactPaths, artifactsReady, artifactCounts: { findings: unified.length }, inventorySummary, scaleProfile: { ...scaleProfile, truncatedFindings: truncated }, shardLedger, deepMode, ...gate0(), coverageAudit }));
|
|
823
|
+
if (sizeOf() > LIMIT) reportMarkdown = null;
|
|
824
|
+
while (sizeOf() > LIMIT && returned.length > 10) {
|
|
825
|
+
returned = returned.slice(0, Math.ceil(returned.length / 2));
|
|
826
|
+
truncated = true;
|
|
827
|
+
}
|
|
828
|
+
return envelope("ok", { summary, counts, findings: returned, truncatedFindings: truncated, reportMarkdown, leafOutcomes, domainExtras, partialCoverage, artifactPaths, artifactsReady, artifactCounts: { findings: unified.length }, inventorySummary, scaleProfile: { ...scaleProfile, truncatedFindings: truncated }, shardLedger, deepMode, ...materializationGate({ truncated, reportMarkdown, unifiedCount: unified.length, auditor: coverageAudit, artifactsReady, artifactFailed }), coverageAudit });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return fitWithinBudget();
|