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 +2 -2
- package/README.zh-CN.md +1 -1
- package/dist/index.js +5 -4
- package/dist/tools/decision-log.js +21 -2
- package/lib/client.js +168 -1
- package/lib/parse.js +192 -0
- package/package.json +1 -1
- package/src/client/i18n.ts +8 -0
- package/src/client/index.ts +39 -0
- package/src/client/run-compare.ts +26 -0
- package/src/index.ts +5 -4
- package/src/tools/decision-log.ts +20 -2
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
|
|
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 新增跨会话对比**:把当前运行**存为对比基准**,下次运行一键**与基准对比**,逐项并排 +
|
|
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
|
|
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
|
|
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/ —
|
|
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
|
|
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:
|
|
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.
|
|
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",
|
package/src/client/i18n.ts
CHANGED
|
@@ -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',
|
package/src/client/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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/ —
|
|
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
|
|
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,
|