solidity-argus 0.3.7 → 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/AGENTS.md +13 -6
- package/README.md +24 -12
- package/package.json +7 -3
- package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +1 -0
- package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +1 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +1 -0
- package/skills/checklists/cyfrin-defi-integrations/SKILL.md +1 -0
- package/skills/checklists/cyfrin-gas/SKILL.md +1 -0
- package/skills/checklists/general-audit/SKILL.md +1 -0
- package/skills/methodology/audit-workflow/SKILL.md +1 -0
- package/skills/methodology/report-template/SKILL.md +1 -0
- package/skills/methodology/severity-classification/SKILL.md +1 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +1 -0
- package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +1 -0
- package/skills/protocol-patterns/dao-governance/SKILL.md +1 -0
- package/skills/protocol-patterns/lending-borrowing/SKILL.md +1 -0
- package/skills/protocol-patterns/staking-vesting/SKILL.md +1 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +0 -50
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +0 -63
- package/src/agents/argus-prompt.ts +98 -33
- package/src/agents/pythia-prompt.ts +24 -2
- package/src/agents/scribe-prompt.ts +34 -10
- package/src/agents/sentinel-prompt.ts +19 -0
- package/src/agents/themis-prompt.ts +110 -0
- package/src/cli/commands/doctor.ts +29 -17
- package/src/cli/commands/install.ts +74 -33
- package/src/config/loader.ts +29 -5
- package/src/config/schema.ts +45 -45
- package/src/constants/defaults.ts +1 -0
- package/src/create-hooks.ts +806 -173
- package/src/create-managers.ts +4 -2
- package/src/create-tools.ts +5 -1
- package/src/features/audit-enforcer/audit-enforcer.ts +1 -11
- package/src/features/background-agent/background-manager.ts +32 -5
- package/src/features/error-recovery/tool-error-recovery.ts +1 -0
- package/src/features/persistent-state/audit-state-manager.ts +272 -29
- package/src/features/persistent-state/event-sink.ts +96 -25
- package/src/features/persistent-state/findings-materializer.ts +68 -2
- package/src/features/persistent-state/global-run-index.ts +86 -8
- package/src/features/persistent-state/index.ts +7 -1
- package/src/features/persistent-state/run-finalizer.ts +116 -7
- package/src/features/persistent-state/run-pruner.ts +93 -0
- package/src/hooks/agent-tracker.ts +14 -2
- package/src/hooks/compaction-hook.ts +7 -16
- package/src/hooks/config-handler.ts +83 -29
- package/src/hooks/context-budget.ts +4 -5
- package/src/hooks/event-hook.ts +213 -57
- package/src/hooks/knowledge-sync-hook.ts +2 -3
- package/src/hooks/safe-create-hook.ts +13 -1
- package/src/hooks/system-prompt-hook.ts +20 -39
- package/src/hooks/tool-tracking-hook.ts +602 -323
- package/src/index.ts +15 -1
- package/src/knowledge/scvd-client.ts +2 -4
- package/src/knowledge/scvd-errors.ts +25 -2
- package/src/knowledge/scvd-index.ts +7 -5
- package/src/knowledge/scvd-sync.ts +6 -6
- package/src/managers/types.ts +20 -2
- package/src/shared/agent-names.ts +23 -0
- package/src/shared/audit-artifact-resolver.ts +8 -3
- package/src/shared/audit-phases.ts +12 -0
- package/src/shared/cache-paths.ts +41 -0
- package/src/shared/drop-diagnostics.ts +2 -2
- package/src/shared/forge-errors.ts +31 -0
- package/src/shared/forge-runner.ts +30 -0
- package/src/shared/format-error.ts +3 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/key-tools.ts +39 -0
- package/src/shared/logger.ts +7 -7
- package/src/shared/path-containment.ts +25 -0
- package/src/shared/path-utils.ts +11 -0
- package/src/shared/report-path-resolver.ts +4 -2
- package/src/shared/safe-emit.ts +24 -0
- package/src/shared/token-utils.ts +5 -0
- package/src/shared/type-guards.ts +8 -0
- package/src/shared/validation-constants.ts +52 -0
- package/src/skills/analysis/cluster.ts +1 -114
- package/src/skills/analysis/normalize.ts +2 -114
- package/src/skills/analysis/stopwords.ts +109 -0
- package/src/skills/argus-skill-resolver.ts +6 -3
- package/src/solodit-lifecycle.ts +153 -37
- package/src/state/adapters.ts +60 -66
- package/src/state/finding-aggregation.ts +6 -8
- package/src/state/finding-fingerprint.ts +1 -1
- package/src/state/finding-store.ts +31 -9
- package/src/state/index.ts +1 -1
- package/src/state/projectors.ts +27 -19
- package/src/state/schemas.ts +8 -32
- package/src/state/types.ts +3 -0
- package/src/tools/contract-analyzer-tool.ts +4 -6
- package/src/tools/forge-coverage-tool.ts +10 -35
- package/src/tools/forge-fuzz-tool.ts +21 -51
- package/src/tools/forge-test-tool.ts +25 -47
- package/src/tools/gas-analysis-tool.ts +12 -41
- package/src/tools/pattern-checker-tool.ts +37 -15
- package/src/tools/pattern-loader.ts +18 -4
- package/src/tools/persist-deduped-tool.ts +94 -0
- package/src/tools/proxy-detection-tool.ts +35 -34
- package/src/tools/read-findings-tool.ts +390 -0
- package/src/tools/record-finding-tool.ts +130 -25
- package/src/tools/report-generator-tool.ts +475 -327
- package/src/tools/report-preflight.ts +5 -1
- package/src/tools/slither-tool.ts +55 -16
- package/src/tools/solodit-search-tool.ts +260 -112
- package/src/tools/sync-knowledge-tool.ts +2 -3
- package/src/utils/solidity-parser.ts +39 -24
- package/src/features/migration/index.ts +0 -14
- package/src/features/migration/migration-adapter.ts +0 -151
- package/src/features/migration/parity-telemetry.ts +0 -133
|
@@ -140,6 +140,36 @@ export function parseExternalCalls(sourceText: string): string[] {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
async function spawnForgeInspect(
|
|
144
|
+
contractName: string,
|
|
145
|
+
inspectType: string,
|
|
146
|
+
cwd: string,
|
|
147
|
+
): Promise<{ success: boolean; stdout: string; stderr: string }> {
|
|
148
|
+
const proc = Bun.spawn(["forge", "inspect", contractName, inspectType, "--json"], {
|
|
149
|
+
cwd,
|
|
150
|
+
stdout: "pipe",
|
|
151
|
+
stderr: "pipe",
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const timeout = 15_000
|
|
155
|
+
let timerId: ReturnType<typeof setTimeout> | undefined
|
|
156
|
+
const timer = new Promise<never>((_, reject) => {
|
|
157
|
+
timerId = setTimeout(() => {
|
|
158
|
+
proc.kill()
|
|
159
|
+
reject(new Error(`forge inspect ${inspectType} timed out after ${timeout}ms`))
|
|
160
|
+
}, timeout)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const exitCode = await Promise.race([proc.exited, timer])
|
|
165
|
+
const stdout = await new Response(proc.stdout).text()
|
|
166
|
+
const stderr = await new Response(proc.stderr).text()
|
|
167
|
+
return { success: exitCode === 0, stdout, stderr }
|
|
168
|
+
} finally {
|
|
169
|
+
if (timerId !== undefined) clearTimeout(timerId)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
143
173
|
/**
|
|
144
174
|
* Extract contract information using forge inspect
|
|
145
175
|
* Runs forge inspect <contractName> abi and storage-layout
|
|
@@ -162,39 +192,24 @@ export async function extractContractInfo(
|
|
|
162
192
|
}
|
|
163
193
|
|
|
164
194
|
try {
|
|
165
|
-
// Run forge inspect
|
|
166
|
-
const abiResult =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
timeout: 15_000,
|
|
171
|
-
})
|
|
195
|
+
// Run both forge inspect commands in parallel (async, non-blocking)
|
|
196
|
+
const [abiResult, storageResult] = await Promise.all([
|
|
197
|
+
spawnForgeInspect(contractName, "abi", projectDir),
|
|
198
|
+
spawnForgeInspect(contractName, "storage-layout", projectDir),
|
|
199
|
+
])
|
|
172
200
|
|
|
173
201
|
if (!abiResult.success) {
|
|
174
|
-
|
|
175
|
-
result.error = `Failed to inspect ABI: ${errorMsg}`
|
|
202
|
+
result.error = `Failed to inspect ABI: ${abiResult.stderr}`
|
|
176
203
|
return result
|
|
177
204
|
}
|
|
178
205
|
|
|
179
|
-
// Run forge inspect storage-layout
|
|
180
|
-
const storageResult = Bun.spawnSync(
|
|
181
|
-
["forge", "inspect", contractName, "storage-layout", "--json"],
|
|
182
|
-
{
|
|
183
|
-
cwd: projectDir,
|
|
184
|
-
stdout: "pipe",
|
|
185
|
-
stderr: "pipe",
|
|
186
|
-
timeout: 15_000,
|
|
187
|
-
},
|
|
188
|
-
)
|
|
189
|
-
|
|
190
206
|
if (!storageResult.success) {
|
|
191
|
-
|
|
192
|
-
result.error = `Failed to inspect storage layout: ${errorMsg}`
|
|
207
|
+
result.error = `Failed to inspect storage layout: ${storageResult.stderr}`
|
|
193
208
|
return result
|
|
194
209
|
}
|
|
195
210
|
|
|
196
211
|
// Parse ABI
|
|
197
|
-
const abiRaw = abiResult.stdout
|
|
212
|
+
const abiRaw = abiResult.stdout || "[]"
|
|
198
213
|
const abiOutput = extractJson(abiRaw, "[")
|
|
199
214
|
let abi: ABIFunction[] = []
|
|
200
215
|
try {
|
|
@@ -205,7 +220,7 @@ export async function extractContractInfo(
|
|
|
205
220
|
}
|
|
206
221
|
|
|
207
222
|
// Parse storage layout
|
|
208
|
-
const storageRaw = storageResult.stdout
|
|
223
|
+
const storageRaw = storageResult.stdout || "{}"
|
|
209
224
|
const storageOutput = extractJson(storageRaw, "{")
|
|
210
225
|
let storageLayout: StorageLayout = { storage: [], types: {} }
|
|
211
226
|
try {
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
adaptLegacyFindings,
|
|
3
|
-
adaptLegacyStateToReportInput,
|
|
4
|
-
getMigrationMode,
|
|
5
|
-
type MigrationMode,
|
|
6
|
-
validateStrictCompatibility,
|
|
7
|
-
} from "./migration-adapter"
|
|
8
|
-
|
|
9
|
-
export {
|
|
10
|
-
computeParityMetrics,
|
|
11
|
-
formatParityReport,
|
|
12
|
-
type ParityMetrics,
|
|
13
|
-
type SeverityDistribution,
|
|
14
|
-
} from "./parity-telemetry"
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createDropDiagnosticsCollector,
|
|
3
|
-
type DropDiagnosticsCollector,
|
|
4
|
-
} from "../../shared/drop-diagnostics"
|
|
5
|
-
import { normalizeLegacyFindingsArray, normalizeToCanonicalFinding } from "../../state/adapters"
|
|
6
|
-
import type { CanonicalFinding, ReportInput } from "../../state/schemas"
|
|
7
|
-
import { SCHEMA_VERSION } from "../../state/schemas"
|
|
8
|
-
import type { AuditState, Finding } from "../../state/types"
|
|
9
|
-
|
|
10
|
-
export type MigrationMode = "legacy" | "dual" | "strict"
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Returns the active migration mode from config, defaulting to "legacy".
|
|
14
|
-
*/
|
|
15
|
-
export function getMigrationMode(config: { migration?: { mode?: MigrationMode } }): MigrationMode {
|
|
16
|
-
return config.migration?.mode ?? "legacy"
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Adapts a legacy `AuditState` into canonical `CanonicalFinding[]`.
|
|
21
|
-
*
|
|
22
|
-
* In legacy mode: returns the raw findings as-is (backward compatible).
|
|
23
|
-
* In dual mode: normalizes findings to canonical AND returns both.
|
|
24
|
-
* In strict mode: normalizes to canonical, rejects payloads missing required canonical fields.
|
|
25
|
-
*/
|
|
26
|
-
export function adaptLegacyFindings(
|
|
27
|
-
state: AuditState,
|
|
28
|
-
mode: MigrationMode,
|
|
29
|
-
runId: string,
|
|
30
|
-
): {
|
|
31
|
-
legacyFindings: Finding[]
|
|
32
|
-
canonicalFindings: CanonicalFinding[]
|
|
33
|
-
diagnostics: ReturnType<DropDiagnosticsCollector["getDiagnostics"]>
|
|
34
|
-
} {
|
|
35
|
-
const legacyFindings = state.findings
|
|
36
|
-
|
|
37
|
-
if (mode === "legacy") {
|
|
38
|
-
return {
|
|
39
|
-
legacyFindings,
|
|
40
|
-
canonicalFindings: [],
|
|
41
|
-
diagnostics: [],
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const policy = mode === "strict" ? "strict-fail" : "warn"
|
|
46
|
-
const diag = createDropDiagnosticsCollector(policy, "migration-adapter")
|
|
47
|
-
|
|
48
|
-
const { findings: canonicalFindings, diagnostics: adapterDiags } = normalizeLegacyFindingsArray(
|
|
49
|
-
legacyFindings as unknown as unknown[],
|
|
50
|
-
runId,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
for (const d of adapterDiags) {
|
|
54
|
-
if (d.level === "error") {
|
|
55
|
-
diag.error(d.code, d.message, d.field)
|
|
56
|
-
} else {
|
|
57
|
-
diag.warn(d.code, d.message, d.field)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// In strict mode, validate that all legacy findings survived normalization
|
|
62
|
-
if (mode === "strict" && canonicalFindings.length < legacyFindings.length) {
|
|
63
|
-
const dropped = legacyFindings.length - canonicalFindings.length
|
|
64
|
-
diag.error(
|
|
65
|
-
"STRICT_FINDINGS_DROPPED",
|
|
66
|
-
`${dropped} legacy finding(s) could not be normalized to canonical format`,
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Throws DropDiagnosticsError in strict mode if errors exist
|
|
71
|
-
diag.throwIfStrict()
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
legacyFindings,
|
|
75
|
-
canonicalFindings,
|
|
76
|
-
diagnostics: diag.getDiagnostics(),
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Adapts a legacy `AuditState` into a canonical `ReportInput`.
|
|
82
|
-
*
|
|
83
|
-
* Maps legacy AuditState fields to the canonical ReportInput contract.
|
|
84
|
-
*/
|
|
85
|
-
export function adaptLegacyStateToReportInput(
|
|
86
|
-
state: AuditState,
|
|
87
|
-
mode: MigrationMode,
|
|
88
|
-
runId: string,
|
|
89
|
-
): {
|
|
90
|
-
reportInput: ReportInput
|
|
91
|
-
diagnostics: ReturnType<DropDiagnosticsCollector["getDiagnostics"]>
|
|
92
|
-
} {
|
|
93
|
-
const { canonicalFindings, diagnostics } = adaptLegacyFindings(
|
|
94
|
-
state,
|
|
95
|
-
mode === "legacy" ? "dual" : mode,
|
|
96
|
-
runId,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
const reportInput: ReportInput = {
|
|
100
|
-
run_id: runId,
|
|
101
|
-
seq: 0,
|
|
102
|
-
session_id: state.sessionId,
|
|
103
|
-
tool_call_id: "",
|
|
104
|
-
source: "migration-adapter",
|
|
105
|
-
schema_version: SCHEMA_VERSION,
|
|
106
|
-
projectDir: state.projectDir,
|
|
107
|
-
findings: canonicalFindings,
|
|
108
|
-
toolsExecuted: state.toolsExecuted.map((t) => ({
|
|
109
|
-
...t,
|
|
110
|
-
run_id: runId,
|
|
111
|
-
schema_version: SCHEMA_VERSION,
|
|
112
|
-
})),
|
|
113
|
-
scope: state.scope,
|
|
114
|
-
soloditResults: state.soloditResults,
|
|
115
|
-
fuzzCounterexamples: state.fuzzCounterexamples,
|
|
116
|
-
coverageReport: state.coverageReport,
|
|
117
|
-
gasHotspots: state.gasHotspots,
|
|
118
|
-
proxyContracts: state.proxyContracts,
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return { reportInput, diagnostics }
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Validates that a legacy AuditState is compatible with strict mode.
|
|
126
|
-
* Returns true if ALL findings can be normalized without errors.
|
|
127
|
-
*/
|
|
128
|
-
export function validateStrictCompatibility(
|
|
129
|
-
state: AuditState,
|
|
130
|
-
runId: string,
|
|
131
|
-
): { compatible: boolean; errors: string[] } {
|
|
132
|
-
const errors: string[] = []
|
|
133
|
-
|
|
134
|
-
for (const [index, finding] of state.findings.entries()) {
|
|
135
|
-
const result = normalizeToCanonicalFinding(
|
|
136
|
-
finding as unknown as Record<string, unknown>,
|
|
137
|
-
runId,
|
|
138
|
-
index + 1,
|
|
139
|
-
)
|
|
140
|
-
const hasErrors = result.diagnostics.some((d) => d.level === "error")
|
|
141
|
-
if (hasErrors) {
|
|
142
|
-
errors.push(
|
|
143
|
-
...result.diagnostics
|
|
144
|
-
.filter((d) => d.level === "error")
|
|
145
|
-
.map((d) => `[finding:${index}] ${d.message}`),
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return { compatible: errors.length === 0, errors }
|
|
151
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { stableHash } from "../../state/projectors"
|
|
2
|
-
import type { CanonicalFinding } from "../../state/schemas"
|
|
3
|
-
import type { Finding, FindingSeverity } from "../../state/types"
|
|
4
|
-
|
|
5
|
-
const SEVERITIES: readonly FindingSeverity[] = [
|
|
6
|
-
"Critical",
|
|
7
|
-
"High",
|
|
8
|
-
"Medium",
|
|
9
|
-
"Low",
|
|
10
|
-
"Informational",
|
|
11
|
-
] as const
|
|
12
|
-
|
|
13
|
-
export interface SeverityDistribution {
|
|
14
|
-
Critical: number
|
|
15
|
-
High: number
|
|
16
|
-
Medium: number
|
|
17
|
-
Low: number
|
|
18
|
-
Informational: number
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ParityMetrics {
|
|
22
|
-
legacyFindingCount: number
|
|
23
|
-
canonicalFindingCount: number
|
|
24
|
-
findingCountDiff: number
|
|
25
|
-
legacySeverityDistribution: SeverityDistribution
|
|
26
|
-
canonicalSeverityDistribution: SeverityDistribution
|
|
27
|
-
severityDiffs: Partial<Record<FindingSeverity, number>>
|
|
28
|
-
legacyContentHash: string
|
|
29
|
-
canonicalContentHash: string
|
|
30
|
-
hashMatch: boolean
|
|
31
|
-
onlyInLegacy: string[]
|
|
32
|
-
onlyInCanonical: string[]
|
|
33
|
-
timestamp: number
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function computeSeverityDistribution(
|
|
37
|
-
findings: Array<{ severity: FindingSeverity }>,
|
|
38
|
-
): SeverityDistribution {
|
|
39
|
-
const dist: SeverityDistribution = {
|
|
40
|
-
Critical: 0,
|
|
41
|
-
High: 0,
|
|
42
|
-
Medium: 0,
|
|
43
|
-
Low: 0,
|
|
44
|
-
Informational: 0,
|
|
45
|
-
}
|
|
46
|
-
for (const f of findings) {
|
|
47
|
-
if (f.severity in dist) {
|
|
48
|
-
dist[f.severity]++
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return dist
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function findingIds(findings: Array<{ id: string }>): Set<string> {
|
|
55
|
-
return new Set(findings.map((f) => f.id))
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function computeParityMetrics(
|
|
59
|
-
legacyFindings: Finding[],
|
|
60
|
-
canonicalFindings: CanonicalFinding[],
|
|
61
|
-
): ParityMetrics {
|
|
62
|
-
const legacySeverity = computeSeverityDistribution(legacyFindings)
|
|
63
|
-
const canonicalSeverity = computeSeverityDistribution(canonicalFindings)
|
|
64
|
-
|
|
65
|
-
const severityDiffs: Partial<Record<FindingSeverity, number>> = {}
|
|
66
|
-
for (const sev of SEVERITIES) {
|
|
67
|
-
const diff = canonicalSeverity[sev] - legacySeverity[sev]
|
|
68
|
-
if (diff !== 0) {
|
|
69
|
-
severityDiffs[sev] = diff
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const legacyIds = findingIds(legacyFindings)
|
|
74
|
-
const canonicalIds = findingIds(canonicalFindings)
|
|
75
|
-
|
|
76
|
-
const onlyInLegacy = [...legacyIds].filter((id) => !canonicalIds.has(id))
|
|
77
|
-
const onlyInCanonical = [...canonicalIds].filter((id) => !legacyIds.has(id))
|
|
78
|
-
|
|
79
|
-
const legacyContentHash = stableHash(
|
|
80
|
-
legacyFindings.map((f) => ({ id: f.id, check: f.check, severity: f.severity, file: f.file })),
|
|
81
|
-
)
|
|
82
|
-
const canonicalContentHash = stableHash(
|
|
83
|
-
canonicalFindings.map((f) => ({
|
|
84
|
-
id: f.id,
|
|
85
|
-
check: f.check,
|
|
86
|
-
severity: f.severity,
|
|
87
|
-
file: f.file,
|
|
88
|
-
})),
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
legacyFindingCount: legacyFindings.length,
|
|
93
|
-
canonicalFindingCount: canonicalFindings.length,
|
|
94
|
-
findingCountDiff: canonicalFindings.length - legacyFindings.length,
|
|
95
|
-
legacySeverityDistribution: legacySeverity,
|
|
96
|
-
canonicalSeverityDistribution: canonicalSeverity,
|
|
97
|
-
severityDiffs,
|
|
98
|
-
legacyContentHash,
|
|
99
|
-
canonicalContentHash,
|
|
100
|
-
hashMatch: legacyContentHash === canonicalContentHash,
|
|
101
|
-
onlyInLegacy,
|
|
102
|
-
onlyInCanonical,
|
|
103
|
-
timestamp: Date.now(),
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function formatParityReport(metrics: ParityMetrics): string {
|
|
108
|
-
const lines: string[] = [
|
|
109
|
-
"=== Migration Parity Report ===",
|
|
110
|
-
`Finding count: legacy=${metrics.legacyFindingCount} canonical=${metrics.canonicalFindingCount} diff=${metrics.findingCountDiff}`,
|
|
111
|
-
`Content hash match: ${metrics.hashMatch}`,
|
|
112
|
-
]
|
|
113
|
-
|
|
114
|
-
const sevDiffs = Object.entries(metrics.severityDiffs)
|
|
115
|
-
if (sevDiffs.length > 0) {
|
|
116
|
-
lines.push(
|
|
117
|
-
`Severity diffs: ${sevDiffs.map(([k, v]) => `${k}=${v > 0 ? "+" : ""}${v}`).join(", ")}`,
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (metrics.onlyInLegacy.length > 0) {
|
|
122
|
-
lines.push(
|
|
123
|
-
`Only in legacy (${metrics.onlyInLegacy.length}): ${metrics.onlyInLegacy.join(", ")}`,
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
if (metrics.onlyInCanonical.length > 0) {
|
|
127
|
-
lines.push(
|
|
128
|
-
`Only in canonical (${metrics.onlyInCanonical.length}): ${metrics.onlyInCanonical.join(", ")}`,
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return lines.join("\n")
|
|
133
|
-
}
|