iterate-plugin 3.3.1 → 3.4.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.
- package/README.md +24 -9
- package/README.zh-CN.md +19 -9
- package/dist/config-write.js +27 -0
- package/dist/git-scope.js +3 -2
- package/dist/review-scope.js +11 -3
- package/dist/review.js +13 -3
- package/dist/skill-prompt.js +16 -5
- package/dist/tools/checkpoint.js +63 -5
- package/dist/tools/defense-events.js +3 -0
- package/dist/tools/defense-store.js +17 -3
- package/dist/tools/history.js +1 -1
- package/dist/tools/quality-store.js +44 -2
- package/dist/transcript.js +6 -0
- package/lib/client.js +81 -11
- package/lib/parse.js +53 -6
- package/package.json +4 -2
- package/src/client/index.ts +49 -7
- package/src/config-write.ts +27 -0
- package/src/git-scope.ts +3 -2
- package/src/review-scope.ts +10 -3
- package/src/review.ts +18 -10
- package/src/skill-prompt.ts +16 -5
- package/src/tools/checkpoint.ts +71 -6
- package/src/tools/defense-events.ts +4 -0
- package/src/tools/defense-store.ts +21 -4
- package/src/tools/history.ts +1 -1
- package/src/tools/quality-store.ts +48 -3
- package/src/transcript.ts +7 -0
package/src/skill-prompt.ts
CHANGED
|
@@ -143,9 +143,20 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
143
143
|
// Feed the DEDUPED + already-filtered set back (not raw findings) so the known
|
|
144
144
|
// list stays bounded and reviewers never see the same issue twice.
|
|
145
145
|
if (agg && agg.report && Array.isArray(agg.report.findings)) known = agg.report.findings
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
// A round counts as "converged" ONLY when the aggregate accepted it
|
|
147
|
+
// (schema-valid) AND it genuinely found zero NEW findings. If reviewers all
|
|
148
|
+
// failed or their output stayed schema-invalid after retries, the round is
|
|
149
|
+
// INCONCLUSIVE — never report an empty-but-broken round as converged (an
|
|
150
|
+
// invalid/failed round must fail the run, not masquerade as a clean pass).
|
|
151
|
+
if (!schemaInvalid) {
|
|
152
|
+
const newCount =
|
|
153
|
+
agg && agg.report && agg.report.convergence
|
|
154
|
+
? agg.report.convergence.findingsByRound[r - 1]
|
|
155
|
+
: thisRound.findings.length
|
|
156
|
+
if (newCount === 0) {
|
|
157
|
+
log('round ' + r + ' found 0 new findings — converged')
|
|
158
|
+
break
|
|
159
|
+
}
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
|
|
@@ -238,7 +249,7 @@ if (checkpoint) {
|
|
|
238
249
|
|
|
239
250
|
phase('plan')
|
|
240
251
|
const configRes = await agent(
|
|
241
|
-
'Call iterate_config({
|
|
252
|
+
'Call iterate_config({}) and return the config JSON.',
|
|
242
253
|
Object.assign({ label: 'config:read' }, backend)
|
|
243
254
|
)
|
|
244
255
|
const cfg = (configRes && configRes.config) ? configRes.config : null
|
|
@@ -421,7 +432,7 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
421
432
|
Object.assign({ label: 'checkpoint:save:r' + r }, backend)
|
|
422
433
|
)
|
|
423
434
|
|
|
424
|
-
if (atomic.length === 0 && remaining.length === 0) {
|
|
435
|
+
if (atomic.length === 0 && remaining.length === 0 && !schemaInvalid) {
|
|
425
436
|
log('round ' + r + ' found nothing to fix — converged')
|
|
426
437
|
converged = true
|
|
427
438
|
break
|
package/src/tools/checkpoint.ts
CHANGED
|
@@ -16,7 +16,10 @@ import { resolveProjectRootForExec } from '../config-loader.ts'
|
|
|
16
16
|
import { checkpointPath, iterateDir, transcriptPath } from '../paths.ts'
|
|
17
17
|
import { readRegistry } from './fix.ts'
|
|
18
18
|
import { readDecisionEntries } from './decision-log.ts'
|
|
19
|
-
import
|
|
19
|
+
import { readQualityGate } from './quality-store.ts'
|
|
20
|
+
import { readExperienceBank } from './experience-store.ts'
|
|
21
|
+
import { readDefenseEvents } from './defense-store.ts'
|
|
22
|
+
import type { DefenseEventType, IterationCheckpoint, IterationStatus, QualityGateSnapshot } from '../types.ts'
|
|
20
23
|
|
|
21
24
|
// ─── Pure helpers (exported for unit tests) ─────────────────────────────────
|
|
22
25
|
|
|
@@ -90,6 +93,12 @@ export function computeStatus(input: {
|
|
|
90
93
|
taskMode?: 'code' | 'iterate' | null
|
|
91
94
|
decisionEntries: { timestamp: string; type: string; round?: number; data?: Record<string, unknown> }[]
|
|
92
95
|
fixRegistry: { rounds: { round: number; fixedCount: number; failedCount: number }[] }
|
|
96
|
+
/** v3.0: persisted quality-gate snapshot (optional; absent → omitted). */
|
|
97
|
+
qualityGate?: QualityGateSnapshot | null
|
|
98
|
+
/** v3.0: experience bank summary (optional; absent → omitted). */
|
|
99
|
+
experienceBank?: { totalEntries: number; totalHits: number } | null
|
|
100
|
+
/** v3.0: defense events summary (optional; absent → omitted). */
|
|
101
|
+
defenseEvents?: { totalEvents: number; counts: Record<DefenseEventType, number> } | null
|
|
93
102
|
}): IterationStatus {
|
|
94
103
|
const checkpoint = input.checkpoint
|
|
95
104
|
const taskMode = input.taskMode ?? null
|
|
@@ -134,6 +143,12 @@ export function computeStatus(input: {
|
|
|
134
143
|
resumeCount: checkpoint?.resumeCount ?? 0,
|
|
135
144
|
checkpoint,
|
|
136
145
|
lastUpdated,
|
|
146
|
+
// v3.0: quality command-center snapshots (present only when the caller
|
|
147
|
+
// supplied a real snapshot — the status never fabricates one that is not
|
|
148
|
+
// on disk, and never emits null for an absent optional field).
|
|
149
|
+
...(input.qualityGate != null ? { qualityGate: input.qualityGate } : {}),
|
|
150
|
+
...(input.experienceBank != null ? { experienceBank: input.experienceBank } : {}),
|
|
151
|
+
...(input.defenseEvents != null ? { defenseEvents: input.defenseEvents } : {}),
|
|
137
152
|
}
|
|
138
153
|
}
|
|
139
154
|
|
|
@@ -148,14 +163,16 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
|
|
|
148
163
|
defineTool({
|
|
149
164
|
name: 'iterate_checkpoint',
|
|
150
165
|
description:
|
|
151
|
-
'Save / load / clear the iteration checkpoint. The workflow saves a checkpoint at the start of ' +
|
|
152
|
-
'each round (so a long run can resume) and clears it when the iteration completes.'
|
|
166
|
+
'Save / load / resume / clear the iteration checkpoint. The workflow saves a checkpoint at the start of ' +
|
|
167
|
+
'each round (so a long run can resume) and clears it when the iteration completes. ' +
|
|
168
|
+
'`resume` loads an existing checkpoint, bumps its resumeCount, and persists it back — ' +
|
|
169
|
+
'call it when continuing an interrupted run so the resume counter stays accurate.',
|
|
153
170
|
parameters: {
|
|
154
171
|
operation: {
|
|
155
172
|
type: 'string',
|
|
156
173
|
required: true,
|
|
157
|
-
description: '"save" to persist the current progress, "load" to read it back, "clear" to remove it.',
|
|
158
|
-
enum: ['save', 'load', 'clear'],
|
|
174
|
+
description: '"save" to persist the current progress, "load" to read it back, "resume" to load + count a resumption, "clear" to remove it.',
|
|
175
|
+
enum: ['save', 'load', 'resume', 'clear'],
|
|
159
176
|
},
|
|
160
177
|
mode: { type: 'string', description: 'Required for save: "dry-run" or "normal".' },
|
|
161
178
|
round: { type: 'integer', description: 'Required for save: current round number (0 = none started).' },
|
|
@@ -194,6 +211,28 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
|
|
|
194
211
|
return { operation: 'load', ok: true, checkpoint: checkpoint as unknown as JsonValue | null }
|
|
195
212
|
}
|
|
196
213
|
|
|
214
|
+
if (args.operation === 'resume') {
|
|
215
|
+
const current = readCheckpoint(projectRoot)
|
|
216
|
+
if (!current) {
|
|
217
|
+
return { operation: 'resume', ok: false, error: 'no checkpoint to resume — run `save` first' }
|
|
218
|
+
}
|
|
219
|
+
const resumed: IterationCheckpoint = {
|
|
220
|
+
...current,
|
|
221
|
+
resumeCount: (current.resumeCount ?? 0) + 1,
|
|
222
|
+
updatedAt: new Date().toISOString(),
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
mkdirSync(iterateDir(projectRoot), { recursive: true })
|
|
226
|
+
const cpPath = checkpointPath(projectRoot)
|
|
227
|
+
const tmpPath = `${cpPath}.tmp-${Date.now()}`
|
|
228
|
+
writeFileSync(tmpPath, JSON.stringify(resumed, null, 2), 'utf-8')
|
|
229
|
+
renameSync(tmpPath, cpPath)
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return { operation: 'resume', ok: false, error: `failed to persist resumed checkpoint: ${String(err)}` }
|
|
232
|
+
}
|
|
233
|
+
return { operation: 'resume', ok: true, checkpoint: resumed as unknown as JsonValue }
|
|
234
|
+
}
|
|
235
|
+
|
|
197
236
|
if (args.operation === 'clear') {
|
|
198
237
|
const existed = existsSync(checkpointPath(projectRoot))
|
|
199
238
|
if (existed) {
|
|
@@ -239,7 +278,7 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
|
|
|
239
278
|
return { operation: 'save', ok: true, checkpoint: checkpoint as unknown as JsonValue }
|
|
240
279
|
}
|
|
241
280
|
|
|
242
|
-
return { operation: args.operation, ok: false, error: 'unknown operation. Use "save", "load", or "clear".' }
|
|
281
|
+
return { operation: args.operation, ok: false, error: 'unknown operation. Use "save", "load", "resume", or "clear".' }
|
|
243
282
|
},
|
|
244
283
|
}),
|
|
245
284
|
)
|
|
@@ -280,6 +319,9 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
280
319
|
interrupted: { type: 'boolean', description: 'True when a checkpoint exists, meaning the previous run was interrupted before finishing.' },
|
|
281
320
|
resumeCount: { type: 'integer', description: 'How many times the current checkpoint has already been resumed.' },
|
|
282
321
|
lastUpdated: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
322
|
+
qualityGate: { type: 'json', description: 'v3.0: persisted quality-gate snapshot (.iterate/quality-gate.json), when present.' },
|
|
323
|
+
experienceBank: { type: 'json', description: 'v3.0: experience bank summary (.iterate/experience.json), when present.' },
|
|
324
|
+
defenseEvents: { type: 'json', description: 'v3.0: defense events summary (.iterate/defense-events.json), when present.' },
|
|
283
325
|
error: { type: 'string' },
|
|
284
326
|
},
|
|
285
327
|
},
|
|
@@ -302,11 +344,31 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
302
344
|
const resolved = resolveProjectRootForExec(exec, args.path)
|
|
303
345
|
if (!resolved.ok) return { ok: false, error: resolved.reason }
|
|
304
346
|
const projectRoot = resolved.root
|
|
347
|
+
// v3.0: surface the persisted quality command-center snapshots so a
|
|
348
|
+
// single `iterate_status` call reports the whole run state — the gate,
|
|
349
|
+
// the experience bank, and the defense event stream. Each read is
|
|
350
|
+
// defensive (missing/malformed files yield an empty snapshot), so the
|
|
351
|
+
// status never crashes on absent artifacts.
|
|
352
|
+
const qualityGate = readQualityGate(projectRoot)
|
|
353
|
+
const experienceBank = readExperienceBank(projectRoot)
|
|
354
|
+
const defenseEvents = readDefenseEvents(projectRoot)
|
|
305
355
|
const status = computeStatus({
|
|
306
356
|
checkpoint: readCheckpoint(projectRoot),
|
|
307
357
|
taskMode: readTranscriptTaskMode(projectRoot),
|
|
308
358
|
decisionEntries: readDecisionEntries(projectRoot),
|
|
309
359
|
fixRegistry: readRegistry(projectRoot),
|
|
360
|
+
qualityGate:
|
|
361
|
+
qualityGate.dimensions.length > 0 || qualityGate.overallStatus === 'pass' || qualityGate.overallStatus === 'fail'
|
|
362
|
+
? qualityGate
|
|
363
|
+
: null,
|
|
364
|
+
experienceBank: {
|
|
365
|
+
totalEntries: experienceBank.entries.length,
|
|
366
|
+
totalHits: experienceBank.totalHits ?? 0,
|
|
367
|
+
},
|
|
368
|
+
defenseEvents: {
|
|
369
|
+
totalEvents: defenseEvents.events.length,
|
|
370
|
+
counts: defenseEvents.counts,
|
|
371
|
+
},
|
|
310
372
|
})
|
|
311
373
|
return {
|
|
312
374
|
ok: true,
|
|
@@ -322,6 +384,9 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
322
384
|
interrupted: status.interrupted,
|
|
323
385
|
resumeCount: status.resumeCount,
|
|
324
386
|
lastUpdated: status.lastUpdated ?? null,
|
|
387
|
+
qualityGate: status.qualityGate ? (status.qualityGate as unknown as JsonValue) : null,
|
|
388
|
+
experienceBank: status.experienceBank ? (status.experienceBank as unknown as JsonValue) : null,
|
|
389
|
+
defenseEvents: status.defenseEvents ? (status.defenseEvents as unknown as JsonValue) : null,
|
|
325
390
|
}
|
|
326
391
|
},
|
|
327
392
|
}),
|
|
@@ -55,6 +55,7 @@ function validateRecordInput(args: {
|
|
|
55
55
|
defense?: unknown
|
|
56
56
|
outcome?: unknown
|
|
57
57
|
severity?: unknown
|
|
58
|
+
line?: unknown
|
|
58
59
|
}): string[] {
|
|
59
60
|
const errors: string[] = []
|
|
60
61
|
if (typeof args.type !== 'string' || !EVENT_TYPES.includes(args.type as DefenseEventType)) {
|
|
@@ -72,6 +73,9 @@ function validateRecordInput(args: {
|
|
|
72
73
|
if (typeof args.outcome !== 'string' || !args.outcome.trim()) {
|
|
73
74
|
errors.push('outcome is required')
|
|
74
75
|
}
|
|
76
|
+
if (args.line !== undefined && (typeof args.line !== 'number' || !Number.isInteger(args.line) || args.line < 0)) {
|
|
77
|
+
errors.push('line must be a non-negative integer when present')
|
|
78
|
+
}
|
|
75
79
|
const severity = args.severity
|
|
76
80
|
if (severity !== 'critical' && severity !== 'high' && severity !== 'medium' && severity !== 'low') {
|
|
77
81
|
errors.push('severity must be one of critical, high, medium, low')
|
|
@@ -44,14 +44,28 @@ function emptyStream(): DefenseEventStream {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Read the defense events stream from disk.
|
|
49
|
+
* Normalizes the persisted stream so a hand-edited / partial file can never
|
|
50
|
+
* produce NaN counts: `counts` is recomputed from the events when missing or
|
|
51
|
+
* malformed, and every type key is guaranteed present.
|
|
52
|
+
*/
|
|
48
53
|
export function readDefenseEvents(projectRoot: string): DefenseEventStream {
|
|
49
54
|
const filePath = path.join(projectRoot, '.iterate', DEFENSE_EVENTS_FILE)
|
|
50
55
|
try {
|
|
51
56
|
const content = fs.readFileSync(filePath, 'utf-8')
|
|
52
|
-
const parsed = JSON.parse(content) as DefenseEventStream
|
|
57
|
+
const parsed = JSON.parse(content) as Partial<DefenseEventStream>
|
|
53
58
|
if (parsed && Array.isArray(parsed.events)) {
|
|
54
|
-
|
|
59
|
+
const events = parsed.events.filter(
|
|
60
|
+
(e): e is DefenseEvent =>
|
|
61
|
+
!!e && typeof e === 'object' && typeof (e as DefenseEvent).type === 'string',
|
|
62
|
+
)
|
|
63
|
+
const counts = computeCounts(events)
|
|
64
|
+
return {
|
|
65
|
+
events,
|
|
66
|
+
lastUpdated: typeof parsed.lastUpdated === 'string' ? parsed.lastUpdated : emptyStream().lastUpdated,
|
|
67
|
+
counts,
|
|
68
|
+
}
|
|
55
69
|
}
|
|
56
70
|
} catch {
|
|
57
71
|
// File not found or invalid JSON
|
|
@@ -95,7 +109,10 @@ export function addDefenseEvent(
|
|
|
95
109
|
...event,
|
|
96
110
|
}
|
|
97
111
|
|
|
98
|
-
|
|
112
|
+
// Always recompute from the events array instead of mutating a possibly
|
|
113
|
+
// stale/malformed persisted `counts` object — guarantees the stream counts
|
|
114
|
+
// can never drift from (or NaN out against) its events.
|
|
115
|
+
const newCounts = computeCounts(stream.events)
|
|
99
116
|
bumpCount(newCounts, event.type)
|
|
100
117
|
|
|
101
118
|
return {
|
package/src/tools/history.ts
CHANGED
|
@@ -130,7 +130,7 @@ export function registerHistoryTool(ctx: { tools: { register: (def: ReturnType<t
|
|
|
130
130
|
? `Fixes: ${fixes.totalFixed} applied · ${fixes.totalFailed} failed · across ${fixes.roundCount} round(s)`
|
|
131
131
|
: 'Fixes: none',
|
|
132
132
|
'',
|
|
133
|
-
...log.map((e) => `[${e.timestamp}] r${e.round} ${e.type}: ${JSON.stringify(e.data ?? {})}`),
|
|
133
|
+
...log.map((e) => `[${e.timestamp}] r${e.round ?? '?'} ${e.type}: ${JSON.stringify(e.data ?? {})}`),
|
|
134
134
|
]
|
|
135
135
|
return [{ type: 'text', text: lines.join('\n') }]
|
|
136
136
|
},
|
|
@@ -31,14 +31,59 @@ function emptySnapshot(): QualityGateSnapshot {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Read the quality gate snapshot from disk.
|
|
36
|
+
* Normalizes a hand-edited / partial file so readers and the tool's `render`
|
|
37
|
+
* never crash on missing arrays or non-numeric fields: `dimensions` is
|
|
38
|
+
* guaranteed to be an array and every numeric field degrades to 0.
|
|
39
|
+
*/
|
|
35
40
|
export function readQualityGate(projectRoot: string): QualityGateSnapshot {
|
|
36
41
|
const filePath = path.join(projectRoot, '.iterate', QUALITY_GATE_FILE)
|
|
37
42
|
try {
|
|
38
43
|
const content = fs.readFileSync(filePath, 'utf-8')
|
|
39
|
-
const parsed = JSON.parse(content) as QualityGateSnapshot
|
|
44
|
+
const parsed = JSON.parse(content) as Partial<QualityGateSnapshot> | null
|
|
40
45
|
if (parsed && typeof parsed === 'object') {
|
|
41
|
-
|
|
46
|
+
const num = (v: unknown): number =>
|
|
47
|
+
typeof v === 'number' && Number.isFinite(v) ? v : 0
|
|
48
|
+
const status: QualityGateSnapshot['overallStatus'] =
|
|
49
|
+
parsed.overallStatus === 'pass' || parsed.overallStatus === 'fail' || parsed.overallStatus === 'pending'
|
|
50
|
+
? parsed.overallStatus
|
|
51
|
+
: 'pending'
|
|
52
|
+
const dimensions = Array.isArray(parsed.dimensions)
|
|
53
|
+
? (parsed.dimensions as unknown as Array<Record<string, unknown>>)
|
|
54
|
+
.filter((d): d is Record<string, unknown> => !!d && typeof d === 'object' && typeof d.dimension === 'string')
|
|
55
|
+
.map((d) => {
|
|
56
|
+
const dimStatus: QualityGateDimension['status'] =
|
|
57
|
+
d.status === 'pass' || d.status === 'fail' || d.status === 'warn' ? d.status : 'warn'
|
|
58
|
+
return {
|
|
59
|
+
dimension: d.dimension as string,
|
|
60
|
+
convergenceRate: num(d.convergenceRate),
|
|
61
|
+
findingsCount: num(d.findingsCount),
|
|
62
|
+
fixedCount: num(d.fixedCount),
|
|
63
|
+
score: num(d.score),
|
|
64
|
+
status: dimStatus,
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
: []
|
|
68
|
+
return {
|
|
69
|
+
timestamp: typeof parsed.timestamp === 'string' ? parsed.timestamp : emptySnapshot().timestamp,
|
|
70
|
+
overallStatus: status,
|
|
71
|
+
overallScore: num(parsed.overallScore),
|
|
72
|
+
dimensions,
|
|
73
|
+
verificationPassRate: num(parsed.verificationPassRate),
|
|
74
|
+
totalChecks: num(parsed.totalChecks),
|
|
75
|
+
passedChecks: num(parsed.passedChecks),
|
|
76
|
+
failedChecks: num(parsed.failedChecks),
|
|
77
|
+
totalFindings: num(parsed.totalFindings),
|
|
78
|
+
criticalCount: num(parsed.criticalCount),
|
|
79
|
+
highCount: num(parsed.highCount),
|
|
80
|
+
mediumCount: num(parsed.mediumCount),
|
|
81
|
+
lowCount: num(parsed.lowCount),
|
|
82
|
+
// Only carry an own failReason when it is a real string — an absent
|
|
83
|
+
// persisted reason must not surface as `failReason: undefined` (which
|
|
84
|
+
// deep-equals differently than the JSON round-trip of computeQualityGate).
|
|
85
|
+
...(typeof parsed.failReason === 'string' ? { failReason: parsed.failReason } : {}),
|
|
86
|
+
}
|
|
42
87
|
}
|
|
43
88
|
} catch {
|
|
44
89
|
// File not found or invalid JSON
|
package/src/transcript.ts
CHANGED
|
@@ -49,6 +49,10 @@ const MAX_FINDINGS_TOTAL = 2000
|
|
|
49
49
|
/** Max timeline entries kept (newest wins). */
|
|
50
50
|
const MAX_TIMELINE = 500
|
|
51
51
|
|
|
52
|
+
/** Max applied-fix records kept (newest wins; bounded so a long run cannot
|
|
53
|
+
* grow the manifest payload without limit). */
|
|
54
|
+
const MAX_FIXES = 200
|
|
55
|
+
|
|
52
56
|
/** Thresholds applied when reducing a string list under a cap. */
|
|
53
57
|
function clampStringList(source: string[], cap: number): string[] {
|
|
54
58
|
const out: string[] = []
|
|
@@ -317,6 +321,9 @@ export class ReviewTranscriptBuilder {
|
|
|
317
321
|
typeof record.linesRemoved === 'number' ? Math.floor(record.linesRemoved) : 0,
|
|
318
322
|
success: record.success !== false,
|
|
319
323
|
})
|
|
324
|
+
if (this.fixes.length > MAX_FIXES) {
|
|
325
|
+
this.fixes.splice(0, this.fixes.length - MAX_FIXES)
|
|
326
|
+
}
|
|
320
327
|
this.touch()
|
|
321
328
|
}
|
|
322
329
|
|