iterate-plugin 3.16.0 → 3.17.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/README.md CHANGED
@@ -107,7 +107,7 @@ Both modes share:
107
107
  ### UI layer (build-free client slots, v3.12: 12 tabs, bilingual)
108
108
 
109
109
  - **ConvergenceDashboard** (`conversation.input.dock`) — live round progress bar, severity stats, dimension badges, trend mini-chart above the input; normal mode also shows fix-count badges; plus a live workflow-phase chip (current phase + running/stopped) and a **task_mode indicator (code / iterate)**, now backed by the persisted transcript (`iterate_status` / `iterate_transcript` emit `taskMode`). While a run is active the dashboard switches to a running state (round + phase) instead of the static empty state, and the start buttons are hidden.
110
- - **ObservatoryPanel** (`conversation.input.dock`) — a **twelve-tab** runtime observatory below the input: **live activity stream** (default tab, type filter) + **F1** review threads (expand / collapse all), **F2** convergence trend (incl. the per-round comparison table: findings raised / fixed / severity mix / validation outcomes, plus **v3.16 a cross-session comparison**: save the current run as a baseline (`存为对比基准`), then compare any later run against it side by side (`与基准对比`) with per-metric deltas tinted by direction), **F3** finding locations (severity / dimension / search filter), **F4** fixes + rollback, **F5** checkpoint resume, **F6** run console, **F7** decision timeline (type / round filter + search), **F8** Quality Gate, **F9** Experience Bank, **F10** Defense Events, **F11** Preflight. Number-key shortcuts: `1`–`9` jump to F1–F9, `0` to F10, `-` back to the live stream (F11 is click-only). **F8/F9/F10 are live** — they scan the session stream for the latest `iterate_quality_gate` / `iterate_experience` / `iterate_defense_events` results and render real data (PASS/FAIL chip + per-dimension score bars, experience entries with adopt buttons + client-side search, defense type-count chips + filtered event stream); when the session stream has none they distinguish "no data on disk" from "not pulled yet" and offer a **拉取磁盘快照 (pull disk snapshot)** instruction that has the model call the read-side tools and echo the `.iterate/` artifacts back into the stream. One-click JSON export of all observatory data — manifest + live + `qualityGate` / `experienceBank` / `defenseEvents` / `report` (download, copy fallback). **v3.9: report-export buttons** (导出报告 / 导出 SARIF, **v3.11 + 导出 JSON 报告**) render the *same shared data model* as the `iterate_report` tool — a PR-pasteable Markdown run report, a SARIF 2.1.0 log for CI / GitHub Code Scanning, and (v3.11) the complete normalized run model as machine-readable JSON. **v3.5.3: reset actions surfaced** — F5 copies an `iterate_checkpoint clear` instruction, F8 an `iterate_quality_gate clear` instruction, F10 an `iterate_defense_events clear` instruction. **v3.12: pre-run readiness (Preflight)** — F11 renders the latest `iterate_preflight` checklist scanned from the session stream (validation commands configured / git repo / dirty tree / stale FAIL gate / interrupted checkpoint), each warn/fail row offering the exact fix instruction (configure `validation.commands`, clear the stale gate, resume the checkpoint) so the environment is repaired BEFORE a round is spent on it; the empty state offers a **运行预检** instruction that has the model call `iterate_preflight`. **v3.13: the blocking validation row auto-detects commands from the project manifest and offers a one-click 「应用建议」 (copies an `iterate_config write` payload).**
110
+ - **ObservatoryPanel** (`conversation.input.dock`) — a **twelve-tab** runtime observatory below the input: **live activity stream** (default tab, type filter) + **F1** review threads (expand / collapse all), **F2** convergence trend (incl. the per-round comparison table: findings raised / fixed / severity mix / validation outcomes, plus **v3.16 a cross-session comparison**: save the current run as a baseline (`存为对比基准`), then compare any later run against it side by side (`与基准对比`) with per-metric deltas tinted by direction, **v3.17 the comparison is copyable as a Markdown table** (`复制对比表`, paste it straight into a PR / issue / CI comment)), **F3** finding locations (severity / dimension / search filter), **F4** fixes + rollback, **F5** checkpoint resume, **F6** run console, **F7** decision timeline (type / round filter + search), **F8** Quality Gate (query / **v3.17 one-click recompute** — the `compute` payload's per-dimension per-round convergence series is derived from the session transcript, so you no longer hand-write the `findingsByRound` JSON), **F9** Experience Bank, **F10** Defense Events, **F11** Preflight. Number-key shortcuts: `1`–`9` jump to F1–F9, `0` to F10, `-` back to the live stream (F11 is click-only). **F8/F9/F10 are live** — they scan the session stream for the latest `iterate_quality_gate` / `iterate_experience` / `iterate_defense_events` results and render real data (PASS/FAIL chip + per-dimension score bars, experience entries with adopt buttons + client-side search, defense type-count chips + filtered event stream); when the session stream has none they distinguish "no data on disk" from "not pulled yet" and offer a **拉取磁盘快照 (pull disk snapshot)** instruction that has the model call the read-side tools and echo the `.iterate/` artifacts back into the stream. One-click JSON export of all observatory data — manifest + live + `qualityGate` / `experienceBank` / `defenseEvents` / `report` (download, copy fallback). **v3.9: report-export buttons** (导出报告 / 导出 SARIF, **v3.11 + 导出 JSON 报告**) render the *same shared data model* as the `iterate_report` tool — a PR-pasteable Markdown run report, a SARIF 2.1.0 log for CI / GitHub Code Scanning, and (v3.11) the complete normalized run model as machine-readable JSON. **v3.5.3: reset actions surfaced** — F5 copies an `iterate_checkpoint clear` instruction, F8 an `iterate_quality_gate clear` instruction, F10 an `iterate_defense_events clear` instruction. **v3.12: pre-run readiness (Preflight)** — F11 renders the latest `iterate_preflight` checklist scanned from the session stream (validation commands configured / git repo / dirty tree / stale FAIL gate / interrupted checkpoint), each warn/fail row offering the exact fix instruction (configure `validation.commands`, clear the stale gate, resume the checkpoint) so the environment is repaired BEFORE a round is spent on it; the empty state offers a **运行预检** instruction that has the model call `iterate_preflight`. **v3.13: the blocking validation row auto-detects commands from the project manifest and offers a one-click 「应用建议」 (copies an `iterate_config write` payload).**
111
111
  - **TriagePanel** (`conversation.chat.turnTail`) — per-finding triage with `y` (keep → fix), `n` (skip), `a` (known-intentional) keyboard shortcuts and `↑`/`↓` navigation (focus-guarded: only active while the panel has focus), filtering, batch ops (incl. select-all), localStorage persistence. **Write-back closed loop**: the primary button copies an `iterate_triage apply` instruction for the marked entries (the tool validates, dedupes, backs up, rolls back on failure); a secondary button copies the raw `known_intentional` YAML; **回读已写入条目** copies an `iterate_triage list` instruction to confirm what actually landed. Copy payloads are aligned to the target tool schemas. **v3.2: native command buttons** (approve architectural fix, trigger new round, rollback to checkpoint). **v3.3: §8 指派修复 (assign)** — one click copies an `iterate_fix` instruction carrying every in-scope finding (respecting the batch toggle).
112
112
  - **StatsCard** (`conversation.chat.turnTail`) — convergence stats, round history, trend chart and completion summary when no findings remain.
113
113
  - **iterate theme skin** (`theme.overrideTokens`) — a warm-amber 13-token `--dsw-*` override, light/dark modes, togglable in settings.
@@ -121,7 +121,7 @@ The UI layer is **defensive by design**: it degrades gracefully if any of `slots
121
121
 
122
122
  ### Behavior beyond the tools
123
123
 
124
- Beyond the 17 registered tools (full reference further down), the plugin closes several loops end to end:
124
+ Beyond the 19 registered tools (full reference further down), the plugin closes several loops end to end:
125
125
 
126
126
  - **Findings triage loop** — review → UI triage (y/n/a) → `iterate_triage` writes back `known_intentional` → auto-filtered next round
127
127
  - **Structured fix system** — each fix backs up first, writes a registry entry, records the diff; a failed validation can be reverted with `iterate_rollback`
package/README.zh-CN.md CHANGED
@@ -106,7 +106,7 @@ dsh plugin --profile web add iterate-plugin
106
106
  ### UI 层(客户端免构建槽位,v3.12:12 个页签,双语)
107
107
 
108
108
  - **收敛看板 `ConvergenceDashboard`**(`conversation.input.dock`)— 输入框上方实时显示轮次进度条、严重度统计、维度徽章、趋势迷你图;normal 模式另显示修复计数徽章;还有运行阶段芯片(当前工作流阶段 + 运行中/已结束)与 **task_mode 指示器(code / iterate)**。运行期间看板切换为"运行中"状态(显示轮次与阶段),并隐藏启动按钮。
109
- - **运行时观测台 `ObservatoryPanel`**(`conversation.input.dock`)— 输入框下方**十二个页签**运行时观测台:**实时活动流**(默认页,支持按活动类型筛选)+ **F1** 审查线程(全部展开/全部收起)、**F2** 收敛趋势(含跨轮对比表:每轮 findings / fixed / 严重度分布 / 验证结果;**v3.16 新增跨会话对比**:把当前运行**存为对比基准**,下次运行一键**与基准对比**,逐项并排 + 带方向的差值着色)、**F3** 发现定位(按严重度/维度/关键词筛选)、**F4** 修复与回滚、**F5** 断点恢复、**F6** 运行控制台、**F7** 决策时间线(按类型/轮次筛选与关键词搜索)、**F8** 质量门禁、**F9** 经验银行、**F10** 防御事件、**F11** 开跑预检。数字键快捷键:`1`–`9` 跳到 F1–F9,`0` 到 F10,`-` 回实时活动流(F11 仅点击进入)。**F8/F9/F10 为实时数据**——扫描会话流中最近一次 `iterate_quality_gate` / `iterate_experience` / `iterate_defense_events` 结果并渲染真实数据(PASS/FAIL 徽章 + 分维度评分条、经验条目(含采纳按钮 + 客户端搜索)、防御类型计数芯片 + 筛选后的事件流);会话流中没有数据时区分"磁盘无数据"与"尚未拉取",并提供**拉取磁盘快照**指令(让模型调用只读工具、把 `.iterate/` 产物回显进会话流)。一键导出全部观测数据为 JSON——manifest + live + `qualityGate` / `experienceBank` / `defenseEvents` / `report`(优先下载,失败回退复制)。**v3.9 新增报告导出按钮**(导出报告 / 导出 SARIF,**v3.11 新增导出 JSON 报告**),与 `iterate_report` 工具共用同一份数据模型——分别产出可直接粘贴进 PR 的 Markdown 运行报告、供 CI / GitHub Code Scanning 消费的 SARIF 2.1.0 日志,以及(v3.11)完整的归一化运行模型 JSON(无损格式)。**v3.5.3 新增重置操作**:F5 复制 `iterate_checkpoint clear`、F8 复制 `iterate_quality_gate clear`、F10 复制 `iterate_defense_events clear` 指令。**v3.12 新增开跑预检(Preflight)**——F11 渲染会话流中最近一次 `iterate_preflight` 清单(验证命令是否配置 / 是否 Git 仓库 / 脏工作区 / 陈旧 FAIL 门禁 / 中断断点),每个 warn/fail 行都给出精确的修复指令(配置 `validation.commands`、清除陈旧门禁、恢复断点),让环境在**花掉一轮之前**就被修好;空态提供**运行预检**指令(让模型调用 `iterate_preflight`)。**v3.13:阻断的验证命令行自动检测项目 manifest,提供一键「应用建议」**(复制 `iterate_config` 的 `operation:"write"` 指令,携带完整 `validation.commands.default` 载荷)。
109
+ - **运行时观测台 `ObservatoryPanel`**(`conversation.input.dock`)— 输入框下方**十二个页签**运行时观测台:**实时活动流**(默认页,支持按活动类型筛选)+ **F1** 审查线程(全部展开/全部收起)、**F2** 收敛趋势(含跨轮对比表:每轮 findings / fixed / 严重度分布 / 验证结果;**v3.16 新增跨会话对比**:把当前运行**存为对比基准**,下次运行一键**与基准对比**,逐项并排 + 带方向的差值着色,**v3.17 新增「复制对比表」**:把并排表复制成 Markdown 表格,可直接粘贴进 PR / issue / CI 注释)、**F3** 发现定位(按严重度/维度/关键词筛选)、**F4** 修复与回滚、**F5** 断点恢复、**F6** 运行控制台、**F7** 决策时间线(按类型/轮次筛选与关键词搜索)、**F8** 质量门禁(查询 / **v3.17 一键重算**——`compute` 载荷里每维度每轮的收敛序列直接从当前会话 transcript 推导,无需再手写 `findingsByRound` JSON)、**F9** 经验银行、**F10** 防御事件、**F11** 开跑预检。数字键快捷键:`1`–`9` 跳到 F1–F9,`0` 到 F10,`-` 回实时活动流(F11 仅点击进入)。**F8/F9/F10 为实时数据**——扫描会话流中最近一次 `iterate_quality_gate` / `iterate_experience` / `iterate_defense_events` 结果并渲染真实数据(PASS/FAIL 徽章 + 分维度评分条、经验条目(含采纳按钮 + 客户端搜索)、防御类型计数芯片 + 筛选后的事件流);会话流中没有数据时区分"磁盘无数据"与"尚未拉取",并提供**拉取磁盘快照**指令(让模型调用只读工具、把 `.iterate/` 产物回显进会话流)。一键导出全部观测数据为 JSON——manifest + live + `qualityGate` / `experienceBank` / `defenseEvents` / `report`(优先下载,失败回退复制)。**v3.9 新增报告导出按钮**(导出报告 / 导出 SARIF,**v3.11 新增导出 JSON 报告**),与 `iterate_report` 工具共用同一份数据模型——分别产出可直接粘贴进 PR 的 Markdown 运行报告、供 CI / GitHub Code Scanning 消费的 SARIF 2.1.0 日志,以及(v3.11)完整的归一化运行模型 JSON(无损格式)。**v3.5.3 新增重置操作**:F5 复制 `iterate_checkpoint clear`、F8 复制 `iterate_quality_gate clear`、F10 复制 `iterate_defense_events clear` 指令。**v3.12 新增开跑预检(Preflight)**——F11 渲染会话流中最近一次 `iterate_preflight` 清单(验证命令是否配置 / 是否 Git 仓库 / 脏工作区 / 陈旧 FAIL 门禁 / 中断断点),每个 warn/fail 行都给出精确的修复指令(配置 `validation.commands`、清除陈旧门禁、恢复断点),让环境在**花掉一轮之前**就被修好;空态提供**运行预检**指令(让模型调用 `iterate_preflight`)。**v3.13:阻断的验证命令行自动检测项目 manifest,提供一键「应用建议」**(复制 `iterate_config` 的 `operation:"write"` 指令,携带完整 `validation.commands.default` 载荷)。
110
110
  - **Findings 分诊面板 `TriagePanel`**(`conversation.chat.turnTail`)— 逐条分诊,键盘快捷键 `y`(保留→修复)、`n`(跳过)、`a`(已知有意)与 `↑`/`↓` 移动(焦点守卫:仅面板内焦点时生效),支持筛选、批量(含一键全选所有 findings)、localStorage 持久化。**写回闭环**:主按钮复制已标记条目的 `iterate_triage apply` 指令(工具负责校验、去重、备份、失败回滚);次按钮复制裸 `known_intentional` YAML;**回读已写入条目**复制 `iterate_triage list` 指令核对实际落盘内容。复制载荷与目标工具 schema 对齐。**v3.2:原生命令按钮**(批准架构修复、触发新一轮、回滚到断点)。
111
111
  - **收敛统计卡片 `StatsCard`**(`conversation.chat.turnTail`)— 无 findings 时显示收敛统计、历史轮次、趋势图、完成摘要。
112
112
  - **iterate 主题皮肤**(`theme.overrideTokens`)— 暖琥珀配色的 13 个 `--dsw-*` token 覆盖,明暗双模式,可在设置页开关。
package/dist/index.js CHANGED
@@ -2,16 +2,17 @@
2
2
  * iterate-plugin — dsh plugin for the iterate autonomous closed-loop workflow
3
3
  *
4
4
  * Architecture:
5
- * - The plugin registers 18 tools (14 original + 3 v3.0 quality command center
6
- * tools + 1 v3.9 report export)
5
+ * - The plugin registers 19 tools (14 original + 3 v3.0 quality command center
6
+ * tools + 1 v3.9 report export + 1 v3.12 pre-run preflight)
7
7
  * Original: config, validate, decision-log, context, review, triage, fix, diff,
8
8
  * rollback, checkpoint, status, history, prune, transcript
9
9
  * v3.0: experience, quality_gate, defense_events
10
10
  * v3.9: report
11
+ * v3.12: preflight
11
12
  * - The plugin injects a system prompt section teaching the iterate workflow pattern
12
13
  * - The model (prompted by the skill) writes a workflow script using dsh's `workflow` tool
13
14
  * - The workflow script uses `agent()` / `parallel()` / `phase()` / `log()` to orchestrate
14
- * - Subagents use the 18 tools to do real work (read config, run validation, log decisions,
15
+ * - Subagents use the 19 tools to do real work (read config, run validation, log decisions,
15
16
  * review, triage, apply/rollback/fixing, checkpoint, status, history, prune, transcript,
16
17
  * query experience bank, check quality gates, query defense events, export a report)
17
18
  * - A `tools/pre-execute` hook gates destructive iterate calls behind human approval
@@ -25,7 +26,7 @@
25
26
  *
26
27
  * Key files:
27
28
  * - src/index.ts — Plugin entry: register tools + inject skill prompt
28
- * - src/tools/ — 18 tool implementations (14 original + 3 v3.0 + 1 v3.9)
29
+ * - src/tools/ — 19 tool implementations (14 original + 3 v3.0 + 1 v3.9 + 1 v3.12)
29
30
  * - src/config-loader.ts — YAML config loading
30
31
  * - src/types.ts — Shared types
31
32
  */
@@ -265,6 +265,26 @@ function decodedLines(filePath) {
265
265
  }
266
266
  return out;
267
267
  }
268
+ /**
269
+ * Rebuild the exact ledger text the kernel verifies, dropping only the artifact a
270
+ * trailing newline leaves. Unlike `decodedLines` this keeps interior blank lines:
271
+ * `kernelVerifyLedgerText` splits on "\n" and `JSON.parse`s each line, so an interior
272
+ * blank line is a corrupt entry it must report — renumbering the lines that follow it
273
+ * (by dropping it here) would mask a real break as a healthy ledger.
274
+ */
275
+ function ledgerTextForVerify(filePath) {
276
+ let text;
277
+ try {
278
+ text = readFileSync(filePath, 'utf-8');
279
+ }
280
+ catch {
281
+ return '';
282
+ }
283
+ const lines = text.endsWith('\n') ? text.slice(0, -1) : text;
284
+ if (lines.length === 0)
285
+ return '';
286
+ return `${lines}\n`;
287
+ }
268
288
  /** The 1-based line number of the first legacy record AFTER the chain began, else `null`. */
269
289
  export function contestedLedgerLine(filePath) {
270
290
  const kinds = decodedLines(filePath).map((l) => classifyDecisionLine(l.decoded));
@@ -670,8 +690,7 @@ export function readDecisionEntries(projectRoot) {
670
690
  export function verifyDecisionLedger(projectRoot) {
671
691
  const filePath = decisionLogPath(projectRoot);
672
692
  try {
673
- const lines = decodedLines(filePath);
674
- const body = `${lines.map((l) => l.raw).join('\n')}${lines.length > 0 ? '\n' : ''}`;
693
+ const body = ledgerTextForVerify(filePath);
675
694
  const verified = kernelVerifyLedgerText(body);
676
695
  return {
677
696
  ok: verified.ok,
package/lib/client.js CHANGED
@@ -1,4 +1,4 @@
1
- /* iterate-plugin client source-digest: 55b24ad8a8a5d8abf400d42314a6bf75394e8e529fc587ac991bb4e50d868393 */
1
+ /* iterate-plugin client source-digest: 1396cad72b6ae5e0e09c4fac31f0e74b0f17719bb8a1d3c2c30cc749e34b70c1 */
2
2
  window.__ModuleLoader__.load({ id: "iterate-plugin", factory: (require) => {
3
3
  var module = { exports: {} };
4
4
  var exports = module.exports;
@@ -1343,6 +1343,125 @@ function buildQualityGateClearInstruction() {
1343
1343
  ${JSON.stringify({ operation: "clear" }, null, 2)}
1344
1344
  \`\`\``;
1345
1345
  }
1346
+ function buildQualityGateComputeInstruction(input) {
1347
+ const src = input && typeof input === "object" ? input : {};
1348
+ const cleanStr = (v) => typeof v === "string" && v.length > 0;
1349
+ const dimensions = Array.isArray(src.dimensions) ? src.dimensions.filter((d) => typeof d === "string" && d.length > 0) : [];
1350
+ const findings = Array.isArray(src.findings) ? src.findings.filter((f) => f && typeof f === "object" && cleanStr(f.dimension) && cleanStr(f.file)).map((f) => ({
1351
+ dimension: f.dimension,
1352
+ severity: typeof f.severity === "string" && f.severity ? f.severity : "medium",
1353
+ file: f.file,
1354
+ ...typeof f.line === "number" && Number.isFinite(f.line) && f.line > 0 ? { line: f.line } : {}
1355
+ })) : [];
1356
+ const validationResults = Array.isArray(src.validationResults) ? src.validationResults.filter((v) => v && typeof v === "object" && cleanStr(v.command) && typeof v.exitCode === "number" && Number.isFinite(v.exitCode)).map((v) => ({ command: v.command, exitCode: v.exitCode })) : [];
1357
+ const findingsByRound = {};
1358
+ if (src.findingsByRound && typeof src.findingsByRound === "object" && !Array.isArray(src.findingsByRound)) {
1359
+ for (const [dim, series] of Object.entries(src.findingsByRound)) {
1360
+ const clean = Array.isArray(series) ? series.filter((n) => typeof n === "number" && Number.isFinite(n) && n >= 0) : [];
1361
+ if (clean.length > 0) findingsByRound[dim] = clean;
1362
+ }
1363
+ }
1364
+ const fixedByDimension = {};
1365
+ if (src.fixedByDimension && typeof src.fixedByDimension === "object" && !Array.isArray(src.fixedByDimension)) {
1366
+ for (const [dim, n] of Object.entries(src.fixedByDimension)) {
1367
+ if (typeof n === "number" && Number.isFinite(n) && n >= 0) fixedByDimension[dim] = n;
1368
+ }
1369
+ }
1370
+ const payload = { operation: "compute" };
1371
+ if (dimensions.length > 0) payload.dimensions = dimensions;
1372
+ if (findings.length > 0) payload.findings = findings;
1373
+ if (validationResults.length > 0) payload.validationResults = validationResults;
1374
+ if (Object.keys(findingsByRound).length > 0) payload.findingsByRound = findingsByRound;
1375
+ if (Object.keys(fixedByDimension).length > 0) payload.fixedByDimension = fixedByDimension;
1376
+ return `\u8BF7\u8C03\u7528 \`iterate_quality_gate\` \u91CD\u7B97\u8D28\u91CF\u95E8\u7981\u8BC1\u4E66\uFF08\u4ECE\u5F53\u524D\u4F1A\u8BDD\u63A8\u5BFC\u7EF4\u5EA6/\u53D1\u73B0/\u9A8C\u8BC1\u7ED3\u679C\u5E76\u6301\u4E45\u5316\uFF09\uFF1A
1377
+
1378
+ \`\`\`json
1379
+ ${JSON.stringify(payload, null, 2)}
1380
+ \`\`\``;
1381
+ }
1382
+ function deriveQualityGateComputeInput(manifest, qualityGate) {
1383
+ const m = manifest && typeof manifest === "object" ? manifest : {};
1384
+ const asArray2 = (v) => Array.isArray(v) ? v : [];
1385
+ const dims = [];
1386
+ for (const r of asArray2(m.rounds)) {
1387
+ for (const t2 of asArray2(r && r.threads)) {
1388
+ const d = t2 && typeof t2.dimension === "string" ? t2.dimension : "";
1389
+ if (d && !dims.includes(d)) dims.push(d);
1390
+ }
1391
+ }
1392
+ if (dims.length === 0 && qualityGate && Array.isArray(qualityGate.dimensions)) {
1393
+ for (const d of qualityGate.dimensions) {
1394
+ const name2 = d && typeof d.dimension === "string" ? d.dimension : "";
1395
+ if (name2 && !dims.includes(name2)) dims.push(name2);
1396
+ }
1397
+ }
1398
+ const findings = [];
1399
+ for (const f of asArray2(m.findings)) {
1400
+ if (!f || typeof f !== "object") continue;
1401
+ if (typeof f.dimension !== "string" || !f.dimension) continue;
1402
+ if (typeof f.file !== "string" || !f.file) continue;
1403
+ findings.push({
1404
+ dimension: f.dimension,
1405
+ severity: typeof f.severity === "string" && f.severity ? f.severity : "medium",
1406
+ file: f.file,
1407
+ ...typeof f.line === "number" && Number.isFinite(f.line) && f.line > 0 ? { line: f.line } : {}
1408
+ });
1409
+ }
1410
+ const validationResults = [];
1411
+ for (const v of asArray2(m.validations)) {
1412
+ if (!v || typeof v !== "object") continue;
1413
+ if (typeof v.command !== "string" || !v.command) continue;
1414
+ if (typeof v.exitCode !== "number" || !Number.isFinite(v.exitCode)) continue;
1415
+ validationResults.push({ command: v.command, exitCode: v.exitCode });
1416
+ }
1417
+ const rounds = asArray2(m.rounds);
1418
+ const allDims = dims.slice();
1419
+ for (const r of rounds) {
1420
+ for (const t2 of asArray2(r && r.threads)) {
1421
+ const d = t2 && typeof t2.dimension === "string" ? t2.dimension : "";
1422
+ if (d && !allDims.includes(d)) allDims.push(d);
1423
+ }
1424
+ }
1425
+ const byRound = {};
1426
+ for (const d of allDims) {
1427
+ byRound[d] = [];
1428
+ }
1429
+ for (const r of rounds) {
1430
+ for (const d of allDims) {
1431
+ let n = 0;
1432
+ for (const t2 of asArray2(r && r.threads)) {
1433
+ if (!t2 || t2.dimension !== d) continue;
1434
+ for (const f of asArray2(t2 && t2.findings)) {
1435
+ if (f && typeof f === "object" && typeof f.dimension === "string" && f.dimension === d) n += 1;
1436
+ }
1437
+ }
1438
+ byRound[d].push(n);
1439
+ }
1440
+ }
1441
+ const fixedByDimension = {};
1442
+ const findingsIndex = findings;
1443
+ for (const fx of asArray2(m.fixes)) {
1444
+ if (!fx || typeof fx !== "object" || fx.success !== true) continue;
1445
+ const file = typeof fx.file === "string" ? fx.file : "";
1446
+ if (!file) continue;
1447
+ let dim = "";
1448
+ for (const f of findingsIndex) {
1449
+ if (f.file !== file) continue;
1450
+ if (typeof f.line === "number" && typeof fx.line === "number" && f.line !== fx.line) continue;
1451
+ dim = f.dimension;
1452
+ break;
1453
+ }
1454
+ if (!dim) continue;
1455
+ fixedByDimension[dim] = (fixedByDimension[dim] || 0) + 1;
1456
+ }
1457
+ return {
1458
+ dimensions: dims,
1459
+ findings,
1460
+ validationResults,
1461
+ findingsByRound: byRound,
1462
+ fixedByDimension
1463
+ };
1464
+ }
1346
1465
  function buildPreflightInstruction() {
1347
1466
  return `\u8BF7\u8C03\u7528 \`iterate_preflight\` \u8FD0\u884C\u5F00\u8DD1\u524D\u9884\u68C0\uFF08\u53EA\u8BFB\uFF1A\u6821\u9A8C validation.commands\u3001git\u3001\u810F\u5DE5\u4F5C\u533A\u3001\u9648\u65E7\u8D28\u91CF\u95E8\u7981\u4E0E\u4E2D\u65AD\u65AD\u70B9\uFF09\uFF1A
1348
1467
 
@@ -2663,6 +2782,8 @@ var EN = {
2663
2782
  \u57FA\u51C6: "Baseline",
2664
2783
  \u5F53\u524D: "Current",
2665
2784
  \u53D8\u5316: "Delta",
2785
+ \u590D\u5236\u5BF9\u6BD4\u8868: "Copy comparison table",
2786
+ "\u590D\u5236\u8DE8\u4F1A\u8BDD\u5BF9\u6BD4\u8868\u4E3A Markdown\uFF08\u53EF\u7C98\u8D34\u8FDB PR / issue / CI \u6CE8\u91CA\uFF09": "Copy the cross-session comparison as a Markdown table (paste into a PR / issue / CI comment)",
2666
2787
  // ── F3 finding locations ─────────────────────────────────────────────────
2667
2788
  \u6682\u65E0\u53D1\u73B0: "No findings",
2668
2789
  "{a0}\uFF08{a1} \u9879\uFF09": "{a0} ({a1} findings)",
@@ -2729,6 +2850,9 @@ var EN = {
2729
2850
  \u67E5\u8BE2\u95E8\u7981: "Query gate",
2730
2851
  "\u590D\u5236 iterate_quality_gate clear \u6307\u4EE4\u6587\u672C\uFF08\u91CD\u7F6E\u9648\u65E7\u7684 FAIL \u8BC1\u4E66\uFF09": "Copy the iterate_quality_gate clear instruction (resets a stale FAIL certificate)",
2731
2852
  \u6E05\u9664\u95E8\u7981: "Clear gate",
2853
+ \u91CD\u7B97\u95E8\u7981: "Recompute gate",
2854
+ "\u590D\u5236 iterate_quality_gate compute \u6307\u4EE4\uFF1A\u4ECE\u5F53\u524D\u4F1A\u8BDD\u63A8\u5BFC\u7EF4\u5EA6/\u53D1\u73B0/\u9A8C\u8BC1\u7ED3\u679C\uFF0C\u4E00\u952E\u91CD\u7B97\u5E76\u6301\u4E45\u5316\u95E8\u7981\u8BC1\u4E66": "Copy the iterate_quality_gate compute instruction: derive dimensions / findings / validation results from this session and recompute + persist the gate certificate",
2855
+ "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u53EF\u91CD\u7B97\u7684\u95E8\u7981\u6570\u636E\uFF08\u9700\u5148\u6709\u5BA1\u67E5/\u9A8C\u8BC1\u7ED3\u679C\uFF09": "No gate data in this session to recompute (need review / validation results first)",
2732
2856
  "\u590D\u5236\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u6307\u4EE4\uFF1A\u8BA9\u6A21\u578B\u56DE\u663E .iterate/ \u4E0B\u7684\u95E8\u7981\u3001\u7ECF\u9A8C\u3001\u9632\u5FA1\u4E8B\u4EF6\u4E0E transcript": "Copy the disk-snapshot instruction: have the model echo the gate, experience, defense events and transcript artifacts under .iterate/",
2733
2857
  \u8D28\u91CF\u95E8\u7981\u89C6\u56FE: "Quality gate view",
2734
2858
  \u8D28\u91CF\u95E8\u7981\u8BC1\u4E66: "Quality gate certificate",
@@ -3153,6 +3277,21 @@ function compareRuns(baseline, current3) {
3153
3277
  ];
3154
3278
  return rows;
3155
3279
  }
3280
+ function buildRunCompareMarkdown(rows, labels) {
3281
+ const cell = (v) => v === null ? "\u2014" : String(v);
3282
+ const header = ["\u6307\u6807", "\u57FA\u51C6", "\u5F53\u524D", "\u53D8\u5316"].map((h) => labels[h] ?? h);
3283
+ const lines = [
3284
+ `| ${header.join(" | ")} |`,
3285
+ `| ${header.map(() => "---").join(" | ")} |`
3286
+ ];
3287
+ for (const row of rows) {
3288
+ const delta = row.baseline === null || row.current === null ? "\u2014" : String(row.current - row.baseline);
3289
+ lines.push(
3290
+ `| ${labels[row.key] ?? row.key} | ${cell(row.baseline)} | ${cell(row.current)} | ${delta} |`
3291
+ );
3292
+ }
3293
+ return lines.join("\n");
3294
+ }
3156
3295
 
3157
3296
  // src/client/index.ts
3158
3297
  var React = require("react");
@@ -5375,6 +5514,20 @@ function ObservatoryPanel(props) {
5375
5514
  title: t("\u663E\u793A\u6216\u9690\u85CF\u4E0E\u5DF2\u4FDD\u5B58\u57FA\u51C6\u7684\u5BF9\u6BD4\u8868"),
5376
5515
  onClick: () => setShowCompare((v) => !v)
5377
5516
  }, showCompare ? t("\u6536\u8D77\u5BF9\u6BD4") : t("\u4E0E\u57FA\u51C6\u5BF9\u6BD4")) : null,
5517
+ baseline && current3 && showCompare ? React.createElement("button", {
5518
+ className: "iterate-btn",
5519
+ "data-copied": copiedKey === "rc-copy" ? "" : void 0,
5520
+ onClick: () => copyInstruction("rc-copy", buildRunCompareMarkdown(compareRuns(baseline, current3), {
5521
+ \u6307\u6807: t("\u6307\u6807"),
5522
+ \u57FA\u51C6: t("\u57FA\u51C6"),
5523
+ \u5F53\u524D: t("\u5F53\u524D"),
5524
+ \u53D8\u5316: t("\u53D8\u5316"),
5525
+ ...Object.fromEntries(
5526
+ Object.entries(RUN_COMPARE_METRIC_LABELS).map(([k, v]) => [k, v[getLang()]])
5527
+ )
5528
+ })),
5529
+ title: t("\u590D\u5236\u8DE8\u4F1A\u8BDD\u5BF9\u6BD4\u8868\u4E3A Markdown\uFF08\u53EF\u7C98\u8D34\u8FDB PR / issue / CI \u6CE8\u91CA\uFF09")
5530
+ }, copiedKey === "rc-copy" ? t("\u5DF2\u590D\u5236") : t("\u590D\u5236\u5BF9\u6BD4\u8868")) : null,
5378
5531
  baseline ? React.createElement("button", {
5379
5532
  className: "iterate-btn",
5380
5533
  "data-ghost": "",
@@ -5820,6 +5973,12 @@ function ObservatoryPanel(props) {
5820
5973
  const renderQualityGate = () => {
5821
5974
  const gateInstruction = buildQualityGateQueryInstruction();
5822
5975
  const gateClearInstruction = buildQualityGateClearInstruction();
5976
+ const computeInput = deriveQualityGateComputeInput(
5977
+ manifest,
5978
+ qualityGate
5979
+ );
5980
+ const computeInstruction = buildQualityGateComputeInstruction(computeInput);
5981
+ const hasComputeData = computeInput.dimensions.length > 0 || computeInput.findings.length > 0 || computeInput.validationResults.length > 0;
5823
5982
  const gate = qualityGate;
5824
5983
  const dims = gate && gate.dimensions ? gate.dimensions : [];
5825
5984
  const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0);
@@ -5838,6 +5997,14 @@ function ObservatoryPanel(props) {
5838
5997
  onClick: () => copyInstruction("qgate", gateInstruction),
5839
5998
  title: t("\u590D\u5236 iterate_quality_gate \u67E5\u8BE2\u6307\u4EE4\uFF08\u542B JSON \u53C2\u6570\uFF09")
5840
5999
  }, copiedKey === "qgate" ? t("\u5DF2\u590D\u5236") : t("\u67E5\u8BE2\u95E8\u7981")),
6000
+ React.createElement("button", {
6001
+ className: "iterate-btn",
6002
+ "data-primary": "",
6003
+ "data-copied": copiedKey === "qgate-compute" ? "" : void 0,
6004
+ disabled: !hasComputeData,
6005
+ onClick: () => copyInstruction("qgate-compute", computeInstruction),
6006
+ title: hasComputeData ? t("\u590D\u5236 iterate_quality_gate compute \u6307\u4EE4\uFF1A\u4ECE\u5F53\u524D\u4F1A\u8BDD\u63A8\u5BFC\u7EF4\u5EA6/\u53D1\u73B0/\u9A8C\u8BC1\u7ED3\u679C\uFF0C\u4E00\u952E\u91CD\u7B97\u5E76\u6301\u4E45\u5316\u95E8\u7981\u8BC1\u4E66") : t("\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u53EF\u91CD\u7B97\u7684\u95E8\u7981\u6570\u636E\uFF08\u9700\u5148\u6709\u5BA1\u67E5/\u9A8C\u8BC1\u7ED3\u679C\uFF09")
6007
+ }, copiedKey === "qgate-compute" ? t("\u5DF2\u590D\u5236") : t("\u91CD\u7B97\u95E8\u7981")),
5841
6008
  React.createElement("button", {
5842
6009
  className: "iterate-btn",
5843
6010
  "data-danger": "",
package/lib/parse.js CHANGED
@@ -1967,6 +1967,198 @@ export function buildQualityGateClearInstruction() {
1967
1967
  )
1968
1968
  }
1969
1969
 
1970
+ /**
1971
+ * `iterate_quality_gate` compute payload — the one-click "recompute the gate
1972
+ * from THIS session" builder.
1973
+ *
1974
+ * The gate's `compute` operation needs a hand-shaped payload
1975
+ * (`dimensions` / `findings` / `validationResults` / `findingsByRound` /
1976
+ * `fixedByDimension`) that a human cannot reliably type by hand — especially
1977
+ * the per-dimension per-round convergence series. This builder turns the
1978
+ * observatory's manifest (already normalized into the session stream) into
1979
+ * that payload, so the model applies the exact data the user is looking at.
1980
+ *
1981
+ * Every field is optional and only included when non-empty (an empty findings
1982
+ * list is still a legal `compute` — it yields a `pending` certificate, never
1983
+ * a fabricated FAIL — so a caller may still emit a bare `{ operation }`).
1984
+ * The returned text is a paste-able natural-language instruction, mirroring
1985
+ * the rest of the panel's copy-to-command payloads.
1986
+ *
1987
+ * @param {{
1988
+ * dimensions?: string[],
1989
+ * findings?: Array<{ dimension: string, severity: string, file: string, line?: number }>,
1990
+ * validationResults?: Array<{ command: string, exitCode: number }>,
1991
+ * findingsByRound?: Record<string, number[]>,
1992
+ * fixedByDimension?: Record<string, number>,
1993
+ * } | null | undefined} input
1994
+ * @returns {string}
1995
+ */
1996
+ export function buildQualityGateComputeInstruction(input) {
1997
+ const src = input && typeof input === 'object' ? input : {}
1998
+ const cleanStr = (v) => typeof v === 'string' && v.length > 0
1999
+ const dimensions = Array.isArray(src.dimensions)
2000
+ ? src.dimensions.filter((d) => typeof d === 'string' && d.length > 0)
2001
+ : []
2002
+ const findings = Array.isArray(src.findings)
2003
+ ? src.findings.filter((f) => f && typeof f === 'object' && cleanStr(f.dimension) && cleanStr(f.file))
2004
+ .map((f) => ({
2005
+ dimension: f.dimension,
2006
+ severity: typeof f.severity === 'string' && f.severity ? f.severity : 'medium',
2007
+ file: f.file,
2008
+ ...(typeof f.line === 'number' && Number.isFinite(f.line) && f.line > 0 ? { line: f.line } : {}),
2009
+ }))
2010
+ : []
2011
+ const validationResults = Array.isArray(src.validationResults)
2012
+ ? src.validationResults
2013
+ .filter((v) => v && typeof v === 'object' && cleanStr(v.command) && typeof v.exitCode === 'number' && Number.isFinite(v.exitCode))
2014
+ .map((v) => ({ command: v.command, exitCode: v.exitCode }))
2015
+ : []
2016
+ const findingsByRound = {}
2017
+ if (src.findingsByRound && typeof src.findingsByRound === 'object' && !Array.isArray(src.findingsByRound)) {
2018
+ for (const [dim, series] of Object.entries(src.findingsByRound)) {
2019
+ const clean = Array.isArray(series)
2020
+ ? series.filter((n) => typeof n === 'number' && Number.isFinite(n) && n >= 0)
2021
+ : []
2022
+ if (clean.length > 0) findingsByRound[dim] = clean
2023
+ }
2024
+ }
2025
+ const fixedByDimension = {}
2026
+ if (src.fixedByDimension && typeof src.fixedByDimension === 'object' && !Array.isArray(src.fixedByDimension)) {
2027
+ for (const [dim, n] of Object.entries(src.fixedByDimension)) {
2028
+ if (typeof n === 'number' && Number.isFinite(n) && n >= 0) fixedByDimension[dim] = n
2029
+ }
2030
+ }
2031
+ const payload = { operation: 'compute' }
2032
+ if (dimensions.length > 0) payload.dimensions = dimensions
2033
+ if (findings.length > 0) payload.findings = findings
2034
+ if (validationResults.length > 0) payload.validationResults = validationResults
2035
+ if (Object.keys(findingsByRound).length > 0) payload.findingsByRound = findingsByRound
2036
+ if (Object.keys(fixedByDimension).length > 0) payload.fixedByDimension = fixedByDimension
2037
+ return (
2038
+ '请调用 `iterate_quality_gate` 重算质量门禁证书(从当前会话推导维度/发现/验证结果并持久化):\n\n' +
2039
+ '```json\n' +
2040
+ `${JSON.stringify(payload, null, 2)}\n` +
2041
+ '```'
2042
+ )
2043
+ }
2044
+
2045
+ /**
2046
+ * Reduce a normalized transcript manifest (+ the session-scanned quality gate)
2047
+ * into the `iterate_quality_gate compute` input. Pure: reads only the fields
2048
+ * the observatory already renders, so the one-click recompute can never
2049
+ * disagree with what the user is looking at.
2050
+ *
2051
+ * @param {Record<string, unknown> | null | undefined} manifest normalized transcript
2052
+ * @param {{ dimensions?: Array<{ dimension?: string }> } | null | undefined} qualityGate
2053
+ * @returns {{
2054
+ * dimensions: string[],
2055
+ * findings: Array<{ dimension: string, severity: string, file: string, line?: number }>,
2056
+ * validationResults: Array<{ command: string, exitCode: number }>,
2057
+ * findingsByRound: Record<string, number[]>,
2058
+ * fixedByDimension: Record<string, number>,
2059
+ * }}
2060
+ */
2061
+ export function deriveQualityGateComputeInput(manifest, qualityGate) {
2062
+ const m = manifest && typeof manifest === 'object' ? manifest : {}
2063
+ const asArray = (v) => (Array.isArray(v) ? v : [])
2064
+
2065
+ // dimensions: union of every thread dimension across rounds, falling back
2066
+ // to the already-computed gate's dimension names (a dry-run with no threads
2067
+ // but a previous certificate still recomputes with the same gated set).
2068
+ const dims = []
2069
+ for (const r of asArray(m.rounds)) {
2070
+ for (const t of asArray(r && r.threads)) {
2071
+ const d = t && typeof t.dimension === 'string' ? t.dimension : ''
2072
+ if (d && !dims.includes(d)) dims.push(d)
2073
+ }
2074
+ }
2075
+ if (dims.length === 0 && qualityGate && Array.isArray(qualityGate.dimensions)) {
2076
+ for (const d of qualityGate.dimensions) {
2077
+ const name = d && typeof d.dimension === 'string' ? d.dimension : ''
2078
+ if (name && !dims.includes(name)) dims.push(name)
2079
+ }
2080
+ }
2081
+
2082
+ const findings = []
2083
+ for (const f of asArray(m.findings)) {
2084
+ if (!f || typeof f !== 'object') continue
2085
+ if (typeof f.dimension !== 'string' || !f.dimension) continue
2086
+ if (typeof f.file !== 'string' || !f.file) continue
2087
+ findings.push({
2088
+ dimension: f.dimension,
2089
+ severity: typeof f.severity === 'string' && f.severity ? f.severity : 'medium',
2090
+ file: f.file,
2091
+ ...(typeof f.line === 'number' && Number.isFinite(f.line) && f.line > 0 ? { line: f.line } : {}),
2092
+ })
2093
+ }
2094
+
2095
+ const validationResults = []
2096
+ for (const v of asArray(m.validations)) {
2097
+ if (!v || typeof v !== 'object') continue
2098
+ if (typeof v.command !== 'string' || !v.command) continue
2099
+ if (typeof v.exitCode !== 'number' || !Number.isFinite(v.exitCode)) continue
2100
+ validationResults.push({ command: v.command, exitCode: v.exitCode })
2101
+ }
2102
+
2103
+ // findingsByRound: per dimension, per review round, the number of findings
2104
+ // raised that round — a true convergence series (latest last), padded to the
2105
+ // round count so the gate's convergenceRate measures the actual first→last
2106
+ // shrink even when a dimension is absent from a round's threads (0).
2107
+ const rounds = asArray(m.rounds)
2108
+ const allDims = dims.slice()
2109
+ for (const r of rounds) {
2110
+ for (const t of asArray(r && r.threads)) {
2111
+ const d = t && typeof t.dimension === 'string' ? t.dimension : ''
2112
+ if (d && !allDims.includes(d)) allDims.push(d)
2113
+ }
2114
+ }
2115
+ const byRound = {}
2116
+ for (const d of allDims) {
2117
+ byRound[d] = []
2118
+ }
2119
+ for (const r of rounds) {
2120
+ for (const d of allDims) {
2121
+ let n = 0
2122
+ for (const t of asArray(r && r.threads)) {
2123
+ if (!t || t.dimension !== d) continue
2124
+ for (const f of asArray(t && t.findings)) {
2125
+ if (f && typeof f === 'object' && typeof f.dimension === 'string' && f.dimension === d) n += 1
2126
+ }
2127
+ }
2128
+ byRound[d].push(n)
2129
+ }
2130
+ }
2131
+
2132
+ // fixedByDimension: attribute successful fixes to a dimension by matching
2133
+ // the fixed file (and line, when both sides carry one) to a finding. A fix
2134
+ // with no match is left unattributed — the field is optional and a gate
2135
+ // recompute stays correct without it.
2136
+ const fixedByDimension = {}
2137
+ const findingsIndex = findings
2138
+ for (const fx of asArray(m.fixes)) {
2139
+ if (!fx || typeof fx !== 'object' || fx.success !== true) continue
2140
+ const file = typeof fx.file === 'string' ? fx.file : ''
2141
+ if (!file) continue
2142
+ let dim = ''
2143
+ for (const f of findingsIndex) {
2144
+ if (f.file !== file) continue
2145
+ if (typeof f.line === 'number' && typeof fx.line === 'number' && f.line !== fx.line) continue
2146
+ dim = f.dimension
2147
+ break
2148
+ }
2149
+ if (!dim) continue
2150
+ fixedByDimension[dim] = (fixedByDimension[dim] || 0) + 1
2151
+ }
2152
+
2153
+ return {
2154
+ dimensions: dims,
2155
+ findings,
2156
+ validationResults,
2157
+ findingsByRound: byRound,
2158
+ fixedByDimension,
2159
+ }
2160
+ }
2161
+
1970
2162
  /**
1971
2163
  * `iterate_preflight` readiness-check payload (runs the pre-run checklist).
1972
2164
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "3.16.0",
3
+ "version": "3.17.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",
@@ -317,6 +317,9 @@ export const EN: Record<string, string> = {
317
317
  基准: 'Baseline',
318
318
  当前: 'Current',
319
319
  变化: 'Delta',
320
+ 复制对比表: 'Copy comparison table',
321
+ '复制跨会话对比表为 Markdown(可粘贴进 PR / issue / CI 注释)':
322
+ 'Copy the cross-session comparison as a Markdown table (paste into a PR / issue / CI comment)',
320
323
 
321
324
  // ── F3 finding locations ─────────────────────────────────────────────────
322
325
  暂无发现: 'No findings',
@@ -398,6 +401,11 @@ export const EN: Record<string, string> = {
398
401
  '复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)':
399
402
  'Copy the iterate_quality_gate clear instruction (resets a stale FAIL certificate)',
400
403
  清除门禁: 'Clear gate',
404
+ 重算门禁: 'Recompute gate',
405
+ '复制 iterate_quality_gate compute 指令:从当前会话推导维度/发现/验证结果,一键重算并持久化门禁证书':
406
+ 'Copy the iterate_quality_gate compute instruction: derive dimensions / findings / validation results from this session and recompute + persist the gate certificate',
407
+ '当前会话没有可重算的门禁数据(需先有审查/验证结果)':
408
+ 'No gate data in this session to recompute (need review / validation results first)',
401
409
  '复制拉取磁盘快照指令:让模型回显 .iterate/ 下的门禁、经验、防御事件与 transcript':
402
410
  'Copy the disk-snapshot instruction: have the model echo the gate, experience, defense events and transcript artifacts under .iterate/',
403
411
  质量门禁视图: 'Quality gate view',
@@ -101,6 +101,8 @@ import {
101
101
  buildRollbackInstruction, // #10: shared rollback payload
102
102
  buildQualityGateQueryInstruction, // #10: JSON-carrying query instruction
103
103
  buildQualityGateClearInstruction, // #10: JSON-carrying clear instruction
104
+ buildQualityGateComputeInstruction, // v3.17: one-click recompute payload from THIS session
105
+ deriveQualityGateComputeInput, // v3.17: manifest → compute input reducer
104
106
  buildTriageReadbackInstruction, // #7: close the write-back loop
105
107
  buildExperienceListInstruction, // #3/#10: JSON list payload for F9
106
108
  buildDefenseEventsListInstruction, // #3/#10: JSON list payload for F10
@@ -153,6 +155,7 @@ import {
153
155
  buildRunBaseline,
154
156
  parseRunBaseline,
155
157
  compareRuns,
158
+ buildRunCompareMarkdown,
156
159
  RUN_BASELINE_STORAGE_KEY,
157
160
  type RunBaseline,
158
161
  type RunCompareMetric,
@@ -2920,6 +2923,21 @@ function ObservatoryPanel(props: SlotProps) {
2920
2923
  onClick: () => setShowCompare((v) => !v),
2921
2924
  }, showCompare ? t('收起对比') : t('与基准对比'))
2922
2925
  : null,
2926
+ baseline && current && showCompare
2927
+ ? React.createElement('button', {
2928
+ className: 'iterate-btn', 'data-copied': copiedKey === 'rc-copy' ? '' : undefined,
2929
+ onClick: () => copyInstruction('rc-copy', buildRunCompareMarkdown(compareRuns(baseline, current), {
2930
+ 指标: t('指标'),
2931
+ 基准: t('基准'),
2932
+ 当前: t('当前'),
2933
+ 变化: t('变化'),
2934
+ ...Object.fromEntries(
2935
+ Object.entries(RUN_COMPARE_METRIC_LABELS).map(([k, v]) => [k, v[getLang()]]),
2936
+ ),
2937
+ })),
2938
+ title: t('复制跨会话对比表为 Markdown(可粘贴进 PR / issue / CI 注释)'),
2939
+ }, copiedKey === 'rc-copy' ? t('已复制') : t('复制对比表'))
2940
+ : null,
2923
2941
  baseline
2924
2942
  ? React.createElement('button', {
2925
2943
  className: 'iterate-btn', 'data-ghost': '',
@@ -3327,6 +3345,19 @@ function ObservatoryPanel(props: SlotProps) {
3327
3345
  // Reset a stale FAIL certificate before a fresh iteration — the tool's
3328
3346
  // `clear` operation existed since v3.5.1 but had no client entry point.
3329
3347
  const gateClearInstruction = buildQualityGateClearInstruction()
3348
+ // v3.17: one-click recompute. The gate's `compute` payload needs a
3349
+ // hand-shaped `findingsByRound` convergence series that a human cannot
3350
+ // type by hand — this derives it from the session manifest the panel is
3351
+ // already rendering, so the recompute can never disagree with the view.
3352
+ const computeInput = deriveQualityGateComputeInput(
3353
+ manifest as unknown as Record<string, unknown> | null | undefined,
3354
+ qualityGate as unknown as Record<string, unknown> | null | undefined,
3355
+ )
3356
+ const computeInstruction = buildQualityGateComputeInstruction(computeInput)
3357
+ const hasComputeData =
3358
+ computeInput.dimensions.length > 0 ||
3359
+ computeInput.findings.length > 0 ||
3360
+ computeInput.validationResults.length > 0
3330
3361
  const gate = qualityGate
3331
3362
  const dims = gate && gate.dimensions ? gate.dimensions : []
3332
3363
  const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0)
@@ -3342,6 +3373,14 @@ function ObservatoryPanel(props: SlotProps) {
3342
3373
  onClick: () => copyInstruction('qgate', gateInstruction),
3343
3374
  title: t('复制 iterate_quality_gate 查询指令(含 JSON 参数)'),
3344
3375
  }, copiedKey === 'qgate' ? t('已复制') : t('查询门禁')),
3376
+ React.createElement('button', {
3377
+ className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate-compute' ? '' : undefined,
3378
+ disabled: !hasComputeData,
3379
+ onClick: () => copyInstruction('qgate-compute', computeInstruction),
3380
+ title: hasComputeData
3381
+ ? t('复制 iterate_quality_gate compute 指令:从当前会话推导维度/发现/验证结果,一键重算并持久化门禁证书')
3382
+ : t('当前会话没有可重算的门禁数据(需先有审查/验证结果)'),
3383
+ }, copiedKey === 'qgate-compute' ? t('已复制') : t('重算门禁')),
3345
3384
  React.createElement('button', {
3346
3385
  className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'qgate-clear' ? '' : undefined,
3347
3386
  onClick: () => copyInstruction('qgate-clear', gateClearInstruction),
@@ -211,3 +211,29 @@ export function compareRuns(baseline: RunBaseline, current: RunBaseline): RunCom
211
211
  ]
212
212
  return rows
213
213
  }
214
+
215
+ /**
216
+ * Render the side-by-side comparison as a Markdown table that can be pasted
217
+ * into a PR / issue / CI comment. `labels` is resolved by the caller from the
218
+ * current UI language (this module stays dependency-free and Chinese-free).
219
+ * A `null` cell renders as `—` (that side had no such measurement).
220
+ *
221
+ * @param rows - the rows from {@link compareRuns}
222
+ * @param labels - metric key → localized column name
223
+ */
224
+ export function buildRunCompareMarkdown(rows: RunCompareMetric[], labels: Record<string, string>): string {
225
+ const cell = (v: number | null): string => (v === null ? '—' : String(v))
226
+ const header = ['指标', '基准', '当前', '变化'].map((h) => labels[h] ?? h)
227
+ const lines = [
228
+ `| ${header.join(' | ')} |`,
229
+ `| ${header.map(() => '---').join(' | ')} |`,
230
+ ]
231
+ for (const row of rows) {
232
+ const delta =
233
+ row.baseline === null || row.current === null ? '—' : String(row.current - row.baseline)
234
+ lines.push(
235
+ `| ${labels[row.key] ?? row.key} | ${cell(row.baseline)} | ${cell(row.current)} | ${delta} |`,
236
+ )
237
+ }
238
+ return lines.join('\n')
239
+ }
package/src/index.ts CHANGED
@@ -2,16 +2,17 @@
2
2
  * iterate-plugin — dsh plugin for the iterate autonomous closed-loop workflow
3
3
  *
4
4
  * Architecture:
5
- * - The plugin registers 18 tools (14 original + 3 v3.0 quality command center
6
- * tools + 1 v3.9 report export)
5
+ * - The plugin registers 19 tools (14 original + 3 v3.0 quality command center
6
+ * tools + 1 v3.9 report export + 1 v3.12 pre-run preflight)
7
7
  * Original: config, validate, decision-log, context, review, triage, fix, diff,
8
8
  * rollback, checkpoint, status, history, prune, transcript
9
9
  * v3.0: experience, quality_gate, defense_events
10
10
  * v3.9: report
11
+ * v3.12: preflight
11
12
  * - The plugin injects a system prompt section teaching the iterate workflow pattern
12
13
  * - The model (prompted by the skill) writes a workflow script using dsh's `workflow` tool
13
14
  * - The workflow script uses `agent()` / `parallel()` / `phase()` / `log()` to orchestrate
14
- * - Subagents use the 18 tools to do real work (read config, run validation, log decisions,
15
+ * - Subagents use the 19 tools to do real work (read config, run validation, log decisions,
15
16
  * review, triage, apply/rollback/fixing, checkpoint, status, history, prune, transcript,
16
17
  * query experience bank, check quality gates, query defense events, export a report)
17
18
  * - A `tools/pre-execute` hook gates destructive iterate calls behind human approval
@@ -25,7 +26,7 @@
25
26
  *
26
27
  * Key files:
27
28
  * - src/index.ts — Plugin entry: register tools + inject skill prompt
28
- * - src/tools/ — 18 tool implementations (14 original + 3 v3.0 + 1 v3.9)
29
+ * - src/tools/ — 19 tool implementations (14 original + 3 v3.0 + 1 v3.9 + 1 v3.12)
29
30
  * - src/config-loader.ts — YAML config loading
30
31
  * - src/types.ts — Shared types
31
32
  */
@@ -323,6 +323,25 @@ function decodedLines(filePath: string): Array<{ raw: string; decoded: unknown }
323
323
  return out
324
324
  }
325
325
 
326
+ /**
327
+ * Rebuild the exact ledger text the kernel verifies, dropping only the artifact a
328
+ * trailing newline leaves. Unlike `decodedLines` this keeps interior blank lines:
329
+ * `kernelVerifyLedgerText` splits on "\n" and `JSON.parse`s each line, so an interior
330
+ * blank line is a corrupt entry it must report — renumbering the lines that follow it
331
+ * (by dropping it here) would mask a real break as a healthy ledger.
332
+ */
333
+ function ledgerTextForVerify(filePath: string): string {
334
+ let text: string
335
+ try {
336
+ text = readFileSync(filePath, 'utf-8')
337
+ } catch {
338
+ return ''
339
+ }
340
+ const lines = text.endsWith('\n') ? text.slice(0, -1) : text
341
+ if (lines.length === 0) return ''
342
+ return `${lines}\n`
343
+ }
344
+
326
345
  /** The 1-based line number of the first legacy record AFTER the chain began, else `null`. */
327
346
  export function contestedLedgerLine(filePath: string): number | null {
328
347
  const kinds = decodedLines(filePath).map((l) => classifyDecisionLine(l.decoded))
@@ -798,8 +817,7 @@ export function verifyDecisionLedger(projectRoot: string): {
798
817
  } {
799
818
  const filePath = decisionLogPath(projectRoot)
800
819
  try {
801
- const lines = decodedLines(filePath)
802
- const body = `${lines.map((l) => l.raw).join('\n')}${lines.length > 0 ? '\n' : ''}`
820
+ const body = ledgerTextForVerify(filePath)
803
821
  const verified = kernelVerifyLedgerText(body)
804
822
  return {
805
823
  ok: verified.ok,