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.
@@ -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 in `runPlanPhase` before any remediation
341
- * edit exists) cannot be the run's edit, so it is excluded here — otherwise a
342
- * resolved item declaring a file the run never actually touched would sweep
343
- * pre-existing user WIP into the closing commit (the exact over-inclusion class
344
- * this manifest exists to close). Ground-truth entries from source (1) are
345
- * NEVER excluded by the snapshot: git proved the run landed those paths, and a
346
- * tool-merged edit to a file the user ALSO had dirty at run start is a path the
347
- * tool legitimately owns. A state without `run_start_dirty` (pre-field, or a
348
- * plan flow that bypasses `runPlanPhase`) means no exclusions.
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
- * Extracted so that both runPlanPhase (fast-path JSON reports) and
107
- * handlePendingExtractedPlan (LLM-extracted plans) run identical post-dedup
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":"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"}
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, findingNeedsVerificationBeforeFix, interpretFreeFormIntent, } from "audit-tools/shared";
4
- import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { isValidAuditFindingsReport } from "audit-tools/shared";
3
+ import { readdirSync, statSync } from "node:fs";
5
4
  import { snapshotAffectedFileHashes } from "../utils/fileIntegrity.js";
6
- import { groundExtractedFindings, } from "./grounding.js";
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
- * Extracted so that both runPlanPhase (fast-path JSON reports) and
649
- * handlePendingExtractedPlan (LLM-extracted plans) run identical post-dedup
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