@jameslovespancakes/pi-plus 1.0.11 → 1.0.12
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/README.md +44 -20
- package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
- package/package.json +6 -3
- package/src/core/accounts/oauth-pool.ts +102 -0
- package/src/core/accounts/registry.ts +8 -25
- package/src/core/accounts/routing.ts +122 -0
- package/src/core/anthropic/client-identity.ts +9 -64
- package/src/core/anthropic/quota.ts +8 -53
- package/src/core/anthropic/routing.ts +40 -115
- package/src/core/anthropic/store.ts +7 -9
- package/src/core/catalog/quality.ts +33 -15
- package/src/core/codex/quota.ts +8 -2
- package/src/core/codex/store.ts +12 -9
- package/src/core/config.ts +4 -17
- package/src/core/store.ts +13 -12
- package/src/domains/models/provider-picker.ts +1 -1
- package/src/domains/setup/index.ts +23 -23
- package/src/domains/subscriptions/accounts-picker.ts +18 -37
- package/src/domains/subscriptions/accounts.ts +23 -29
- package/src/domains/subscriptions/footer.ts +48 -29
- package/src/domains/subscriptions/index.ts +11 -27
- package/src/domains/subscriptions/provider.ts +38 -103
- package/src/domains/subscriptions/providers/anthropic.ts +5 -17
- package/src/domains/subscriptions/providers/codex.ts +171 -4
- package/src/domains/subscriptions/providers/hosted.ts +18 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +273 -0
- package/src/domains/subscriptions/routing.ts +19 -11
- package/src/domains/workflows/LICENSE.md +21 -0
- package/src/domains/workflows/index.ts +836 -0
- package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
- package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
- package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
- package/src/domains/workflows/runtime/agent-attempt.ts +147 -0
- package/src/domains/workflows/runtime/agent-limits.ts +43 -0
- package/src/domains/workflows/runtime/agent-replay.ts +399 -0
- package/src/domains/workflows/runtime/agent-retry.ts +116 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
- package/src/domains/workflows/runtime/agent-runner.ts +203 -0
- package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
- package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
- package/src/domains/workflows/runtime/agent-session.ts +382 -0
- package/src/domains/workflows/runtime/agent-skills.ts +270 -0
- package/src/domains/workflows/runtime/agent-workspace.ts +103 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
- package/src/domains/workflows/runtime/background-workflows.ts +492 -0
- package/src/domains/workflows/runtime/budget.ts +53 -0
- package/src/domains/workflows/runtime/cancellation.ts +87 -0
- package/src/domains/workflows/runtime/command-completions.ts +36 -0
- package/src/domains/workflows/runtime/concurrency.ts +403 -0
- package/src/domains/workflows/runtime/debug.ts +3 -0
- package/src/domains/workflows/runtime/diff-capture.ts +81 -0
- package/src/domains/workflows/runtime/discovery.ts +137 -0
- package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
- package/src/domains/workflows/runtime/dynamax.ts +330 -0
- package/src/domains/workflows/runtime/engine.ts +543 -0
- package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
- package/src/domains/workflows/runtime/finalizers.ts +66 -0
- package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
- package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
- package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
- package/src/domains/workflows/runtime/journal.ts +313 -0
- package/src/domains/workflows/runtime/model-profiles.ts +310 -0
- package/src/domains/workflows/runtime/options.ts +157 -0
- package/src/domains/workflows/runtime/perf.ts +146 -0
- package/src/domains/workflows/runtime/pi-compat.ts +32 -0
- package/src/domains/workflows/runtime/process-runner.ts +260 -0
- package/src/domains/workflows/runtime/progress-types.ts +59 -0
- package/src/domains/workflows/runtime/progress.ts +400 -0
- package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
- package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
- package/src/domains/workflows/runtime/research-contract.ts +89 -0
- package/src/domains/workflows/runtime/research-evidence.ts +289 -0
- package/src/domains/workflows/runtime/resume-context.ts +751 -0
- package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
- package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
- package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
- package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
- package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
- package/src/domains/workflows/runtime/review/review-format.ts +133 -0
- package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
- package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
- package/src/domains/workflows/runtime/review/review-report.ts +42 -0
- package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
- package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
- package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
- package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
- package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
- package/src/domains/workflows/runtime/session-identity.ts +6 -0
- package/src/domains/workflows/runtime/structured-output.ts +32 -0
- package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
- package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
- package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
- package/src/domains/workflows/runtime/types.ts +255 -0
- package/src/domains/workflows/runtime/ui/display-text.ts +13 -0
- package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +398 -0
- package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
- package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
- package/src/domains/workflows/runtime/unknown-error.ts +8 -0
- package/src/domains/workflows/runtime/usage.ts +341 -0
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
- package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
- package/src/domains/workflows/runtime/workflow-module.ts +75 -0
- package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
- package/src/domains/workflows/runtime/workflow-run-history.ts +162 -0
- package/src/domains/workflows/runtime/workflow-run-record.ts +635 -0
- package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
- package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
- package/src/domains/workflows/runtime/workflows.ts +40 -0
- package/src/domains/workflows/runtime/worktree.ts +581 -0
- package/src/domains/workflows/workflows/code-review.ts +232 -0
- package/src/domains/workflows/workflows/diagnose.ts +154 -0
- package/src/domains/workflows/workflows/perf-review.ts +149 -0
- package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
- package/src/domains/workflows/workflows/research.ts +169 -0
- package/src/services/usage-service.ts +6 -30
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AgentRowSnapshot, WorkflowProgressSnapshot } from "../progress-types.ts";
|
|
3
|
+
import { formatWorkflowUsageLine } from "../usage.ts";
|
|
4
|
+
import { formatDuration, statusIcon } from "./workflow-format.ts";
|
|
5
|
+
import { toDisplayLine } from "./display-text.ts";
|
|
6
|
+
|
|
7
|
+
const MAX_WIDGET_LINES = 9;
|
|
8
|
+
|
|
9
|
+
/** Compact live board shown above the editor. */
|
|
10
|
+
export function renderWorkflowWidgetLines(snapshot: WorkflowProgressSnapshot, theme: Theme): string[] {
|
|
11
|
+
const agents = snapshot.phases.flatMap((phase) => phase.agents);
|
|
12
|
+
const ordered = [...agents].sort((left, right) => statusOrder(left.status) - statusOrder(right.status));
|
|
13
|
+
const counts = countAgents(agents);
|
|
14
|
+
const elapsed = formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt);
|
|
15
|
+
const active = counts.running + counts.queued;
|
|
16
|
+
const icon = active > 0 ? theme.fg("accent", "●") : theme.fg("success", "✓");
|
|
17
|
+
const summary = [
|
|
18
|
+
`${counts.running} running`,
|
|
19
|
+
`${counts.queued} queued`,
|
|
20
|
+
`${counts.done}/${counts.total} done`,
|
|
21
|
+
...(counts.failed > 0 ? [`${counts.failed} failed`] : []),
|
|
22
|
+
elapsed,
|
|
23
|
+
].join(` ${theme.fg("dim", "·")} `);
|
|
24
|
+
const lines = [
|
|
25
|
+
`${icon} ${theme.bold(snapshot.title)} ${theme.fg("dim", `· ${snapshot.currentPhase} · ${summary}`)}`,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const footer = footerLine(snapshot, theme);
|
|
29
|
+
const bodyLimit = Math.max(0, MAX_WIDGET_LINES - lines.length - 1);
|
|
30
|
+
for (const agent of ordered.slice(0, bodyLimit)) lines.push(agentLine(agent, theme));
|
|
31
|
+
const hidden = ordered.length - Math.min(ordered.length, bodyLimit);
|
|
32
|
+
if (hidden > 0 && lines.length < MAX_WIDGET_LINES) {
|
|
33
|
+
lines.push(theme.fg("dim", ` +${hidden} more · /workflow`));
|
|
34
|
+
} else if (footer && lines.length < MAX_WIDGET_LINES) {
|
|
35
|
+
lines.push(footer);
|
|
36
|
+
}
|
|
37
|
+
return lines.map((line) => line.replace(/[\r\n]+/g, " "));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface AgentCounts {
|
|
41
|
+
queued: number;
|
|
42
|
+
running: number;
|
|
43
|
+
done: number;
|
|
44
|
+
failed: number;
|
|
45
|
+
total: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function countAgents(agents: readonly AgentRowSnapshot[]): AgentCounts {
|
|
49
|
+
const counts: AgentCounts = { queued: 0, running: 0, done: 0, failed: 0, total: agents.length };
|
|
50
|
+
for (const agent of agents) counts[agent.status]++;
|
|
51
|
+
return counts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function statusOrder(status: AgentRowSnapshot["status"]): number {
|
|
55
|
+
return status === "running" ? 0 : status === "queued" ? 1 : status === "failed" ? 2 : 3;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function agentLine(agent: AgentRowSnapshot, theme: Theme): string {
|
|
59
|
+
const model = toDisplayLine(shortModel(agent.model ?? "host default"), 48);
|
|
60
|
+
const details: string[] = [];
|
|
61
|
+
if (agent.lastTool) details.push(toDisplayLine(agent.lastTool, 32));
|
|
62
|
+
if (agent.toolUses > 0) details.push(`${agent.toolUses} tool${agent.toolUses === 1 ? "" : "s"}`);
|
|
63
|
+
if (agent.startedAt !== undefined) details.push(formatDuration((agent.doneAt ?? Date.now()) - agent.startedAt));
|
|
64
|
+
const activity = details.length > 0 ? ` ${theme.fg("dim", `· ${details.join(" · ")}`)}` : "";
|
|
65
|
+
const color = agent.status === "failed" ? "error" : agent.status === "running" ? "text" : "muted";
|
|
66
|
+
return ` ${statusIcon(agent.status, theme)} ${theme.fg(color, toDisplayLine(agent.label, 64))} ${theme.fg("accent", model)}${activity}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function footerLine(snapshot: WorkflowProgressSnapshot, theme: Theme): string {
|
|
70
|
+
const usage = formatWorkflowUsageLine(snapshot.usage);
|
|
71
|
+
const latest = snapshot.logs.at(-1);
|
|
72
|
+
const parts = [usage, latest ? toDisplayLine(latest, 100) : undefined, "/workflow"].filter(Boolean);
|
|
73
|
+
return theme.fg("dim", ` ${parts.join(" · ")}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function shortModel(model: string): string {
|
|
77
|
+
return model.replace(/^openai-codex\//, "codex/").replace(/^anthropic\//, "");
|
|
78
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Read an unknown thrown value without allowing hostile coercion to mask the original failure. */
|
|
2
|
+
export function unknownErrorMessage(error: unknown): string {
|
|
3
|
+
try {
|
|
4
|
+
return error instanceof Error ? error.message : String(error);
|
|
5
|
+
} catch {
|
|
6
|
+
return "unknown error";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
export interface WorkflowUsageCost {
|
|
2
|
+
readonly input: number;
|
|
3
|
+
readonly output: number;
|
|
4
|
+
readonly cacheRead: number;
|
|
5
|
+
readonly cacheWrite: number;
|
|
6
|
+
readonly total: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type WorkflowUsageCoverage = "none" | "partial" | "complete";
|
|
10
|
+
|
|
11
|
+
export interface WorkflowUsageComponentCoverage {
|
|
12
|
+
readonly input: WorkflowUsageCoverage;
|
|
13
|
+
readonly output: WorkflowUsageCoverage;
|
|
14
|
+
readonly cacheRead: WorkflowUsageCoverage;
|
|
15
|
+
readonly cacheWrite: WorkflowUsageCoverage;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WorkflowUsageTotals {
|
|
19
|
+
readonly input: number;
|
|
20
|
+
readonly output: number;
|
|
21
|
+
readonly cacheRead: number;
|
|
22
|
+
readonly cacheWrite: number;
|
|
23
|
+
readonly totalTokens: number;
|
|
24
|
+
readonly coverage: WorkflowUsageComponentCoverage;
|
|
25
|
+
readonly cost: WorkflowUsageCost;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkflowAgentUsage {
|
|
29
|
+
readonly label: string;
|
|
30
|
+
readonly phase?: string;
|
|
31
|
+
readonly provider?: string;
|
|
32
|
+
readonly model?: string;
|
|
33
|
+
readonly assistantMessages: number;
|
|
34
|
+
readonly usage: WorkflowUsageTotals;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WorkflowUsageSnapshot {
|
|
38
|
+
readonly agents: readonly WorkflowAgentUsage[];
|
|
39
|
+
readonly totals: WorkflowUsageTotals;
|
|
40
|
+
readonly assistantMessages: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WorkflowUsageSink {
|
|
44
|
+
recordAgentSession(input: { label: string; phase?: string; messages: readonly unknown[] }): void;
|
|
45
|
+
snapshot(): WorkflowUsageSnapshot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ZERO_COST: WorkflowUsageCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
49
|
+
const ZERO_COVERAGE: WorkflowUsageComponentCoverage = {
|
|
50
|
+
input: "none",
|
|
51
|
+
output: "none",
|
|
52
|
+
cacheRead: "none",
|
|
53
|
+
cacheWrite: "none",
|
|
54
|
+
};
|
|
55
|
+
const ZERO_TOTALS: WorkflowUsageTotals = {
|
|
56
|
+
input: 0,
|
|
57
|
+
output: 0,
|
|
58
|
+
cacheRead: 0,
|
|
59
|
+
cacheWrite: 0,
|
|
60
|
+
totalTokens: 0,
|
|
61
|
+
coverage: ZERO_COVERAGE,
|
|
62
|
+
cost: ZERO_COST,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface AssistantUsageMessage {
|
|
66
|
+
readonly provider?: string;
|
|
67
|
+
readonly model?: string;
|
|
68
|
+
readonly usage: WorkflowUsageTotals;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class WorkflowUsageRecorder implements WorkflowUsageSink {
|
|
72
|
+
private readonly agents: WorkflowAgentUsage[] = [];
|
|
73
|
+
private readonly onSnapshot?: (snapshot: WorkflowUsageSnapshot) => void;
|
|
74
|
+
|
|
75
|
+
constructor(onSnapshot?: (snapshot: WorkflowUsageSnapshot) => void) {
|
|
76
|
+
this.onSnapshot = onSnapshot;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
recordAgentSession(input: { label: string; phase?: string; messages: readonly unknown[] }): void {
|
|
80
|
+
const assistantMessages = input.messages.flatMap((message) => {
|
|
81
|
+
const parsed = parseAssistantUsageMessage(message);
|
|
82
|
+
return parsed ? [parsed] : [];
|
|
83
|
+
});
|
|
84
|
+
if (assistantMessages.length === 0) return;
|
|
85
|
+
|
|
86
|
+
const usage = sumTotals(assistantMessages.map((message) => message.usage));
|
|
87
|
+
const latestMetadata = assistantMessages.findLast((message) => message.provider !== undefined || message.model !== undefined);
|
|
88
|
+
this.agents.push({
|
|
89
|
+
label: input.label,
|
|
90
|
+
phase: input.phase,
|
|
91
|
+
provider: latestMetadata?.provider,
|
|
92
|
+
model: latestMetadata?.model,
|
|
93
|
+
assistantMessages: assistantMessages.length,
|
|
94
|
+
usage,
|
|
95
|
+
});
|
|
96
|
+
this.onSnapshot?.(this.snapshot());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
snapshot(): WorkflowUsageSnapshot {
|
|
100
|
+
const agents = this.agents.map((agent) => ({
|
|
101
|
+
...agent,
|
|
102
|
+
usage: cloneTotals(agent.usage),
|
|
103
|
+
}));
|
|
104
|
+
return {
|
|
105
|
+
agents,
|
|
106
|
+
totals: sumTotals(agents.map((agent) => agent.usage)),
|
|
107
|
+
assistantMessages: agents.reduce((sum, agent) => sum + agent.assistantMessages, 0),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createWorkflowUsageRecorder(onSnapshot?: (snapshot: WorkflowUsageSnapshot) => void): WorkflowUsageSink {
|
|
113
|
+
return new WorkflowUsageRecorder(onSnapshot);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function emptyWorkflowUsageTotals(): WorkflowUsageTotals {
|
|
117
|
+
return cloneTotals(ZERO_TOTALS);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isWorkflowUsageSnapshot(value: unknown): value is WorkflowUsageSnapshot {
|
|
121
|
+
if (!isRecord(value)) return false;
|
|
122
|
+
if (!Array.isArray(value.agents) || !value.agents.every(isWorkflowAgentUsage)) return false;
|
|
123
|
+
if (!isWorkflowUsageTotals(value.totals)) return false;
|
|
124
|
+
return finiteNumber(value.assistantMessages) !== undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function hasWorkflowUsage(snapshot: unknown): snapshot is WorkflowUsageSnapshot {
|
|
128
|
+
if (!isWorkflowUsageSnapshot(snapshot)) return false;
|
|
129
|
+
return snapshot.totals.totalTokens > 0 || snapshot.totals.cost.total > 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function formatWorkflowUsageLine(snapshot: unknown): string | undefined {
|
|
133
|
+
if (!hasWorkflowUsage(snapshot)) return undefined;
|
|
134
|
+
const parts = formatWorkflowUsageComponents(snapshot.totals);
|
|
135
|
+
parts.push(`cost $${snapshot.totals.cost.total.toFixed(3)}`);
|
|
136
|
+
parts.push(`agents ${snapshot.agents.length}`);
|
|
137
|
+
return `Usage: ${parts.join(" · ")}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatWorkflowUsageComponents(totals: WorkflowUsageTotals): string[] {
|
|
141
|
+
const parts: string[] = [];
|
|
142
|
+
const completeCacheBreakdown =
|
|
143
|
+
totals.coverage.cacheRead === "complete" && totals.coverage.cacheWrite === "complete";
|
|
144
|
+
appendUsageComponent(parts, completeCacheBreakdown ? "fresh" : "input", totals.input, totals.coverage.input);
|
|
145
|
+
appendUsageComponent(parts, "cache read", totals.cacheRead, totals.coverage.cacheRead);
|
|
146
|
+
appendUsageComponent(parts, "cache write", totals.cacheWrite, totals.coverage.cacheWrite);
|
|
147
|
+
appendUsageComponent(parts, "output", totals.output, totals.coverage.output);
|
|
148
|
+
|
|
149
|
+
const knownComponents = totals.input + totals.output + totals.cacheRead + totals.cacheWrite;
|
|
150
|
+
if (parts.length === 0 && totals.totalTokens > 0) parts.push(`tokens ${formatUsageCount(totals.totalTokens)}`);
|
|
151
|
+
else if (!hasCompleteCoverage(totals.coverage) && totals.totalTokens !== knownComponents) {
|
|
152
|
+
parts.push(`total ${formatUsageCount(totals.totalTokens)}`);
|
|
153
|
+
}
|
|
154
|
+
return parts;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function appendUsageComponent(
|
|
158
|
+
parts: string[],
|
|
159
|
+
label: string,
|
|
160
|
+
value: number,
|
|
161
|
+
coverage: WorkflowUsageCoverage,
|
|
162
|
+
): void {
|
|
163
|
+
if (value <= 0 || coverage === "none") return;
|
|
164
|
+
const count = formatUsageCount(value);
|
|
165
|
+
parts.push(`${label} ${coverage === "partial" ? `≥${count}` : count}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isWorkflowAgentUsage(value: unknown): value is WorkflowAgentUsage {
|
|
169
|
+
if (!isRecord(value)) return false;
|
|
170
|
+
if (typeof value.label !== "string") return false;
|
|
171
|
+
if (value.phase !== undefined && typeof value.phase !== "string") return false;
|
|
172
|
+
if (value.provider !== undefined && typeof value.provider !== "string") return false;
|
|
173
|
+
if (value.model !== undefined && typeof value.model !== "string") return false;
|
|
174
|
+
if (finiteNumber(value.assistantMessages) === undefined) return false;
|
|
175
|
+
return isWorkflowUsageTotals(value.usage);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isWorkflowUsageTotals(value: unknown): value is WorkflowUsageTotals {
|
|
179
|
+
if (!isRecord(value)) return false;
|
|
180
|
+
if (finiteNumber(value.input) === undefined) return false;
|
|
181
|
+
if (finiteNumber(value.output) === undefined) return false;
|
|
182
|
+
if (finiteNumber(value.cacheRead) === undefined) return false;
|
|
183
|
+
if (finiteNumber(value.cacheWrite) === undefined) return false;
|
|
184
|
+
if (finiteNumber(value.totalTokens) === undefined) return false;
|
|
185
|
+
if (!isWorkflowUsageCoverage(value.coverage)) return false;
|
|
186
|
+
return isWorkflowUsageCost(value.cost);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isWorkflowUsageCoverage(value: unknown): value is WorkflowUsageComponentCoverage {
|
|
190
|
+
if (!isRecord(value)) return false;
|
|
191
|
+
return (
|
|
192
|
+
isCoverageValue(value.input) &&
|
|
193
|
+
isCoverageValue(value.output) &&
|
|
194
|
+
isCoverageValue(value.cacheRead) &&
|
|
195
|
+
isCoverageValue(value.cacheWrite)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isWorkflowUsageCost(value: unknown): value is WorkflowUsageCost {
|
|
200
|
+
if (!isRecord(value)) return false;
|
|
201
|
+
return (
|
|
202
|
+
finiteNumber(value.input) !== undefined &&
|
|
203
|
+
finiteNumber(value.output) !== undefined &&
|
|
204
|
+
finiteNumber(value.cacheRead) !== undefined &&
|
|
205
|
+
finiteNumber(value.cacheWrite) !== undefined &&
|
|
206
|
+
finiteNumber(value.total) !== undefined
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseAssistantUsageMessage(message: unknown): AssistantUsageMessage | undefined {
|
|
211
|
+
if (!isRecord(message)) return undefined;
|
|
212
|
+
if (message.role !== "assistant") return undefined;
|
|
213
|
+
const usage = parseUsageTotals(message.usage);
|
|
214
|
+
if (!usage) return undefined;
|
|
215
|
+
return {
|
|
216
|
+
provider: typeof message.provider === "string" ? message.provider : undefined,
|
|
217
|
+
model: typeof message.model === "string" ? message.model : undefined,
|
|
218
|
+
usage,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseUsageTotals(value: unknown): WorkflowUsageTotals | undefined {
|
|
223
|
+
if (!isRecord(value)) return undefined;
|
|
224
|
+
const input = finiteNumber(value.input);
|
|
225
|
+
const output = finiteNumber(value.output);
|
|
226
|
+
const cacheRead = finiteNumber(value.cacheRead);
|
|
227
|
+
const cacheWrite = finiteNumber(value.cacheWrite);
|
|
228
|
+
const reportedTotal = finiteNumber(value.totalTokens);
|
|
229
|
+
const cost = parseUsageCost(value.cost);
|
|
230
|
+
if (!cost) return undefined;
|
|
231
|
+
if (input === undefined && output === undefined && cacheRead === undefined && cacheWrite === undefined && reportedTotal === undefined) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
const coverage: WorkflowUsageComponentCoverage = {
|
|
235
|
+
input: input === undefined ? "none" : "complete",
|
|
236
|
+
output: output === undefined ? "none" : "complete",
|
|
237
|
+
cacheRead: cacheRead === undefined ? "none" : "complete",
|
|
238
|
+
cacheWrite: cacheWrite === undefined ? "none" : "complete",
|
|
239
|
+
};
|
|
240
|
+
const componentTotal = (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0);
|
|
241
|
+
return {
|
|
242
|
+
input: input ?? 0,
|
|
243
|
+
output: output ?? 0,
|
|
244
|
+
cacheRead: cacheRead ?? 0,
|
|
245
|
+
cacheWrite: cacheWrite ?? 0,
|
|
246
|
+
totalTokens: hasCompleteCoverage(coverage) ? componentTotal : (reportedTotal ?? componentTotal),
|
|
247
|
+
coverage,
|
|
248
|
+
cost,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseUsageCost(value: unknown): WorkflowUsageCost | undefined {
|
|
253
|
+
if (!isRecord(value)) return undefined;
|
|
254
|
+
const input = finiteNumber(value.input);
|
|
255
|
+
const output = finiteNumber(value.output);
|
|
256
|
+
const cacheRead = finiteNumber(value.cacheRead);
|
|
257
|
+
const cacheWrite = finiteNumber(value.cacheWrite);
|
|
258
|
+
const total = finiteNumber(value.total);
|
|
259
|
+
if (total === undefined) return undefined;
|
|
260
|
+
return {
|
|
261
|
+
input: input ?? 0,
|
|
262
|
+
output: output ?? 0,
|
|
263
|
+
cacheRead: cacheRead ?? 0,
|
|
264
|
+
cacheWrite: cacheWrite ?? 0,
|
|
265
|
+
total,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function sumTotals(values: readonly WorkflowUsageTotals[]): WorkflowUsageTotals {
|
|
270
|
+
const [first, ...rest] = values;
|
|
271
|
+
if (!first) return emptyWorkflowUsageTotals();
|
|
272
|
+
return rest.reduce(addTotals, cloneTotals(first));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function addTotals(left: WorkflowUsageTotals, right: WorkflowUsageTotals): WorkflowUsageTotals {
|
|
276
|
+
return {
|
|
277
|
+
input: left.input + right.input,
|
|
278
|
+
output: left.output + right.output,
|
|
279
|
+
cacheRead: left.cacheRead + right.cacheRead,
|
|
280
|
+
cacheWrite: left.cacheWrite + right.cacheWrite,
|
|
281
|
+
totalTokens: left.totalTokens + right.totalTokens,
|
|
282
|
+
coverage: {
|
|
283
|
+
input: combineCoverage(left.coverage.input, right.coverage.input),
|
|
284
|
+
output: combineCoverage(left.coverage.output, right.coverage.output),
|
|
285
|
+
cacheRead: combineCoverage(left.coverage.cacheRead, right.coverage.cacheRead),
|
|
286
|
+
cacheWrite: combineCoverage(left.coverage.cacheWrite, right.coverage.cacheWrite),
|
|
287
|
+
},
|
|
288
|
+
cost: {
|
|
289
|
+
input: left.cost.input + right.cost.input,
|
|
290
|
+
output: left.cost.output + right.cost.output,
|
|
291
|
+
cacheRead: left.cost.cacheRead + right.cost.cacheRead,
|
|
292
|
+
cacheWrite: left.cost.cacheWrite + right.cost.cacheWrite,
|
|
293
|
+
total: left.cost.total + right.cost.total,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function cloneTotals(value: WorkflowUsageTotals): WorkflowUsageTotals {
|
|
299
|
+
return {
|
|
300
|
+
input: value.input,
|
|
301
|
+
output: value.output,
|
|
302
|
+
cacheRead: value.cacheRead,
|
|
303
|
+
cacheWrite: value.cacheWrite,
|
|
304
|
+
totalTokens: value.totalTokens,
|
|
305
|
+
coverage: { ...value.coverage },
|
|
306
|
+
cost: { ...value.cost },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function combineCoverage(left: WorkflowUsageCoverage, right: WorkflowUsageCoverage): WorkflowUsageCoverage {
|
|
311
|
+
return left === right ? left : "partial";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function hasCompleteCoverage(coverage: WorkflowUsageComponentCoverage): boolean {
|
|
315
|
+
return (
|
|
316
|
+
coverage.input === "complete" &&
|
|
317
|
+
coverage.output === "complete" &&
|
|
318
|
+
coverage.cacheRead === "complete" &&
|
|
319
|
+
coverage.cacheWrite === "complete"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isCoverageValue(value: unknown): value is WorkflowUsageCoverage {
|
|
324
|
+
return value === "none" || value === "partial" || value === "complete";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function formatUsageCount(count: number): string {
|
|
328
|
+
if (count < 1000) return count.toString();
|
|
329
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
330
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
331
|
+
if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
|
|
332
|
+
return `${Math.round(count / 1000000)}M`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
336
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
340
|
+
return typeof value === "object" && value !== null;
|
|
341
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
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
|
+
import type { AgentOptions, WorkflowApi, WorkflowProgressEvent, WorkflowRunStats } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export interface AdvisoryLens {
|
|
6
|
+
label: string;
|
|
7
|
+
category: string;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type AdvisoryVerified = IdentifiedAdvisoryCandidate & {
|
|
12
|
+
verdict: AdvisoryVerdict["verdict"];
|
|
13
|
+
evidence: string[];
|
|
14
|
+
confidence?: AdvisoryVerdict["confidence"];
|
|
15
|
+
challenge?: import("./advisory-challenge.ts").ChallengeRecord;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface LensVerificationPipelineResult {
|
|
19
|
+
verified: AdvisoryVerified[];
|
|
20
|
+
rawCandidates: number;
|
|
21
|
+
dropped: number;
|
|
22
|
+
refuted: number;
|
|
23
|
+
coverage: AdvisoryStageCoverage[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Default concrete read/inspect tools for advisory workflows. */
|
|
27
|
+
export const DEFAULT_ADVISORY_TOOLS: NonNullable<AgentOptions["tools"]> = ["read", "bash", "grep", "find", "ls"];
|
|
28
|
+
|
|
29
|
+
/** Dynamically include installed grep/find/search-like extension tools. */
|
|
30
|
+
export const DEFAULT_ADVISORY_TOOL_HINTS: NonNullable<AgentOptions["toolHints"]> = ["search"];
|
|
31
|
+
|
|
32
|
+
export function emptyAdvisoryReport<Stats extends WorkflowRunStats>(
|
|
33
|
+
summary: string,
|
|
34
|
+
nextSteps: string[],
|
|
35
|
+
stats: Stats,
|
|
36
|
+
): AdvisoryReport & { stats: Stats } {
|
|
37
|
+
return { summary, findings: [], nextSteps, stats };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function publishVerifiedKeptProgress(
|
|
41
|
+
api: Pick<WorkflowApi, "progress" | "log">,
|
|
42
|
+
verified: number,
|
|
43
|
+
kept: number,
|
|
44
|
+
): void {
|
|
45
|
+
api.progress({ type: "counter", key: "verified", label: "verified", value: verified });
|
|
46
|
+
api.progress({ type: "counter", key: "kept", label: "kept", value: kept });
|
|
47
|
+
api.progress({ type: "summary", key: "verified", value: verified });
|
|
48
|
+
api.progress({ type: "summary", key: "kept", value: kept });
|
|
49
|
+
api.log(`${verified} verified → ${kept} kept`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface LensVerificationPipelineOptions {
|
|
53
|
+
api: Pick<WorkflowApi, "agent" | "parallel" | "phase" | "progress" | "log">;
|
|
54
|
+
lenses: readonly AdvisoryLens[];
|
|
55
|
+
perLens: number;
|
|
56
|
+
finderPhase?: "Find" | "Hypothesize";
|
|
57
|
+
finderPrompt(lens: AdvisoryLens): string;
|
|
58
|
+
verifierPrompt(candidate: IdentifiedAdvisoryCandidate): string;
|
|
59
|
+
boundCandidate?(candidate: IdentifiedAdvisoryCandidate, lens: AdvisoryLens): IdentifiedAdvisoryCandidate | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AdvisoryBackfillDefaults {
|
|
63
|
+
impact: string;
|
|
64
|
+
recommendation?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
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[] = [];
|
|
73
|
+
let rawCandidates = 0;
|
|
74
|
+
let refuted = 0;
|
|
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,
|
|
81
|
+
});
|
|
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;
|
|
89
|
+
for (const candidate of candidates) {
|
|
90
|
+
api.progress({ type: "lane_item", lane: finderPhase === "Hypothesize" ? "Hypotheses" : "Candidates",
|
|
91
|
+
title: candidate.summary, subtitle: formatLocation(candidate), status: "pending", details: candidate.impact });
|
|
92
|
+
}
|
|
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)`);
|
|
100
|
+
}
|
|
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 };
|
|
112
|
+
},
|
|
113
|
+
})), coverage);
|
|
114
|
+
return { verified, rawCandidates, dropped, refuted, coverage };
|
|
115
|
+
}
|
|
116
|
+
|
|
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);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function finishAdvisoryReport<T extends AdvisoryReport>(
|
|
135
|
+
report: T, coverage: AdvisoryStageCoverage[], verification: AdvisoryVerified[] = [],
|
|
136
|
+
) {
|
|
137
|
+
return { ...withAdvisoryCoverage(report, coverage), verification };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function primaryLocation(candidate: Pick<AdvisoryCandidate, "locations">): AdvisoryLocation {
|
|
141
|
+
return candidate.locations[0] ?? { file: "" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function formatLocation(candidate: Pick<AdvisoryCandidate, "locations">): string {
|
|
145
|
+
const location = primaryLocation(candidate);
|
|
146
|
+
const line = location.line != null ? `:${location.line}` : "";
|
|
147
|
+
const symbol = location.symbol ? ` (${location.symbol})` : "";
|
|
148
|
+
return `${location.file}${line}${symbol}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function formatEvidence(evidence: readonly string[]): string {
|
|
152
|
+
return evidence.join("; ");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function normalizePath(path: string): string {
|
|
156
|
+
return path.replace(/^\.\//, "").replace(/^[ab]\//, "");
|
|
157
|
+
}
|
|
158
|
+
|
|
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
|
+
}>;
|
|
167
|
+
|
|
168
|
+
export function verdictConfidence(verdict: AdvisoryVerdict["verdict"]): "high" | "medium" | "low" {
|
|
169
|
+
return VERDICT_PRESENTATION[verdict].confidence;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function recordVerdictProgress(
|
|
173
|
+
progress: (event: WorkflowProgressEvent) => void,
|
|
174
|
+
candidate: Pick<AdvisoryCandidate, "locations" | "summary">,
|
|
175
|
+
verdict: Pick<AdvisoryVerdict, "verdict" | "evidence">,
|
|
176
|
+
onRefuted?: () => void,
|
|
177
|
+
): void {
|
|
178
|
+
progress({ type: "counter_delta", key: `verdict.${verdict.verdict.toLowerCase()}`, label: verdict.verdict, delta: 1 });
|
|
179
|
+
if (verdict.verdict === "REFUTED") {
|
|
180
|
+
onRefuted?.();
|
|
181
|
+
progress({ type: "counter_delta", key: "refuted", label: "refuted", delta: 1 });
|
|
182
|
+
}
|
|
183
|
+
progress({
|
|
184
|
+
type: "lane_item",
|
|
185
|
+
lane: VERDICT_PRESENTATION[verdict.verdict].lane,
|
|
186
|
+
title: candidate.summary,
|
|
187
|
+
subtitle: formatLocation(candidate),
|
|
188
|
+
status: VERDICT_PRESENTATION[verdict.verdict].status,
|
|
189
|
+
details: formatEvidence(verdict.evidence),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function backfillAdvisoryFindings<Source extends AdvisoryVerified>(
|
|
194
|
+
findings: AdvisorySynthesis["findings"],
|
|
195
|
+
ranked: readonly Source[],
|
|
196
|
+
defaults: AdvisoryBackfillDefaults,
|
|
197
|
+
): AdvisoryReport["findings"] {
|
|
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
|
+
}
|
|
224
|
+
|
|
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) {
|
|
233
|
+
return {
|
|
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."],
|
|
237
|
+
};
|
|
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 };
|
|
245
|
+
}
|