solidity-argus 0.5.6 → 0.5.7
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/package.json +1 -1
- package/src/agents/pythia-prompt.ts +6 -1
- package/src/agents/scribe-prompt.ts +4 -2
- package/src/cli/commands/install.ts +74 -33
- package/src/create-hooks.ts +10 -26
- package/src/features/persistent-state/findings-materializer.ts +34 -0
- package/src/hooks/tool-tracking-hook.ts +5 -0
- package/src/tools/record-finding-tool.ts +10 -0
- package/src/tools/report-generator-tool.ts +85 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solidity-argus",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Solidity smart contract security auditing plugin for OpenCode — 5 specialized agents, 15 tools (14 core + optional Solodit), and a curated vulnerability knowledge base",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"solidity",
|
|
@@ -124,7 +124,12 @@ This ensures Pythia always delivers research value, even when Solodit has no dir
|
|
|
124
124
|
|
|
125
125
|
## SKILLS SYSTEM
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
The Argus knowledge base includes 75+ curated SKILL.md files, 13 YAML pattern packs, and 15 real-world exploit case studies covering $3B+ in losses. You load them with \`argus_skill_load\`.
|
|
128
|
+
|
|
129
|
+
**CRITICAL — use the right tool**:
|
|
130
|
+
- For ALL vulnerability, protocol, checklist, methodology, and case-study knowledge, use \`argus_skill_load\` with the exact skill name (e.g. \`argus_skill_load({ name: "reentrancy" })\`).
|
|
131
|
+
- **NEVER** call the generic OpenCode \`skill\` tool. It does not know about Argus skills like \`reentrancy\`, \`access-control\`, \`oracle-manipulation\`, etc., and will return "Skill or command not found" errors.
|
|
132
|
+
- If you are unsure whether a name is an Argus skill, default to \`argus_skill_load\` — it is the only correct loader for audit knowledge.
|
|
128
133
|
|
|
129
134
|
**How to use**:
|
|
130
135
|
- Load a relevant skill before deep research when protocol context is non-trivial.
|
|
@@ -65,10 +65,12 @@ Argus provides you with a \`run_id\`. Your job: read findings, deduplicate, enri
|
|
|
65
65
|
|
|
66
66
|
This writes the source-of-truth JSON to disk at \`.argus/runs/{run_id}/deduped-findings.json\`.
|
|
67
67
|
|
|
68
|
-
5. **Generate report**: Call \`argus_generate_report\` with:
|
|
68
|
+
5. **Generate report**: Call \`argus_generate_report\` with EXACTLY these arguments (and nothing else):
|
|
69
69
|
- \`project_name\`: the project name
|
|
70
70
|
- \`scope\`: list of audited files
|
|
71
|
-
- \`run_id\`: the run ID (the tool reads your persisted deduped findings from disk)
|
|
71
|
+
- \`run_id\`: the run ID (the tool reads your persisted deduped findings from disk and resolves the canonical envelope automatically)
|
|
72
|
+
|
|
73
|
+
**DO NOT** pass \`report_input\`, \`findings\`, \`toolsExecuted\`, \`session_id\`, or any other field — the tool reads them from durable state on disk. Passing them risks contract-mismatch failures.
|
|
72
74
|
|
|
73
75
|
6. **Limitations disclosure**: If any tool failed or was absent, add a \`## Limitations\` section.
|
|
74
76
|
|
|
@@ -1,57 +1,98 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
2
|
import { homedir } from "node:os"
|
|
3
|
-
import { join } from "node:path"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
4
|
import { cliOutput } from "../cli-output"
|
|
5
|
+
import { confirm } from "../tui-prompts"
|
|
5
6
|
import type { CliCommand } from "../types"
|
|
6
7
|
|
|
7
8
|
const GREEN = "\x1b[32m"
|
|
8
9
|
const YELLOW = "\x1b[33m"
|
|
9
10
|
const RESET = "\x1b[0m"
|
|
10
11
|
|
|
12
|
+
function resolveHome(homeOverride?: string): string {
|
|
13
|
+
if (homeOverride && homeOverride.length > 0) return homeOverride
|
|
14
|
+
const envHome = process.env.HOME ?? process.env.USERPROFILE
|
|
15
|
+
if (envHome && envHome.length > 0) return envHome
|
|
16
|
+
return homedir()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function localConfigPath(): string {
|
|
20
|
+
return join(process.cwd(), "opencode.json")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function globalConfigPath(homeOverride?: string): string {
|
|
24
|
+
return join(resolveHome(homeOverride), ".config", "opencode", "opencode.json")
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
export function findOpencodeConfig(homeOverride?: string): string | null {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
if (existsSync(localPath)) return localPath
|
|
28
|
+
const local = localConfigPath()
|
|
29
|
+
if (existsSync(local)) return local
|
|
15
30
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (existsSync(globalPath)) return globalPath
|
|
31
|
+
const global = globalConfigPath(homeOverride)
|
|
32
|
+
if (existsSync(global)) return global
|
|
19
33
|
|
|
20
34
|
return null
|
|
21
35
|
}
|
|
22
36
|
|
|
37
|
+
function addPluginToConfig(configPath: string): { added: boolean; ok: boolean } {
|
|
38
|
+
try {
|
|
39
|
+
let config: Record<string, unknown>
|
|
40
|
+
if (existsSync(configPath)) {
|
|
41
|
+
const content = readFileSync(configPath, "utf-8")
|
|
42
|
+
config = JSON.parse(content)
|
|
43
|
+
} else {
|
|
44
|
+
mkdirSync(dirname(configPath), { recursive: true })
|
|
45
|
+
config = {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const plugins = Array.isArray(config.plugin) ? (config.plugin as string[]) : []
|
|
49
|
+
if (plugins.includes("solidity-argus")) {
|
|
50
|
+
cliOutput.log(`${GREEN}✓${RESET} solidity-argus already registered in ${configPath}`)
|
|
51
|
+
return { added: false, ok: true }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
plugins.push("solidity-argus")
|
|
55
|
+
config.plugin = plugins
|
|
56
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
|
|
57
|
+
cliOutput.log(`${GREEN}✓${RESET} Added solidity-argus to ${configPath}`)
|
|
58
|
+
return { added: true, ok: true }
|
|
59
|
+
} catch (_error) {
|
|
60
|
+
cliOutput.error(`${YELLOW}⚠${RESET} Failed to update ${configPath}`)
|
|
61
|
+
return { added: false, ok: false }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
23
65
|
export const installCommand: CliCommand = {
|
|
24
66
|
name: "install",
|
|
25
|
-
description: "Register solidity-argus in your OpenCode config",
|
|
26
|
-
async execute(
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
if (!configPath) {
|
|
30
|
-
cliOutput.error(
|
|
31
|
-
`${YELLOW}⚠${RESET} opencode.json not found — create one first, or run: opencode init`,
|
|
32
|
-
)
|
|
33
|
-
return 1
|
|
34
|
-
}
|
|
67
|
+
description: "Register solidity-argus in your OpenCode config (use --global for ~/.config/opencode)",
|
|
68
|
+
async execute(args: string[]): Promise<number> {
|
|
69
|
+
const isGlobal = args.includes("--global") || args.includes("-g")
|
|
70
|
+
const local = localConfigPath()
|
|
35
71
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const plugins: string[] = config.plugin ?? []
|
|
72
|
+
if (existsSync(local) && !isGlobal) {
|
|
73
|
+
return addPluginToConfig(local).ok ? 0 : 1
|
|
74
|
+
}
|
|
40
75
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
76
|
+
if (isGlobal) {
|
|
77
|
+
return addPluginToConfig(globalConfigPath()).ok ? 0 : 1
|
|
78
|
+
}
|
|
45
79
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
const global = globalConfigPath()
|
|
81
|
+
cliOutput.warn(
|
|
82
|
+
`${YELLOW}⚠${RESET} No opencode.json found in current directory (${process.cwd()}).`,
|
|
83
|
+
)
|
|
84
|
+
cliOutput.warn(
|
|
85
|
+
` Installing globally would write to ${global} and load solidity-argus in EVERY OpenCode session.`,
|
|
86
|
+
)
|
|
87
|
+
cliOutput.warn(` To install globally on purpose, re-run with: argus install --global`)
|
|
88
|
+
cliOutput.warn(` To install for this project, first create an opencode.json in this directory.`)
|
|
49
89
|
|
|
50
|
-
|
|
90
|
+
const proceed = await confirm("Install globally anyway?", false)
|
|
91
|
+
if (!proceed) {
|
|
92
|
+
cliOutput.log("Aborted. No changes made.")
|
|
51
93
|
return 0
|
|
52
|
-
} catch (_error) {
|
|
53
|
-
cliOutput.error(`${YELLOW}⚠${RESET} Failed to update ${configPath}`)
|
|
54
|
-
return 1
|
|
55
94
|
}
|
|
95
|
+
|
|
96
|
+
return addPluginToConfig(global).ok ? 0 : 1
|
|
56
97
|
},
|
|
57
98
|
}
|
package/src/create-hooks.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./features/persistent-state/event-sink"
|
|
16
16
|
import {
|
|
17
17
|
materializeFindings,
|
|
18
|
+
materializeFindingsForRun,
|
|
18
19
|
materializeReportInput,
|
|
19
20
|
} from "./features/persistent-state/findings-materializer"
|
|
20
21
|
import { recordRun, updateRunStatus } from "./features/persistent-state/global-run-index"
|
|
@@ -874,34 +875,17 @@ export function createHooks(args: {
|
|
|
874
875
|
)
|
|
875
876
|
: undefined
|
|
876
877
|
|
|
877
|
-
const
|
|
878
|
+
const runMaterializeFindings = (
|
|
878
879
|
runId: string,
|
|
879
880
|
projectDirForRun: string,
|
|
880
881
|
sessionIdForRun: string | undefined,
|
|
881
882
|
trigger: "session.idle" | "session.deleted" | "tool.execute.after",
|
|
882
883
|
failFast = false,
|
|
883
|
-
): Promise<void> =>
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
try {
|
|
889
|
-
await materializeFindings(runId, projectDirForRun, sessionIdForRun, {
|
|
890
|
-
validateSessionId: false,
|
|
891
|
-
requireEvents: true,
|
|
892
|
-
})
|
|
893
|
-
} catch (error) {
|
|
894
|
-
if (failFast) {
|
|
895
|
-
throw new Error(
|
|
896
|
-
`Failed to materialize findings artifact on ${trigger} for run ${runId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
897
|
-
)
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
logger.warn(
|
|
901
|
-
`Failed to materialize findings artifact on ${trigger} for run ${runId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
902
|
-
)
|
|
903
|
-
}
|
|
904
|
-
}
|
|
884
|
+
): Promise<void> =>
|
|
885
|
+
materializeFindingsForRun(runId, projectDirForRun, sessionIdForRun, trigger, {
|
|
886
|
+
failFast,
|
|
887
|
+
warn: (msg) => logger.warn(msg),
|
|
888
|
+
})
|
|
905
889
|
|
|
906
890
|
const safeEventHook = isHookEnabled("event")
|
|
907
891
|
? safeCreateHook(
|
|
@@ -920,7 +904,7 @@ export function createHooks(args: {
|
|
|
920
904
|
|
|
921
905
|
if (hasNewFinalization && finalizationResult.runId.length > 0) {
|
|
922
906
|
try {
|
|
923
|
-
await
|
|
907
|
+
await runMaterializeFindings(
|
|
924
908
|
finalizationResult.runId,
|
|
925
909
|
projectDir,
|
|
926
910
|
eventSessionId,
|
|
@@ -1093,12 +1077,12 @@ export function createHooks(args: {
|
|
|
1093
1077
|
)
|
|
1094
1078
|
}
|
|
1095
1079
|
|
|
1096
|
-
await
|
|
1080
|
+
await runMaterializeFindings(
|
|
1097
1081
|
state.sessionId,
|
|
1098
1082
|
state.projectDir,
|
|
1099
1083
|
input.sessionID,
|
|
1100
1084
|
"tool.execute.after",
|
|
1101
|
-
|
|
1085
|
+
false,
|
|
1102
1086
|
)
|
|
1103
1087
|
|
|
1104
1088
|
try {
|
|
@@ -12,6 +12,16 @@ import type { CanonicalFinding, CanonicalToolExecution, ReportInput } from "../.
|
|
|
12
12
|
import { SCHEMA_VERSION } from "../../state/schemas"
|
|
13
13
|
import { readEvents } from "./event-sink"
|
|
14
14
|
|
|
15
|
+
export type MaterializeFindingsTrigger =
|
|
16
|
+
| "session.idle"
|
|
17
|
+
| "session.deleted"
|
|
18
|
+
| "tool.execute.after"
|
|
19
|
+
|
|
20
|
+
export interface MaterializeFindingsForRunOptions {
|
|
21
|
+
failFast?: boolean
|
|
22
|
+
warn?: (message: string) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
export interface FindingsArtifact {
|
|
16
26
|
run_id: string
|
|
17
27
|
session_id: string
|
|
@@ -78,6 +88,30 @@ export async function materializeFindings(
|
|
|
78
88
|
return artifact
|
|
79
89
|
}
|
|
80
90
|
|
|
91
|
+
export async function materializeFindingsForRun(
|
|
92
|
+
runId: string,
|
|
93
|
+
projectDir: string,
|
|
94
|
+
sessionId: string | undefined,
|
|
95
|
+
trigger: MaterializeFindingsTrigger,
|
|
96
|
+
options: MaterializeFindingsForRunOptions = {},
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
if (!runId || runId.length === 0) return
|
|
99
|
+
|
|
100
|
+
const { failFast = false, warn } = options
|
|
101
|
+
try {
|
|
102
|
+
await materializeFindings(runId, projectDir, sessionId, {
|
|
103
|
+
validateSessionId: false,
|
|
104
|
+
requireEvents: true,
|
|
105
|
+
})
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const message = `Failed to materialize findings artifact on ${trigger} for run ${runId}: ${error instanceof Error ? error.message : String(error)}`
|
|
108
|
+
if (failFast) {
|
|
109
|
+
throw new Error(message)
|
|
110
|
+
}
|
|
111
|
+
warn?.(message)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
81
115
|
export async function materializeReportInput(
|
|
82
116
|
runId: string,
|
|
83
117
|
projectDir: string,
|
|
@@ -348,6 +348,11 @@ function processToolResult(
|
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
if (config.extractOptionalFields) {
|
|
351
|
+
findingPayload.impact = typeof item.impact === "string" ? item.impact : undefined
|
|
352
|
+
findingPayload.recommendation =
|
|
353
|
+
typeof item.recommendation === "string" ? item.recommendation : undefined
|
|
354
|
+
findingPayload.proofOfConcept =
|
|
355
|
+
typeof item.proofOfConcept === "string" ? item.proofOfConcept : undefined
|
|
351
356
|
findingPayload.remediation =
|
|
352
357
|
typeof item.remediation === "string" ? item.remediation : undefined
|
|
353
358
|
findingPayload.exploitReference =
|
|
@@ -16,10 +16,15 @@ type RecordFindingResponse = {
|
|
|
16
16
|
id: string
|
|
17
17
|
check: string
|
|
18
18
|
severity: string
|
|
19
|
+
confidence: string
|
|
19
20
|
file: string
|
|
20
21
|
description: string
|
|
21
22
|
lines: [number, number]
|
|
22
23
|
source: string
|
|
24
|
+
reported_by_agent: string
|
|
25
|
+
impact?: string
|
|
26
|
+
recommendation?: string
|
|
27
|
+
proofOfConcept?: string
|
|
23
28
|
}>
|
|
24
29
|
schema_version: string
|
|
25
30
|
note: string
|
|
@@ -178,10 +183,15 @@ export async function executeRecordFinding(
|
|
|
178
183
|
id: f.id,
|
|
179
184
|
check: f.check,
|
|
180
185
|
severity: f.severity,
|
|
186
|
+
confidence: f.confidence,
|
|
181
187
|
file: f.file,
|
|
182
188
|
description: f.description,
|
|
183
189
|
lines: f.lines,
|
|
184
190
|
source: f.source,
|
|
191
|
+
reported_by_agent: f.reported_by_agent,
|
|
192
|
+
...(f.impact !== undefined ? { impact: f.impact } : {}),
|
|
193
|
+
...(f.recommendation !== undefined ? { recommendation: f.recommendation } : {}),
|
|
194
|
+
...(f.proofOfConcept !== undefined ? { proofOfConcept: f.proofOfConcept } : {}),
|
|
185
195
|
})),
|
|
186
196
|
schema_version: SCHEMA_VERSION,
|
|
187
197
|
note: "Findings recorded to event journal. The system assigns the canonical run_id automatically — use the run_id from <argus-context> for Scribe dispatch.",
|
|
@@ -14,13 +14,14 @@ import { resolveProjectDir } from "../shared/project-utils"
|
|
|
14
14
|
import { resolveReportPath } from "../shared/report-path-resolver"
|
|
15
15
|
import { isNonEmptyString } from "../shared/type-guards"
|
|
16
16
|
import { SEVERITY_RANK } from "../shared/validation-constants"
|
|
17
|
+
import { normalizeToCanonicalFinding } from "../state/adapters"
|
|
17
18
|
import {
|
|
18
19
|
compareIssueFingerprintSets,
|
|
19
20
|
dedupeFindingsForFinalOutput,
|
|
20
21
|
} from "../state/finding-aggregation"
|
|
21
22
|
import { projectFindings, stableHash } from "../state/projectors"
|
|
22
23
|
import { type ReportInput, SCHEMA_VERSION, validateReportInput } from "../state/schemas"
|
|
23
|
-
import type { AuditState, Finding, FindingSeverity } from "../state/types"
|
|
24
|
+
import type { ArgusAgentName, AuditState, Finding, FindingSeverity } from "../state/types"
|
|
24
25
|
import { checkReportPreflight } from "./report-preflight"
|
|
25
26
|
|
|
26
27
|
type SeverityThreshold = "critical" | "high" | "medium" | "low" | "informational"
|
|
@@ -304,6 +305,37 @@ type ParseReportInputResult = {
|
|
|
304
305
|
diagnostics: DropDiagnostic[]
|
|
305
306
|
}
|
|
306
307
|
|
|
308
|
+
const VALID_AGENT_VALUES = new Set<ArgusAgentName>([
|
|
309
|
+
"argus",
|
|
310
|
+
"sentinel",
|
|
311
|
+
"pythia",
|
|
312
|
+
"scribe",
|
|
313
|
+
"unknown",
|
|
314
|
+
])
|
|
315
|
+
|
|
316
|
+
function normalizeDedupedFindings(
|
|
317
|
+
rawFindings: unknown[],
|
|
318
|
+
runId: string,
|
|
319
|
+
projectDir: string,
|
|
320
|
+
dedupedBy: string,
|
|
321
|
+
): Record<string, unknown>[] {
|
|
322
|
+
const reportedByAgent: ArgusAgentName = VALID_AGENT_VALUES.has(dedupedBy as ArgusAgentName)
|
|
323
|
+
? (dedupedBy as ArgusAgentName)
|
|
324
|
+
: "scribe"
|
|
325
|
+
return rawFindings.map((raw, index) => {
|
|
326
|
+
const input = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {}
|
|
327
|
+
const normalized = normalizeRawFinding(input)
|
|
328
|
+
const result = normalizeToCanonicalFinding(
|
|
329
|
+
normalized,
|
|
330
|
+
runId,
|
|
331
|
+
index + 1,
|
|
332
|
+
{ reportedByAgent },
|
|
333
|
+
projectDir,
|
|
334
|
+
)
|
|
335
|
+
return result.data as unknown as Record<string, unknown>
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
307
339
|
function diagnosticsSummary(diagnostics: DropDiagnostic[]): string {
|
|
308
340
|
return diagnostics.map((diag) => `${diag.reason.code}:${diag.reason.message}`).join("; ")
|
|
309
341
|
}
|
|
@@ -576,6 +608,7 @@ function parseReportInputPayload(
|
|
|
576
608
|
try {
|
|
577
609
|
const dedupedArtifact = JSON.parse(readFileSync(dedupedFile, "utf-8")) as {
|
|
578
610
|
findings?: unknown[]
|
|
611
|
+
deduped_by?: string
|
|
579
612
|
}
|
|
580
613
|
if (Array.isArray(dedupedArtifact.findings) && dedupedArtifact.findings.length > 0) {
|
|
581
614
|
const reportInputFile = resolver.paths().reportInputFile
|
|
@@ -590,15 +623,64 @@ function parseReportInputPayload(
|
|
|
590
623
|
/* use empty base */
|
|
591
624
|
}
|
|
592
625
|
}
|
|
593
|
-
const
|
|
626
|
+
const normalizedFindings = normalizeDedupedFindings(
|
|
627
|
+
dedupedArtifact.findings,
|
|
628
|
+
effectiveRunId,
|
|
629
|
+
projectDir,
|
|
630
|
+
typeof dedupedArtifact.deduped_by === "string"
|
|
631
|
+
? dedupedArtifact.deduped_by
|
|
632
|
+
: "scribe",
|
|
633
|
+
)
|
|
634
|
+
const merged: Record<string, unknown> = {
|
|
594
635
|
...baseInput,
|
|
595
636
|
run_id: effectiveRunId,
|
|
596
|
-
findings:
|
|
637
|
+
findings: normalizedFindings,
|
|
638
|
+
}
|
|
639
|
+
normalizeToolsExecutedDefaults(merged, effectiveRunId, diagnostics)
|
|
640
|
+
if (typeof merged.seq !== "number" || (merged.seq as number) < 0) {
|
|
641
|
+
merged.seq = 0
|
|
642
|
+
}
|
|
643
|
+
if (typeof merged.session_id !== "string" || (merged.session_id as string).length === 0) {
|
|
644
|
+
merged.session_id = "unknown"
|
|
645
|
+
}
|
|
646
|
+
if (
|
|
647
|
+
typeof merged.tool_call_id !== "string" ||
|
|
648
|
+
(merged.tool_call_id as string).length === 0
|
|
649
|
+
) {
|
|
650
|
+
merged.tool_call_id = `deduped:${effectiveRunId}`
|
|
651
|
+
}
|
|
652
|
+
if (typeof merged.source !== "string" || (merged.source as string).length === 0) {
|
|
653
|
+
merged.source = "deduped-findings"
|
|
654
|
+
}
|
|
655
|
+
if (
|
|
656
|
+
typeof merged.schema_version !== "string" ||
|
|
657
|
+
merged.schema_version !== SCHEMA_VERSION
|
|
658
|
+
) {
|
|
659
|
+
merged.schema_version = SCHEMA_VERSION
|
|
660
|
+
}
|
|
661
|
+
if (
|
|
662
|
+
typeof merged.projectDir !== "string" ||
|
|
663
|
+
(merged.projectDir as string).length === 0
|
|
664
|
+
) {
|
|
665
|
+
merged.projectDir = projectDir
|
|
666
|
+
}
|
|
667
|
+
if (!Array.isArray(merged.scope)) {
|
|
668
|
+
merged.scope = []
|
|
669
|
+
}
|
|
670
|
+
if (!Array.isArray(merged.toolsExecuted)) {
|
|
671
|
+
merged.toolsExecuted = []
|
|
597
672
|
}
|
|
598
673
|
const validation = validateReportInput(merged)
|
|
599
674
|
if (validation.success) {
|
|
600
675
|
return finalizeReportInputSelection(validation.data, diagnostics, expectedRunId)
|
|
601
676
|
}
|
|
677
|
+
for (const error of validation.errors) {
|
|
678
|
+
diagnostics.warn(
|
|
679
|
+
"REPORT_INPUT_DEDUPED_VALIDATION_FAILED",
|
|
680
|
+
`${error.field}: ${error.message}`,
|
|
681
|
+
error.field,
|
|
682
|
+
)
|
|
683
|
+
}
|
|
602
684
|
}
|
|
603
685
|
} catch {
|
|
604
686
|
/* deduped file unreadable — fall through to report-input.json */
|