solidity-argus 0.3.3 → 0.3.5

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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/agents/argus-prompt.ts +67 -8
  3. package/src/agents/scribe-prompt.ts +13 -5
  4. package/src/cli/commands/init.ts +1 -1
  5. package/src/cli/index.ts +0 -0
  6. package/src/config/schema.ts +7 -2
  7. package/src/create-hooks.ts +116 -27
  8. package/src/features/audit-enforcer/audit-enforcer.ts +31 -2
  9. package/src/features/migration/index.ts +14 -0
  10. package/src/features/migration/migration-adapter.ts +151 -0
  11. package/src/features/migration/parity-telemetry.ts +133 -0
  12. package/src/features/persistent-state/audit-state-manager.ts +28 -6
  13. package/src/features/persistent-state/event-sink.ts +175 -0
  14. package/src/features/persistent-state/findings-materializer.ts +51 -0
  15. package/src/features/persistent-state/index.ts +2 -0
  16. package/src/features/persistent-state/run-finalizer.ts +192 -0
  17. package/src/features/persistent-state/run-journal.ts +15 -4
  18. package/src/hooks/agent-tracker.ts +15 -0
  19. package/src/hooks/event-hook.ts +93 -1
  20. package/src/hooks/system-prompt-hook.ts +20 -0
  21. package/src/hooks/tool-tracking-hook.ts +263 -33
  22. package/src/shared/audit-artifact-resolver.ts +75 -0
  23. package/src/shared/drop-diagnostics.ts +108 -0
  24. package/src/shared/file-utils.ts +7 -2
  25. package/src/shared/index.ts +14 -0
  26. package/src/shared/path-root-resolver.ts +34 -0
  27. package/src/shared/report-path-resolver.ts +70 -0
  28. package/src/solodit-lifecycle.ts +86 -7
  29. package/src/state/adapters.ts +262 -0
  30. package/src/state/index.ts +15 -0
  31. package/src/state/projectors.ts +437 -0
  32. package/src/state/schemas.ts +453 -0
  33. package/src/state/types.ts +6 -0
  34. package/src/tools/report-generator-tool.ts +647 -36
  35. package/src/tools/report-preflight.ts +79 -0
  36. package/src/tools/solodit-search-tool.ts +15 -24
  37. package/src/utils/solodit-health.ts +18 -0
@@ -0,0 +1,453 @@
1
+ import type {
2
+ AuditPhase,
3
+ Finding,
4
+ FindingSeverity,
5
+ FuzzCounterexample,
6
+ SoloditResult,
7
+ ToolExecution,
8
+ } from "./types"
9
+
10
+ export const SCHEMA_VERSION = "1.0.0"
11
+
12
+ export type AuditEventType =
13
+ | "session.created"
14
+ | "session.idle"
15
+ | "session.deleted"
16
+ | "tool.started"
17
+ | "tool.completed"
18
+ | "finding.added"
19
+ | "phase.changed"
20
+ | "run.finalized"
21
+
22
+ export interface ValidationError {
23
+ field: string
24
+ code: string
25
+ message: string
26
+ }
27
+
28
+ export type ValidationResult<T> =
29
+ | { success: true; data: T }
30
+ | { success: false; errors: ValidationError[] }
31
+
32
+ export interface AuditEvent {
33
+ type: AuditEventType
34
+ run_id: string
35
+ seq: number
36
+ session_id: string
37
+ tool_call_id?: string
38
+ source: string
39
+ schema_version: string
40
+ timestamp: number
41
+ payload: unknown
42
+ }
43
+
44
+ export interface CanonicalFinding extends Finding {
45
+ run_id: string
46
+ seq: number
47
+ schema_version: string
48
+ }
49
+
50
+ export interface CanonicalToolExecution extends ToolExecution {
51
+ run_id: string
52
+ schema_version: string
53
+ }
54
+
55
+ export interface CoverageReport {
56
+ files: Array<{
57
+ path: string
58
+ linesPct: number
59
+ statementsPct: number
60
+ branchesPct: number
61
+ functionsPct: number
62
+ }>
63
+ }
64
+
65
+ export interface GasHotspot {
66
+ contract: string
67
+ function: string
68
+ avgGas: number
69
+ }
70
+
71
+ export interface ProxyContract {
72
+ file: string
73
+ proxyType: string
74
+ indicators: string[]
75
+ }
76
+
77
+ export interface AuditRunSnapshot {
78
+ run_id: string
79
+ seq: number
80
+ session_id: string
81
+ tool_call_id: string
82
+ source: string
83
+ schema_version: string
84
+ findings: CanonicalFinding[]
85
+ phase: AuditPhase
86
+ tools: CanonicalToolExecution[]
87
+ started_at: number
88
+ finalized_at?: number
89
+ }
90
+
91
+ export interface ReportInput {
92
+ run_id: string
93
+ seq: number
94
+ session_id: string
95
+ tool_call_id: string
96
+ source: string
97
+ schema_version: string
98
+ projectDir: string
99
+ findings: CanonicalFinding[]
100
+ toolsExecuted: CanonicalToolExecution[]
101
+ scope: string[]
102
+ soloditResults?: SoloditResult[]
103
+ fuzzCounterexamples?: FuzzCounterexample[]
104
+ coverageReport?: CoverageReport
105
+ gasHotspots?: GasHotspot[]
106
+ proxyContracts?: ProxyContract[]
107
+ patternVersion?: string
108
+ skillsLoaded?: string[]
109
+ }
110
+
111
+ function pushRequiredRootStringError(
112
+ errors: ValidationError[],
113
+ obj: Record<string, unknown>,
114
+ field: keyof ReportInput,
115
+ ): void {
116
+ if (typeof obj[field] !== "string" || (obj[field] as string).trim().length === 0) {
117
+ errors.push({
118
+ field: String(field),
119
+ code: "required",
120
+ message: `${String(field)} is required and must be a non-empty string`,
121
+ })
122
+ }
123
+ }
124
+
125
+ function pushRequiredRootNumberError(
126
+ errors: ValidationError[],
127
+ obj: Record<string, unknown>,
128
+ field: keyof ReportInput,
129
+ ): void {
130
+ if (typeof obj[field] !== "number" || !Number.isInteger(obj[field] as number)) {
131
+ errors.push({
132
+ field: String(field),
133
+ code: "invalid",
134
+ message: `${String(field)} is required and must be an integer`,
135
+ })
136
+ }
137
+ }
138
+
139
+ const VALID_SEVERITIES: ReadonlySet<FindingSeverity> = new Set([
140
+ "Critical",
141
+ "High",
142
+ "Medium",
143
+ "Low",
144
+ "Informational",
145
+ ])
146
+ const VALID_CONFIDENCES: ReadonlySet<CanonicalFinding["confidence"]> = new Set([
147
+ "High",
148
+ "Medium",
149
+ "Low",
150
+ ])
151
+ const VALID_SOURCES: ReadonlySet<CanonicalFinding["source"]> = new Set([
152
+ "slither",
153
+ "manual",
154
+ "pattern",
155
+ "scvd",
156
+ "solodit",
157
+ "fuzz",
158
+ ])
159
+
160
+ function isRecord(value: unknown): value is Record<string, unknown> {
161
+ return typeof value === "object" && value !== null && !Array.isArray(value)
162
+ }
163
+
164
+ function pushRequiredStringError(
165
+ errors: ValidationError[],
166
+ obj: Record<string, unknown>,
167
+ field: keyof CanonicalFinding,
168
+ ): void {
169
+ if (typeof obj[field] !== "string" || (obj[field] as string).trim().length === 0) {
170
+ errors.push({
171
+ field,
172
+ code: "required",
173
+ message: `${field} is required and must be a non-empty string`,
174
+ })
175
+ }
176
+ }
177
+
178
+ export function validateCanonicalFinding(raw: unknown): ValidationResult<CanonicalFinding> {
179
+ if (!isRecord(raw)) {
180
+ return {
181
+ success: false,
182
+ errors: [
183
+ {
184
+ field: "$root",
185
+ code: "type",
186
+ message: "canonical finding must be an object",
187
+ },
188
+ ],
189
+ }
190
+ }
191
+
192
+ const errors: ValidationError[] = []
193
+
194
+ pushRequiredStringError(errors, raw, "id")
195
+ pushRequiredStringError(errors, raw, "check")
196
+ pushRequiredStringError(errors, raw, "description")
197
+ pushRequiredStringError(errors, raw, "file")
198
+ pushRequiredStringError(errors, raw, "run_id")
199
+ pushRequiredStringError(errors, raw, "schema_version")
200
+
201
+ if (typeof raw.seq !== "number" || !Number.isInteger(raw.seq) || raw.seq < 0) {
202
+ errors.push({
203
+ field: "seq",
204
+ code: "invalid",
205
+ message: "seq must be a non-negative integer",
206
+ })
207
+ }
208
+
209
+ if (!Array.isArray(raw.lines) || raw.lines.length !== 2) {
210
+ errors.push({
211
+ field: "lines",
212
+ code: "invalid",
213
+ message: "lines must be a [start, end] tuple",
214
+ })
215
+ } else {
216
+ const [start, end] = raw.lines
217
+ if (typeof start !== "number" || typeof end !== "number") {
218
+ errors.push({
219
+ field: "lines",
220
+ code: "invalid",
221
+ message: "lines must contain numbers",
222
+ })
223
+ }
224
+ }
225
+
226
+ if (typeof raw.severity !== "string" || !VALID_SEVERITIES.has(raw.severity as FindingSeverity)) {
227
+ errors.push({
228
+ field: "severity",
229
+ code: "enum",
230
+ message: "severity must be one of: Critical, High, Medium, Low, Informational",
231
+ })
232
+ }
233
+
234
+ if (
235
+ typeof raw.confidence !== "string" ||
236
+ !VALID_CONFIDENCES.has(raw.confidence as CanonicalFinding["confidence"])
237
+ ) {
238
+ errors.push({
239
+ field: "confidence",
240
+ code: "enum",
241
+ message: "confidence must be one of: High, Medium, Low",
242
+ })
243
+ }
244
+
245
+ if (
246
+ typeof raw.source !== "string" ||
247
+ !VALID_SOURCES.has(raw.source as CanonicalFinding["source"])
248
+ ) {
249
+ errors.push({
250
+ field: "source",
251
+ code: "enum",
252
+ message: "source must be one of: slither, manual, pattern, scvd, solodit, fuzz",
253
+ })
254
+ }
255
+
256
+ if (errors.length > 0) {
257
+ return { success: false, errors }
258
+ }
259
+
260
+ return { success: true, data: raw as unknown as CanonicalFinding }
261
+ }
262
+
263
+
264
+ export function validateCanonicalToolExecution(
265
+ raw: unknown,
266
+ ): ValidationResult<CanonicalToolExecution> {
267
+ if (!isRecord(raw)) {
268
+ return {
269
+ success: false,
270
+ errors: [
271
+ {
272
+ field: "$root",
273
+ code: "type",
274
+ message: "canonical tool execution must be an object",
275
+ },
276
+ ],
277
+ }
278
+ }
279
+
280
+ const errors: ValidationError[] = []
281
+
282
+ if (typeof raw.tool !== "string" || raw.tool.trim().length === 0) {
283
+ errors.push({
284
+ field: "tool",
285
+ code: "required",
286
+ message: "tool is required and must be a non-empty string",
287
+ })
288
+ }
289
+
290
+ if (typeof raw.startTime !== "number" || !Number.isInteger(raw.startTime) || raw.startTime <= 0) {
291
+ errors.push({
292
+ field: "startTime",
293
+ code: "invalid",
294
+ message: "startTime must be a positive integer",
295
+ })
296
+ }
297
+
298
+ if (raw.endTime != null && (typeof raw.endTime !== "number" || !Number.isInteger(raw.endTime))) {
299
+ errors.push({
300
+ field: "endTime",
301
+ code: "invalid",
302
+ message: "endTime must be an integer when provided",
303
+ })
304
+ }
305
+
306
+ if (typeof raw.success !== "boolean") {
307
+ errors.push({
308
+ field: "success",
309
+ code: "required",
310
+ message: "success is required and must be a boolean",
311
+ })
312
+ }
313
+
314
+ if (
315
+ typeof raw.findingsCount !== "number" ||
316
+ !Number.isInteger(raw.findingsCount) ||
317
+ raw.findingsCount < 0
318
+ ) {
319
+ errors.push({
320
+ field: "findingsCount",
321
+ code: "invalid",
322
+ message: "findingsCount must be a non-negative integer",
323
+ })
324
+ }
325
+
326
+ if (typeof raw.run_id !== "string" || raw.run_id.trim().length === 0) {
327
+ errors.push({
328
+ field: "run_id",
329
+ code: "required",
330
+ message: "run_id is required and must be a non-empty string",
331
+ })
332
+ }
333
+
334
+ if (typeof raw.schema_version !== "string" || raw.schema_version.trim().length === 0) {
335
+ errors.push({
336
+ field: "schema_version",
337
+ code: "required",
338
+ message: "schema_version is required and must be a non-empty string",
339
+ })
340
+ }
341
+
342
+ if (errors.length > 0) {
343
+ return { success: false, errors }
344
+ }
345
+
346
+ return { success: true, data: raw as unknown as CanonicalToolExecution }
347
+ }
348
+ export function validateReportInput(raw: unknown): ValidationResult<ReportInput> {
349
+ if (!isRecord(raw)) {
350
+ return {
351
+ success: false,
352
+ errors: [
353
+ {
354
+ field: "$root",
355
+ code: "type",
356
+ message: "report input must be an object",
357
+ },
358
+ ],
359
+ }
360
+ }
361
+
362
+ const errors: ValidationError[] = []
363
+
364
+ pushRequiredRootStringError(errors, raw, "run_id")
365
+ pushRequiredRootNumberError(errors, raw, "seq")
366
+ pushRequiredRootStringError(errors, raw, "session_id")
367
+ pushRequiredRootStringError(errors, raw, "tool_call_id")
368
+ pushRequiredRootStringError(errors, raw, "source")
369
+ pushRequiredRootStringError(errors, raw, "schema_version")
370
+ pushRequiredRootStringError(errors, raw, "projectDir")
371
+
372
+ if (raw.schema_version !== SCHEMA_VERSION) {
373
+ errors.push({
374
+ field: "schema_version",
375
+ code: "version_mismatch",
376
+ message: `schema_version must be ${SCHEMA_VERSION}`,
377
+ })
378
+ }
379
+
380
+ if (!Array.isArray(raw.scope) || !raw.scope.every((item) => typeof item === "string")) {
381
+ errors.push({
382
+ field: "scope",
383
+ code: "invalid",
384
+ message: "scope must be an array of strings",
385
+ })
386
+ }
387
+
388
+ if (!Array.isArray(raw.toolsExecuted)) {
389
+ errors.push({
390
+ field: "toolsExecuted",
391
+ code: "invalid",
392
+ message: "toolsExecuted must be an array",
393
+ })
394
+ } else {
395
+ for (const [index, entry] of raw.toolsExecuted.entries()) {
396
+ const toolValidation = validateCanonicalToolExecution(entry)
397
+ if (toolValidation.success) continue
398
+ for (const toolError of toolValidation.errors) {
399
+ errors.push({
400
+ field: `toolsExecuted[${index}].${toolError.field}`,
401
+ code: toolError.code,
402
+ message: toolError.message,
403
+ })
404
+ }
405
+ }
406
+ }
407
+
408
+ if (raw.patternVersion != null && typeof raw.patternVersion !== "string") {
409
+ errors.push({
410
+ field: "patternVersion",
411
+ code: "invalid",
412
+ message: "patternVersion must be a string when provided",
413
+ })
414
+ }
415
+
416
+ if (
417
+ raw.skillsLoaded != null &&
418
+ (!Array.isArray(raw.skillsLoaded) ||
419
+ !raw.skillsLoaded.every((item) => typeof item === "string"))
420
+ ) {
421
+ errors.push({
422
+ field: "skillsLoaded",
423
+ code: "invalid",
424
+ message: "skillsLoaded must be an array of strings when provided",
425
+ })
426
+ }
427
+
428
+ if (!Array.isArray(raw.findings)) {
429
+ errors.push({
430
+ field: "findings",
431
+ code: "invalid",
432
+ message: "findings must be an array",
433
+ })
434
+ } else {
435
+ for (const [index, finding] of raw.findings.entries()) {
436
+ const findingValidation = validateCanonicalFinding(finding)
437
+ if (findingValidation.success) continue
438
+ for (const findingError of findingValidation.errors) {
439
+ errors.push({
440
+ field: `findings[${index}].${findingError.field}`,
441
+ code: findingError.code,
442
+ message: findingError.message,
443
+ })
444
+ }
445
+ }
446
+ }
447
+
448
+ if (errors.length > 0) {
449
+ return { success: false, errors }
450
+ }
451
+
452
+ return { success: true, data: raw as unknown as ReportInput }
453
+ }
@@ -110,4 +110,10 @@ export interface PersistentAuditState extends AuditState {
110
110
  savedAt: number
111
111
  version: string
112
112
  filePath: string
113
+ /** Whether this snapshot was projected from events or loaded from a prior snapshot */
114
+ source_of_truth?: "events" | "snapshot"
115
+ /** Sequence number of the last event included in this snapshot */
116
+ last_event_seq?: number
117
+ /** Hash of the event stream for staleness detection */
118
+ event_stream_hash?: string
113
119
  }