iterate-plugin 2.12.3 → 3.3.0

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.
@@ -0,0 +1,196 @@
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
+ const write = writeQualityGate(projectRoot, snapshot)
175
+ if (!write.ok) {
176
+ return { ok: false, kind: 'quality_gate', operation: 'compute', error: write.error }
177
+ }
178
+ return {
179
+ ok: true,
180
+ kind: 'quality_gate',
181
+ operation: 'compute',
182
+ snapshot: snapshot as unknown as JsonValue,
183
+ }
184
+ }
185
+
186
+ const snapshot = readQualityGate(projectRoot)
187
+ return {
188
+ ok: true,
189
+ kind: 'quality_gate',
190
+ operation: 'read',
191
+ snapshot: snapshot as unknown as JsonValue,
192
+ }
193
+ },
194
+ }),
195
+ )
196
+ }
@@ -0,0 +1,208 @@
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
+ /**
50
+ * Write the quality gate snapshot to disk.
51
+ * Returns `{ ok: true }` on success or `{ ok: false, error }` when the write
52
+ * fails — a caller must surface the failure instead of reporting success for
53
+ * a snapshot that was never persisted.
54
+ */
55
+ export function writeQualityGate(
56
+ projectRoot: string,
57
+ snapshot: QualityGateSnapshot,
58
+ ): { ok: true } | { ok: false; error: string } {
59
+ const dirPath = path.join(projectRoot, '.iterate')
60
+ const filePath = path.join(dirPath, QUALITY_GATE_FILE)
61
+
62
+ try {
63
+ if (!fs.existsSync(dirPath)) {
64
+ fs.mkdirSync(dirPath, { recursive: true })
65
+ }
66
+ fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2), 'utf-8')
67
+ } catch (err) {
68
+ return { ok: false, error: `unable to write ${filePath}: ${String(err)}` }
69
+ }
70
+ return { ok: true }
71
+ }
72
+
73
+ /**
74
+ * Compute the convergence rate for a dimension.
75
+ *
76
+ * Convergence measures how much NEW-finding volume shrank across rounds:
77
+ * `(first - last) / first` from the dimension's per-round findings series,
78
+ * expressed as a 0-100 percentage, clamped. A series with no fresh findings
79
+ * (or a dimension never reporting a first-round reading) counts as fully
80
+ * converged (100). Returns 0 — no measurable improvement — when a reading
81
+ * exists but the series is empty or malformed.
82
+ */
83
+ export function convergenceRateFor(series: number[] | undefined, currentCount: number): number {
84
+ if (Array.isArray(series) && series.length > 0) {
85
+ const first = series.find((n) => typeof n === 'number' && Number.isFinite(n))
86
+ const last = [...series].reverse().find((n) => typeof n === 'number' && Number.isFinite(n))
87
+ if (first === undefined || last === undefined) return currentCount === 0 ? 100 : 0
88
+ if (first <= 0) return currentCount === 0 ? 100 : 0
89
+ const raw = ((first - Math.max(0, last)) / first) * 100
90
+ return Math.max(0, Math.min(100, Math.round(raw)))
91
+ }
92
+ return currentCount === 0 ? 100 : 0
93
+ }
94
+
95
+ /** Compute a quality gate snapshot from review data. */
96
+ export function computeQualityGate(opts: {
97
+ dimensions: string[]
98
+ findings: Array<{
99
+ dimension: string
100
+ severity: string
101
+ file: string
102
+ line?: number
103
+ }>
104
+ validationResults?: Array<{
105
+ command: string
106
+ exitCode: number
107
+ }>
108
+ /** Per-dimension sequence of NEW-finding counts across rounds, newest last. */
109
+ findingsByRound?: Record<string, number[]>
110
+ /** Per-dimension count of findings already fixed this iteration. */
111
+ fixedByDimension?: Record<string, number>
112
+ }): QualityGateSnapshot {
113
+ const { dimensions, findings, validationResults, findingsByRound, fixedByDimension } = opts
114
+
115
+ // Count findings by severity
116
+ const criticalCount = findings.filter((f) => f.severity === 'critical').length
117
+ const highCount = findings.filter((f) => f.severity === 'high').length
118
+ const mediumCount = findings.filter((f) => f.severity === 'medium').length
119
+ const lowCount = findings.filter((f) => f.severity === 'low').length
120
+ const totalFindings = findings.length
121
+
122
+ // Compute dimension scores
123
+ const dimensionStats: Record<string, { count: number; critical: number; high: number; medium: number; low: number }> = {}
124
+ for (const dim of dimensions) {
125
+ dimensionStats[dim] = { count: 0, critical: 0, high: 0, medium: 0, low: 0 }
126
+ }
127
+
128
+ for (const finding of findings) {
129
+ const stats = dimensionStats[finding.dimension]
130
+ if (stats) {
131
+ stats.count++
132
+ if (finding.severity === 'critical') stats.critical++
133
+ else if (finding.severity === 'high') stats.high++
134
+ else if (finding.severity === 'medium') stats.medium++
135
+ else stats.low++
136
+ }
137
+ }
138
+
139
+ // Compute dimension-level quality gates
140
+ const dimensionGates: QualityGateDimension[] = dimensions.map((dim) => {
141
+ const stats = dimensionStats[dim] || { count: 0, critical: 0, high: 0, medium: 0, low: 0 }
142
+ // Score: 100 - (critical*30 + high*15 + medium*5 + low*1), capped at 0
143
+ const penalty = stats.critical * 30 + stats.high * 15 + stats.medium * 5 + stats.low * 1
144
+ const score = Math.max(0, 100 - penalty)
145
+ const status: 'pass' | 'warn' | 'fail' = score >= 80 ? 'pass' : score >= 50 ? 'warn' : 'fail'
146
+ const series = findingsByRound?.[dim]
147
+ const convergenceRate = convergenceRateFor(Array.isArray(series) ? series : undefined, stats.count)
148
+
149
+ return {
150
+ dimension: dim,
151
+ convergenceRate,
152
+ findingsCount: stats.count,
153
+ fixedCount: fixedByDimension?.[dim] ?? 0,
154
+ score,
155
+ status,
156
+ }
157
+ })
158
+
159
+ // Compute verification pass rate
160
+ const totalChecks = validationResults?.length ?? 0
161
+ const passedChecks = validationResults?.filter((r) => r.exitCode === 0).length ?? 0
162
+ const failedChecks = totalChecks - passedChecks
163
+ const verificationPassRate = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 0
164
+
165
+ // Compute overall score (weighted average of dimension scores)
166
+ const overallScore = dimensionGates.length > 0
167
+ ? Math.round(dimensionGates.reduce((sum, d) => sum + d.score, 0) / dimensionGates.length)
168
+ : 0
169
+
170
+ // Determine overall status
171
+ const hasCritical = criticalCount > 0
172
+ const hasHighFail = dimensionGates.some((d) => d.status === 'fail')
173
+ const verificationFails = totalChecks > 0 && failedChecks > 0
174
+
175
+ let overallStatus: 'pass' | 'fail' | 'pending' = 'pass'
176
+ let failReason: string | undefined
177
+
178
+ if (hasCritical) {
179
+ overallStatus = 'fail'
180
+ failReason = `${criticalCount} critical findings present`
181
+ } else if (hasHighFail) {
182
+ overallStatus = 'fail'
183
+ failReason = 'One or more dimensions failed quality gate'
184
+ } else if (verificationFails) {
185
+ overallStatus = 'fail'
186
+ failReason = `${failedChecks} validation checks failed`
187
+ } else if (overallScore < 70) {
188
+ overallStatus = 'fail'
189
+ failReason = `Overall score ${overallScore} below threshold (70)`
190
+ }
191
+
192
+ return {
193
+ timestamp: new Date().toISOString(),
194
+ overallStatus,
195
+ overallScore,
196
+ dimensions: dimensionGates,
197
+ verificationPassRate,
198
+ totalChecks,
199
+ passedChecks,
200
+ failedChecks,
201
+ failReason,
202
+ totalFindings,
203
+ criticalCount,
204
+ highCount,
205
+ mediumCount,
206
+ lowCount,
207
+ }
208
+ }
@@ -136,6 +136,11 @@ export function registerTranscriptTool(ctx: {
136
136
  description: 'For `capture`: run mode ("dry-run" | "normal"). Default dry-run.',
137
137
  enum: ['dry-run', 'normal'],
138
138
  },
139
+ taskMode: {
140
+ type: 'string',
141
+ description: 'For `capture`: harness execution mode ("code" | "iterate"). Default derives from the review loop (iterate).',
142
+ enum: ['code', 'iterate'],
143
+ },
139
144
  goal: { type: 'string', description: 'For `capture`: run goal.' },
140
145
  maxRounds: { type: 'integer', description: 'For `capture`: round cap.' },
141
146
  roundsExecuted: { type: 'integer', description: 'For `capture`: number of rounds actually executed.' },
@@ -229,10 +234,11 @@ export function registerTranscriptTool(ctx: {
229
234
 
230
235
  // capture
231
236
  const mode = args.mode === 'normal' ? 'normal' : 'dry-run'
237
+ const taskMode = args.taskMode === 'code' || args.taskMode === 'iterate' ? args.taskMode : undefined
232
238
  const goal = typeof args.goal === 'string' ? args.goal : ''
233
239
  const maxRounds =
234
240
  typeof args.maxRounds === 'number' ? Math.floor(args.maxRounds) : 0
235
- const builder = new ReviewTranscriptBuilder({ project: projectRoot, mode, approval, goal, maxRounds })
241
+ const builder = new ReviewTranscriptBuilder({ project: projectRoot, mode, taskMode, approval, goal, maxRounds })
236
242
  const report = args.report as Record<string, unknown> | null | undefined
237
243
  const reportFindings: unknown =
238
244
  report && typeof report === 'object' && Array.isArray(report.findings)
@@ -300,6 +306,7 @@ function rehydrateBuilder(manifest: TranscriptManifest, approval: 'ask' | 'deny'
300
306
  const builder = new ReviewTranscriptBuilder({
301
307
  project: manifest.project,
302
308
  mode: manifest.mode ?? null,
309
+ taskMode: manifest.taskMode ?? null,
303
310
  approval,
304
311
  goal: manifest.goal,
305
312
  maxRounds: manifest.maxRounds,
package/src/transcript.ts CHANGED
@@ -126,6 +126,7 @@ function mergeReportIntoThread(
126
126
  export class ReviewTranscriptBuilder {
127
127
  private readonly project: string
128
128
  private readonly mode: 'dry-run' | 'normal' | null
129
+ private readonly taskMode: 'code' | 'iterate' | null
129
130
  private readonly approval: 'ask' | 'deny' | 'allow'
130
131
  private goal = ''
131
132
  private readonly phases: string[] = []
@@ -144,6 +145,7 @@ export class ReviewTranscriptBuilder {
144
145
  constructor(input: {
145
146
  project: string
146
147
  mode?: 'dry-run' | 'normal' | null
148
+ taskMode?: 'code' | 'iterate' | null
147
149
  approval?: 'ask' | 'deny' | 'allow'
148
150
  goal?: string
149
151
  maxRounds?: number
@@ -152,6 +154,14 @@ export class ReviewTranscriptBuilder {
152
154
  this.project = input.project || ''
153
155
  this.mode =
154
156
  input.mode === 'dry-run' || input.mode === 'normal' ? input.mode : null
157
+ // v3.0: task_mode indicator. An explicit valid value wins; otherwise a
158
+ // run that exercises the review loop (any mode) defaults to "iterate".
159
+ this.taskMode =
160
+ input.taskMode === 'code' || input.taskMode === 'iterate'
161
+ ? input.taskMode
162
+ : input.mode !== null && input.mode !== undefined
163
+ ? 'iterate'
164
+ : null
155
165
  this.approval =
156
166
  input.approval === 'ask' || input.approval === 'deny' || input.approval === 'allow'
157
167
  ? input.approval
@@ -393,6 +403,7 @@ export class ReviewTranscriptBuilder {
393
403
  updatedAt: this.updatedAt,
394
404
  active: this.active,
395
405
  mode: this.mode,
406
+ taskMode: this.taskMode,
396
407
  goal: this.goal,
397
408
  phases: this.phases,
398
409
  round: this.round,
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
  }