iterate-plugin 3.5.4 → 3.5.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.
- package/dist/atomic-fs.js +22 -4
- package/dist/config-loader.js +8 -3
- package/dist/config-write.js +50 -9
- package/dist/evidence.js +28 -1
- package/dist/git-scope.js +14 -5
- package/dist/jobs.js +15 -1
- package/dist/live.js +39 -22
- package/dist/meta-review.js +2 -2
- package/dist/review-scope.js +6 -2
- package/dist/review.js +7 -6
- package/dist/skill-prompt.js +3 -3
- package/dist/tools/decision-log.js +142 -17
- package/dist/tools/fix.js +24 -3
- package/dist/tools/prune.js +35 -21
- package/dist/tools/triage.js +3 -2
- package/dist/transcript.js +58 -32
- package/lib/client.js +4 -0
- package/lib/parse.js +6 -0
- package/package.json +8 -7
- package/src/atomic-fs.ts +20 -4
- package/src/config-loader.ts +8 -3
- package/src/config-write.ts +59 -9
- package/src/evidence.ts +29 -1
- package/src/git-scope.ts +14 -3
- package/src/jobs.ts +15 -1
- package/src/live.ts +41 -22
- package/src/meta-review.ts +2 -2
- package/src/review-scope.ts +6 -2
- package/src/review.ts +7 -6
- package/src/skill-prompt.ts +3 -3
- package/src/tools/decision-log.ts +136 -16
- package/src/tools/fix.ts +25 -3
- package/src/tools/prune.ts +30 -18
- package/src/tools/triage.ts +3 -2
- package/src/transcript.ts +59 -29
package/src/transcript.ts
CHANGED
|
@@ -71,6 +71,20 @@ function clampStringList(source: string[], cap: number): string[] {
|
|
|
71
71
|
return out
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Deep-clone a decision-timeline payload so it is decoupled from the caller's
|
|
76
|
+
* live object. structuredClone is available in every supported Node release;
|
|
77
|
+
* on the off chance it is missing, fall back to a JSON round-trip (decision
|
|
78
|
+
* payloads are JSON-serializable by construction).
|
|
79
|
+
*/
|
|
80
|
+
function cloneDecisionData(data: Record<string, unknown>): Record<string, unknown> {
|
|
81
|
+
try {
|
|
82
|
+
return structuredClone(data)
|
|
83
|
+
} catch {
|
|
84
|
+
return JSON.parse(JSON.stringify(data)) as Record<string, unknown>
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
/** Normalize a single finding, dropping malformed entries. */
|
|
75
89
|
function normalizeFinding(input: unknown): TranscriptFinding | null {
|
|
76
90
|
if (!input || typeof input !== 'object') return null
|
|
@@ -113,6 +127,19 @@ interface LiveRound {
|
|
|
113
127
|
threads: LiveThread[]
|
|
114
128
|
}
|
|
115
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Append one raw finding to a thread's list, bound to MAX_FINDINGS_PER_THREAD
|
|
132
|
+
* with NEWEST-WINS eviction (drop the oldest findings once the cap is hit).
|
|
133
|
+
* A single oversized fixer agent could otherwise OOM the client with an
|
|
134
|
+
* unbounded finding dump inside one thread.
|
|
135
|
+
*/
|
|
136
|
+
function pushThreadFinding(thread: LiveThread, raw: unknown): void {
|
|
137
|
+
thread.findings.push(raw)
|
|
138
|
+
if (thread.findings.length > MAX_FINDINGS_PER_THREAD) {
|
|
139
|
+
thread.findings.splice(0, thread.findings.length - MAX_FINDINGS_PER_THREAD)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
116
143
|
/** Merge a report snapshot's findings/readFiles into a thread by dimension. */
|
|
117
144
|
function mergeReportIntoThread(
|
|
118
145
|
thread: LiveThread,
|
|
@@ -120,8 +147,7 @@ function mergeReportIntoThread(
|
|
|
120
147
|
readFiles: readonly unknown[] | undefined,
|
|
121
148
|
): void {
|
|
122
149
|
for (const raw of findings) {
|
|
123
|
-
|
|
124
|
-
if (f) thread.findings.push(raw)
|
|
150
|
+
if (normalizeFinding(raw)) pushThreadFinding(thread, raw)
|
|
125
151
|
}
|
|
126
152
|
for (const r of readFiles ?? []) {
|
|
127
153
|
if (typeof r === 'string') thread.readFiles.push(r)
|
|
@@ -146,6 +172,7 @@ export class ReviewTranscriptBuilder {
|
|
|
146
172
|
private readonly rounds: LiveRound[] = []
|
|
147
173
|
private readonly convergence: number[] = []
|
|
148
174
|
private readonly globalFindings: TranscriptFinding[] = []
|
|
175
|
+
private readonly globalSeenKeys = new Set<string>()
|
|
149
176
|
private readonly fixes: TranscriptFix[] = []
|
|
150
177
|
private checkpoint: TranscriptCheckpoint | null = null
|
|
151
178
|
private readonly timeline: TranscriptEntry[] = []
|
|
@@ -282,12 +309,11 @@ export class ReviewTranscriptBuilder {
|
|
|
282
309
|
for (const raw of findings) {
|
|
283
310
|
const f = normalizeFinding(raw)
|
|
284
311
|
if (f) {
|
|
285
|
-
thread
|
|
286
|
-
this.
|
|
312
|
+
pushThreadFinding(thread, raw)
|
|
313
|
+
this.addGlobal(f)
|
|
287
314
|
}
|
|
288
315
|
}
|
|
289
316
|
}
|
|
290
|
-
this.reevaluateGlobal()
|
|
291
317
|
this.touch()
|
|
292
318
|
}
|
|
293
319
|
|
|
@@ -299,9 +325,8 @@ export class ReviewTranscriptBuilder {
|
|
|
299
325
|
mergeReportIntoThread(thread, findings, readFiles)
|
|
300
326
|
for (const raw of findings) {
|
|
301
327
|
const f = normalizeFinding(raw)
|
|
302
|
-
if (f) this.
|
|
328
|
+
if (f) this.addGlobal(f)
|
|
303
329
|
}
|
|
304
|
-
this.reevaluateGlobal()
|
|
305
330
|
this.touch()
|
|
306
331
|
}
|
|
307
332
|
|
|
@@ -390,8 +415,12 @@ export class ReviewTranscriptBuilder {
|
|
|
390
415
|
: this.round,
|
|
391
416
|
type,
|
|
392
417
|
data:
|
|
393
|
-
entry.data && typeof entry.data === 'object'
|
|
394
|
-
|
|
418
|
+
entry.data && typeof entry.data === 'object' && !Array.isArray(entry.data)
|
|
419
|
+
// Deep clone so a caller mutating its own object later can never
|
|
420
|
+
// alias into the timeline (serialize would then emit the mutated
|
|
421
|
+
// values instead of what was actually decided). Decision-log payloads
|
|
422
|
+
// are small JSON, so the clone cost is negligible.
|
|
423
|
+
? cloneDecisionData(entry.data as Record<string, unknown>)
|
|
395
424
|
: {},
|
|
396
425
|
})
|
|
397
426
|
if (this.timeline.length > MAX_TIMELINE) {
|
|
@@ -441,10 +470,9 @@ export class ReviewTranscriptBuilder {
|
|
|
441
470
|
maxRounds: this.maxRounds,
|
|
442
471
|
rounds,
|
|
443
472
|
convergence: this.convergence,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
: this.globalFindings,
|
|
473
|
+
// addGlobal maintains the newest-wins cap incrementally, so the array is
|
|
474
|
+
// already ≤ MAX_FINDINGS_TOTAL — no post-hoc slice needed.
|
|
475
|
+
findings: this.globalFindings,
|
|
448
476
|
fixes: this.fixes,
|
|
449
477
|
checkpoint: this.checkpoint,
|
|
450
478
|
timeline: this.timeline,
|
|
@@ -477,22 +505,24 @@ export class ReviewTranscriptBuilder {
|
|
|
477
505
|
return live.threads.length
|
|
478
506
|
}
|
|
479
507
|
|
|
480
|
-
/**
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
508
|
+
/**
|
|
509
|
+
* Insert one deduplicated finding into the compact global list.
|
|
510
|
+
* NEWEST-WINS: when the global cap (MAX_FINDINGS_TOTAL) is hit, the oldest
|
|
511
|
+
* kept finding is evicted so fresh, still-relevant findings survive — this
|
|
512
|
+
* is the documented contract. Amortized O(1): dedupe via a seen-key set and
|
|
513
|
+
* eviction is one `shift` per overflow insert.
|
|
514
|
+
*/
|
|
515
|
+
private addGlobal(f: TranscriptFinding): void {
|
|
516
|
+
const key = `${f.file}\u0000${f.line ?? 0}\u0000${f.dimension}\u0000${f.summary}`
|
|
517
|
+
if (this.globalSeenKeys.has(key)) return
|
|
518
|
+
this.globalSeenKeys.add(key)
|
|
519
|
+
this.globalFindings.push(f)
|
|
520
|
+
if (this.globalFindings.length > MAX_FINDINGS_TOTAL) {
|
|
521
|
+
const evicted = this.globalFindings.shift()
|
|
522
|
+
if (evicted) {
|
|
523
|
+
this.globalSeenKeys.delete(
|
|
524
|
+
`${evicted.file}\u0000${evicted.line ?? 0}\u0000${evicted.dimension}\u0000${evicted.summary}`,
|
|
525
|
+
)
|
|
496
526
|
}
|
|
497
527
|
}
|
|
498
528
|
}
|