iterate-plugin 3.12.0 → 3.14.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 +8 -8
- package/README.zh-CN.md +5 -4
- package/dist/tools/config.js +109 -9
- package/dist/tools/preflight.js +45 -4
- package/dist/validation-detect.js +193 -0
- package/lib/client.js +267 -7
- package/lib/parse.js +52 -0
- package/package.json +22 -20
- package/src/client/context-menu.ts +289 -0
- package/src/client/i18n.ts +3 -0
- package/src/client/index.ts +145 -8
- package/src/tools/config.ts +113 -10
- package/src/tools/preflight.ts +52 -4
- package/src/validation-detect.ts +207 -0
package/README.md
CHANGED
|
@@ -107,13 +107,14 @@ 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), **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`.
|
|
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), **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).**
|
|
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.
|
|
114
114
|
- **ProgressCapsule** (`shell.overlay`) — popup notification on each round completion / convergence (incl. convergence confirm).
|
|
115
115
|
- **SettingsPanel** (`settings.section`) — theme toggle, triage-persistence notes, config-management guide (covers `validation.commands` / `language` / `personalization.known_intentional` with an explicit "empty validation.commands = unprotected" warning; a field picker generates the matching `iterate_config write` instruction), **v3.7: a 回读当前配置 (read back current config) button that closes the write → read-back loop** — it copies an `iterate_config read` instruction and, once the model echoes the result into the session, shows the current value of the selected field, runtime status overview (artifact layout + view/cleanup tool guide + disk-snapshot pull), one-click triage data reset.
|
|
116
116
|
- **Bilingual UI (v3.7)** — every piece of UI chrome (panels, empty states, tabs, badges, settings, stop-reason and completion labels) ships in 简体中文 and English. Pick the language under **Settings → 界面语言 / Interface language**; the choice persists in `localStorage` (`iterate-ui-lang`) and otherwise auto-detects from `navigator.language` (a stored choice always wins, and auto-detection never writes a preference). **Model-facing copy deliberately stays Chinese** — the `build*Instruction` payloads and the copy-paste guides are contract text mirroring `SKILL.md`, so the two expanded guides carry a 「中文(粘贴给模型的契约文案)」 chip instead of looking like a missed translation.
|
|
117
|
+
- **Right-click menu (v3.14, dsh ≥ 0.2.2-alpha.3)** — the iterate entry points now also hang off the **native unified context menus**: right-click a **Session** in the sidebar to `打开 iterate 质量指挥中心` (expands the docked observatory and jumps to the live stream), copy the preflight / dry-run / checkpoint-resume / disk-snapshot instructions, or right-click a **file** (with line) / **directory** to copy a *scoped* dry-run review instruction. Bilingual labels, copy-failure recovery guidance, and a shared success toast; a host without `contextMenus` degrades to no menu items (the docked panel remains the entry point).
|
|
117
118
|
|
|
118
119
|
The UI layer is **defensive by design**: it degrades gracefully if any of `slots` / `theme` / `React` is unavailable — it never crashes the client.
|
|
119
120
|
|
|
@@ -290,7 +291,7 @@ validation:
|
|
|
290
291
|
|
|
291
292
|
## 🔧 Registered tools (19)
|
|
292
293
|
|
|
293
|
-
- `iterate_config` — read / write `iterate.config.yaml`. `operation=read` returns the full config or a named section; `operation=write` schema-validates, backs up, then merges and writes — auto rollback on failure
|
|
294
|
+
- `iterate_config` — read / write `iterate.config.yaml`. `operation=read` returns the full config or a named section; `operation=write` schema-validates, backs up, then merges and writes — auto rollback on failure; **v3.13 `operation=detect` inspects the project manifest (package.json scripts / pytest / Cargo / Go / Make) and proposes a ready-to-apply `validation.commands.default` set** (`apply:true` installs it in one call, refused unless `force:true` when commands already exist)
|
|
294
295
|
- `iterate_validate` — run a whitelisted validation command, return the result
|
|
295
296
|
- `iterate_decision_log` — append a decision log entry (append-only, never edits old ones), stored in `.iterate/decision-log.jsonl`
|
|
296
297
|
- `iterate_context` — read the `SKILL.md` / `ITERATE.md` context
|
|
@@ -308,7 +309,7 @@ validation:
|
|
|
308
309
|
- `iterate_quality_gate` — **v3.2** read the quality certificate (`read`), or recompute + persist a fresh one (`compute`) from findings, validation results, `findingsByRound`, and `fixedByDimension`. Real per-dimension convergence rates. **v3.5: `clear` removes the persisted certificate to reset a stale gate before a fresh iteration**
|
|
309
310
|
- `iterate_defense_events` — **v3.2** query defense events (list / counts), `record` a new one, or **v3.5.3 `clear` the persisted stream** so a fresh iteration starts without stale defensive data. Human-readable labels follow the project language (en / zh)
|
|
310
311
|
- `iterate_report` — **v3.9** export the run as a portable report. `format:"markdown"` (default) renders a PR-pasteable report (run header, quality gate, findings, fixes, validations, defense events, experience bank); `format:"sarif"` renders a SARIF 2.1.0 log for CI / GitHub Code Scanning (one rule per review dimension, severity → error/warning/note, file+line locations, suggested fix attached). **v3.11: `format:"json"` renders the COMPLETE normalized run model as machine-readable JSON** (findings, fixes, validations, convergence, quality gate, experience bank, defense events — the lossless format for CI dashboards / PR bots). `write:true` persists it to `.iterate/report.md` / `.iterate/report.sarif` / `.iterate/report.json`; `lang` (zh | en) selects the report language. Read-only apart from the optional report file
|
|
311
|
-
- `iterate_preflight` — **v3.12** read-only pre-run readiness check. Returns a structured checklist: `validation.commands` configured (an empty set means the loop cannot prove fixes — **blocking**), whether the project is a git repository, how many files are uncommitted, whether a stale FAIL quality gate is persisted, and whether an interrupted checkpoint is waiting. Each check carries structured facts (`{ id, status: pass|warn|fail, detail }`) the observatory F11 panel localizes into a sentence + offers the exact fix instruction. Never mutates config, files, or artifacts
|
|
312
|
+
- `iterate_preflight` — **v3.12** read-only pre-run readiness check. Returns a structured checklist: `validation.commands` configured (an empty set means the loop cannot prove fixes — **blocking**), whether the project is a git repository, how many files are uncommitted, whether a stale FAIL quality gate is persisted, and whether an interrupted checkpoint is waiting. Each check carries structured facts (`{ id, status: pass|warn|fail, detail }`) the observatory F11 panel localizes into a sentence + offers the exact fix instruction. **v3.13: when nothing is configured, the blocking check auto-detects commands from the manifest and ships them as `suggestedCommands`/`ecosystem`/`detectedFrom`** so F11 offers a one-click 「应用建议」. Never mutates config, files, or artifacts
|
|
312
313
|
|
|
313
314
|
---
|
|
314
315
|
|
|
@@ -346,14 +347,13 @@ All runtime state lives under `.iterate/` at the project root (can be excluded v
|
|
|
346
347
|
`0.1.7-alpha.1`, `0.1.7-alpha.2`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`,
|
|
347
348
|
`0.2.0-rc.2`, `0.2.1-alpha.1`, `0.2.1-alpha.2`, `0.2.1-alpha.3`,
|
|
348
349
|
`0.2.1-alpha.4`, `0.2.1-alpha.5`, `0.2.1-rc.1`, `0.2.1-rc.2`,
|
|
349
|
-
`0.2.1-rc.3`, `0.2.1-rc.4`, `0.2.2-alpha.2
|
|
350
|
-
`0.1.1-rc.1` …
|
|
351
|
-
`0.2.2-alpha.2`).
|
|
350
|
+
`0.2.1-rc.3`, `0.2.1-rc.4`, `0.2.1-rc.5`, `0.2.2-alpha.2`,
|
|
351
|
+
`0.2.2-alpha.3` (29 releases, `0.1.1-rc.1` … `0.2.2-alpha.3`).
|
|
352
352
|
The plugin is built against `@deepseek-ai/dsh-tools` /
|
|
353
|
-
`@deepseek-ai/dsh-util-values` `0.2.2-alpha.
|
|
353
|
+
`@deepseek-ai/dsh-util-values` `0.2.2-alpha.3` and uses only public contracts
|
|
354
354
|
(tool registration, client slots + theme, bundle patch, and — since v3.9 —
|
|
355
355
|
the session state-record API for session-linked run records).
|
|
356
|
-
It also declares `engines.dsh` (`>=0.1.1-rc.1`) so the dsh ≥ 0.2.2-alpha.
|
|
356
|
+
It also declares `engines.dsh` (`>=0.1.1-rc.1`) so the dsh ≥ 0.2.2-alpha.3
|
|
357
357
|
runtime evaluates compatibility from that single range (earlier runtimes fall
|
|
358
358
|
back to the `@deepseek-ai/dsh-*` peer versions listed in `package.json`).
|
|
359
359
|
- **Disposable-Profile evidence (real, `dsh` CLI `0.1.1-rc.1`)**: on a temp
|
package/README.zh-CN.md
CHANGED
|
@@ -106,13 +106,14 @@ 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 / 严重度分布 / 验证结果)、**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
|
|
109
|
+
- **运行时观测台 `ObservatoryPanel`**(`conversation.input.dock`)— 输入框下方**十二个页签**运行时观测台:**实时活动流**(默认页,支持按活动类型筛选)+ **F1** 审查线程(全部展开/全部收起)、**F2** 收敛趋势(含跨轮对比表:每轮 findings / fixed / 严重度分布 / 验证结果)、**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` 载荷)。
|
|
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 覆盖,明暗双模式,可在设置页开关。
|
|
113
113
|
- **进度胶囊 `ProgressCapsule`**(`shell.overlay`)— 每轮完成 / 收敛时右下角弹出通知(含收敛确认)。
|
|
114
114
|
- **iterate 设置区 `SettingsPanel`**(`settings.section`)— 主题开关、分诊持久化说明、配置管理指引(覆盖 `validation.commands` / `language` / `personalization.known_intentional`,并显式警告"validation.commands 为空 = 本轮不受任何测试保护";字段选择器可生成对应的 `iterate_config write` 指令),**v3.7:新增「回读当前配置」补齐写入 → 回读闭环**——复制 `iterate_config read` 指令,模型把结果回显进会话后,卡片就地展示所选字段的当前值;运行时状态概览(产物布局 + 查看/清理工具指引 + 磁盘快照拉取)、一键清空分诊数据。
|
|
115
115
|
- **双语 UI(v3.7)**——所有界面文案(面板、空态、页签、徽标、设置、停止原因与完成汇总)均提供简体中文与 English。在**设置 → 界面语言 / Interface language**切换;选择持久化在 `localStorage`(`iterate-ui-lang`),否则按 `navigator.language` 自动探测(已保存的选择始终优先,且自动探测不会写入偏好)。**面向模型的文案有意保持中文**——`build*Instruction` 指令与可粘贴指引是与 `SKILL.md` 对齐的契约文本,因此两处展开的指引带「中文(粘贴给模型的契约文案)」标签,避免被误当成漏翻。
|
|
116
|
+
- **右键菜单(v3.14,dsh ≥ 0.2.2-alpha.3)**——iterate 入口现在也挂在**原生统一右键菜单**上:右键侧边栏的**会话**可 `打开 iterate 质量指挥中心`(展开停靠观测台并跳到实时流)、复制预检 / dry-run / 断点恢复 / 磁盘快照指令;右键**文件**(带行号)/ **目录**可复制**定向** dry-run 审查指令(仅审查该文件/目录)。双语标签、复制失败的自救指引、共享成功 toast;宿主无 `contextMenus` 时降级为无菜单项(停靠面板仍是入口)。
|
|
116
117
|
|
|
117
118
|
UI 层为**防御式设计**:`slots` / `theme` / `React` 任一不可用时自动降级,不会崩溃客户端。
|
|
118
119
|
|
|
@@ -289,7 +290,7 @@ validation:
|
|
|
289
290
|
|
|
290
291
|
## 🔧 注册工具(19 个)
|
|
291
292
|
|
|
292
|
-
- `iterate_config` — 读取 / 写入 `iterate.config.yaml`。`operation=read` 返回完整配置或指定 section;`operation=write` 做 schema
|
|
293
|
+
- `iterate_config` — 读取 / 写入 `iterate.config.yaml`。`operation=read` 返回完整配置或指定 section;`operation=write` 做 schema 校验、备份后局部合并写入,失败自动回滚;**v3.13 新增 `operation=detect`**——读取项目 manifest(package.json 脚本 / pytest / Cargo / Go / Make),自动提出一份可直接应用的 `validation.commands.default` 命令集(`apply:true` 一键写入,已配置时需 `force:true` 才覆盖)
|
|
293
294
|
- `iterate_validate` — 运行白名单验证命令,返回结果
|
|
294
295
|
- `iterate_decision_log` — 追加决策日志(只追加,不改旧),存储于 `.iterate/decision-log.jsonl`
|
|
295
296
|
- `iterate_context` — 读取 `SKILL.md` / `ITERATE.md` 上下文
|
|
@@ -307,7 +308,7 @@ validation:
|
|
|
307
308
|
- `iterate_quality_gate` — **v3.2** 读取质量凭证(`read`),或基于 findings、验证结果、`findingsByRound` 与 `fixedByDimension` 重新计算并持久化一份新凭证(`compute`)。真实的逐维度收敛率。**v3.5 新增 `clear`:删除持久化的质量凭证**,用于新一轮迭代前重置陈旧的门禁
|
|
308
309
|
- `iterate_defense_events` — **v3.2** 查询防御事件(list / counts),`record` 记录一条新事件,或 **v3.5.3 `clear` 清除持久化事件流**,让新一轮迭代不带陈旧防御数据起跑。可读标签跟随项目语言(en / zh)
|
|
309
310
|
- `iterate_report` — **v3.9** 把本次运行导出为可移植报告。`format:"markdown"`(默认)产出可直接粘贴进 PR 的报告(运行头部、质量门禁、发现、修复、验证、防御事件、经验银行);`format:"sarif"` 产出 SARIF 2.1.0 日志供 CI / GitHub Code Scanning 消费(每个审查维度一条规则,严重度 → error/warning/note,携带文件+行号位置与建议修复)。**v3.11:`format:"json"` 产出完整归一化运行模型为机器可读 JSON**(发现、修复、验证、收敛率、质量门禁、经验银行、防御事件——供 CI 仪表盘 / PR 机器人消费的无损格式)。`write:true` 持久化到 `.iterate/report.md` / `.iterate/report.sarif` / `.iterate/report.json`;`lang`(zh | en)选择报告语言。除可选的报告文件外均为只读
|
|
310
|
-
- `iterate_preflight` — **v3.12** 开跑前只读预检。返回结构化清单:`validation.commands` 是否已配置(为空意味着本轮无法证明修复——**阻断项**)、是否 Git 仓库、多少个未提交文件、磁盘上是否残留 FAIL 质量门禁、是否有中断断点在等待。每项都携带结构化事实(`{ id, status: pass|warn|fail, detail }`),观测台 F11
|
|
311
|
+
- `iterate_preflight` — **v3.12** 开跑前只读预检。返回结构化清单:`validation.commands` 是否已配置(为空意味着本轮无法证明修复——**阻断项**)、是否 Git 仓库、多少个未提交文件、磁盘上是否残留 FAIL 质量门禁、是否有中断断点在等待。每项都携带结构化事实(`{ id, status: pass|warn|fail, detail }`),观测台 F11 面板据此本地化为一句话并给出精确修复指令。**v3.13:未配置时自动检测项目 manifest,把 `suggestedCommands`/`ecosystem`/`detectedFrom` 附到阻断项**,F11 据此提供一键「应用建议」。绝不修改配置、文件或产物
|
|
311
312
|
|
|
312
313
|
---
|
|
313
314
|
|
|
@@ -336,7 +337,7 @@ validation:
|
|
|
336
337
|
### Node.js / DSH 兼容范围
|
|
337
338
|
|
|
338
339
|
- **Node.js**:`>=20`(`package.json` 的 `engines.node`)。
|
|
339
|
-
- **DSH**:`package.json` 的 `dsh.compatibility.dshReleases` 字段是发布兼容性的**唯一真源**(本 README 逐字镜像该字段;两者需同步更新)。以下官方版本声明为 `compatible`:`0.1.1-rc.1`、`0.1.2-alpha.4`、`0.1.2-alpha.5`、`0.1.2-rc.1`、`0.1.3-alpha.1`、`0.1.5-alpha.1`、`0.1.5-alpha.2`、`0.1.5-rc.1`、`0.1.5-rc.2`、`0.1.6-alpha.1`、`0.1.6-alpha.2`、`0.1.7-alpha.1`、`0.1.7-alpha.2`、`0.1.7-rc.1`、`0.1.7-rc.2`、`0.2.0-rc.1`、`0.2.0-rc.2`、`0.2.1-alpha.1`、`0.2.1-alpha.2`、`0.2.1-alpha.3`、`0.2.1-alpha.4`、`0.2.1-alpha.5`、`0.2.1-rc.1`、`0.2.1-rc.2`、`0.2.1-rc.3`、`0.2.1-rc.4`、`0.2.2-alpha.2`(共
|
|
340
|
+
- **DSH**:`package.json` 的 `dsh.compatibility.dshReleases` 字段是发布兼容性的**唯一真源**(本 README 逐字镜像该字段;两者需同步更新)。以下官方版本声明为 `compatible`:`0.1.1-rc.1`、`0.1.2-alpha.4`、`0.1.2-alpha.5`、`0.1.2-rc.1`、`0.1.3-alpha.1`、`0.1.5-alpha.1`、`0.1.5-alpha.2`、`0.1.5-rc.1`、`0.1.5-rc.2`、`0.1.6-alpha.1`、`0.1.6-alpha.2`、`0.1.7-alpha.1`、`0.1.7-alpha.2`、`0.1.7-rc.1`、`0.1.7-rc.2`、`0.2.0-rc.1`、`0.2.0-rc.2`、`0.2.1-alpha.1`、`0.2.1-alpha.2`、`0.2.1-alpha.3`、`0.2.1-alpha.4`、`0.2.1-alpha.5`、`0.2.1-rc.1`、`0.2.1-rc.2`、`0.2.1-rc.3`、`0.2.1-rc.4`、`0.2.1-rc.5`、`0.2.2-alpha.2`、`0.2.2-alpha.3`(共 29 个版本,`0.1.1-rc.1` … `0.2.2-alpha.3`)。插件基于 `@deepseek-ai/dsh-tools` / `@deepseek-ai/dsh-util-values` `0.2.2-alpha.3` 构建,只使用公开契约(工具注册、客户端 slots + theme、bundle patch,以及 v3.9 起的会话状态记录接口)。插件同时声明 `engines.dsh`(`>=0.1.1-rc.1`),使 dsh ≥ 0.2.2-alpha.3 运行时只依据该范围判断兼容性(更早的运行时仍回退校验 `package.json` 中列出的 `@deepseek-ai/dsh-*` peer 版本)。
|
|
340
341
|
- **一次性 Profile 证据(真实,`dsh` CLI `0.1.1-rc.1`)**:在临时 `$DSH_HOME` 下
|
|
341
342
|
`dsh plugin --profile <p> add <本仓库>` 约 449ms 完成安装;`dsh --profile <p>
|
|
342
343
|
--dump-config` 正确合成 `iterate-plugin` bundle patch;`dsh plugin --profile <p>
|
package/dist/tools/config.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
4
|
-
import { defaultConfig, loadEffectiveConfig, validateConfig, resolveProjectRootForExec } from "../config-loader.js";
|
|
4
|
+
import { defaultConfig, loadEffectiveConfig, validateConfig, resolveProjectRootForExec, flattenCommands } from "../config-loader.js";
|
|
5
5
|
import { applyConfigUpdates, readRawConfig, validateConfigUpdates, writeConfigFile, } from "../config-write.js";
|
|
6
6
|
import { withProjectLock } from "../file-lock.js";
|
|
7
|
+
import { buildValidationUpdates, detectValidationCommands } from "../validation-detect.js";
|
|
7
8
|
/**
|
|
8
9
|
* Register the `iterate_config` tool.
|
|
9
10
|
* Reads and returns the iterate.config.yaml configuration, or writes a
|
|
@@ -13,16 +14,24 @@ import { withProjectLock } from "../file-lock.js";
|
|
|
13
14
|
export function registerConfigTool(ctx) {
|
|
14
15
|
ctx.tools.register(defineTool({
|
|
15
16
|
name: 'iterate_config',
|
|
16
|
-
// `read` never writes; `write` persists a config update
|
|
17
|
-
//
|
|
18
|
-
isConcurrencySafe: (args) =>
|
|
17
|
+
// `read` never writes; `write` persists a config update. `detect` is a
|
|
18
|
+
// read UNLESS it is asked to apply the detected commands.
|
|
19
|
+
isConcurrencySafe: (args) => {
|
|
20
|
+
const a = args;
|
|
21
|
+
return a.operation !== 'write' && !(a.operation === 'detect' && a.apply === true);
|
|
22
|
+
},
|
|
19
23
|
// Pending-call card: a config read vs a validated write.
|
|
20
24
|
presentCall: (args) => {
|
|
21
25
|
const a = args;
|
|
22
|
-
const isWrite = a.operation === 'write';
|
|
26
|
+
const isWrite = a.operation === 'write' || (a.operation === 'detect' && a.apply === true);
|
|
27
|
+
const title = isWrite
|
|
28
|
+
? 'Update iterate.config.yaml'
|
|
29
|
+
: a.operation === 'detect'
|
|
30
|
+
? 'Detect validation commands'
|
|
31
|
+
: 'Read iterate.config.yaml';
|
|
23
32
|
return {
|
|
24
33
|
card: 'generic',
|
|
25
|
-
title
|
|
34
|
+
title,
|
|
26
35
|
kind: isWrite ? 'edit' : 'read',
|
|
27
36
|
...(typeof a.section === 'string' && a.section ? { rawInput: { section: a.section } } : {}),
|
|
28
37
|
};
|
|
@@ -30,12 +39,15 @@ export function registerConfigTool(ctx) {
|
|
|
30
39
|
description: 'Read or update the iterate.config.yaml configuration from the project root. ' +
|
|
31
40
|
'Returns the full parsed config, a specific section, or validation errors. ' +
|
|
32
41
|
'Use this to discover available dimensions, validation commands, git settings, and personalization rules, ' +
|
|
33
|
-
'or to write back validated changes (goal, dimensions, max_rounds, review, atomic, validation, git, etc.).'
|
|
42
|
+
'or to write back validated changes (goal, dimensions, max_rounds, review, atomic, validation, git, etc.). ' +
|
|
43
|
+
'operation "detect" inspects the project manifest (package.json scripts / pytest / Cargo / Go / Make) and ' +
|
|
44
|
+
'proposes a ready-to-apply validation.commands set; pass apply:true to install it in one call.',
|
|
34
45
|
parameters: {
|
|
35
46
|
operation: {
|
|
36
47
|
type: 'string',
|
|
37
|
-
description: 'Default "read". "write" validates and applies a partial config update (backed up first).'
|
|
38
|
-
|
|
48
|
+
description: 'Default "read". "write" validates and applies a partial config update (backed up first). ' +
|
|
49
|
+
'"detect" proposes validation commands from the project manifest.',
|
|
50
|
+
enum: ['read', 'write', 'detect'],
|
|
39
51
|
},
|
|
40
52
|
path: {
|
|
41
53
|
type: 'string',
|
|
@@ -49,6 +61,15 @@ export function registerConfigTool(ctx) {
|
|
|
49
61
|
type: 'boolean',
|
|
50
62
|
description: 'If true, validate the config schema and return any missing fields.',
|
|
51
63
|
},
|
|
64
|
+
apply: {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
description: 'For operation "detect": when true, install the detected commands under validation.commands.default ' +
|
|
67
|
+
'(refused if commands are already configured unless force:true).',
|
|
68
|
+
},
|
|
69
|
+
force: {
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
description: 'For operation "detect" with apply:true: replace an existing default command set.',
|
|
72
|
+
},
|
|
52
73
|
updates: {
|
|
53
74
|
type: 'json',
|
|
54
75
|
description: 'For operation "write": a partial config object to merge in, e.g. ' +
|
|
@@ -70,6 +91,11 @@ export function registerConfigTool(ctx) {
|
|
|
70
91
|
availableSections: { type: 'array', items: { type: 'string' } },
|
|
71
92
|
operation: { type: 'string' },
|
|
72
93
|
ok: { type: 'boolean' },
|
|
94
|
+
applied: { type: 'boolean' },
|
|
95
|
+
ecosystem: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
96
|
+
commands: { type: 'array', items: { type: 'string' } },
|
|
97
|
+
sources: { type: 'json' },
|
|
98
|
+
updates: { type: 'json' },
|
|
73
99
|
backupPath: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
74
100
|
error: { type: 'string' },
|
|
75
101
|
},
|
|
@@ -84,6 +110,80 @@ export function registerConfigTool(ctx) {
|
|
|
84
110
|
return { found: false, error: resolved.reason };
|
|
85
111
|
}
|
|
86
112
|
const projectRoot = resolved.root;
|
|
113
|
+
// ── Detect operation (validation-command auto-detection) ───────────
|
|
114
|
+
// Reads the project manifest and proposes a curated command set. With
|
|
115
|
+
// apply:true it installs the set under validation.commands.default in
|
|
116
|
+
// the same validated, backed-up, locked write path as operation:write.
|
|
117
|
+
if (args.operation === 'detect') {
|
|
118
|
+
const configPath = join(projectRoot, 'iterate.config.yaml');
|
|
119
|
+
const configExists = existsSync(configPath);
|
|
120
|
+
const detection = detectValidationCommands(projectRoot);
|
|
121
|
+
const updates = buildValidationUpdates(detection.commands);
|
|
122
|
+
const base = {
|
|
123
|
+
operation: 'detect',
|
|
124
|
+
found: configExists,
|
|
125
|
+
ecosystem: detection.ecosystem,
|
|
126
|
+
commands: detection.commands,
|
|
127
|
+
sources: detection.sources,
|
|
128
|
+
updates: updates,
|
|
129
|
+
};
|
|
130
|
+
if (detection.commands.length === 0) {
|
|
131
|
+
return {
|
|
132
|
+
...base,
|
|
133
|
+
ok: false,
|
|
134
|
+
applied: false,
|
|
135
|
+
error: 'no validation commands detected — the project does not advertise a test/typecheck/lint/build ' +
|
|
136
|
+
'command; configure validation.commands manually',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (args.apply !== true) {
|
|
140
|
+
return { ...base, ok: true, applied: false };
|
|
141
|
+
}
|
|
142
|
+
// Refuse to silently clobber a hand-tuned command set.
|
|
143
|
+
const current = loadEffectiveConfig(projectRoot).config;
|
|
144
|
+
const existing = flattenCommands(current.validation?.commands);
|
|
145
|
+
if (existing.length > 0 && args.force !== true) {
|
|
146
|
+
return {
|
|
147
|
+
...base,
|
|
148
|
+
ok: false,
|
|
149
|
+
applied: false,
|
|
150
|
+
error: `validation.commands already has ${existing.length} command(s); ` +
|
|
151
|
+
'pass force:true to replace the "default" module',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return withProjectLock(projectRoot, 'config-write', () => {
|
|
155
|
+
let cfgBase;
|
|
156
|
+
try {
|
|
157
|
+
cfgBase = configExists
|
|
158
|
+
? readRawConfig(configPath)
|
|
159
|
+
: defaultConfig();
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
return { ...base, ok: false, applied: false, error: `failed to read config: ${String(err)}` };
|
|
163
|
+
}
|
|
164
|
+
const merged = applyConfigUpdates(cfgBase, updates);
|
|
165
|
+
const schemaErrors = validateConfig(merged);
|
|
166
|
+
if (schemaErrors.length > 0) {
|
|
167
|
+
return {
|
|
168
|
+
...base,
|
|
169
|
+
ok: false,
|
|
170
|
+
applied: false,
|
|
171
|
+
errors: schemaErrors.map((e) => `missing/required field: ${e}`),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const result = writeConfigFile(projectRoot, merged);
|
|
175
|
+
if (!result.ok)
|
|
176
|
+
return { ...base, ok: false, applied: false, error: result.error };
|
|
177
|
+
const { config } = loadEffectiveConfig(projectRoot);
|
|
178
|
+
return {
|
|
179
|
+
...base,
|
|
180
|
+
ok: true,
|
|
181
|
+
applied: true,
|
|
182
|
+
backupPath: result.backupPath ?? null,
|
|
183
|
+
config: config,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
87
187
|
// ── Write operation ────────────────────────────────────────────────
|
|
88
188
|
if (args.operation === 'write') {
|
|
89
189
|
const updates = args.updates;
|
package/dist/tools/preflight.js
CHANGED
|
@@ -23,6 +23,7 @@ import { readCheckpoint, computeStatus } from "./checkpoint.js";
|
|
|
23
23
|
import { readDecisionEntries } from "./decision-log.js";
|
|
24
24
|
import { readRegistry } from "./fix.js";
|
|
25
25
|
import { resultHeadline } from "./present.js";
|
|
26
|
+
import { detectValidationCommands } from "../validation-detect.js";
|
|
26
27
|
/**
|
|
27
28
|
* Build the readiness checklist from already-gathered facts (pure).
|
|
28
29
|
*
|
|
@@ -44,11 +45,23 @@ import { resultHeadline } from "./present.js";
|
|
|
44
45
|
* progressed; `warn` + resume or clear.
|
|
45
46
|
*/
|
|
46
47
|
export function buildPreflightReport(input) {
|
|
48
|
+
const suggestions = Array.isArray(input.suggestedCommands) ? input.suggestedCommands.filter((c) => typeof c === 'string' && c.length > 0) : [];
|
|
47
49
|
const checks = [
|
|
48
50
|
{
|
|
49
51
|
id: 'validation',
|
|
50
52
|
status: input.validationCommandCount > 0 ? 'pass' : 'fail',
|
|
51
|
-
detail: {
|
|
53
|
+
detail: {
|
|
54
|
+
commandCount: input.validationCommandCount,
|
|
55
|
+
// Only meaningful when the blocking case is hit; the client turns these
|
|
56
|
+
// into a one-click "apply detected commands" action.
|
|
57
|
+
...(input.validationCommandCount === 0 && suggestions.length > 0
|
|
58
|
+
? {
|
|
59
|
+
suggestedCommands: suggestions,
|
|
60
|
+
ecosystem: input.ecosystem ?? null,
|
|
61
|
+
detectedFrom: Array.isArray(input.detectedFrom) ? input.detectedFrom : [],
|
|
62
|
+
}
|
|
63
|
+
: {}),
|
|
64
|
+
},
|
|
52
65
|
},
|
|
53
66
|
{
|
|
54
67
|
id: 'git',
|
|
@@ -80,9 +93,16 @@ function describeCheck(id, detail) {
|
|
|
80
93
|
switch (id) {
|
|
81
94
|
case 'validation': {
|
|
82
95
|
const n = Number(detail.commandCount ?? 0);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
if (n > 0)
|
|
97
|
+
return `${n} validation command(s) configured`;
|
|
98
|
+
const suggested = Array.isArray(detail.suggestedCommands)
|
|
99
|
+
? detail.suggestedCommands.map((c) => String(c))
|
|
100
|
+
: [];
|
|
101
|
+
if (suggested.length > 0) {
|
|
102
|
+
return ('no validation.commands configured — the run cannot prove fixes (blocking); ' +
|
|
103
|
+
`detected ${String(detail.ecosystem ?? 'project')} commands: ${suggested.join(', ')}`);
|
|
104
|
+
}
|
|
105
|
+
return 'no validation.commands configured — the run cannot prove fixes (blocking)';
|
|
86
106
|
}
|
|
87
107
|
case 'git':
|
|
88
108
|
return detail.isRepo === true
|
|
@@ -180,6 +200,24 @@ export function registerPreflightTool(ctx) {
|
|
|
180
200
|
catch {
|
|
181
201
|
validationCommandCount = 0;
|
|
182
202
|
}
|
|
203
|
+
// 1b. When nothing is configured, inspect the project manifest so the
|
|
204
|
+
// blocking check ships with an actionable, ready-to-apply set.
|
|
205
|
+
let suggestedCommands = [];
|
|
206
|
+
let detectedEcosystem = null;
|
|
207
|
+
let detectedFrom = [];
|
|
208
|
+
if (validationCommandCount === 0) {
|
|
209
|
+
try {
|
|
210
|
+
const detection = detectValidationCommands(projectRoot);
|
|
211
|
+
suggestedCommands = detection.commands;
|
|
212
|
+
detectedEcosystem = detection.ecosystem;
|
|
213
|
+
detectedFrom = detection.sources.map((s) => s.file);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
suggestedCommands = [];
|
|
217
|
+
detectedEcosystem = null;
|
|
218
|
+
detectedFrom = [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
183
221
|
// 2/3. git repo + dirty tree (best-effort; any failure degrades to unknown).
|
|
184
222
|
let isGitRepo = false;
|
|
185
223
|
let changedFiles = null;
|
|
@@ -232,6 +270,9 @@ export function registerPreflightTool(ctx) {
|
|
|
232
270
|
gateOverallStatus,
|
|
233
271
|
interrupted,
|
|
234
272
|
resumeCount,
|
|
273
|
+
suggestedCommands,
|
|
274
|
+
ecosystem: detectedEcosystem,
|
|
275
|
+
detectedFrom,
|
|
235
276
|
});
|
|
236
277
|
return { ...report, checks: report.checks };
|
|
237
278
|
},
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/validation-detect.ts — detect a project's validation commands.
|
|
3
|
+
*
|
|
4
|
+
* Why it exists: the iterate loop is fail-closed on `validation.commands` — an
|
|
5
|
+
* empty set means it cannot tell a fix from a regression, so `iterate_preflight`
|
|
6
|
+
* blocks the run. Until now the only remedy was to hand-write the YAML, which is
|
|
7
|
+
* error-prone (wrong nesting, wrong module key, commands the project does not
|
|
8
|
+
* actually expose). This module inspects the project's own manifest files and
|
|
9
|
+
* proposes a small, curated command set that the config tool can apply in one
|
|
10
|
+
* call.
|
|
11
|
+
*
|
|
12
|
+
* Detection is advisory and read-only: it only proposes commands the project's
|
|
13
|
+
* manifest actually advertises (package.json scripts, pytest/ruff/mypy config,
|
|
14
|
+
* Cargo, Go, Make targets). It never invents a command and never executes one.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
/** Hard cap so a project with many scripts cannot flood the config. */
|
|
19
|
+
const MAX_COMMANDS = 6;
|
|
20
|
+
function readText(path) {
|
|
21
|
+
try {
|
|
22
|
+
return readFileSync(path, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readJsonObject(path) {
|
|
29
|
+
const text = readText(path);
|
|
30
|
+
if (text === null)
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(text);
|
|
34
|
+
// A JSON array / scalar is not a manifest object — treat it as absent.
|
|
35
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
36
|
+
? parsed
|
|
37
|
+
: null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function existsFile(root, name) {
|
|
44
|
+
try {
|
|
45
|
+
return existsSync(join(root, name));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function asRecord(value) {
|
|
52
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
53
|
+
? value
|
|
54
|
+
: {};
|
|
55
|
+
}
|
|
56
|
+
/** Node: only scripts the manifest explicitly defines, in a curated order. */
|
|
57
|
+
function detectNode(root, add) {
|
|
58
|
+
const pkg = readJsonObject(join(root, 'package.json'));
|
|
59
|
+
if (!pkg)
|
|
60
|
+
return false;
|
|
61
|
+
const scripts = asRecord(pkg.scripts);
|
|
62
|
+
const has = (name) => typeof scripts[name] === 'string' && scripts[name].trim().length > 0;
|
|
63
|
+
// test/typecheck/lint/build are the four signals that actually prove a change
|
|
64
|
+
// is sound. Arbitrary user scripts are intentionally NOT auto-run.
|
|
65
|
+
if (has('test'))
|
|
66
|
+
add('npm test', 'package.json', 'scripts.test');
|
|
67
|
+
if (has('typecheck'))
|
|
68
|
+
add('npm run typecheck', 'package.json', 'scripts.typecheck');
|
|
69
|
+
if (has('lint'))
|
|
70
|
+
add('npm run lint', 'package.json', 'scripts.lint');
|
|
71
|
+
if (has('build'))
|
|
72
|
+
add('npm run build', 'package.json', 'scripts.build');
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/** Python: pytest plus configured ruff / mypy. A bare pyproject is not pytest. */
|
|
76
|
+
function detectPython(root, add) {
|
|
77
|
+
const pyproject = readText(join(root, 'pyproject.toml'));
|
|
78
|
+
const setupCfg = readText(join(root, 'setup.cfg'));
|
|
79
|
+
const pytestIni = existsFile(root, 'pytest.ini');
|
|
80
|
+
const toxIni = existsFile(root, 'tox.ini');
|
|
81
|
+
const testsDir = existsFile(root, 'tests');
|
|
82
|
+
const pyprojectPytest = pyproject !== null && /\[tool\.pytest/.test(pyproject);
|
|
83
|
+
const setupCfgPytest = setupCfg !== null && /\[tool:pytest\]/.test(setupCfg);
|
|
84
|
+
const matched = pyproject !== null || setupCfg !== null || pytestIni || toxIni || testsDir;
|
|
85
|
+
if (!matched)
|
|
86
|
+
return false;
|
|
87
|
+
if (pytestIni)
|
|
88
|
+
add('pytest -q', 'pytest.ini', 'pytest');
|
|
89
|
+
else if (toxIni)
|
|
90
|
+
add('pytest -q', 'tox.ini', 'pytest');
|
|
91
|
+
else if (pyprojectPytest)
|
|
92
|
+
add('pytest -q', 'pyproject.toml', 'tool.pytest');
|
|
93
|
+
else if (setupCfgPytest)
|
|
94
|
+
add('pytest -q', 'setup.cfg', 'tool:pytest');
|
|
95
|
+
else if (testsDir)
|
|
96
|
+
add('pytest -q', 'tests', 'tests-dir');
|
|
97
|
+
if (pyproject !== null && /\[tool\.ruff/.test(pyproject))
|
|
98
|
+
add('ruff check .', 'pyproject.toml', 'tool.ruff');
|
|
99
|
+
if (pyproject !== null && /\[tool\.mypy\]/.test(pyproject))
|
|
100
|
+
add('mypy .', 'pyproject.toml', 'tool.mypy');
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
/** Rust: cargo test, plus clippy/rustfmt when the project configures them. */
|
|
104
|
+
function detectRust(root, add) {
|
|
105
|
+
if (!existsFile(root, 'Cargo.toml'))
|
|
106
|
+
return false;
|
|
107
|
+
add('cargo test', 'Cargo.toml', 'cargo.test');
|
|
108
|
+
if (existsFile(root, 'clippy.toml') || existsFile(root, '.clippy.toml')) {
|
|
109
|
+
add('cargo clippy --all-targets -- -D warnings', 'clippy.toml', 'cargo.clippy');
|
|
110
|
+
}
|
|
111
|
+
if (existsFile(root, 'rustfmt.toml') || existsFile(root, '.rustfmt.toml')) {
|
|
112
|
+
add('cargo fmt --check', 'rustfmt.toml', 'cargo.fmt');
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
/** Go: test + vet are the two checks a go module always supports. */
|
|
117
|
+
function detectGo(root, add) {
|
|
118
|
+
if (!existsFile(root, 'go.mod'))
|
|
119
|
+
return false;
|
|
120
|
+
add('go test ./...', 'go.mod', 'go.test');
|
|
121
|
+
add('go vet ./...', 'go.mod', 'go.vet');
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
/** Make: the generic fallback, only when no ecosystem-specific command exists. */
|
|
125
|
+
function detectMake(root, add) {
|
|
126
|
+
const makefile = ['Makefile', 'makefile', 'GNUmakefile'].find((name) => existsFile(root, name));
|
|
127
|
+
if (!makefile)
|
|
128
|
+
return false;
|
|
129
|
+
const text = readText(join(root, makefile)) ?? '';
|
|
130
|
+
const targets = new Set();
|
|
131
|
+
for (const line of text.split(/\r?\n/)) {
|
|
132
|
+
// `target:`, but NOT a `VAR := value` / `VAR = value` assignment. The
|
|
133
|
+
// lookahead has to skip the optional space before `=` (`test := nope`).
|
|
134
|
+
const match = /^([A-Za-z0-9_.-]+)\s*:(?!=|\s*=)/.exec(line);
|
|
135
|
+
if (match)
|
|
136
|
+
targets.add(match[1]);
|
|
137
|
+
}
|
|
138
|
+
if (targets.has('test'))
|
|
139
|
+
add('make test', makefile, 'target.test');
|
|
140
|
+
if (targets.has('check'))
|
|
141
|
+
add('make check', makefile, 'target.check');
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Inspect `projectRoot` and return a curated, ordered validation-command set.
|
|
146
|
+
*
|
|
147
|
+
* Never throws: unreadable or malformed manifests simply contribute nothing.
|
|
148
|
+
* The returned `commands` and `sources` always have the same length.
|
|
149
|
+
*/
|
|
150
|
+
export function detectValidationCommands(projectRoot) {
|
|
151
|
+
const commands = [];
|
|
152
|
+
const sources = [];
|
|
153
|
+
const seen = new Set();
|
|
154
|
+
const add = (command, file, reason) => {
|
|
155
|
+
if (commands.length >= MAX_COMMANDS)
|
|
156
|
+
return;
|
|
157
|
+
if (seen.has(command))
|
|
158
|
+
return;
|
|
159
|
+
seen.add(command);
|
|
160
|
+
commands.push(command);
|
|
161
|
+
sources.push({ file, reason });
|
|
162
|
+
};
|
|
163
|
+
let ecosystem = null;
|
|
164
|
+
const run = (name, detect) => {
|
|
165
|
+
const before = commands.length;
|
|
166
|
+
let matched = false;
|
|
167
|
+
try {
|
|
168
|
+
matched = detect(add);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
matched = false;
|
|
172
|
+
}
|
|
173
|
+
if (matched && commands.length > before && ecosystem === null)
|
|
174
|
+
ecosystem = name;
|
|
175
|
+
};
|
|
176
|
+
run('node', (a) => detectNode(projectRoot, a));
|
|
177
|
+
run('python', (a) => detectPython(projectRoot, a));
|
|
178
|
+
run('rust', (a) => detectRust(projectRoot, a));
|
|
179
|
+
run('go', (a) => detectGo(projectRoot, a));
|
|
180
|
+
// Make is the generic wrapper: only consult it when nothing else produced a
|
|
181
|
+
// command, so a Makefile that just calls `npm test` does not double-report.
|
|
182
|
+
if (commands.length === 0)
|
|
183
|
+
run('make', (a) => detectMake(projectRoot, a));
|
|
184
|
+
return { ecosystem, commands, sources };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Build the partial config update that installs a detected command set under
|
|
188
|
+
* the `default` module. The config tool deep-merges this into an existing
|
|
189
|
+
* `validation.commands` map, so other modules are preserved.
|
|
190
|
+
*/
|
|
191
|
+
export function buildValidationUpdates(commands) {
|
|
192
|
+
return { validation: { commands: { default: [...commands] } } };
|
|
193
|
+
}
|