iterate-plugin 2.12.2 → 3.2.1

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 (41) hide show
  1. package/README.md +28 -12
  2. package/README.zh-CN.md +2 -1
  3. package/dist/approval-gate.js +16 -2
  4. package/dist/config-loader.js +5 -0
  5. package/dist/git-scope.js +61 -7
  6. package/dist/index.js +16 -6
  7. package/dist/session-hooks.js +36 -11
  8. package/dist/skill-prompt.js +3 -0
  9. package/dist/tools/decision-log.js +10 -1
  10. package/dist/tools/defense-events.js +260 -0
  11. package/dist/tools/defense-store.js +97 -0
  12. package/dist/tools/experience-bank.js +248 -0
  13. package/dist/tools/experience-store.js +132 -0
  14. package/dist/tools/quality-gate.js +180 -0
  15. package/dist/tools/quality-store.js +174 -0
  16. package/lib/client.js +662 -103
  17. package/lib/parse.js +93 -0
  18. package/package.json +7 -6
  19. package/src/approval-gate.ts +14 -2
  20. package/src/client/index.ts +542 -49
  21. package/src/config-loader.ts +5 -0
  22. package/src/git-scope.ts +48 -7
  23. package/src/index.ts +16 -6
  24. package/src/session-hooks.ts +33 -11
  25. package/src/skill-prompt.ts +3 -0
  26. package/src/tools/checkpoint.ts +1 -1
  27. package/src/tools/config.ts +1 -1
  28. package/src/tools/decision-log.ts +11 -2
  29. package/src/tools/defense-events.ts +295 -0
  30. package/src/tools/defense-store.ts +113 -0
  31. package/src/tools/experience-bank.ts +264 -0
  32. package/src/tools/experience-store.ts +160 -0
  33. package/src/tools/fix.ts +1 -1
  34. package/src/tools/history.ts +1 -1
  35. package/src/tools/prune.ts +1 -1
  36. package/src/tools/quality-gate.ts +193 -0
  37. package/src/tools/quality-store.ts +199 -0
  38. package/src/tools/review.ts +1 -1
  39. package/src/tools/transcript.ts +1 -1
  40. package/src/tools/triage.ts +1 -1
  41. package/src/types.ts +118 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * src/tools/quality-gate.ts — quality gate query & write tool.
3
+ *
4
+ * iterate_quality_gate — query the persisted quality certificate, or compute
5
+ * and persist a new one from review/validation data.
6
+ *
7
+ * Provides a machine-readable quality certificate for the current iteration.
8
+ */
9
+
10
+ import { defineTool } from '@deepseek-ai/dsh-tools'
11
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values'
12
+ import { resolveProjectRootForExec } from '../config-loader.ts'
13
+ import { readQualityGate, writeQualityGate, computeQualityGate } from './quality-store.ts'
14
+ import type { QualityGateSnapshot } from '../types.ts'
15
+
16
+ /** Validate a single finding object; returns true when well-formed. */
17
+ function isValidFinding(raw: unknown): raw is { dimension: string; severity: string; file: string; line?: number } {
18
+ if (!raw || typeof raw !== 'object') return false
19
+ const f = raw as Record<string, unknown>
20
+ return (
21
+ typeof f.dimension === 'string' &&
22
+ typeof f.severity === 'string' &&
23
+ typeof f.file === 'string' &&
24
+ (f.line === undefined || typeof f.line === 'number')
25
+ )
26
+ }
27
+
28
+ /** Validate a single validation result; returns true when well-formed. */
29
+ function isValidValidationResult(raw: unknown): raw is { command: string; exitCode: number } {
30
+ if (!raw || typeof raw !== 'object') return false
31
+ const r = raw as Record<string, unknown>
32
+ return typeof r.command === 'string' && typeof r.exitCode === 'number' && Number.isFinite(r.exitCode)
33
+ }
34
+
35
+ /** Sanitize a caller-supplied per-dimension number map. */
36
+ function sanitizeNumberMap(raw: unknown): Record<string, number> | undefined {
37
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined
38
+ const out: Record<string, number> = {}
39
+ for (const [key, value] of Object.entries(raw)) {
40
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) out[key] = value
41
+ }
42
+ return Object.keys(out).length > 0 ? out : undefined
43
+ }
44
+
45
+ /** Sanitize a caller-supplied per-dimension round series map. */
46
+ function sanitizeRoundSeries(raw: unknown): Record<string, number[]> | undefined {
47
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined
48
+ const out: Record<string, number[]> = {}
49
+ for (const [key, value] of Object.entries(raw)) {
50
+ if (Array.isArray(value)) {
51
+ const series = value
52
+ .filter((n): n is number => typeof n === 'number' && Number.isFinite(n) && n >= 0)
53
+ if (series.length > 0) out[key] = series
54
+ }
55
+ }
56
+ return Object.keys(out).length > 0 ? out : undefined
57
+ }
58
+
59
+ /**
60
+ * Register the `iterate_quality_gate` tool.
61
+ * Reads the persisted quality certificate, or computes + persists a new one.
62
+ */
63
+ export function registerQualityGateTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
64
+ ctx.tools.register(
65
+ defineTool({
66
+ name: 'iterate_quality_gate',
67
+ description:
68
+ 'Query or write the quality gate status: dimension convergence rates, verification pass rates, ' +
69
+ 'and overall PASS/FAIL status. ' +
70
+ 'Operation "read" (default) returns the persisted machine-readable quality certificate. ' +
71
+ 'Operation "compute" computes a fresh snapshot from this round\'s findings/validation results, ' +
72
+ 'persists it to .iterate/quality-gate.json, and returns it.',
73
+ parameters: {
74
+ operation: {
75
+ type: 'string',
76
+ description: 'Operation: read (load persisted certificate) or compute (recompute + persist). Default: read.',
77
+ enum: ['read', 'compute'],
78
+ },
79
+ dimensions: {
80
+ type: 'array',
81
+ items: { type: 'string' },
82
+ description: 'Dimensions to gate (required for compute).',
83
+ },
84
+ findings: {
85
+ type: 'json',
86
+ description:
87
+ 'Findings array (required for compute). Each item: { dimension, severity (critical|high|medium|low), file, line? }.',
88
+ },
89
+ validationResults: {
90
+ type: 'json',
91
+ description: 'Validation results array (optional for compute). Each item: { command, exitCode }.',
92
+ },
93
+ findingsByRound: {
94
+ type: 'json',
95
+ description:
96
+ 'Optional per-dimension NEW-finding counts across rounds (latest last) — used to compute real convergence rates. ' +
97
+ 'Example: { "correctness": [5, 2, 0] }.',
98
+ },
99
+ fixedByDimension: {
100
+ type: 'json',
101
+ description: 'Optional per-dimension count of fixed findings, e.g. { "correctness": 3 }.',
102
+ },
103
+ path: {
104
+ type: 'string',
105
+ description: 'Project root directory (default: current working directory).',
106
+ },
107
+ },
108
+
109
+ output: {
110
+ schema: {
111
+ type: 'object',
112
+ additionalProperties: false,
113
+ properties: {
114
+ ok: { type: 'boolean', required: true },
115
+ kind: { type: 'string' },
116
+ operation: { type: 'string' },
117
+ snapshot: { type: 'json' },
118
+ error: { type: 'string' },
119
+ },
120
+ },
121
+ render: (_args, value) => {
122
+ if (!value.ok) return [{ type: 'text', text: `quality gate query failed: ${value.error}` }]
123
+ const operation = typeof value.operation === 'string' ? value.operation : 'read'
124
+ const snapshot = value.snapshot as unknown as QualityGateSnapshot
125
+ if (!snapshot) return [{ type: 'text', text: 'No quality gate data available.' }]
126
+
127
+ const statusEmoji = snapshot.overallStatus === 'pass' ? '✓' : snapshot.overallStatus === 'fail' ? '✗' : '○'
128
+ const lines = [
129
+ `${statusEmoji} Quality Gate: ${snapshot.overallStatus.toUpperCase()} (score: ${snapshot.overallScore})`,
130
+ `Verification: ${snapshot.passedChecks}/${snapshot.totalChecks} passed (${snapshot.verificationPassRate}%)`,
131
+ `Findings: ${snapshot.totalFindings} total (${snapshot.criticalCount} critical, ${snapshot.highCount} high, ${snapshot.mediumCount} medium, ${snapshot.lowCount} low)`,
132
+ '',
133
+ 'Dimension Breakdown:',
134
+ ...snapshot.dimensions.map((d) => {
135
+ const dimStatus = d.status === 'pass' ? '✓' : d.status === 'warn' ? '!' : '✗'
136
+ return ` ${dimStatus} ${d.dimension}: score=${d.score}, convergence=${d.convergenceRate}%, findings=${d.findingsCount}, fixed=${d.fixedCount}`
137
+ }),
138
+ ]
139
+ if (snapshot.failReason) {
140
+ lines.push('', `Fail Reason: ${snapshot.failReason}`)
141
+ }
142
+ if (operation === 'compute') {
143
+ lines.push('', 'Quality gate snapshot computed and persisted.')
144
+ }
145
+ return [{ type: 'text', text: lines.join('\n') }]
146
+ },
147
+ },
148
+
149
+ async execute(args, exec) {
150
+ const resolved = resolveProjectRootForExec(exec, args.path)
151
+ if (!resolved.ok) return { ok: false, kind: 'quality_gate', error: resolved.reason }
152
+ const projectRoot = resolved.root
153
+
154
+ const operation = typeof args.operation === 'string' ? args.operation : 'read'
155
+
156
+ if (operation === 'compute') {
157
+ const dimensions = Array.isArray(args.dimensions)
158
+ ? args.dimensions.filter((d): d is string => typeof d === 'string' && d.length > 0)
159
+ : []
160
+ const findings = Array.isArray(args.findings) ? args.findings.filter(isValidFinding) : []
161
+ const validationResults = Array.isArray(args.validationResults)
162
+ ? args.validationResults.filter(isValidValidationResult)
163
+ : undefined
164
+ const findingsByRound = sanitizeRoundSeries(args.findingsByRound)
165
+ const fixedByDimension = sanitizeNumberMap(args.fixedByDimension)
166
+
167
+ const snapshot = computeQualityGate({
168
+ dimensions,
169
+ findings,
170
+ validationResults,
171
+ findingsByRound,
172
+ fixedByDimension,
173
+ })
174
+ writeQualityGate(projectRoot, snapshot)
175
+ return {
176
+ ok: true,
177
+ kind: 'quality_gate',
178
+ operation: 'compute',
179
+ snapshot: snapshot as unknown as JsonValue,
180
+ }
181
+ }
182
+
183
+ const snapshot = readQualityGate(projectRoot)
184
+ return {
185
+ ok: true,
186
+ kind: 'quality_gate',
187
+ operation: 'read',
188
+ snapshot: snapshot as unknown as JsonValue,
189
+ }
190
+ },
191
+ }),
192
+ )
193
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * src/tools/quality-store.ts — quality gate storage layer.
3
+ *
4
+ * Provides read/write access to quality gate data stored in
5
+ * .iterate/quality-gate.json. Quality gate snapshots are generated
6
+ * from review results and validation outcomes.
7
+ */
8
+
9
+ import * as fs from 'node:fs'
10
+ import * as path from 'node:path'
11
+ import type { QualityGateSnapshot, QualityGateDimension } from '../types.ts'
12
+
13
+ const QUALITY_GATE_FILE = 'quality-gate.json'
14
+
15
+ /** Default empty quality gate snapshot. */
16
+ function emptySnapshot(): QualityGateSnapshot {
17
+ return {
18
+ timestamp: new Date().toISOString(),
19
+ overallStatus: 'pending',
20
+ overallScore: 0,
21
+ dimensions: [],
22
+ verificationPassRate: 0,
23
+ totalChecks: 0,
24
+ passedChecks: 0,
25
+ failedChecks: 0,
26
+ totalFindings: 0,
27
+ criticalCount: 0,
28
+ highCount: 0,
29
+ mediumCount: 0,
30
+ lowCount: 0,
31
+ }
32
+ }
33
+
34
+ /** Read the quality gate snapshot from disk. */
35
+ export function readQualityGate(projectRoot: string): QualityGateSnapshot {
36
+ const filePath = path.join(projectRoot, '.iterate', QUALITY_GATE_FILE)
37
+ try {
38
+ const content = fs.readFileSync(filePath, 'utf-8')
39
+ const parsed = JSON.parse(content) as QualityGateSnapshot
40
+ if (parsed && typeof parsed === 'object') {
41
+ return parsed
42
+ }
43
+ } catch {
44
+ // File not found or invalid JSON
45
+ }
46
+ return emptySnapshot()
47
+ }
48
+
49
+ /** Write the quality gate snapshot to disk. */
50
+ export function writeQualityGate(projectRoot: string, snapshot: QualityGateSnapshot): void {
51
+ const dirPath = path.join(projectRoot, '.iterate')
52
+ const filePath = path.join(dirPath, QUALITY_GATE_FILE)
53
+
54
+ try {
55
+ if (!fs.existsSync(dirPath)) {
56
+ fs.mkdirSync(dirPath, { recursive: true })
57
+ }
58
+ fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2), 'utf-8')
59
+ } catch {
60
+ // Silently fail - quality gate is not critical
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Compute the convergence rate for a dimension.
66
+ *
67
+ * Convergence measures how much NEW-finding volume shrank across rounds:
68
+ * `(first - last) / first` from the dimension's per-round findings series,
69
+ * expressed as a 0-100 percentage, clamped. A series with no fresh findings
70
+ * (or a dimension never reporting a first-round reading) counts as fully
71
+ * converged (100). Returns 0 — no measurable improvement — when a reading
72
+ * exists but the series is empty or malformed.
73
+ */
74
+ export function convergenceRateFor(series: number[] | undefined, currentCount: number): number {
75
+ if (Array.isArray(series) && series.length > 0) {
76
+ const first = series.find((n) => typeof n === 'number' && Number.isFinite(n))
77
+ const last = [...series].reverse().find((n) => typeof n === 'number' && Number.isFinite(n))
78
+ if (first === undefined || last === undefined) return currentCount === 0 ? 100 : 0
79
+ if (first <= 0) return currentCount === 0 ? 100 : 0
80
+ const raw = ((first - Math.max(0, last)) / first) * 100
81
+ return Math.max(0, Math.min(100, Math.round(raw)))
82
+ }
83
+ return currentCount === 0 ? 100 : 0
84
+ }
85
+
86
+ /** Compute a quality gate snapshot from review data. */
87
+ export function computeQualityGate(opts: {
88
+ dimensions: string[]
89
+ findings: Array<{
90
+ dimension: string
91
+ severity: string
92
+ file: string
93
+ line?: number
94
+ }>
95
+ validationResults?: Array<{
96
+ command: string
97
+ exitCode: number
98
+ }>
99
+ /** Per-dimension sequence of NEW-finding counts across rounds, newest last. */
100
+ findingsByRound?: Record<string, number[]>
101
+ /** Per-dimension count of findings already fixed this iteration. */
102
+ fixedByDimension?: Record<string, number>
103
+ }): QualityGateSnapshot {
104
+ const { dimensions, findings, validationResults, findingsByRound, fixedByDimension } = opts
105
+
106
+ // Count findings by severity
107
+ const criticalCount = findings.filter((f) => f.severity === 'critical').length
108
+ const highCount = findings.filter((f) => f.severity === 'high').length
109
+ const mediumCount = findings.filter((f) => f.severity === 'medium').length
110
+ const lowCount = findings.filter((f) => f.severity === 'low').length
111
+ const totalFindings = findings.length
112
+
113
+ // Compute dimension scores
114
+ const dimensionStats: Record<string, { count: number; critical: number; high: number; medium: number; low: number }> = {}
115
+ for (const dim of dimensions) {
116
+ dimensionStats[dim] = { count: 0, critical: 0, high: 0, medium: 0, low: 0 }
117
+ }
118
+
119
+ for (const finding of findings) {
120
+ const stats = dimensionStats[finding.dimension]
121
+ if (stats) {
122
+ stats.count++
123
+ if (finding.severity === 'critical') stats.critical++
124
+ else if (finding.severity === 'high') stats.high++
125
+ else if (finding.severity === 'medium') stats.medium++
126
+ else stats.low++
127
+ }
128
+ }
129
+
130
+ // Compute dimension-level quality gates
131
+ const dimensionGates: QualityGateDimension[] = dimensions.map((dim) => {
132
+ const stats = dimensionStats[dim] || { count: 0, critical: 0, high: 0, medium: 0, low: 0 }
133
+ // Score: 100 - (critical*30 + high*15 + medium*5 + low*1), capped at 0
134
+ const penalty = stats.critical * 30 + stats.high * 15 + stats.medium * 5 + stats.low * 1
135
+ const score = Math.max(0, 100 - penalty)
136
+ const status: 'pass' | 'warn' | 'fail' = score >= 80 ? 'pass' : score >= 50 ? 'warn' : 'fail'
137
+ const series = findingsByRound?.[dim]
138
+ const convergenceRate = convergenceRateFor(Array.isArray(series) ? series : undefined, stats.count)
139
+
140
+ return {
141
+ dimension: dim,
142
+ convergenceRate,
143
+ findingsCount: stats.count,
144
+ fixedCount: fixedByDimension?.[dim] ?? 0,
145
+ score,
146
+ status,
147
+ }
148
+ })
149
+
150
+ // Compute verification pass rate
151
+ const totalChecks = validationResults?.length ?? 0
152
+ const passedChecks = validationResults?.filter((r) => r.exitCode === 0).length ?? 0
153
+ const failedChecks = totalChecks - passedChecks
154
+ const verificationPassRate = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 0
155
+
156
+ // Compute overall score (weighted average of dimension scores)
157
+ const overallScore = dimensionGates.length > 0
158
+ ? Math.round(dimensionGates.reduce((sum, d) => sum + d.score, 0) / dimensionGates.length)
159
+ : 0
160
+
161
+ // Determine overall status
162
+ const hasCritical = criticalCount > 0
163
+ const hasHighFail = dimensionGates.some((d) => d.status === 'fail')
164
+ const verificationFails = totalChecks > 0 && failedChecks > 0
165
+
166
+ let overallStatus: 'pass' | 'fail' | 'pending' = 'pass'
167
+ let failReason: string | undefined
168
+
169
+ if (hasCritical) {
170
+ overallStatus = 'fail'
171
+ failReason = `${criticalCount} critical findings present`
172
+ } else if (hasHighFail) {
173
+ overallStatus = 'fail'
174
+ failReason = 'One or more dimensions failed quality gate'
175
+ } else if (verificationFails) {
176
+ overallStatus = 'fail'
177
+ failReason = `${failedChecks} validation checks failed`
178
+ } else if (overallScore < 70) {
179
+ overallStatus = 'fail'
180
+ failReason = `Overall score ${overallScore} below threshold (70)`
181
+ }
182
+
183
+ return {
184
+ timestamp: new Date().toISOString(),
185
+ overallStatus,
186
+ overallScore,
187
+ dimensions: dimensionGates,
188
+ verificationPassRate,
189
+ totalChecks,
190
+ passedChecks,
191
+ failedChecks,
192
+ failReason,
193
+ totalFindings,
194
+ criticalCount,
195
+ highCount,
196
+ mediumCount,
197
+ lowCount,
198
+ }
199
+ }
@@ -1,5 +1,5 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools'
2
- import type { JsonValue } from '@deepseek-ai/dsh-session'
2
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values'
3
3
  import { loadEffectiveConfig, resolveProjectRootForExec } from '../config-loader.ts'
4
4
  import { runWithJob } from '../jobs.ts'
5
5
  import {
@@ -22,7 +22,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
22
22
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
23
23
  import { existsSync } from 'node:fs'
24
24
  import { dirname } from 'node:path'
25
- import type { JsonValue } from '@deepseek-ai/dsh-session'
25
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values'
26
26
  import {
27
27
  loadEffectiveConfig,
28
28
  resolveProjectRootForExec,
@@ -1,7 +1,7 @@
1
1
  import { copyFileSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { defineTool } from '@deepseek-ai/dsh-tools'
4
- import type { JsonValue } from '@deepseek-ai/dsh-session'
4
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values'
5
5
  import yaml from 'js-yaml'
6
6
  import { resolveProjectRootForExec } from '../config-loader.ts'
7
7
  import type { KnownIntentional } from '../types.ts'
package/src/types.ts CHANGED
@@ -230,6 +230,20 @@ export interface IterationStatus {
230
230
  resumeCount: number
231
231
  checkpoint: IterationCheckpoint | null
232
232
  lastUpdated: string | null
233
+ /** v3.0: Quality gate snapshot */
234
+ qualityGate?: QualityGateSnapshot
235
+ /** v3.0: Experience bank summary */
236
+ experienceBank?: {
237
+ totalEntries: number
238
+ totalHits: number
239
+ }
240
+ /** v3.0: Defense events summary */
241
+ defenseEvents?: {
242
+ totalEvents: number
243
+ counts: Record<DefenseEventType, number>
244
+ }
245
+ /** v3.0: task_mode from harness */
246
+ taskMode?: 'code' | 'iterate' | null
233
247
  }
234
248
 
235
249
  /** ─── Runtime observatory (transcript) ───────────────────────────────────── */
@@ -331,4 +345,108 @@ export interface TranscriptManifest {
331
345
  active: boolean
332
346
  policy: 'ask' | 'deny' | 'allow'
333
347
  }
348
+ /** v3.0: task_mode indicator from harness status */
349
+ taskMode?: 'code' | 'iterate' | null
350
+ }
351
+
352
+ // ─── v3.0: Quality Gate ──────────────────────────────────────────────────────
353
+
354
+ /** A single dimension's quality gate status. */
355
+ export interface QualityGateDimension {
356
+ dimension: string
357
+ convergenceRate: number
358
+ findingsCount: number
359
+ fixedCount: number
360
+ /** 0-100 score based on findings severity and count */
361
+ score: number
362
+ status: 'pass' | 'warn' | 'fail'
363
+ }
364
+
365
+ /** Quality gate snapshot for the current iteration. */
366
+ export interface QualityGateSnapshot {
367
+ timestamp: string
368
+ overallStatus: 'pass' | 'fail' | 'pending'
369
+ overallScore: number
370
+ dimensions: QualityGateDimension[]
371
+ verificationPassRate: number
372
+ totalChecks: number
373
+ passedChecks: number
374
+ failedChecks: number
375
+ /** Reason for overall FAIL status, if applicable */
376
+ failReason?: string
377
+ /** Total findings across all dimensions */
378
+ totalFindings: number
379
+ /** Findings by severity */
380
+ criticalCount: number
381
+ highCount: number
382
+ mediumCount: number
383
+ lowCount: number
384
+ }
385
+
386
+ // ─── v3.0: Experience Bank ───────────────────────────────────────────────────
387
+
388
+ /** A single experience entry in the experience bank. */
389
+ export interface ExperienceEntry {
390
+ id: string
391
+ timestamp: string
392
+ dimension: string
393
+ pattern: string
394
+ description: string
395
+ /** The fix that was applied and verified */
396
+ verifiedFix: string
397
+ /** Files involved in this experience */
398
+ files: string[]
399
+ /** How many times this pattern has been encountered */
400
+ hitCount: number
401
+ /** Last time this experience was hit */
402
+ lastHitAt?: string
403
+ /** Tags for categorization */
404
+ tags: string[]
405
+ /** Related finding summary */
406
+ findingSummary: string
407
+ /** Severity of the original finding */
408
+ severity: 'critical' | 'high' | 'medium' | 'low'
409
+ }
410
+
411
+ /** Experience bank state for the project. */
412
+ export interface ExperienceBank {
413
+ entries: ExperienceEntry[]
414
+ lastUpdated: string
415
+ totalHits: number
416
+ }
417
+
418
+ // ─── v3.0: Defense Events ────────────────────────────────────────────────────
419
+
420
+ /** Defense event types */
421
+ export type DefenseEventType =
422
+ | 'precondition_failed'
423
+ | 'rollback'
424
+ | 'invariant_violated'
425
+ | 'assumption_falsified'
426
+
427
+ /** A single defense event recorded during iteration. */
428
+ export interface DefenseEvent {
429
+ id: string
430
+ timestamp: string
431
+ round: number
432
+ type: DefenseEventType
433
+ /** What was being checked */
434
+ description: string
435
+ /** The defense that was triggered */
436
+ defense: string
437
+ /** Outcome: what was protected against */
438
+ outcome: string
439
+ /** Optional file/location context */
440
+ file?: string
441
+ line?: number
442
+ /** Severity of the event */
443
+ severity: 'critical' | 'high' | 'medium' | 'low'
444
+ }
445
+
446
+ /** Defense events stream for the current iteration. */
447
+ export interface DefenseEventStream {
448
+ events: DefenseEvent[]
449
+ lastUpdated: string
450
+ /** Summary counts by type */
451
+ counts: Record<DefenseEventType, number>
334
452
  }