audit-tools 0.32.46 → 0.32.47
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/dist/remediate/phases/close.js +9 -9
- package/dist/remediate/phases/plan.d.ts +2 -30
- package/dist/remediate/phases/plan.d.ts.map +1 -1
- package/dist/remediate/phases/plan.js +5 -494
- package/dist/remediate/phases/plan.js.map +1 -1
- package/dist/remediate/state/store.d.ts +5 -6
- package/dist/remediate/state/store.d.ts.map +1 -1
- package/dist/remediate/state/store.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +1 -2
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/package.json +1 -1
|
@@ -337,15 +337,15 @@ const AUDIT_TOOLS_EXCLUDE_PATTERN = /^\.audit-tools\//;
|
|
|
337
337
|
* RUN-START-DIRTY GUARD on source (2) ONLY: declared surfaces are plan-time
|
|
338
338
|
* DECLARATIONS (write-access grants) / audit evidence — never verified against
|
|
339
339
|
* an actual diff. A declared file that was ALREADY dirty when the run started
|
|
340
|
-
* (`state.run_start_dirty`, captured
|
|
341
|
-
* edit exists) cannot be the run's edit, so it is excluded here —
|
|
342
|
-
* resolved item declaring a file the run never actually touched
|
|
343
|
-
* pre-existing user WIP into the closing commit (the exact
|
|
344
|
-
* this manifest exists to close). Ground-truth entries
|
|
345
|
-
* NEVER excluded by the snapshot: git proved the run
|
|
346
|
-
* tool-merged edit to a file the user ALSO had dirty
|
|
347
|
-
* tool legitimately owns. A state without
|
|
348
|
-
*
|
|
340
|
+
* (`state.run_start_dirty`, captured at the extracted-plan join site before any
|
|
341
|
+
* remediation edit exists) cannot be the run's edit, so it is excluded here —
|
|
342
|
+
* otherwise a resolved item declaring a file the run never actually touched
|
|
343
|
+
* would sweep pre-existing user WIP into the closing commit (the exact
|
|
344
|
+
* over-inclusion class this manifest exists to close). Ground-truth entries
|
|
345
|
+
* from source (1) are NEVER excluded by the snapshot: git proved the run
|
|
346
|
+
* landed those paths, and a tool-merged edit to a file the user ALSO had dirty
|
|
347
|
+
* at run start is a path the tool legitimately owns. A state without
|
|
348
|
+
* `run_start_dirty` (pre-field) means no exclusions.
|
|
349
349
|
*
|
|
350
350
|
* Both sources empty is legitimate (e.g. a run that only produced
|
|
351
351
|
* `resolved_no_change` / skipped items) and correctly yields an empty
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { RemediationState } from "../state/store.js";
|
|
2
|
-
import { OrchestratorOptions } from "../types/options.js";
|
|
3
1
|
import { RemediationPlan, Finding, RemediationBlock, RemediationItemState, CoverageLedger } from "../state/types.js";
|
|
4
2
|
import type { AuditFindingsReport, FindingTheme } from "audit-tools/shared";
|
|
5
|
-
import { runTracked } from "audit-tools/shared";
|
|
6
3
|
/**
|
|
7
4
|
* Parse the auditor's canonical `audit-findings.json` (the machine contract) into
|
|
8
5
|
* remediation findings, work blocks, and synthesis themes. The auditor emits this
|
|
@@ -22,21 +19,6 @@ export declare function parseAuditFindingsReport(report: AuditFindingsReport): {
|
|
|
22
19
|
* contract_version is rejected here rather than silently trusted.
|
|
23
20
|
*/
|
|
24
21
|
export declare function isAuditFindingsReport(value: unknown): value is AuditFindingsReport;
|
|
25
|
-
interface PlanPhaseDeps {
|
|
26
|
-
enumerateTestFiles?: (root: string) => string[];
|
|
27
|
-
runCommand?: typeof runTracked;
|
|
28
|
-
now?: () => number;
|
|
29
|
-
/** Test seam: replaces the provider-backed free-form extraction worker. */
|
|
30
|
-
extractFindings?: (content: string, options: OrchestratorOptions) => Promise<{
|
|
31
|
-
findings: Finding[];
|
|
32
|
-
blocks: RemediationBlock[];
|
|
33
|
-
}>;
|
|
34
|
-
/** Test seam: replaces the provider-backed bounded path-repair worker. */
|
|
35
|
-
repairExtractedFindingPaths?: (requests: {
|
|
36
|
-
finding: Finding;
|
|
37
|
-
phantomPaths: string[];
|
|
38
|
-
}[]) => Promise<Map<string, string[]>>;
|
|
39
|
-
}
|
|
40
22
|
export declare function deriveBlocksFromTestGraph(findings: Finding[], testFiles: string[]): {
|
|
41
23
|
blocks: RemediationBlock[];
|
|
42
24
|
useful: boolean;
|
|
@@ -103,21 +85,11 @@ export declare function mergeBlocksSharingFiles(blocks: RemediationBlock[], find
|
|
|
103
85
|
* 2. splitBlocksByContextBudget — keeps each block within the agent context window
|
|
104
86
|
* 3. snapshotAffectedFileHashes — records baseline hashes for integrity checks
|
|
105
87
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* logic and the two paths cannot drift apart.
|
|
88
|
+
* Sole caller is handlePendingExtractedPlan (LLM-extracted plans join site);
|
|
89
|
+
* kept as its own function so the post-dedup logic has one home.
|
|
109
90
|
*/
|
|
110
91
|
export declare function applyPlanPipeline(plan: RemediationPlan, options: {
|
|
111
92
|
root: string;
|
|
112
93
|
artifactsDir?: string;
|
|
113
94
|
}): Promise<RemediationPlan>;
|
|
114
|
-
/**
|
|
115
|
-
* Prune blocks so that every item reference is still in the kept-findings set,
|
|
116
|
-
* and drop blocks whose item list becomes empty after pruning.
|
|
117
|
-
* Used in runPlanPhase wherever a reduction step drops some findings.
|
|
118
|
-
* Extracted to eliminate the three formerly-duplicated inline occurrences of
|
|
119
|
-
* blocks.map(b => ({...b, items: b.items.filter(id => keptIds.has(id))})).filter(...)
|
|
120
|
-
*/
|
|
121
|
-
export declare function pruneBlocksForKeptFindings(blocks: RemediationBlock[], keptFindings: Finding[]): RemediationBlock[];
|
|
122
|
-
export declare function runPlanPhase(state: RemediationState, options: OrchestratorOptions, deps?: PlanPhaseDeps): Promise<RemediationState>;
|
|
123
95
|
//# sourceMappingURL=plan.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../../src/remediate/phases/plan.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../../src/remediate/phases/plan.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,OAAO,EACP,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EAEf,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAe5E;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,mBAAmB,GAAG;IACrE,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB,CAgBA;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,mBAAmB,CAE9B;AAuBD,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,OAAO,EAAE,EACnB,SAAS,EAAE,MAAM,EAAE,GAClB;IAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAiDjD;AAKD,OAAO,EACL,gCAAgC,IAAI,2BAA2B,EAC/D,8BAA8B,IAAI,iCAAiC,GACpE,MAAM,oBAAoB,CAAC;AAoH5B,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAAE,EACpB,QAAQ,EAAE,OAAO,EAAE,EACnB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAMnC,IAAI,CAAC,EAAE,MAAM,GACZ,MAAM,CAeR;AA2BD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,gBAAgB,EAAE,EAC1B,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,GACpB,gBAAgB,EAAE,CA+FpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,EAAE,CAAC;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5C,oEAAoE;IACpE,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5C;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;CAC7C,GAAG,cAAc,CAwFjB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,EAAE,EAC1B,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,SAAM,GACT,gBAAgB,EAAE,CA2DpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,OAAO,CAAC,eAAe,CAAC,CAkB1B"}
|
|
@@ -1,55 +1,9 @@
|
|
|
1
|
-
import { writeFile, readFile } from "node:fs/promises";
|
|
2
1
|
import { isAbsolute, join } from "node:path";
|
|
3
|
-
import { isValidAuditFindingsReport
|
|
4
|
-
import {
|
|
2
|
+
import { isValidAuditFindingsReport } from "audit-tools/shared";
|
|
3
|
+
import { readdirSync, statSync } from "node:fs";
|
|
5
4
|
import { snapshotAffectedFileHashes } from "../utils/fileIntegrity.js";
|
|
6
|
-
import {
|
|
7
|
-
import { readOptionalJsonFile, readValidatedSessionConfig, writeJsonFile, readJsonFile, formatValidationIssues, stagedAndUntracked, discoverProjectCommands, resolveContextBudget, estimateTokensFromBytes, ESTIMATED_PROMPT_OVERHEAD_TOKENS, ESTIMATED_ITEM_OVERHEAD_TOKENS, chunkByBudget, runTracked, } from "audit-tools/shared";
|
|
8
|
-
import { createFreshSessionProvider } from "../providers/index.js";
|
|
5
|
+
import { readValidatedSessionConfig, resolveContextBudget, estimateTokensFromBytes, ESTIMATED_PROMPT_OVERHEAD_TOKENS, ESTIMATED_ITEM_OVERHEAD_TOKENS, chunkByBudget, } from "audit-tools/shared";
|
|
9
6
|
import { canonicalizeFilePath } from "../dispatch/ownershipRegistry.js";
|
|
10
|
-
import { deduplicateCrossLensFindings, fixupBlocksAfterDedup, } from "../dedup/crossLensDedup.js";
|
|
11
|
-
import { filterFindingsByCheckpoint } from "../intent/checkpointFilter.js";
|
|
12
|
-
import { applyIntentOrdering } from "../intent/intentOrdering.js";
|
|
13
|
-
import { validateRemediationPlan, } from "../validation/remediationState.js";
|
|
14
|
-
import { createLaunchInputForTask, createRemediationWorkerTask, } from "./workerTasks.js";
|
|
15
|
-
function enumerateTestFiles(root) {
|
|
16
|
-
if (!existsSync(join(root, "package.json"))) {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
|
-
// Try vitest first, then jest, and return the list of test file paths
|
|
20
|
-
const vitestResult = runTracked(["npx", "vitest", "--reporter=verbose", "--run", "--passWithNoTests", "list"], {
|
|
21
|
-
cwd: root,
|
|
22
|
-
encoding: "utf8",
|
|
23
|
-
timeout: 15000,
|
|
24
|
-
});
|
|
25
|
-
if (vitestResult.status === 0 && vitestResult.stdout) {
|
|
26
|
-
const files = vitestResult.stdout
|
|
27
|
-
.toString()
|
|
28
|
-
.split(/\r?\n/)
|
|
29
|
-
.map((l) => l.trim())
|
|
30
|
-
.filter((l) => l.length > 0 &&
|
|
31
|
-
!l.startsWith("✓") &&
|
|
32
|
-
!l.startsWith("×") &&
|
|
33
|
-
l.includes("."));
|
|
34
|
-
if (files.length > 0)
|
|
35
|
-
return files;
|
|
36
|
-
}
|
|
37
|
-
const jestResult = runTracked(["npx", "jest", "--listTests", "--no-coverage"], {
|
|
38
|
-
cwd: root,
|
|
39
|
-
encoding: "utf8",
|
|
40
|
-
timeout: 15000,
|
|
41
|
-
});
|
|
42
|
-
if (jestResult.status === 0 && jestResult.stdout) {
|
|
43
|
-
const files = jestResult.stdout
|
|
44
|
-
.toString()
|
|
45
|
-
.split(/\r?\n/)
|
|
46
|
-
.map((l) => l.trim())
|
|
47
|
-
.filter((l) => l.length > 0);
|
|
48
|
-
if (files.length > 0)
|
|
49
|
-
return files;
|
|
50
|
-
}
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
7
|
/**
|
|
54
8
|
* Parse the auditor's canonical `audit-findings.json` (the machine contract) into
|
|
55
9
|
* remediation findings, work blocks, and synthesis themes. The auditor emits this
|
|
@@ -83,16 +37,6 @@ export function parseAuditFindingsReport(report) {
|
|
|
83
37
|
export function isAuditFindingsReport(value) {
|
|
84
38
|
return isValidAuditFindingsReport(value);
|
|
85
39
|
}
|
|
86
|
-
/** Parse JSON content into an audit-findings report, or undefined if it is not one. */
|
|
87
|
-
function tryParseFindingsReport(content) {
|
|
88
|
-
try {
|
|
89
|
-
const parsed = JSON.parse(content);
|
|
90
|
-
return isAuditFindingsReport(parsed) ? parsed : undefined;
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
40
|
function createBlockId(counter) {
|
|
97
41
|
return `B-${String(counter).padStart(3, "0")}`;
|
|
98
42
|
}
|
|
@@ -153,63 +97,6 @@ export function deriveBlocksFromTestGraph(findings, testFiles) {
|
|
|
153
97
|
useful: blocks.length < findings.length,
|
|
154
98
|
};
|
|
155
99
|
}
|
|
156
|
-
function collectFileCommits(findings, root, commandRunner) {
|
|
157
|
-
const fileCommits = new Map();
|
|
158
|
-
for (const finding of findings) {
|
|
159
|
-
for (const file of finding.affected_files) {
|
|
160
|
-
if (fileCommits.has(file.path))
|
|
161
|
-
continue;
|
|
162
|
-
try {
|
|
163
|
-
const result = commandRunner(["git", "log", "--format=%H", "--", file.path], { cwd: root, encoding: "utf8" });
|
|
164
|
-
fileCommits.set(file.path, result.status === 0 && result.stdout
|
|
165
|
-
? new Set(result.stdout
|
|
166
|
-
.toString()
|
|
167
|
-
.split("\n")
|
|
168
|
-
.map((s) => s.trim())
|
|
169
|
-
.filter((s) => s.length > 0))
|
|
170
|
-
: new Set());
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
fileCommits.set(file.path, new Set());
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return fileCommits;
|
|
178
|
-
}
|
|
179
|
-
function deriveBlocksFromGitCocommit(findings, fileCommits) {
|
|
180
|
-
const blocks = [];
|
|
181
|
-
const fileToBlock = new Map();
|
|
182
|
-
let blockCounter = 1;
|
|
183
|
-
for (const finding of findings) {
|
|
184
|
-
let assignedBlock = null;
|
|
185
|
-
for (const file of finding.affected_files) {
|
|
186
|
-
if (fileToBlock.has(file.path)) {
|
|
187
|
-
assignedBlock = fileToBlock.get(file.path);
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
const commitsA = fileCommits.get(file.path) ?? new Set();
|
|
191
|
-
for (const [existingFile, existingBlock] of fileToBlock.entries()) {
|
|
192
|
-
const commitsB = fileCommits.get(existingFile) ?? new Set();
|
|
193
|
-
let intersection = 0;
|
|
194
|
-
for (const c of commitsA)
|
|
195
|
-
if (commitsB.has(c))
|
|
196
|
-
intersection++;
|
|
197
|
-
const union = commitsA.size + commitsB.size - intersection;
|
|
198
|
-
if (union > 0 && intersection / union > 0.5) {
|
|
199
|
-
assignedBlock = existingBlock;
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (assignedBlock)
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
if (!assignedBlock) {
|
|
207
|
-
assignedBlock = createBlockId(blockCounter++);
|
|
208
|
-
}
|
|
209
|
-
assignFindingToBlock(finding, assignedBlock, blocks, fileToBlock);
|
|
210
|
-
}
|
|
211
|
-
return blocks;
|
|
212
|
-
}
|
|
213
100
|
// Block-sizing constants: now single-sourced from audit-tools/shared.
|
|
214
101
|
// Re-exported under their legacy names so any callers outside this package
|
|
215
102
|
// (and dispatch.ts) can migrate to the shared constants at their own pace.
|
|
@@ -443,32 +330,6 @@ export function splitBlocksByContextBudget(blocks, findings, root, contextBudget
|
|
|
443
330
|
}
|
|
444
331
|
return result;
|
|
445
332
|
}
|
|
446
|
-
function deriveFallbackBlocks(findings, options, deps) {
|
|
447
|
-
if (findings.length === 0)
|
|
448
|
-
return { blocks: [] };
|
|
449
|
-
const testFiles = deps.enumerateTestFiles?.(options.root) ?? enumerateTestFiles(options.root);
|
|
450
|
-
if (testFiles.length > 0) {
|
|
451
|
-
const testGraph = deriveBlocksFromTestGraph(findings, testFiles);
|
|
452
|
-
if (testGraph.useful) {
|
|
453
|
-
return { blocks: testGraph.blocks, blockStrategy: "test_graph" };
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
const fileCommits = collectFileCommits(findings, options.root, deps.runCommand ?? runTracked);
|
|
457
|
-
const gitBlocks = deriveBlocksFromGitCocommit(findings, fileCommits);
|
|
458
|
-
if (gitBlocks.some((b) => b.items.length > 1)) {
|
|
459
|
-
return { blocks: gitBlocks, blockStrategy: "git_cocommit" };
|
|
460
|
-
}
|
|
461
|
-
return {
|
|
462
|
-
blocks: findings.map((finding, index) => ({
|
|
463
|
-
block_id: createBlockId(index + 1),
|
|
464
|
-
items: [finding.id],
|
|
465
|
-
parallel_safe: true,
|
|
466
|
-
// One finding per block — declare its affected files as the block surface.
|
|
467
|
-
touched_files: finding.affected_files.map((af) => af.path),
|
|
468
|
-
})),
|
|
469
|
-
blockStrategy: "file_overlap",
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
333
|
/**
|
|
473
334
|
* Account for every finding the plan received: each is marked `planned` (kept and
|
|
474
335
|
* mapped to a block), `folded_into` (merged into a survivor by cross-lens dedup),
|
|
@@ -645,9 +506,8 @@ export function mergeBlocksSharingFiles(blocks, findings, root = ".") {
|
|
|
645
506
|
* 2. splitBlocksByContextBudget — keeps each block within the agent context window
|
|
646
507
|
* 3. snapshotAffectedFileHashes — records baseline hashes for integrity checks
|
|
647
508
|
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
* logic and the two paths cannot drift apart.
|
|
509
|
+
* Sole caller is handlePendingExtractedPlan (LLM-extracted plans join site);
|
|
510
|
+
* kept as its own function so the post-dedup logic has one home.
|
|
651
511
|
*/
|
|
652
512
|
export async function applyPlanPipeline(plan, options) {
|
|
653
513
|
const { findings } = plan;
|
|
@@ -662,353 +522,4 @@ export async function applyPlanPipeline(plan, options) {
|
|
|
662
522
|
snapshotAffectedFileHashes(options.root, findings);
|
|
663
523
|
return { ...plan, blocks };
|
|
664
524
|
}
|
|
665
|
-
/**
|
|
666
|
-
* Prune blocks so that every item reference is still in the kept-findings set,
|
|
667
|
-
* and drop blocks whose item list becomes empty after pruning.
|
|
668
|
-
* Used in runPlanPhase wherever a reduction step drops some findings.
|
|
669
|
-
* Extracted to eliminate the three formerly-duplicated inline occurrences of
|
|
670
|
-
* blocks.map(b => ({...b, items: b.items.filter(id => keptIds.has(id))})).filter(...)
|
|
671
|
-
*/
|
|
672
|
-
export function pruneBlocksForKeptFindings(blocks, keptFindings) {
|
|
673
|
-
const keptIds = new Set(keptFindings.map((f) => f.id));
|
|
674
|
-
return blocks
|
|
675
|
-
.map((b) => ({ ...b, items: (b.items ?? []).filter((id) => keptIds.has(id)) }))
|
|
676
|
-
.filter((b) => (b.items ?? []).length > 0);
|
|
677
|
-
}
|
|
678
|
-
export async function runPlanPhase(state, options, deps = {}) {
|
|
679
|
-
console.log("Running Plan Phase...");
|
|
680
|
-
let findings = [];
|
|
681
|
-
let blocks = [];
|
|
682
|
-
let themes = [];
|
|
683
|
-
let extractedFromProse = false;
|
|
684
|
-
if (options.input && existsSync(options.input)) {
|
|
685
|
-
const content = await readFile(options.input, "utf8");
|
|
686
|
-
// Canonical hand-off: the auditor's audit-findings.json (the machine
|
|
687
|
-
// contract). Parsed directly; any other input is free-form and flows
|
|
688
|
-
// through the LLM extractor.
|
|
689
|
-
const findingsReport = tryParseFindingsReport(content);
|
|
690
|
-
if (findingsReport) {
|
|
691
|
-
console.log(`Consuming audit-findings report: ${options.input}`);
|
|
692
|
-
const parsed = parseAuditFindingsReport(findingsReport);
|
|
693
|
-
findings = parsed.findings;
|
|
694
|
-
blocks = parsed.blocks;
|
|
695
|
-
themes = parsed.themes;
|
|
696
|
-
// G1 + INV-GND-02: the auditor's grounding pass (S7) marks each finding
|
|
697
|
-
// grounded or ungrounded; a finding with NO grounding verdict is treated
|
|
698
|
-
// as ungrounded (verify-before-fix), never silently trusted. Surface the
|
|
699
|
-
// not-positively-grounded findings here so the operator sees them; the
|
|
700
|
-
// implement prompt additionally instructs the worker to verify such a
|
|
701
|
-
// finding against the cited code before applying any fix (see
|
|
702
|
-
// implementPrompt's grounding bullet). Findings are not dropped for being
|
|
703
|
-
// ungrounded — they are flagged for verification, not blindly fixed.
|
|
704
|
-
const needVerification = findings.filter((f) => findingNeedsVerificationBeforeFix(f));
|
|
705
|
-
if (needVerification.length > 0) {
|
|
706
|
-
console.warn(`Plan: ${needVerification.length} of ${findings.length} audit finding(s) are ungrounded or carry no grounding verdict; they will be verified-before-fix, not blindly applied: ${needVerification
|
|
707
|
-
.map((f) => f.id)
|
|
708
|
-
.join(", ")}`);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
else {
|
|
712
|
-
console.log(`Extracting findings from input via LLM: ${options.input}`);
|
|
713
|
-
const extracted = await (deps.extractFindings
|
|
714
|
-
? deps.extractFindings(content, options)
|
|
715
|
-
: extractFindingsWithProvider(content, state, options, deps));
|
|
716
|
-
findings = extracted.findings;
|
|
717
|
-
blocks = extracted.blocks;
|
|
718
|
-
extractedFromProse = true;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
else {
|
|
722
|
-
console.log("No input provided or file does not exist. Halting Plan phase.");
|
|
723
|
-
throw new Error("Missing valid input for Plan phase.");
|
|
724
|
-
}
|
|
725
|
-
// Coverage accounting: snapshot the findings the plan received before any
|
|
726
|
-
// reduction, so the ledger below can mark every source finding as
|
|
727
|
-
// planned / folded / dropped — never silently lost.
|
|
728
|
-
const sourceFindings = [...findings];
|
|
729
|
-
// Deterministic grounding for LLM-extracted findings ONLY: strip phantom
|
|
730
|
-
// affected_files paths, give all-phantom findings one bounded repair attempt,
|
|
731
|
-
// drop the unrepaired, and classify evidence as grounded/ungrounded. The
|
|
732
|
-
// structured audit-findings path above is exempt — auditor paths are already
|
|
733
|
-
// grounded, and a since-deleted path there is the integrity check's replan
|
|
734
|
-
// concern, not a reason to drop the finding.
|
|
735
|
-
let grounding;
|
|
736
|
-
if (extractedFromProse) {
|
|
737
|
-
grounding = await groundExtractedFindings(findings, {
|
|
738
|
-
root: options.root,
|
|
739
|
-
repairZeroPathFindings: deps.repairExtractedFindingPaths ??
|
|
740
|
-
((requests) => repairExtractedFindingPathsWithProvider(requests, options, deps)),
|
|
741
|
-
});
|
|
742
|
-
findings = grounding.findings;
|
|
743
|
-
if (grounding.phantomPathsByFinding.size > 0) {
|
|
744
|
-
const strippedTotal = [...grounding.phantomPathsByFinding.values()].flat();
|
|
745
|
-
console.warn(`Plan: grounding stripped ${strippedTotal.length} phantom path(s) across ${grounding.phantomPathsByFinding.size} extracted finding(s): ${strippedTotal.join(", ")}`);
|
|
746
|
-
}
|
|
747
|
-
if (grounding.dropped.length > 0) {
|
|
748
|
-
console.warn(`Plan: dropped ${grounding.dropped.length} extracted finding(s) with no real cited path after repair: ${grounding.dropped.map((d) => d.finding.id).join(", ")}`);
|
|
749
|
-
blocks = pruneBlocksForKeptFindings(blocks, findings);
|
|
750
|
-
}
|
|
751
|
-
if (grounding.ungroundedFindingIds.length > 0) {
|
|
752
|
-
console.warn(`Plan: ${grounding.ungroundedFindingIds.length} extracted finding(s) have no evidence citing a real repo path (downgraded to low confidence): ${grounding.ungroundedFindingIds.join(", ")}`);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
// Robustness: a finding with empty evidence fails the plan validator and would
|
|
756
|
-
// abort the entire run. Skip such malformed findings (and prune them from
|
|
757
|
-
// blocks) with a warning instead of crashing — findings are advisory, so one
|
|
758
|
-
// bad finding must not block the whole report.
|
|
759
|
-
const findingsWithoutEvidence = findings
|
|
760
|
-
.filter((f) => !Array.isArray(f.evidence) || f.evidence.length === 0)
|
|
761
|
-
.map((f) => f.id);
|
|
762
|
-
if (findingsWithoutEvidence.length > 0) {
|
|
763
|
-
console.warn(`Plan: skipping ${findingsWithoutEvidence.length} finding(s) with no evidence: ${findingsWithoutEvidence.join(", ")}`);
|
|
764
|
-
findings = findings.filter((f) => Array.isArray(f.evidence) && f.evidence.length > 0);
|
|
765
|
-
blocks = pruneBlocksForKeptFindings(blocks, findings);
|
|
766
|
-
}
|
|
767
|
-
// Cross-lens dedup: merge findings that different audit lenses flagged independently
|
|
768
|
-
const dedup = deduplicateCrossLensFindings(findings);
|
|
769
|
-
findings = dedup.findings;
|
|
770
|
-
blocks = fixupBlocksAfterDedup(blocks, dedup.mergeMap);
|
|
771
|
-
// Intent checkpoint: drop findings the host filtered out (by severity / lens /
|
|
772
|
-
// package / theme) or excluded by path, so only the requested work is planned.
|
|
773
|
-
// Dropped findings are recorded in the coverage ledger and the final report.
|
|
774
|
-
let intentCheckpoint;
|
|
775
|
-
try {
|
|
776
|
-
intentCheckpoint = await readOptionalJsonFile(join(options.artifactsDir, "intent_checkpoint.json"));
|
|
777
|
-
}
|
|
778
|
-
catch {
|
|
779
|
-
console.warn("Plan: intent_checkpoint.json was unreadable; ignoring checkpoint filters.");
|
|
780
|
-
}
|
|
781
|
-
const { kept: keptFindings, droppedIds: droppedByCheckpoint } = filterFindingsByCheckpoint(findings, intentCheckpoint);
|
|
782
|
-
if (droppedByCheckpoint.length > 0) {
|
|
783
|
-
console.warn(`Plan: intent checkpoint dropped ${droppedByCheckpoint.length} finding(s) from remediation.`);
|
|
784
|
-
findings = keptFindings;
|
|
785
|
-
blocks = pruneBlocksForKeptFindings(blocks, findings);
|
|
786
|
-
}
|
|
787
|
-
// Fallback blocks computation if none provided
|
|
788
|
-
let blockStrategy;
|
|
789
|
-
if (blocks.length === 0 && findings.length > 0) {
|
|
790
|
-
const fallback = deriveFallbackBlocks(findings, options, deps);
|
|
791
|
-
blocks = fallback.blocks;
|
|
792
|
-
blockStrategy = fallback.blockStrategy;
|
|
793
|
-
}
|
|
794
|
-
// Apply the shared post-dedup pipeline (file-overlap merge, context-budget
|
|
795
|
-
// split, and baseline file-hash snapshot). Extracted into applyPlanPipeline
|
|
796
|
-
// so the LLM-extracted-plan path runs the exact same logic.
|
|
797
|
-
({
|
|
798
|
-
blocks,
|
|
799
|
-
findings,
|
|
800
|
-
} = await applyPlanPipeline({
|
|
801
|
-
plan_id: "",
|
|
802
|
-
findings,
|
|
803
|
-
blocks,
|
|
804
|
-
project_type: "unknown",
|
|
805
|
-
candidate_closing_actions: ["none"],
|
|
806
|
-
}, options));
|
|
807
|
-
// DC-1: fold the confirmed checkpoint's structured free_form_intent into block
|
|
808
|
-
// and finding ORDERING (never filtering — that already happened above via the
|
|
809
|
-
// checkpoint filters). The raw string is interpreted ONCE by the single shared
|
|
810
|
-
// interpreter into lens/priority/scope signals (INV-S04: the verbatim string is
|
|
811
|
-
// never read here and never reaches a worker prompt); emphasised work sorts
|
|
812
|
-
// first. A blank/absent free_form_intent is a strict no-op.
|
|
813
|
-
if (intentCheckpoint?.confirmed_by === "host" &&
|
|
814
|
-
typeof intentCheckpoint.free_form_intent === "string" &&
|
|
815
|
-
intentCheckpoint.free_form_intent.trim().length > 0) {
|
|
816
|
-
const interpreted = interpretFreeFormIntent(intentCheckpoint.free_form_intent);
|
|
817
|
-
({ findings, blocks } = applyIntentOrdering(findings, blocks, interpreted));
|
|
818
|
-
}
|
|
819
|
-
// Project command discovery (shared; now also covers Go and Python). The
|
|
820
|
-
// RemediationPlan stores commands as strings, so argv arrays are joined.
|
|
821
|
-
const commands = discoverProjectCommands(options.root);
|
|
822
|
-
const testCommand = commands.test ? commands.test.join(" ") : undefined;
|
|
823
|
-
const e2eCommand = commands.e2e ? commands.e2e.join(" ") : undefined;
|
|
824
|
-
let projectType = "unknown";
|
|
825
|
-
if (existsSync(join(options.root, "package.json"))) {
|
|
826
|
-
projectType = "typescript-node";
|
|
827
|
-
}
|
|
828
|
-
else if (existsSync(join(options.root, "go.mod"))) {
|
|
829
|
-
projectType = "go";
|
|
830
|
-
}
|
|
831
|
-
else if (existsSync(join(options.root, "pyproject.toml")) ||
|
|
832
|
-
existsSync(join(options.root, "pytest.ini"))) {
|
|
833
|
-
projectType = "python";
|
|
834
|
-
}
|
|
835
|
-
const plan = {
|
|
836
|
-
plan_id: "PLAN-" + (deps.now?.() ?? Date.now()),
|
|
837
|
-
findings,
|
|
838
|
-
blocks,
|
|
839
|
-
project_type: projectType,
|
|
840
|
-
test_command: testCommand,
|
|
841
|
-
...(e2eCommand ? { e2e_command: e2eCommand } : {}),
|
|
842
|
-
candidate_closing_actions: ["none"],
|
|
843
|
-
...(blockStrategy ? { block_strategy: blockStrategy } : {}),
|
|
844
|
-
...(themes.length > 0 ? { themes } : {}),
|
|
845
|
-
};
|
|
846
|
-
const items = {};
|
|
847
|
-
for (const finding of findings) {
|
|
848
|
-
const block = blocks.find((b) => b.items.includes(finding.id));
|
|
849
|
-
items[finding.id] = {
|
|
850
|
-
finding_id: finding.id,
|
|
851
|
-
status: "pending",
|
|
852
|
-
block_id: block ? block.block_id : "UNKNOWN",
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
// Coverage ledger: make every source finding's disposition auditable, so a
|
|
856
|
-
// large source set consolidated into fewer items is recorded rather than lost.
|
|
857
|
-
const coverage = buildCoverageLedger({
|
|
858
|
-
planId: plan.plan_id,
|
|
859
|
-
sourceFindings,
|
|
860
|
-
droppedNoEvidence: findingsWithoutEvidence,
|
|
861
|
-
droppedByCheckpoint,
|
|
862
|
-
droppedPhantomPaths: new Map((grounding?.dropped ?? []).map((d) => [d.finding.id, d.phantomPaths])),
|
|
863
|
-
phantomPathsRemoved: grounding?.phantomPathsByFinding,
|
|
864
|
-
mergeMap: dedup.mergeMap,
|
|
865
|
-
items,
|
|
866
|
-
});
|
|
867
|
-
console.log(`Plan coverage: ${coverage.planned_count} planned, ${coverage.folded_count} folded, ${coverage.dropped_count} dropped (of ${coverage.source_finding_count} source finding(s)).`);
|
|
868
|
-
const planIssues = validateRemediationPlan(plan);
|
|
869
|
-
if (planIssues.length > 0) {
|
|
870
|
-
console.error(`Plan validation issues:\n${formatValidationIssues(planIssues)}`);
|
|
871
|
-
const errors = planIssues.filter((i) => i.severity === "error");
|
|
872
|
-
if (errors.length > 0) {
|
|
873
|
-
throw new Error(`Plan phase produced an invalid plan:\n${formatValidationIssues(errors)}`);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
// Emit remediation_plan.json
|
|
877
|
-
await writeJsonFile(join(options.artifactsDir, "remediation_plan.json"), plan);
|
|
878
|
-
// Run-start dirty snapshot (V2 staging-manifest fix, finding 1): capture the
|
|
879
|
-
// files that are ALREADY dirty before any remediation edit exists. The close
|
|
880
|
-
// phase excludes these from the DECLARED (fallback) staging-manifest sources —
|
|
881
|
-
// a file dirty before the run started cannot be one of the run's edits, so a
|
|
882
|
-
// plan-time declaration (item_spec.touched_files / affected_files) can never
|
|
883
|
-
// sweep pre-existing user WIP into the closing commit. Captured ONCE: a replan
|
|
884
|
-
// (force-replan after edits landed) must NOT re-capture, or the run's own
|
|
885
|
-
// hand-applied edits would be misclassified as pre-existing dirt.
|
|
886
|
-
const runStartDirty = state.run_start_dirty ?? [...stagedAndUntracked(options.root)].sort();
|
|
887
|
-
return {
|
|
888
|
-
...state,
|
|
889
|
-
status: "planning",
|
|
890
|
-
plan,
|
|
891
|
-
items,
|
|
892
|
-
plan_coverage: coverage,
|
|
893
|
-
run_start_dirty: runStartDirty,
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* Launch one bounded plan-phase LLM worker and read its result JSON. Shared by
|
|
898
|
-
* the free-form extraction pass and the single bounded path-repair pass; the
|
|
899
|
-
* label keys the per-task artifact files (task/prompt/result/stdout/stderr).
|
|
900
|
-
*/
|
|
901
|
-
async function runPlanWorkerTask(params) {
|
|
902
|
-
const { label, obligationId, buildPrompt, options, deps } = params;
|
|
903
|
-
const sessionConfig = (await readValidatedSessionConfig(join(options.root, "session-config.json"))) || {};
|
|
904
|
-
const provider = createFreshSessionProvider(undefined, sessionConfig);
|
|
905
|
-
const workerTimeoutMs = sessionConfig.timeout_ms;
|
|
906
|
-
const taskPath = join(options.artifactsDir, `task_${label}.json`);
|
|
907
|
-
const resultPath = join(options.artifactsDir, `result_${label}.json`);
|
|
908
|
-
const promptPath = join(options.artifactsDir, `prompt_${label}.md`);
|
|
909
|
-
const stdoutPath = join(options.artifactsDir, `stdout_${label}.txt`);
|
|
910
|
-
const stderrPath = join(options.artifactsDir, `stderr_${label}.txt`);
|
|
911
|
-
await writeFile(promptPath, buildPrompt({ taskPath, resultPath }), "utf8");
|
|
912
|
-
const task = createRemediationWorkerTask({
|
|
913
|
-
runId: "PLAN-" + (deps.now?.() ?? Date.now()),
|
|
914
|
-
options,
|
|
915
|
-
obligationId,
|
|
916
|
-
preferredExecutor: provider.name,
|
|
917
|
-
resultPath,
|
|
918
|
-
timeoutMs: workerTimeoutMs,
|
|
919
|
-
});
|
|
920
|
-
await writeJsonFile(taskPath, task);
|
|
921
|
-
await provider.launch(createLaunchInputForTask(options, task, {
|
|
922
|
-
promptPath,
|
|
923
|
-
taskPath,
|
|
924
|
-
stdoutPath,
|
|
925
|
-
stderrPath,
|
|
926
|
-
}));
|
|
927
|
-
return readJsonFile(resultPath);
|
|
928
|
-
}
|
|
929
|
-
async function extractFindingsWithProvider(content, _state, options, deps) {
|
|
930
|
-
try {
|
|
931
|
-
const extracted = await runPlanWorkerTask({
|
|
932
|
-
label: "plan",
|
|
933
|
-
obligationId: "extract-plan",
|
|
934
|
-
options,
|
|
935
|
-
deps,
|
|
936
|
-
buildPrompt: ({ taskPath, resultPath }) => `
|
|
937
|
-
You are the Remediation Assistant. Your task is to extract findings and work blocks from the provided document.
|
|
938
|
-
Produce a JSON output matching the remediation plan structure (specifically the findings and blocks arrays).
|
|
939
|
-
|
|
940
|
-
Grounding requirements (a deterministic validator checks every path you cite):
|
|
941
|
-
- Each \`affected_files[].path\` must be a repo-relative path that exists on disk. Verify before citing; never guess paths from prose.
|
|
942
|
-
- If you cannot identify a real file for a finding, emit an empty \`affected_files\` array instead of inventing one — discovery happens later.
|
|
943
|
-
- Each \`evidence\` entry should cite a real \`path:line\` location when one exists (e.g. "src/auth.ts:42 — token is never revoked"). Quote the source document only when no code location applies.
|
|
944
|
-
|
|
945
|
-
Document Content:
|
|
946
|
-
${content}
|
|
947
|
-
|
|
948
|
-
Your task JSON is at: ${taskPath}
|
|
949
|
-
Write your result JSON to exactly this path: ${resultPath}
|
|
950
|
-
Use the Write tool to create or overwrite that file.
|
|
951
|
-
Do not write to any other path.
|
|
952
|
-
`.trim(),
|
|
953
|
-
});
|
|
954
|
-
return {
|
|
955
|
-
findings: extracted.findings || [],
|
|
956
|
-
blocks: extracted.blocks || [],
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
catch (e) {
|
|
960
|
-
console.error("Failed to extract plan via LLM:", e);
|
|
961
|
-
throw new Error("Plan extraction failed.");
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
/**
|
|
965
|
-
* The single bounded repair attempt for extracted findings whose cited paths
|
|
966
|
-
* were all phantom (WS1). Re-prompts the worker with the phantom paths named;
|
|
967
|
-
* the worker either supplies real repo-relative paths or withdraws the finding.
|
|
968
|
-
* The caller re-validates every returned path — repair output is untrusted.
|
|
969
|
-
*/
|
|
970
|
-
async function repairExtractedFindingPathsWithProvider(requests, options, deps) {
|
|
971
|
-
const findingSections = requests
|
|
972
|
-
.map(({ finding, phantomPaths }) => `
|
|
973
|
-
### ${finding.id} — ${finding.title}
|
|
974
|
-
|
|
975
|
-
- Summary: ${finding.summary}
|
|
976
|
-
- Evidence: ${(finding.evidence ?? []).join(" | ")}
|
|
977
|
-
- Phantom paths cited (do NOT exist in the repository): ${phantomPaths.join(", ")}`)
|
|
978
|
-
.join("\n");
|
|
979
|
-
const repaired = await runPlanWorkerTask({
|
|
980
|
-
label: "plan_path_repair",
|
|
981
|
-
obligationId: "repair-extracted-paths",
|
|
982
|
-
options,
|
|
983
|
-
deps,
|
|
984
|
-
buildPrompt: ({ taskPath, resultPath }) => `
|
|
985
|
-
You are the Remediation Assistant. Earlier extraction cited file paths that do not exist in the repository. For each finding below, locate the REAL repo-relative path(s) the finding is about (search the repository), or withdraw the finding if it does not apply to this codebase.
|
|
986
|
-
${findingSections}
|
|
987
|
-
|
|
988
|
-
Rules:
|
|
989
|
-
- Cite only repo-relative paths that exist on disk; verify each one before writing it.
|
|
990
|
-
- To withdraw a finding, return it with an empty \`affected_files\` array (or omit it).
|
|
991
|
-
- Do not edit source files.
|
|
992
|
-
|
|
993
|
-
Your task JSON is at: ${taskPath}
|
|
994
|
-
Write your result JSON to exactly this path: ${resultPath}
|
|
995
|
-
|
|
996
|
-
\`\`\`json
|
|
997
|
-
{
|
|
998
|
-
"repairs": [
|
|
999
|
-
{ "finding_id": "FINDING-001", "affected_files": ["real/path/to/file.ts"] }
|
|
1000
|
-
]
|
|
1001
|
-
}
|
|
1002
|
-
\`\`\`
|
|
1003
|
-
`.trim(),
|
|
1004
|
-
});
|
|
1005
|
-
return new Map((repaired.repairs ?? [])
|
|
1006
|
-
.filter((entry) => typeof entry?.finding_id === "string")
|
|
1007
|
-
.map((entry) => [
|
|
1008
|
-
entry.finding_id,
|
|
1009
|
-
Array.isArray(entry.affected_files)
|
|
1010
|
-
? entry.affected_files.filter((p) => typeof p === "string")
|
|
1011
|
-
: [],
|
|
1012
|
-
]));
|
|
1013
|
-
}
|
|
1014
525
|
//# sourceMappingURL=plan.js.map
|