iterate-plugin 2.7.1 → 2.7.3

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/lib/parse.js CHANGED
@@ -160,6 +160,139 @@ export function scanSessionForReport(session) {
160
160
  return null
161
161
  }
162
162
 
163
+ // ─── Run-summary / meta-review verdict detection ─────────────────────────────
164
+
165
+ /**
166
+ * Check whether `obj` is an iterate dry-run run-summary object (the structured
167
+ * object returned by the workflow at the end of a dry-run). It wraps the
168
+ * ReviewReport and carries the meta-review verdict:
169
+ * { mode, goal, rounds, converged, ..., report, metaReview, finalReport }
170
+ * The discriminator is `finalReport.verdict`, which only the meta-review
171
+ * closing step produces ("approved" | "needs_revision"). This shape is distinct
172
+ * from a ReviewReport (which has `convergence`/`findings`/`rounds`), so it never
173
+ * collides with `isReviewReport`.
174
+ *
175
+ * @param {unknown} obj
176
+ * @returns {obj is Record<string, unknown>}
177
+ */
178
+ export function isRunSummary(obj) {
179
+ if (!obj || typeof obj !== 'object') return false
180
+ const o = /** @type {Record<string, unknown>} */ (obj)
181
+ const final = o.finalReport
182
+ return !!final &&
183
+ typeof final === 'object' &&
184
+ (final.verdict === 'approved' || final.verdict === 'needs_revision')
185
+ }
186
+
187
+ /**
188
+ * Deep-scan an object tree for the first iterate run-summary (same traversal
189
+ * semantics as `findReportInObject`, with circular-reference + depth guards).
190
+ *
191
+ * @param {unknown} obj
192
+ * @param {Set<unknown>} [seen]
193
+ * @param {number} [maxDepth=20]
194
+ * @returns {Record<string, unknown> | null}
195
+ */
196
+ export function findRunSummaryInObject(obj, seen, maxDepth = 20) {
197
+ if (maxDepth <= 0) return null
198
+ if (!obj || typeof obj !== 'object') return null
199
+
200
+ const s = seen || new Set()
201
+ if (s.has(obj)) return null
202
+ s.add(obj)
203
+
204
+ if (isRunSummary(obj)) return /** @type {Record<string, unknown>} */ (obj)
205
+
206
+ if (Array.isArray(obj)) {
207
+ for (const item of obj) {
208
+ const found = findRunSummaryInObject(item, s, maxDepth - 1)
209
+ if (found) return found
210
+ }
211
+ return null
212
+ }
213
+
214
+ const o = /** @type {Record<string, unknown>} */ (obj)
215
+ for (const key of Object.keys(o)) {
216
+ const val = o[key]
217
+ if (val && typeof val === 'object') {
218
+ const found = findRunSummaryInObject(val, s, maxDepth - 1)
219
+ if (found) return found
220
+ }
221
+ }
222
+
223
+ return null
224
+ }
225
+
226
+ /**
227
+ * Scan a session snapshot (or any object) for the latest iterate dry-run
228
+ * run-summary that exposes a meta-review verdict. Prefers the most recent.
229
+ *
230
+ * @param {unknown} session
231
+ * @returns {Record<string, unknown> | null}
232
+ */
233
+ export function scanSessionForRunSummary(session) {
234
+ if (!session || typeof session !== 'object') return null
235
+
236
+ const direct = findRunSummaryInObject(session)
237
+ if (direct) return direct
238
+
239
+ const s = /** @type {Record<string, unknown>} */ (session)
240
+
241
+ // Common pattern: session.toolCalls[].result contains a run summary.
242
+ if (Array.isArray(s.toolCalls)) {
243
+ const calls = /** @type {Array<Record<string, unknown>>} */ (s.toolCalls)
244
+ for (let i = calls.length - 1; i >= 0; i--) {
245
+ const call = calls[i]
246
+ if (!call) continue
247
+ if (call.tool === 'workflow' || String(call.tool ?? '').endsWith('workflow')) {
248
+ const found = findRunSummaryInObject(call.result, undefined, 24)
249
+ if (found) return found
250
+ }
251
+ }
252
+ }
253
+
254
+ // Common pattern: assistant message content holding the workflow return.
255
+ if (Array.isArray(s.messages)) {
256
+ const msgs = /** @type {Array<Record<string, unknown>>} */ (s.messages)
257
+ for (let i = msgs.length - 1; i >= 0; i--) {
258
+ const msg = msgs[i]
259
+ if (!msg) continue
260
+ const found = findRunSummaryInObject(msg.content)
261
+ if (found) return found
262
+ }
263
+ }
264
+
265
+ return null
266
+ }
267
+
268
+ /**
269
+ * Extract a compact, UI-friendly verdict from a run-summary object.
270
+ * Returns null when the object is not a valid run-summary.
271
+ *
272
+ * @param {Record<string, unknown> | null | undefined} runSummary
273
+ * @returns {{ verdict: 'approved' | 'needs_revision', reportIssues: number, checksRun: number, converged: boolean, totalRounds: number, totalFindings: number } | null}
274
+ */
275
+ export function extractVerdict(runSummary) {
276
+ if (!isRunSummary(runSummary)) return null
277
+ const o = /** @type {Record<string, unknown>} */ (runSummary)
278
+ const final = /** @type {Record<string, unknown>} */ (o.finalReport)
279
+ const meta = final.metaReview && typeof final.metaReview === 'object'
280
+ ? /** @type {Record<string, unknown>} */ (final.metaReview)
281
+ : {}
282
+ const issues = Array.isArray(meta.issues) ? meta.issues : []
283
+ // `totalRounds` may be a bare number (dry-run returns `rounds`) or a count.
284
+ const roundsVal = o.rounds
285
+ const totalRounds = typeof roundsVal === 'number' ? roundsVal : (Array.isArray(roundsVal) ? roundsVal.length : 0)
286
+ return {
287
+ verdict: final.verdict === 'needs_revision' ? 'needs_revision' : 'approved',
288
+ reportIssues: issues.length,
289
+ checksRun: typeof meta.checksRun === 'number' ? meta.checksRun : 0,
290
+ converged: o.converged === true,
291
+ totalRounds,
292
+ totalFindings: typeof o.totalFindings === 'number' ? o.totalFindings : 0,
293
+ }
294
+ }
295
+
163
296
  // ─── Normalization ───────────────────────────────────────────────────────────
164
297
 
165
298
  /**
@@ -193,7 +326,9 @@ export function normalizeReport(report) {
193
326
  }
194
327
 
195
328
  // Compute summary if missing. Always build a NEW object so the input's
196
- // summary (or any other field) is never mutated.
329
+ // summary (or any other field) is never mutated. `fixedCount` (normal mode
330
+ // only) is carried through so the dashboard fix-count metric survives
331
+ // normalization.
197
332
  let summary = report.summary
198
333
  if (!summary || typeof summary !== 'object') {
199
334
  summary = computeSummaryFromFindings(findings)
@@ -209,6 +344,7 @@ export function normalizeReport(report) {
209
344
  byDimension: s.byDimension && typeof s.byDimension === 'object'
210
345
  ? s.byDimension
211
346
  : computed.byDimension,
347
+ ...(typeof s.fixedCount === 'number' ? { fixedCount: s.fixedCount } : {}),
212
348
  }
213
349
  }
214
350
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "2.7.1",
3
+ "version": "2.7.3",
4
4
  "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus a dry-run pure-review mode with multi-round convergence and a meta-review that audits the report and emits a final review report.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,8 +53,10 @@
53
53
  },
54
54
  "scripts": {
55
55
  "build": "tsc -p tsconfig.build.json",
56
- "prepublishOnly": "npm run build",
56
+ "build:client": "node scripts/build-client.mjs",
57
+ "prepublishOnly": "npm run build && npm run build:client",
57
58
  "typecheck": "tsc --noEmit",
59
+ "typecheck:client": "tsc --noEmit -p tsconfig.client.json",
58
60
  "test": "tsx --test test/*.test.ts",
59
61
  "test:validate": "tsx --test test/validate.test.ts"
60
62
  },
@@ -67,6 +69,8 @@
67
69
  "@deepseek-ai/dsh-session": "0.1.0-rc.6",
68
70
  "@types/js-yaml": "4.0.9",
69
71
  "@types/node": "22.15.0",
72
+ "@types/react": "19.2.2",
73
+ "esbuild": "0.25.12",
70
74
  "tsx": "4.20.3",
71
75
  "typescript": "5.9.3"
72
76
  },