pi-workflow-engine 0.12.0 → 0.13.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/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +13 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +16 -2
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +12 -6
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +1 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +53 -3
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -25
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +120 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +43 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/types.ts +3 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +5 -2
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +131 -178
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +7 -0
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +55 -128
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +45 -139
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +25 -67
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +25 -67
- package/USAGE.md +59 -0
- package/package.json +1 -1
|
@@ -55,9 +55,10 @@ export function renderIssueDetails(issue: ReviewIssue, theme: Theme): string {
|
|
|
55
55
|
|
|
56
56
|
export function renderIssueDetailLines(issue: ReviewIssue, theme: Theme, width: number): string[] {
|
|
57
57
|
const finding = issue.finding;
|
|
58
|
-
const metadata = `${finding.category} · severity ${finding.severity} · confidence ${finding.confidence}`;
|
|
58
|
+
const metadata = `${finding.category} · severity ${finding.severity} · confidence ${finding.confidence}${finding.verdict ? ` · ${finding.verdict}` : ""}`;
|
|
59
59
|
const lines = [`${theme.fg("accent", issue.id)} ${theme.fg("text", finding.summary)}`];
|
|
60
60
|
lines.push(...fieldLines("Metadata", metadata, width, theme));
|
|
61
|
+
if (finding.sourceCandidateIds?.length) lines.push(...fieldLines("Sources", finding.sourceCandidateIds.join(", "), width, theme));
|
|
61
62
|
lines.push(...fieldLines("Location", formatIssueLocation(issue), width, theme, "accent"));
|
|
62
63
|
lines.push(...fieldLines("Impact", finding.impact, width, theme));
|
|
63
64
|
lines.push(...fieldLines("Evidence", finding.evidence.join("; ") || "(none cited)", width, theme));
|
|
@@ -37,7 +37,7 @@ export interface SerializedReviewIssue {
|
|
|
37
37
|
|
|
38
38
|
export function toReviewIssues(name: string, report: Pick<AdvisoryReport, "findings">): ReviewIssue[] {
|
|
39
39
|
return report.findings.map((finding, index) => {
|
|
40
|
-
const location = finding.locations[0];
|
|
40
|
+
const location = finding.reviewAnchor ?? finding.locations[0];
|
|
41
41
|
return {
|
|
42
42
|
id: formatIssueId(index),
|
|
43
43
|
index,
|
|
@@ -98,6 +98,7 @@ export type AgentToolHint = "search" | "external-search";
|
|
|
98
98
|
export type AgentResumePolicy = "read-only" | "off";
|
|
99
99
|
|
|
100
100
|
export interface IsolatedAgentResult<T> {
|
|
101
|
+
readonly baselineOid?: string;
|
|
101
102
|
readonly result: T;
|
|
102
103
|
readonly patch: string;
|
|
103
104
|
readonly changed: boolean;
|
|
@@ -152,6 +153,8 @@ export interface AgentOptions<S extends TSchema = TSchema> {
|
|
|
152
153
|
resumeInputs?: readonly string[];
|
|
153
154
|
/** Run this agent in a disposable git worktree and return its patch with the result. */
|
|
154
155
|
isolation?: "worktree";
|
|
156
|
+
/** Apply a captured candidate to a fresh isolated baseline before evaluation. */
|
|
157
|
+
candidatePatch?: { readonly baselineOid: string; readonly patch: string };
|
|
155
158
|
/** Allowlist of concrete tool names the agent may use (e.g. ["read", "bash"]). */
|
|
156
159
|
tools?: string[];
|
|
157
160
|
/**
|
|
@@ -62,15 +62,18 @@ function renderAdvisoryResult(
|
|
|
62
62
|
metadata?: WorkflowRunDisplayMetadata,
|
|
63
63
|
perf?: WorkflowPerfDetails,
|
|
64
64
|
): string {
|
|
65
|
-
const
|
|
65
|
+
const incomplete = result.status === "incomplete";
|
|
66
|
+
const icon = incomplete ? theme.fg("warning", "⚠") : theme.fg("success", "✓");
|
|
66
67
|
const title = theme.fg("accent", theme.bold(`Workflow: ${name}`));
|
|
67
68
|
const lines = [`${icon} ${title}`, theme.fg("muted", result.summary)];
|
|
69
|
+
if (result.coverage?.length) lines.push(theme.fg("dim", result.coverage.map((stage) => `${stage.stage}: ${stage.completed}/${stage.expected} complete, ${stage.failed} failed`).join(" · ")));
|
|
70
|
+
if (expanded) for (const gap of result.gaps ?? []) lines.push(theme.fg("warning", gap));
|
|
68
71
|
const stats = statsLine(result.stats, theme);
|
|
69
72
|
if (stats) lines.push(stats);
|
|
70
73
|
pushWorkflowDetailLines(lines, theme, { usage, metadata, perf });
|
|
71
74
|
|
|
72
75
|
if (result.findings.length === 0) {
|
|
73
|
-
lines.push(theme.fg("success", "No findings."));
|
|
76
|
+
lines.push(incomplete ? theme.fg("warning", "No verified findings; coverage is incomplete.") : theme.fg("success", "No findings."));
|
|
74
77
|
if (expanded && result.nextSteps.length > 0) renderNextSteps(result.nextSteps, lines, theme);
|
|
75
78
|
return lines.join("\n");
|
|
76
79
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AdvisoryCandidatesSchema, AdvisoryVerdictSchema, type AdvisoryCandidate, type
|
|
2
|
-
import {
|
|
1
|
+
import { AdvisoryCandidatesSchema, AdvisoryVerdictSchema, type AdvisoryCandidate, type IdentifiedAdvisoryCandidate, type AdvisoryLocation, type AdvisoryReport, type AdvisoryVerdict } from "./advisory-schema.ts";
|
|
2
|
+
import { AdvisorySynthesisSchema, SYNTHESIS_ID_INSTRUCTIONS, withAdvisoryCoverage, collectAdvisoryStage, dedupeCandidates, identifyCandidates, uniqueLocations, type AdvisoryStageCoverage, type AdvisorySynthesis } from "./advisory-evidence.ts";
|
|
3
3
|
import type { AgentOptions, WorkflowApi, WorkflowProgressEvent, WorkflowRunStats } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
export interface AdvisoryLens {
|
|
@@ -8,17 +8,19 @@ export interface AdvisoryLens {
|
|
|
8
8
|
text: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export type AdvisoryVerified
|
|
11
|
+
export type AdvisoryVerified = IdentifiedAdvisoryCandidate & {
|
|
12
12
|
verdict: AdvisoryVerdict["verdict"];
|
|
13
13
|
evidence: string[];
|
|
14
14
|
confidence?: AdvisoryVerdict["confidence"];
|
|
15
|
+
challenge?: import("./advisory-challenge.ts").ChallengeRecord;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
export interface LensVerificationPipelineResult
|
|
18
|
-
verified:
|
|
18
|
+
export interface LensVerificationPipelineResult {
|
|
19
|
+
verified: AdvisoryVerified[];
|
|
19
20
|
rawCandidates: number;
|
|
20
21
|
dropped: number;
|
|
21
22
|
refuted: number;
|
|
23
|
+
coverage: AdvisoryStageCoverage[];
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/** Default concrete read/inspect tools for advisory workflows. */
|
|
@@ -47,20 +49,14 @@ export function publishVerifiedKeptProgress(
|
|
|
47
49
|
api.log(`${verified} verified → ${kept} kept`);
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
export
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
api: Pick<WorkflowApi, "agent" | "parallel" | "pipeline" | "progress" | "log">;
|
|
54
|
-
lenses: readonly Lens[];
|
|
52
|
+
export interface LensVerificationPipelineOptions {
|
|
53
|
+
api: Pick<WorkflowApi, "agent" | "parallel" | "phase" | "progress" | "log">;
|
|
54
|
+
lenses: readonly AdvisoryLens[];
|
|
55
55
|
perLens: number;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
schedulingMode?: AdvisorySchedulingMode;
|
|
61
|
-
finderPrompt(lens: Lens): string;
|
|
62
|
-
verifierPrompt(candidate: AdvisoryCandidate): string;
|
|
63
|
-
makeVerified(candidate: AdvisoryCandidate, lens: Lens, verdict: AdvisoryVerdict): Verified;
|
|
56
|
+
finderPhase?: "Find" | "Hypothesize";
|
|
57
|
+
finderPrompt(lens: AdvisoryLens): string;
|
|
58
|
+
verifierPrompt(candidate: IdentifiedAdvisoryCandidate): string;
|
|
59
|
+
boundCandidate?(candidate: IdentifiedAdvisoryCandidate, lens: AdvisoryLens): IdentifiedAdvisoryCandidate | undefined;
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
export interface AdvisoryBackfillDefaults {
|
|
@@ -68,117 +64,77 @@ export interface AdvisoryBackfillDefaults {
|
|
|
68
64
|
recommendation?: string;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
lens: Lens;
|
|
78
|
-
candidate: AdvisoryCandidate;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function runLensVerificationPipeline<Lens extends AdvisoryLens, Verified extends AdvisoryVerified>(
|
|
82
|
-
options: LensVerificationPipelineOptions<Lens, Verified>,
|
|
83
|
-
): Promise<LensVerificationPipelineResult<Verified>> {
|
|
84
|
-
const {
|
|
85
|
-
api,
|
|
86
|
-
lenses,
|
|
87
|
-
perLens,
|
|
88
|
-
tools = DEFAULT_ADVISORY_TOOLS,
|
|
89
|
-
toolHints = DEFAULT_ADVISORY_TOOL_HINTS,
|
|
90
|
-
finderPhase = "Find",
|
|
91
|
-
verifierPhase = "Verify",
|
|
92
|
-
schedulingMode = "pipeline",
|
|
93
|
-
finderPrompt,
|
|
94
|
-
verifierPrompt,
|
|
95
|
-
makeVerified,
|
|
96
|
-
} = options;
|
|
97
|
-
const seen = new Set<string>();
|
|
67
|
+
/** Finish all discovery before verification competes for the shared agent limit. */
|
|
68
|
+
export async function runLensVerificationPipeline(
|
|
69
|
+
options: LensVerificationPipelineOptions,
|
|
70
|
+
): Promise<LensVerificationPipelineResult> {
|
|
71
|
+
const { api, lenses, perLens, finderPhase = "Find", finderPrompt, verifierPrompt, boundCandidate } = options;
|
|
72
|
+
const coverage: AdvisoryStageCoverage[] = [];
|
|
98
73
|
let rawCandidates = 0;
|
|
99
|
-
let dropped = 0;
|
|
100
74
|
let refuted = 0;
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
phase: finderPhase,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
toolHints,
|
|
108
|
-
profile: "small",
|
|
109
|
-
schema: AdvisoryCandidatesSchema,
|
|
75
|
+
api.phase(finderPhase);
|
|
76
|
+
const found = await collectAdvisoryStage(api, "Find", lenses.map((lens) => ({ id: lens.label, run: async () => {
|
|
77
|
+
const result = await api.agent(finderPrompt(lens), {
|
|
78
|
+
phase: finderPhase, label: `${finderPhase.toLowerCase()}:${lens.label}`,
|
|
79
|
+
tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
|
|
80
|
+
profile: "small", schema: AdvisoryCandidatesSchema,
|
|
110
81
|
});
|
|
111
|
-
const
|
|
112
|
-
rawCandidates +=
|
|
113
|
-
api.progress({ type: "counter_delta", key: "candidates", label: "candidates", delta:
|
|
82
|
+
const raw = identifyCandidates(result.candidates.slice(0, perLens), lens.label);
|
|
83
|
+
rawCandidates += raw.length;
|
|
84
|
+
api.progress({ type: "counter_delta", key: "candidates", label: "candidates", delta: raw.length });
|
|
85
|
+
const candidates = boundCandidate ? raw.flatMap((candidate) => {
|
|
86
|
+
const bounded = boundCandidate(candidate, lens);
|
|
87
|
+
return bounded ? [bounded] : [];
|
|
88
|
+
}) : raw;
|
|
114
89
|
for (const candidate of candidates) {
|
|
115
|
-
api.progress({
|
|
116
|
-
|
|
117
|
-
lane: "Candidates",
|
|
118
|
-
title: candidate.summary,
|
|
119
|
-
subtitle: formatLocation(candidate),
|
|
120
|
-
status: "pending",
|
|
121
|
-
details: candidate.impact,
|
|
122
|
-
});
|
|
90
|
+
api.progress({ type: "lane_item", lane: finderPhase === "Hypothesize" ? "Hypotheses" : "Candidates",
|
|
91
|
+
title: candidate.summary, subtitle: formatLocation(candidate), status: "pending", details: candidate.impact });
|
|
123
92
|
}
|
|
124
|
-
return
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
dropped += droppedForLens;
|
|
132
|
-
api.progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: droppedForLens });
|
|
133
|
-
api.log(`find:${found.lens.label}: dropped ${droppedForLens} duplicate candidate(s)`);
|
|
134
|
-
}
|
|
135
|
-
return novel;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const verifyCandidate = async ({ lens, candidate }: NovelCandidate<Lens>): Promise<Verified | null> => {
|
|
139
|
-
const location = primaryLocation(candidate);
|
|
140
|
-
const judged = await api.agent(verifierPrompt(candidate), {
|
|
141
|
-
phase: verifierPhase,
|
|
142
|
-
label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
143
|
-
tools,
|
|
144
|
-
toolHints,
|
|
145
|
-
profile: "small",
|
|
146
|
-
schema: AdvisoryVerdictSchema,
|
|
147
|
-
});
|
|
148
|
-
if (!judged) return null;
|
|
149
|
-
recordVerdictProgress(api.progress, candidate, judged, () => {
|
|
150
|
-
refuted += 1;
|
|
151
|
-
});
|
|
152
|
-
return makeVerified(candidate, lens, judged);
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
if (schedulingMode === "finder-barrier") {
|
|
156
|
-
const found = await api.parallel(lenses.map((lens) => async () => findForLens(lens)));
|
|
157
|
-
const novel = compactResults(found).flatMap(dedupeFound);
|
|
158
|
-
const verdicts = await api.parallel(novel.map((entry) => async () => verifyCandidate(entry)));
|
|
159
|
-
return { verified: compactResults(verdicts), rawCandidates, dropped, refuted };
|
|
93
|
+
return candidates;
|
|
94
|
+
} })), coverage);
|
|
95
|
+
const candidates = dedupeCandidates(found.flat());
|
|
96
|
+
const dropped = rawCandidates - candidates.length;
|
|
97
|
+
if (dropped > 0) {
|
|
98
|
+
api.progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
|
|
99
|
+
api.log(`Dropped ${dropped} duplicate or out-of-scope candidate(s)`);
|
|
160
100
|
}
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
101
|
+
api.phase("Verify");
|
|
102
|
+
const verified = await collectAdvisoryStage(api, "Verify", candidates.map((candidate) => ({
|
|
103
|
+
id: candidate.candidateId, candidate, run: async (): Promise<AdvisoryVerified> => {
|
|
104
|
+
const location = primaryLocation(candidate);
|
|
105
|
+
const judged = await api.agent(`${verifierPrompt(candidate)}\nCandidate record: ${JSON.stringify(candidate)}`, {
|
|
106
|
+
phase: "Verify", label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
107
|
+
tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
|
|
108
|
+
profile: "small", schema: AdvisoryVerdictSchema,
|
|
109
|
+
});
|
|
110
|
+
recordVerdictProgress(api.progress, candidate, judged, () => { refuted += 1; });
|
|
111
|
+
return { ...candidate, verdict: judged.verdict, evidence: judged.evidence, confidence: judged.confidence };
|
|
169
112
|
},
|
|
170
|
-
);
|
|
113
|
+
})), coverage);
|
|
114
|
+
return { verified, rawCandidates, dropped, refuted, coverage };
|
|
115
|
+
}
|
|
171
116
|
|
|
172
|
-
|
|
117
|
+
export async function synthesizeAdvisoryReport(
|
|
118
|
+
api: Pick<WorkflowApi, "agent" | "parallel" | "phase">,
|
|
119
|
+
prompt: string,
|
|
120
|
+
ranked: readonly AdvisoryVerified[],
|
|
121
|
+
coverage: AdvisoryStageCoverage[],
|
|
122
|
+
): Promise<AdvisoryReport> {
|
|
123
|
+
api.phase("Synthesize");
|
|
124
|
+
const [report] = await collectAdvisoryStage(api, "Synthesize", [{ id: "synthesize", run: () => api.agent(
|
|
125
|
+
SYNTHESIS_ID_INSTRUCTIONS + prompt,
|
|
126
|
+
{ phase: "Synthesize", label: "synthesize", tools: [], profile: "medium", resume: "read-only", schema: AdvisorySynthesisSchema },
|
|
127
|
+
) }], coverage);
|
|
128
|
+
return resolveAdvisorySynthesis(report, ranked, {
|
|
129
|
+
impact: "Impact not restated by verification.",
|
|
130
|
+
recommendation: "Inspect the cited evidence and validate the smallest repair.",
|
|
131
|
+
}, coverage);
|
|
173
132
|
}
|
|
174
133
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
seen.add(key);
|
|
180
|
-
return true;
|
|
181
|
-
});
|
|
134
|
+
export function finishAdvisoryReport<T extends AdvisoryReport>(
|
|
135
|
+
report: T, coverage: AdvisoryStageCoverage[], verification: AdvisoryVerified[] = [],
|
|
136
|
+
) {
|
|
137
|
+
return { ...withAdvisoryCoverage(report, coverage), verification };
|
|
182
138
|
}
|
|
183
139
|
|
|
184
140
|
export function primaryLocation(candidate: Pick<AdvisoryCandidate, "locations">): AdvisoryLocation {
|
|
@@ -200,52 +156,17 @@ export function normalizePath(path: string): string {
|
|
|
200
156
|
return path.replace(/^\.\//, "").replace(/^[ab]\//, "");
|
|
201
157
|
}
|
|
202
158
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
case "CONFIRMED":
|
|
212
|
-
return "Confirmed";
|
|
213
|
-
case "PLAUSIBLE":
|
|
214
|
-
return "Plausible";
|
|
215
|
-
case "REFUTED":
|
|
216
|
-
return "Refuted";
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function verdictStatus(verdict: AdvisoryVerdict["verdict"]): "success" | "warning" | "error" {
|
|
221
|
-
switch (verdict) {
|
|
222
|
-
case "CONFIRMED":
|
|
223
|
-
return "success";
|
|
224
|
-
case "PLAUSIBLE":
|
|
225
|
-
return "warning";
|
|
226
|
-
case "REFUTED":
|
|
227
|
-
return "error";
|
|
228
|
-
}
|
|
229
|
-
}
|
|
159
|
+
const VERDICT_PRESENTATION = {
|
|
160
|
+
CONFIRMED: { lane: "Confirmed", status: "success", confidence: "high" },
|
|
161
|
+
PLAUSIBLE: { lane: "Plausible", status: "warning", confidence: "medium" },
|
|
162
|
+
NOT_SUBSTANTIATED: { lane: "Unresolved", status: "warning", confidence: "medium" },
|
|
163
|
+
REFUTED: { lane: "Refuted", status: "error", confidence: "low" },
|
|
164
|
+
} satisfies Record<AdvisoryVerdict["verdict"], {
|
|
165
|
+
lane: string; status: "success" | "warning" | "error"; confidence: NonNullable<AdvisoryVerdict["confidence"]>;
|
|
166
|
+
}>;
|
|
230
167
|
|
|
231
168
|
export function verdictConfidence(verdict: AdvisoryVerdict["verdict"]): "high" | "medium" | "low" {
|
|
232
|
-
|
|
233
|
-
case "CONFIRMED":
|
|
234
|
-
return "high";
|
|
235
|
-
case "PLAUSIBLE":
|
|
236
|
-
return "medium";
|
|
237
|
-
case "REFUTED":
|
|
238
|
-
return "low";
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function sameFinding(candidate: Pick<AdvisoryCandidate, "locations">, finding: Pick<AdvisoryFinding, "locations" | "summary">): boolean {
|
|
243
|
-
return findingLocationKey(candidate) === findingLocationKey(finding);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function findingLocationKey(value: Pick<AdvisoryCandidate, "locations"> | Pick<AdvisoryFinding, "locations">): string {
|
|
247
|
-
const location = primaryLocation(value);
|
|
248
|
-
return `${normalizePath(location.file)}:${location.line ?? "file"}`;
|
|
169
|
+
return VERDICT_PRESENTATION[verdict].confidence;
|
|
249
170
|
}
|
|
250
171
|
|
|
251
172
|
export function recordVerdictProgress(
|
|
@@ -261,32 +182,64 @@ export function recordVerdictProgress(
|
|
|
261
182
|
}
|
|
262
183
|
progress({
|
|
263
184
|
type: "lane_item",
|
|
264
|
-
lane:
|
|
185
|
+
lane: VERDICT_PRESENTATION[verdict.verdict].lane,
|
|
265
186
|
title: candidate.summary,
|
|
266
187
|
subtitle: formatLocation(candidate),
|
|
267
|
-
status:
|
|
188
|
+
status: VERDICT_PRESENTATION[verdict.verdict].status,
|
|
268
189
|
details: formatEvidence(verdict.evidence),
|
|
269
190
|
});
|
|
270
191
|
}
|
|
271
192
|
|
|
272
193
|
export function backfillAdvisoryFindings<Source extends AdvisoryVerified>(
|
|
273
|
-
findings:
|
|
194
|
+
findings: AdvisorySynthesis["findings"],
|
|
274
195
|
ranked: readonly Source[],
|
|
275
196
|
defaults: AdvisoryBackfillDefaults,
|
|
276
197
|
): AdvisoryReport["findings"] {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
198
|
+
const sources = new Map(ranked.flatMap((source) => source.sourceCandidateIds.map((id) => [id, source] as const)));
|
|
199
|
+
const used = new Set<string>();
|
|
200
|
+
return findings.flatMap((selection) => {
|
|
201
|
+
const ids = [...new Set(selection.sourceCandidateIds)];
|
|
202
|
+
if (ids.length === 0 || ids.some((id) => !sources.has(id) || used.has(id))) return [];
|
|
203
|
+
const records = [...new Set(ids.map((id) => sources.get(id)!))];
|
|
204
|
+
if (records.some((record) => record.verdict === "REFUTED")) return [];
|
|
205
|
+
const sourceCandidateIds = [...new Set(records.flatMap((record) => record.sourceCandidateIds))];
|
|
206
|
+
if (sourceCandidateIds.some((id) => used.has(id))) return [];
|
|
207
|
+
sourceCandidateIds.forEach((id) => used.add(id));
|
|
208
|
+
const first = records[0]!;
|
|
209
|
+
return [{
|
|
210
|
+
sourceCandidateIds,
|
|
211
|
+
summary: [...new Set(records.map((record) => record.summary))].join("; "),
|
|
212
|
+
category: first.category,
|
|
213
|
+
severity: selection.severity,
|
|
214
|
+
verdict: records.some((record) => record.verdict === "NOT_SUBSTANTIATED") ? "NOT_SUBSTANTIATED" as const : records.some((record) => record.verdict === "PLAUSIBLE") ? "PLAUSIBLE" as const : "CONFIRMED" as const,
|
|
215
|
+
confidence: records.every((record) => record.verdict === "CONFIRMED") ? "high" as const : "medium" as const,
|
|
216
|
+
locations: uniqueLocations(records.flatMap((record) => record.locations)),
|
|
217
|
+
...(first.reviewAnchor ? { reviewAnchor: first.reviewAnchor } : {}),
|
|
218
|
+
evidence: [...new Set(records.flatMap((record) => [...(record.discoveryEvidence ?? []), ...record.evidence]))],
|
|
219
|
+
impact: [...new Set(records.map((record) => record.impact || defaults.impact))].join("\n"),
|
|
220
|
+
recommendation: selection.recommendation || first.recommendation || defaults.recommendation || "",
|
|
221
|
+
}];
|
|
222
|
+
});
|
|
223
|
+
}
|
|
282
224
|
|
|
283
|
-
|
|
284
|
-
|
|
225
|
+
/** Retain verified records if synthesis fails, and expose invalid ID selections as a gap. */
|
|
226
|
+
export function resolveAdvisorySynthesis<Source extends AdvisoryVerified>(
|
|
227
|
+
report: AdvisorySynthesis | undefined,
|
|
228
|
+
ranked: readonly Source[],
|
|
229
|
+
defaults: AdvisoryBackfillDefaults,
|
|
230
|
+
coverage: AdvisoryStageCoverage[],
|
|
231
|
+
): AdvisoryReport {
|
|
232
|
+
if (!report) {
|
|
285
233
|
return {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
recommendation: finding.recommendation || source?.recommendation || defaults.recommendation || "",
|
|
234
|
+
summary: "Synthesis unavailable; verified records retained.",
|
|
235
|
+
findings: backfillAdvisoryFindings(ranked.map((finding) => ({ sourceCandidateIds: finding.sourceCandidateIds, severity: "medium", recommendation: finding.recommendation ?? defaults.recommendation ?? "Inspect verifier evidence." })), ranked, defaults),
|
|
236
|
+
nextSteps: ["Inspect verifier evidence or rerun synthesis."],
|
|
290
237
|
};
|
|
291
|
-
}
|
|
238
|
+
}
|
|
239
|
+
const findings = backfillAdvisoryFindings(report.findings, ranked, defaults);
|
|
240
|
+
if (findings.length !== report.findings.length) {
|
|
241
|
+
coverage.push({ stage: "Synthesis provenance", expected: report.findings.length, completed: findings.length,
|
|
242
|
+
failed: report.findings.length - findings.length, failures: [{ branch: "synthesize", reason: "Discarded invalid, repeated, or unverified candidate IDs." }] });
|
|
243
|
+
}
|
|
244
|
+
return { ...report, findings };
|
|
292
245
|
}
|
|
@@ -154,6 +154,13 @@ export class WorktreeRegistry {
|
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
async applyPatch(path: string, patch: string, signal?: AbortSignal): Promise<WorktreeGitCommandResult> {
|
|
158
|
+
return this.runner.runGit({
|
|
159
|
+
cwd: path, args: ["apply", "--index", "--binary", "-"],
|
|
160
|
+
stdin: patch, signal, timeoutMs: this.timeoutMs,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
157
164
|
async validatePatch(path: string, candidate: WorktreePatch, signal?: AbortSignal): Promise<WorktreeGitCommandResult> {
|
|
158
165
|
return await validateWorktreePatch({
|
|
159
166
|
worktreePath: path,
|