iterate-plugin 3.6.0 → 3.8.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.
package/lib/parse.js CHANGED
@@ -207,9 +207,15 @@ export function countSessionImages(session) {
207
207
 
208
208
  /**
209
209
  * Check whether `obj` is a valid ReviewReport-like object.
210
- * The minimum requirement: an object with `convergence` (object),
210
+ * The minimum requirement: an object with `convergence` (a RECORD),
211
211
  * `findings` (array), and `rounds` (array).
212
212
  *
213
+ * The convergence guard is also the TranscriptManifest discriminator: a
214
+ * manifest carries `convergence` / `rounds` / `findings` too, but its
215
+ * convergence is the per-round count SERIES (an array). Since
216
+ * `typeof [] === 'object'` in JS, only an explicit array rejection keeps the
217
+ * two shapes apart — see `isTranscriptManifest`.
218
+ *
213
219
  * @param {unknown} obj
214
220
  * @returns {obj is Record<string, unknown>}
215
221
  */
@@ -217,12 +223,8 @@ export function isReviewReport(obj) {
217
223
  if (!obj || typeof obj !== 'object') return false
218
224
  const o = /** @type {Record<string, unknown>} */ (obj)
219
225
  const convergence = safeGet(o, 'convergence')
220
- return (
221
- typeof convergence === 'object' &&
222
- convergence !== null &&
223
- Array.isArray(safeGet(o, 'findings')) &&
224
- Array.isArray(safeGet(o, 'rounds'))
225
- )
226
+ if (!convergence || typeof convergence !== 'object' || Array.isArray(convergence)) return false
227
+ return Array.isArray(safeGet(o, 'findings')) && Array.isArray(safeGet(o, 'rounds'))
226
228
  }
227
229
 
228
230
  /**
@@ -585,7 +587,13 @@ export function scanSessionForTranscript(session) {
585
587
  }
586
588
  }
587
589
 
588
- return null
590
+ // Fallback: a host snapshot without the tool-call envelope (a restored
591
+ // transcript, a rehydrated turn, a shell that surfaces artifacts directly)
592
+ // may still carry a manifest somewhere in the tree. Deep-scan for it so the
593
+ // dashboard and the observatory keep filling instead of degrading to the
594
+ // first-run empty state. The manifest is returned RAW — callers normalize.
595
+ const loose = findFirstInObject(session, isTranscriptManifest)
596
+ return loose ? /** @type {Record<string, unknown>} */ (loose) : null
589
597
  }
590
598
 
591
599
  /**
@@ -975,10 +983,16 @@ export function normalizeQualityGateSnapshot(raw) {
975
983
  */
976
984
  export function scanSessionForQualityGate(session) {
977
985
  const node = latestToolResultNode(session, 'iterate_quality_gate', findQualityGateInObject)
978
- if (node === null) return null
979
- const found = findQualityGateInObject(node)
980
- if (!found) return null
981
- return normalizeQualityGateSnapshot(found)
986
+ if (node !== null) {
987
+ const found = findQualityGateInObject(node)
988
+ if (found) return normalizeQualityGateSnapshot(found)
989
+ }
990
+ // Fallback: a host snapshot without the tool-call envelope (a restored
991
+ // transcript, a rehydrated turn, a shell that surfaces artifacts directly)
992
+ // still carries the snapshot somewhere in the tree — deep-scan for it so
993
+ // the panel fills instead of claiming "not pulled yet".
994
+ const loose = findFirstInObject(session, isQualityGateSnapshot)
995
+ return loose ? normalizeQualityGateSnapshot(loose) : null
982
996
  }
983
997
 
984
998
  /**
@@ -1034,10 +1048,13 @@ export function normalizeExperienceBankResult(raw) {
1034
1048
  */
1035
1049
  export function scanSessionForExperienceBank(session) {
1036
1050
  const node = latestToolResultNode(session, 'iterate_experience', findExperienceResultInObject)
1037
- if (node === null) return null
1038
- const found = findExperienceResultInObject(node)
1039
- if (!found) return null
1040
- return normalizeExperienceBankResult(found)
1051
+ if (node !== null) {
1052
+ const found = findExperienceResultInObject(node)
1053
+ if (found) return normalizeExperienceBankResult(found)
1054
+ }
1055
+ // Same deep-scan fallback as the quality-gate scanner (see above).
1056
+ const loose = findFirstInObject(session, isExperienceBankResult)
1057
+ return loose ? normalizeExperienceBankResult(loose) : null
1041
1058
  }
1042
1059
 
1043
1060
  /**
@@ -1098,10 +1115,13 @@ export function normalizeDefenseEventsResult(raw) {
1098
1115
  */
1099
1116
  export function scanSessionForDefenseEvents(session) {
1100
1117
  const node = latestToolResultNode(session, 'iterate_defense_events', findDefenseResultInObject)
1101
- if (node === null) return null
1102
- const found = findDefenseResultInObject(node)
1103
- if (!found) return null
1104
- return normalizeDefenseEventsResult(found)
1118
+ if (node !== null) {
1119
+ const found = findDefenseResultInObject(node)
1120
+ if (found) return normalizeDefenseEventsResult(found)
1121
+ }
1122
+ // Same deep-scan fallback as the quality-gate scanner (see above).
1123
+ const loose = findFirstInObject(session, isDefenseEventsResult)
1124
+ return loose ? normalizeDefenseEventsResult(loose) : null
1105
1125
  }
1106
1126
 
1107
1127
  // ─── Run-summary / meta-review verdict detection ─────────────────────────────
@@ -1963,10 +1983,16 @@ export function toolCalledInSession(session, toolName) {
1963
1983
  *
1964
1984
  * @param {boolean} pulled whether the backing tool was called this session
1965
1985
  * @param {string} topic what is missing (e.g. "质量门禁证书")
1986
+ * @param {'zh'|'en'} [lang] UI language (the caller localizes `topic` itself)
1966
1987
  * @returns {string}
1967
1988
  */
1968
- export function diskEmptyStateText(pulled, topic) {
1969
- const subject = topic || '数据'
1989
+ export function diskEmptyStateText(pulled, topic, lang = 'zh') {
1990
+ const subject = topic || (lang === 'en' ? 'data' : '数据')
1991
+ if (lang === 'en') {
1992
+ return pulled
1993
+ ? `Pulled from disk: ${subject} has no records yet (the disk is empty).`
1994
+ : `Not pulled from disk yet: ${subject} lives under the project's .iterate/ — copy the "Pull disk snapshot" instruction back into the session and it will fill in.`
1995
+ }
1970
1996
  return pulled
1971
1997
  ? `已从磁盘拉取:${subject}暂无记录(磁盘无数据)。`
1972
1998
  : `尚未从磁盘拉取${subject}:数据存放在项目 .iterate/ 下,点「拉取磁盘快照」复制指令发回会话即可回填。`
@@ -2223,27 +2249,49 @@ export function trendMax(points) {
2223
2249
  //
2224
2250
  // Every value the iterate loop can report as `stoppedReason` — including the
2225
2251
  // schema-retry batch (schema_invalid / no_usable_reviewer_output / inconclusive)
2226
- // — maps to a concrete Chinese label. Unknown values still get a readable
2227
- // default instead of a bare "已结束". Kept in parse.js so both the client
2228
- // observatory badge and Node unit tests share one source of truth.
2252
+ // — maps to a concrete label in zh and en. Unknown values still get a readable
2253
+ // default instead of a bare "已结束" / "Done". Kept in parse.js so both the
2254
+ // client observatory badge and Node unit tests share one source of truth.
2229
2255
 
2230
2256
  /**
2231
- * Chinese badge label for a run's `stoppedReason`, or null/undefined-safe.
2257
+ * Badge label for a run's `stoppedReason`, or null/undefined-safe.
2258
+ *
2259
+ * @param {unknown} reason
2260
+ * @param {'zh'|'en'} [lang] UI language (default zh, the model-facing default)
2232
2261
  *
2233
2262
  * @param {string | null | undefined} reason
2234
2263
  * @returns {string}
2235
2264
  */
2236
- export function stoppedReasonLabel(reason) {
2237
- switch (reason) {
2238
- case 'converged': return '已结束 · 已收敛(无新发现)'
2239
- case 'max_rounds_reached': return '已结束 · 达到轮数上限'
2240
- case 'aborted_by_validation': return '已结束 · 验证失败后回滚停止'
2241
- case 'aborted_by_config': return '已结束 · 验证命令不在白名单(配置需修复)'
2242
- case 'inconclusive': return '已结束 · 轮次结论不明(审查输出无效)'
2243
- case 'schema_invalid': return '已结束 · 审查输出连续 schema 校验失败'
2244
- case 'no_usable_reviewer_output': return '已结束 · 无可用审查输出'
2245
- default: return reason ? `已结束 · ${reason}` : '已结束'
2246
- }
2265
+ export function stoppedReasonLabel(reason, lang = 'zh') {
2266
+ const zh = {
2267
+ converged: '已结束 · 已收敛(无新发现)',
2268
+ max_rounds_reached: '已结束 · 达到轮数上限',
2269
+ aborted_by_validation: '已结束 · 验证失败后回滚停止',
2270
+ aborted_by_config: '已结束 · 验证命令不在白名单(配置需修复)',
2271
+ inconclusive: '已结束 · 轮次结论不明(审查输出无效)',
2272
+ schema_invalid: '已结束 · 审查输出连续 schema 校验失败',
2273
+ no_usable_reviewer_output: '已结束 · 无可用审查输出',
2274
+ }
2275
+ const en = {
2276
+ converged: 'Done \u00b7 converged (no new findings)',
2277
+ max_rounds_reached: 'Done \u00b7 reached the round limit',
2278
+ aborted_by_validation: 'Done \u00b7 rolled back after a validation failure',
2279
+ aborted_by_config: 'Done \u00b7 validation command not allow-listed (config needs fixing)',
2280
+ inconclusive: 'Done \u00b7 inconclusive rounds (invalid review output)',
2281
+ schema_invalid: 'Done \u00b7 repeated schema validation failures in review output',
2282
+ no_usable_reviewer_output: 'Done \u00b7 no usable review output',
2283
+ }
2284
+ // Object.hasOwn, NOT `zh[reason]`: a model/hand-edited `stoppedReason` of
2285
+ // `__proto__` / `constructor` / `toString` would otherwise resolve through
2286
+ // the PROTOTYPE CHAIN to Object.prototype or a function (never a string) and
2287
+ // crash React ("Objects are not valid as a React child") when the badge
2288
+ // renders it. An own-key hit is the only way to return a table value; any
2289
+ // other value degrades to a plain text label.
2290
+ const table = lang === 'en' ? en : zh
2291
+ if (typeof reason === 'string' && Object.hasOwn(table, reason)) return table[reason]
2292
+ const text = typeof reason === 'string' && reason !== '' ? reason : ''
2293
+ if (lang === 'en') return text ? `Done \u00b7 ${text}` : 'Done'
2294
+ return text ? `已结束 · ${text}` : '已结束'
2247
2295
  }
2248
2296
 
2249
2297
  // ─── Completion notification ────────────────────────────────────────────────
@@ -2252,48 +2300,93 @@ export function stoppedReasonLabel(reason) {
2252
2300
  * One-line completion summary for notifications ("已收敛 / 已达最大轮数").
2253
2301
  *
2254
2302
  * @param {Record<string, unknown>} report
2303
+ * @param {'zh'|'en'} [lang] UI language
2255
2304
  * @returns {string}
2256
2305
  */
2257
- export function buildCompletionSummary(report) {
2306
+ export function buildCompletionSummary(report, lang = 'zh') {
2258
2307
  const conv = /** @type {Record<string, unknown>} */ (report.convergence ?? {})
2259
2308
  const rounds = getCurrentRound(report)
2260
2309
  const total = getTotalRounds(report)
2261
2310
  const stats = severityStats(report)
2262
2311
  const converged = conv.converged === true
2263
- const reason = converged ? '已收敛' : `已达最大轮数 ${total}`
2264
2312
  const totalFindings = stats.critical + stats.high + stats.medium + stats.low
2313
+ if (lang === 'en') {
2314
+ const reason = converged ? 'converged' : `reached the round limit ${total}`
2315
+ return `iterate review complete \u00b7 ${rounds}/${total} rounds \u00b7 ${totalFindings} findings \u00b7 ${reason}`
2316
+ }
2317
+ const reason = converged ? '已收敛' : `已达最大轮数 ${total}`
2265
2318
  return `iterate 评审完成 · ${rounds}/${total} 轮 · ${totalFindings} 项发现 · ${reason}`
2266
2319
  }
2267
2320
 
2268
2321
  // ─── Config edit guidance ───────────────────────────────────────────────────
2269
2322
 
2270
2323
  /**
2271
- * Editable config fields (key + label + hint), used by the settings guide.
2272
- * @type {Array<{ key: string, label: string, hint: string }>}
2324
+ * Editable config fields, used by the settings picker (rendered in the UI) and
2325
+ * by the copy-paste guide / instruction builders (model-facing, always zh).
2326
+ * `label`/`hint` are `{ zh, en }` pairs — the UI localizes them with
2327
+ * `field.label[getLang()]`, while the guide builders below read `.zh`.
2328
+ * @type {Array<{ key: string, label: { zh: string, en: string }, hint: { zh: string, en: string } }>}
2273
2329
  */
2274
2330
  export const CONFIG_EDIT_FIELDS = [
2275
- { key: 'goal', label: '目标', hint: '一句话描述本次迭代目标(字符串)' },
2276
- { key: 'dimensions', label: '审查维度', hint: '数组,如 ["correctness","security"]' },
2277
- { key: 'max_rounds', label: '最大轮数', hint: '正整数' },
2278
- { key: 'review.scope', label: '审查范围', hint: '"full" 或 "changed-only"' },
2279
- { key: 'reasoning_effort', label: '审查推理强度', hint: '"low" / "medium" / "high",缺省跟随模型默认' },
2331
+ {
2332
+ key: 'goal',
2333
+ label: { zh: '目标', en: 'Goal' },
2334
+ hint: { zh: '一句话描述本次迭代目标(字符串)', en: "One-sentence description of this iteration's goal (a string)" },
2335
+ },
2336
+ {
2337
+ key: 'dimensions',
2338
+ label: { zh: '审查维度', en: 'Review dimensions' },
2339
+ hint: { zh: '数组,如 ["correctness","security"]', en: 'Array, e.g. ["correctness","security"]' },
2340
+ },
2341
+ {
2342
+ key: 'max_rounds',
2343
+ label: { zh: '最大轮数', en: 'Max rounds' },
2344
+ hint: { zh: '正整数', en: 'Positive integer' },
2345
+ },
2346
+ {
2347
+ key: 'review.scope',
2348
+ label: { zh: '审查范围', en: 'Review scope' },
2349
+ hint: { zh: '"full" 或 "changed-only"', en: '"full" or "changed-only"' },
2350
+ },
2351
+ {
2352
+ key: 'reasoning_effort',
2353
+ label: { zh: '审查推理强度', en: 'Review reasoning effort' },
2354
+ hint: { zh: '"low" / "medium" / "high",缺省跟随模型默认', en: '"low" / "medium" / "high"; omitted = follow the model default' },
2355
+ },
2280
2356
  {
2281
2357
  key: 'validation.commands',
2282
- label: '验证命令',
2283
- hint: '数组,每项是一条白名单命令字符串,如 ["npm test","npm run lint"];每轮验证逐条执行。留空数组 = 不验证(本轮迭代不受任何测试保护)',
2358
+ label: { zh: '验证命令', en: 'Validation commands' },
2359
+ hint: {
2360
+ zh: '数组,每项是一条白名单命令字符串,如 ["npm test","npm run lint"];每轮验证逐条执行。留空数组 = 不验证(本轮迭代不受任何测试保护)',
2361
+ en: 'Array of allow-listed command strings, e.g. ["npm test","npm run lint"]; run one by one every round. An empty array = no verification (this iteration runs with no test protection)',
2362
+ },
2284
2363
  },
2285
2364
  {
2286
2365
  key: 'language',
2287
- label: '界面语言',
2288
- hint: '"zh" 或 "en"(默认 "en"):控制审批理由、防御事件标签等面向人文案的语言',
2366
+ label: { zh: '界面语言', en: 'Interface language' },
2367
+ hint: {
2368
+ zh: '"zh" 或 "en"(默认 "en"):控制审批理由、防御事件标签等面向人文案的语言',
2369
+ en: '"zh" or "en" (default "en"): language of human-facing copy such as approval rationales and defense-event labels',
2370
+ },
2289
2371
  },
2290
2372
  {
2291
2373
  key: 'personalization.known_intentional',
2292
- label: '已知有意问题',
2293
- hint: '数组,每项 {file, line?, dimension, reason};命中条目在评审中被跳过。可用分诊面板的「回读已写入条目」核对',
2374
+ label: { zh: '已知有意问题', en: 'Known intentional issues' },
2375
+ hint: {
2376
+ zh: '数组,每项 {file, line?, dimension, reason};命中条目在评审中被跳过。可用分诊面板的「回读已写入条目」核对',
2377
+ en: 'Array of {file, line?, dimension, reason}; findings hitting an entry are skipped in review. Verify via the triage panel\'s "read back written entries"',
2378
+ },
2379
+ },
2380
+ {
2381
+ key: 'atomic.max_lines',
2382
+ label: { zh: '原子修复上限行数', en: 'Atomic fix line cap' },
2383
+ hint: { zh: '正整数', en: 'Positive integer' },
2384
+ },
2385
+ {
2386
+ key: 'git.push_per_round',
2387
+ label: { zh: '每轮推送', en: 'Push per round' },
2388
+ hint: { zh: 'true / false', en: 'true / false' },
2294
2389
  },
2295
- { key: 'atomic.max_lines', label: '原子修复上限行数', hint: '正整数' },
2296
- { key: 'git.push_per_round', label: '每轮推送', hint: 'true / false' },
2297
2390
  ]
2298
2391
 
2299
2392
  /**
@@ -2307,12 +2400,13 @@ export function buildConfigEditGuide() {
2307
2400
  '---------------------',
2308
2401
  '配置文件:项目根目录 iterate.config.yaml。',
2309
2402
  '',
2310
- '⚠ 未配置 validation.commands 时迭代不被保护:每轮 validate 拿到空结果会被当作',
2311
- ' “验证通过”,等于在没有任何测试保护的情况下继续收敛。第一次启动迭代前,',
2403
+ '⚠ 未配置 validation.commands 时迭代不被保护:该轮会直接中止(fail-closed),',
2404
+ ' 空验证结果/验证未执行绝不当作“验证通过”,运行以 converged_unverified /',
2405
+ ' aborted_by_config 收尾而不是 plain converged。第一次启动迭代前,',
2312
2406
  ' 请先给 validation.commands 配上至少一条真实命令(如 npm test)。',
2313
2407
  '',
2314
2408
  '可编辑字段:',
2315
- ...CONFIG_EDIT_FIELDS.map((f) => `- ${f.key}(${f.label}):${f.hint}`),
2409
+ ...CONFIG_EDIT_FIELDS.map((f) => `- ${f.key}(${f.label.zh}):${f.hint.zh}`),
2316
2410
  '',
2317
2411
  '让模型帮你改:',
2318
2412
  '1. 调用 iterate_config({ operation: "read" }) 查看当前配置;',
@@ -2344,7 +2438,7 @@ export const CONFIG_VALUE_PLACEHOLDER = '<由模型填写:该字段的新值>'
2344
2438
  * Look up an editable config field by its dotted key (gap #8).
2345
2439
  *
2346
2440
  * @param {string} key
2347
- * @returns {{ key: string, label: string, hint: string } | null}
2441
+ * @returns {{ key: string, label: { zh: string, en: string }, hint: { zh: string, en: string } } | null}
2348
2442
  */
2349
2443
  export function configFieldByKey(key) {
2350
2444
  if (typeof key !== 'string' || !key) return null
@@ -2363,8 +2457,9 @@ export function configFieldByKey(key) {
2363
2457
  */
2364
2458
  export function buildConfigFieldInstruction(key, value) {
2365
2459
  const field = configFieldByKey(key)
2366
- const label = field ? field.label : key
2367
- const hint = field ? field.hint : '见 iterate.config.yaml'
2460
+ // Model-facing instruction text: always zh, like every other builder here.
2461
+ const label = field ? field.label.zh : key
2462
+ const hint = field ? field.hint.zh : '见 iterate.config.yaml'
2368
2463
  const updates = { [key]: value === undefined ? CONFIG_VALUE_PLACEHOLDER : value }
2369
2464
  // Delegate the payload to `buildConfigEditInstruction` so the multi-field
2370
2465
  // builder stays a live code path (it used to be test-only dead code).
@@ -2379,6 +2474,116 @@ export function buildConfigFieldInstruction(key, value) {
2379
2474
  ].join('\n')
2380
2475
  }
2381
2476
 
2477
+ // ─── Config read-back (write → read-back loop) ──────────────────────────────
2478
+ //
2479
+ // The triage panel got a read-back instruction (#7); the config editor only
2480
+ // generated writes, so there was no way to SEE what the file actually says
2481
+ // after pasting an instruction. These three pieces close that loop: an
2482
+ // instruction the settings card copies, a session scanner that picks the
2483
+ // echoed result back up, and a dotted-path resolver that surfaces the value
2484
+ // of the field the user currently has selected.
2485
+
2486
+ /**
2487
+ * Copy-paste instruction that makes the model call `iterate_config` read and
2488
+ * echo the full result (including `config`) into the session, where the
2489
+ * settings card can display it.
2490
+ *
2491
+ * @returns {string}
2492
+ */
2493
+ export function buildConfigReadInstruction() {
2494
+ return (
2495
+ '请调用 `iterate_config` 回读当前生效配置,并把完整结果(含 config 字段)原样回显,' +
2496
+ '供我核对界面上展示的当前值:\n\n' +
2497
+ '```json\n' +
2498
+ `${JSON.stringify({ operation: 'read' }, null, 2)}\n` +
2499
+ '```'
2500
+ )
2501
+ }
2502
+
2503
+ /**
2504
+ * Top-level keys of an iterate config, derived from the editable field list
2505
+ * so a newly editable section is covered by the predicate automatically.
2506
+ * @type {string[]}
2507
+ */
2508
+ const CONFIG_TOP_LEVEL_KEYS = [...new Set(CONFIG_EDIT_FIELDS.map((f) => f.key.split('.')[0]))]
2509
+
2510
+ /**
2511
+ * Is `v` an effective iterate config? Requires several known top-level keys:
2512
+ * `loadEffectiveConfig` always merges the built-in defaults, so a real config
2513
+ * is complete — while a stray `{ goal: ... }` record elsewhere in the
2514
+ * transcript cannot impersonate one.
2515
+ *
2516
+ * @param {unknown} v
2517
+ * @returns {boolean}
2518
+ */
2519
+ export function isEffectiveConfig(v) {
2520
+ if (!v || typeof v !== 'object' || Array.isArray(v)) return false
2521
+ const rec = /** @type {Record<string, unknown>} */ (v)
2522
+ let hits = 0
2523
+ for (const key of CONFIG_TOP_LEVEL_KEYS) {
2524
+ if (Object.hasOwn(rec, key)) hits += 1
2525
+ if (hits >= 3) return true
2526
+ }
2527
+ return false
2528
+ }
2529
+
2530
+ /**
2531
+ * Locate the config inside one `iterate_config` tool result. Reads and writes
2532
+ * both report it under `config`; a section read (`data`) is deliberately not
2533
+ * accepted because it is only a slice.
2534
+ *
2535
+ * @param {unknown} v
2536
+ * @returns {Record<string, unknown> | null}
2537
+ */
2538
+ export function findConfigInResult(v) {
2539
+ if (!v || typeof v !== 'object' || Array.isArray(v)) return null
2540
+ const rec = /** @type {Record<string, unknown>} */ (v)
2541
+ const cfg = safeGet(rec, 'config')
2542
+ if (isEffectiveConfig(cfg)) return /** @type {Record<string, unknown>} */ (cfg)
2543
+ return isEffectiveConfig(v) ? rec : null
2544
+ }
2545
+
2546
+ /**
2547
+ * Latest effective config echoed into the session: the newest full
2548
+ * `iterate_config` result (read or post-write), falling back to a deep scan
2549
+ * for a config that reached the stream without the tool envelope (restored
2550
+ * transcript, host that surfaces artifacts directly).
2551
+ *
2552
+ * @param {unknown} session
2553
+ * @returns {Record<string, unknown> | null}
2554
+ */
2555
+ export function scanSessionForConfigRead(session) {
2556
+ const node = latestToolResultNode(session, 'iterate_config', findConfigInResult)
2557
+ if (node !== null) {
2558
+ const cfg = findConfigInResult(node)
2559
+ if (cfg) return cfg
2560
+ }
2561
+ const loose = findFirstInObject(session, isEffectiveConfig)
2562
+ return loose ? /** @type {Record<string, unknown>} */ (loose) : null
2563
+ }
2564
+
2565
+ /**
2566
+ * Resolve a dotted editable-field key (`validation.commands`, `review.scope`)
2567
+ * against a read-back config.
2568
+ *
2569
+ * @param {Record<string, unknown> | null | undefined} config
2570
+ * @param {string} key dotted field key from CONFIG_EDIT_FIELDS
2571
+ * @returns {{ resolved: boolean, value: unknown }} `value` is undefined when unresolved
2572
+ */
2573
+ export function configValueByPath(config, key) {
2574
+ if (!config || typeof config !== 'object' || typeof key !== 'string' || !key) {
2575
+ return { resolved: false, value: undefined }
2576
+ }
2577
+ let node = /** @type {unknown} */ (config)
2578
+ for (const part of key.split('.')) {
2579
+ if (!node || typeof node !== 'object' || Array.isArray(node)) return { resolved: false, value: undefined }
2580
+ const rec = /** @type {Record<string, unknown>} */ (node)
2581
+ if (!Object.hasOwn(rec, part)) return { resolved: false, value: undefined }
2582
+ node = rec[part]
2583
+ }
2584
+ return { resolved: true, value: node }
2585
+ }
2586
+
2382
2587
  /**
2383
2588
  * Keyboard shortcut → triage verdict mapping (used by the triage panel).
2384
2589
  * @type {Record<string, 'keep' | 'skip' | 'ignore'>}
@@ -2594,14 +2799,19 @@ function pickExportExtras(extra) {
2594
2799
  * One row per round: findings raised, findings fixed, severity mix and the
2595
2800
  * validation command outcomes recorded on the manifest.
2596
2801
  *
2802
+ * `fixed` counts SUCCESSFUL fixes applied in that round, taken from the
2803
+ * transcript's `fixes` (each `{ round, success }`) — the review `findings`
2804
+ * schema carries no `status`, so reading `f.status` here always yielded 0.
2805
+ *
2597
2806
  * Cross-*session* comparison is intentionally out of scope — that is what the
2598
2807
  * #4 pull instruction is for (see `buildDiskSnapshotInstruction`).
2599
2808
  *
2600
2809
  * @param {unknown} report
2601
2810
  * @param {unknown} validations
2811
+ * @param {unknown} [fixes] transcript fixes (`{round, success}[]`) for the fixed counts
2602
2812
  * @returns {Array<{round: number, findings: number, fixed: number, severities: Record<string, number>, validations: Array<{command: string, exitCode: number|null, allowed: boolean}>}>}
2603
2813
  */
2604
- export function buildRoundComparison(report, validations) {
2814
+ export function buildRoundComparison(report, validations, fixes) {
2605
2815
  const rounds = new Map()
2606
2816
  const rowsFor = (round) => {
2607
2817
  const n = Number(round)
@@ -2619,7 +2829,15 @@ export function buildRoundComparison(report, validations) {
2619
2829
  row.findings += 1
2620
2830
  const sev = typeof f.severity === 'string' && f.severity ? f.severity : 'unknown'
2621
2831
  row.severities[sev] = (row.severities[sev] || 0) + 1
2622
- if (f.status === 'fixed') row.fixed += 1
2832
+ }
2833
+
2834
+ // Fixed counts come from applied fixes (real data), not a finding `status`
2835
+ // the findings schema never produces.
2836
+ const fixList = Array.isArray(fixes) ? fixes : []
2837
+ for (const fx of fixList) {
2838
+ if (!fx || typeof fx !== 'object') continue
2839
+ if ((/** @type {Record<string, unknown>} */ (fx)).success !== true) continue
2840
+ rowsFor((/** @type {Record<string, unknown>} */ (fx)).round).fixed += 1
2623
2841
  }
2624
2842
 
2625
2843
  const vals = Array.isArray(validations) ? validations : []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -8,7 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/jingzhao-l/iterate-plugin.git"
10
10
  },
11
- "homepage": "https://github.com/jingzhao-l/iterate-plugin",
11
+ "homepage": "https://github.com/jingzhao-l/iterate-plugin#readme",
12
12
  "bugs": {
13
13
  "url": "https://github.com/jingzhao-l/iterate-plugin/issues"
14
14
  },
@@ -45,7 +45,10 @@
45
45
  "0.1.7-rc.2": "compatible",
46
46
  "0.2.0-rc.1": "compatible",
47
47
  "0.2.0-rc.2": "compatible",
48
- "0.2.1-alpha.1": "compatible"
48
+ "0.2.1-alpha.1": "compatible",
49
+ "0.2.1-alpha.2": "compatible",
50
+ "0.2.1-alpha.3": "compatible",
51
+ "0.2.1-alpha.4": "compatible"
49
52
  }
50
53
  },
51
54
  "client": {
@@ -76,29 +79,49 @@
76
79
  "build": "tsc -p tsconfig.build.json",
77
80
  "build:client": "node scripts/build-client.mjs",
78
81
  "prepublishOnly": "npm run build && npm run build:client",
79
- "typecheck": "tsc --noEmit",
82
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.client.json",
80
83
  "typecheck:client": "tsc --noEmit -p tsconfig.client.json",
81
84
  "test": "tsx --test test/*.test.ts",
82
85
  "test:validate": "tsx --test test/validate.test.ts"
83
86
  },
84
87
  "dependencies": {
85
- "@deepseek-ai/cordis": "4.0.5-alpha.1",
86
- "@deepseek-ai/dsh-tools": "0.2.1-alpha.1",
87
- "@deepseek-ai/dsh-util-values": "0.2.1-alpha.1",
88
88
  "js-yaml": "4.3.2"
89
89
  },
90
+ "peerDependencies": {
91
+ "@deepseek-ai/cordis": "^4.0.5-alpha.2",
92
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4",
93
+ "@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4"
94
+ },
95
+ "peerDependenciesMeta": {
96
+ "@deepseek-ai/cordis": {
97
+ "optional": true
98
+ },
99
+ "@deepseek-ai/dsh-tools": {
100
+ "optional": true
101
+ },
102
+ "@deepseek-ai/dsh-util-values": {
103
+ "optional": true
104
+ }
105
+ },
90
106
  "devDependencies": {
91
- "@deepseek-ai/dsh-agent": "0.2.1-alpha.1",
92
- "@deepseek-ai/dsh-jobs": "0.2.1-alpha.1",
93
- "@deepseek-ai/dsh-session": "0.2.1-alpha.1",
107
+ "@deepseek-ai/cordis": "4.0.5-alpha.2",
108
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.4",
109
+ "@deepseek-ai/dsh-jobs": "0.2.1-alpha.4",
110
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.4",
111
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.4",
112
+ "@deepseek-ai/dsh-util-values": "0.2.1-alpha.4",
94
113
  "@types/js-yaml": "4.0.9",
95
114
  "@types/node": "22.15.0",
96
115
  "@types/react": "19.2.2",
97
116
  "esbuild": "0.25.12",
117
+ "jsdom": "26.1.0",
118
+ "react": "19.2.2",
119
+ "react-dom": "19.2.2",
98
120
  "tsx": "4.20.3",
99
121
  "typescript": "5.9.3"
100
122
  },
101
123
  "engines": {
102
- "node": ">=20"
124
+ "node": ">=20",
125
+ "dsh": ">=0.1.1-rc.1"
103
126
  }
104
127
  }