iterate-plugin 3.12.0 → 3.13.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 +4 -4
- package/README.zh-CN.md +4 -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 +38 -7
- package/lib/parse.js +24 -0
- package/package.json +5 -4
- package/src/client/i18n.ts +3 -0
- package/src/client/index.ts +28 -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,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), **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.
|
|
@@ -290,7 +290,7 @@ validation:
|
|
|
290
290
|
|
|
291
291
|
## 🔧 Registered tools (19)
|
|
292
292
|
|
|
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
|
|
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; **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
294
|
- `iterate_validate` — run a whitelisted validation command, return the result
|
|
295
295
|
- `iterate_decision_log` — append a decision log entry (append-only, never edits old ones), stored in `.iterate/decision-log.jsonl`
|
|
296
296
|
- `iterate_context` — read the `SKILL.md` / `ITERATE.md` context
|
|
@@ -308,7 +308,7 @@ validation:
|
|
|
308
308
|
- `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
309
|
- `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
310
|
- `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
|
|
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. **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
312
|
|
|
313
313
|
---
|
|
314
314
|
|
|
@@ -346,7 +346,7 @@ All runtime state lives under `.iterate/` at the project root (can be excluded v
|
|
|
346
346
|
`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
347
|
`0.2.0-rc.2`, `0.2.1-alpha.1`, `0.2.1-alpha.2`, `0.2.1-alpha.3`,
|
|
348
348
|
`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` (
|
|
349
|
+
`0.2.1-rc.3`, `0.2.1-rc.4`, `0.2.1-rc.5`, `0.2.2-alpha.2` (28 releases,
|
|
350
350
|
`0.1.1-rc.1` …
|
|
351
351
|
`0.2.2-alpha.2`).
|
|
352
352
|
The plugin is built against `@deepseek-ai/dsh-tools` /
|
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 / 严重度分布 / 验证结果)、**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 覆盖,明暗双模式,可在设置页开关。
|
|
@@ -289,7 +289,7 @@ validation:
|
|
|
289
289
|
|
|
290
290
|
## 🔧 注册工具(19 个)
|
|
291
291
|
|
|
292
|
-
- `iterate_config` — 读取 / 写入 `iterate.config.yaml`。`operation=read` 返回完整配置或指定 section;`operation=write` 做 schema
|
|
292
|
+
- `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
293
|
- `iterate_validate` — 运行白名单验证命令,返回结果
|
|
294
294
|
- `iterate_decision_log` — 追加决策日志(只追加,不改旧),存储于 `.iterate/decision-log.jsonl`
|
|
295
295
|
- `iterate_context` — 读取 `SKILL.md` / `ITERATE.md` 上下文
|
|
@@ -307,7 +307,7 @@ validation:
|
|
|
307
307
|
- `iterate_quality_gate` — **v3.2** 读取质量凭证(`read`),或基于 findings、验证结果、`findingsByRound` 与 `fixedByDimension` 重新计算并持久化一份新凭证(`compute`)。真实的逐维度收敛率。**v3.5 新增 `clear`:删除持久化的质量凭证**,用于新一轮迭代前重置陈旧的门禁
|
|
308
308
|
- `iterate_defense_events` — **v3.2** 查询防御事件(list / counts),`record` 记录一条新事件,或 **v3.5.3 `clear` 清除持久化事件流**,让新一轮迭代不带陈旧防御数据起跑。可读标签跟随项目语言(en / zh)
|
|
309
309
|
- `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
|
|
310
|
+
- `iterate_preflight` — **v3.12** 开跑前只读预检。返回结构化清单:`validation.commands` 是否已配置(为空意味着本轮无法证明修复——**阻断项**)、是否 Git 仓库、多少个未提交文件、磁盘上是否残留 FAIL 质量门禁、是否有中断断点在等待。每项都携带结构化事实(`{ id, status: pass|warn|fail, detail }`),观测台 F11 面板据此本地化为一句话并给出精确修复指令。**v3.13:未配置时自动检测项目 manifest,把 `suggestedCommands`/`ecosystem`/`detectedFrom` 附到阻断项**,F11 据此提供一键「应用建议」。绝不修改配置、文件或产物
|
|
311
311
|
|
|
312
312
|
---
|
|
313
313
|
|
|
@@ -336,7 +336,7 @@ validation:
|
|
|
336
336
|
### Node.js / DSH 兼容范围
|
|
337
337
|
|
|
338
338
|
- **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`(共
|
|
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.1-rc.5`、`0.2.2-alpha.2`(共 28 个版本,`0.1.1-rc.1` … `0.2.2-alpha.2`)。插件基于 `@deepseek-ai/dsh-tools` / `@deepseek-ai/dsh-util-values` `0.2.2-alpha.2` 构建,只使用公开契约(工具注册、客户端 slots + theme、bundle patch,以及 v3.9 起的会话状态记录接口)。插件同时声明 `engines.dsh`(`>=0.1.1-rc.1`),使 dsh ≥ 0.2.2-alpha.2 运行时只依据该范围判断兼容性(更早的运行时仍回退校验 `package.json` 中列出的 `@deepseek-ai/dsh-*` peer 版本)。
|
|
340
340
|
- **一次性 Profile 证据(真实,`dsh` CLI `0.1.1-rc.1`)**:在临时 `$DSH_HOME` 下
|
|
341
341
|
`dsh plugin --profile <p> add <本仓库>` 约 449ms 完成安装;`dsh --profile <p>
|
|
342
342
|
--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
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* iterate-plugin client source-digest:
|
|
1
|
+
/* iterate-plugin client source-digest: 909db487a440ae131a6de59f253a9d84b240e5fbd55bce9ae83a9876b711a9a2 */
|
|
2
2
|
window.__ModuleLoader__.load({ id: "iterate-plugin", factory: (require) => {
|
|
3
3
|
var module = { exports: {} };
|
|
4
4
|
var exports = module.exports;
|
|
@@ -1343,6 +1343,18 @@ function buildPreflightInstruction() {
|
|
|
1343
1343
|
${JSON.stringify({}, null, 2)}
|
|
1344
1344
|
\`\`\``;
|
|
1345
1345
|
}
|
|
1346
|
+
function buildValidationApplyInstruction(commands) {
|
|
1347
|
+
const list = Array.isArray(commands) ? commands.filter((c) => typeof c === "string" && c.length > 0) : [];
|
|
1348
|
+
const payload = {
|
|
1349
|
+
operation: "write",
|
|
1350
|
+
updates: { validation: { commands: { default: list } } }
|
|
1351
|
+
};
|
|
1352
|
+
return `\u8BF7\u8C03\u7528 \`iterate_config\` \u628A\u68C0\u6D4B\u5230\u7684\u9A8C\u8BC1\u547D\u4EE4\u5199\u5165 \`validation.commands.default\`\uFF08\u4F1A\u5148\u5907\u4EFD\u914D\u7F6E\uFF09\uFF1A
|
|
1353
|
+
|
|
1354
|
+
\`\`\`json
|
|
1355
|
+
${JSON.stringify(payload, null, 2)}
|
|
1356
|
+
\`\`\``;
|
|
1357
|
+
}
|
|
1346
1358
|
function buildRollbackInstruction(id) {
|
|
1347
1359
|
return `\u8BF7\u8C03\u7528 \`iterate_rollback\` \u56DE\u6EDA\u4EE5\u4E0B\u4FEE\u590D\uFF1A
|
|
1348
1360
|
|
|
@@ -2755,6 +2767,7 @@ var EN = {
|
|
|
2755
2767
|
\u590D\u5236\u9884\u68C0\u6307\u4EE4: "Copy preflight instruction",
|
|
2756
2768
|
\u9A8C\u8BC1\u547D\u4EE4: "Validation commands",
|
|
2757
2769
|
"\u5DF2\u914D\u7F6E {a0} \u6761\u9A8C\u8BC1\u547D\u4EE4": "{a0} validation command(s) configured",
|
|
2770
|
+
"\u672A\u914D\u7F6E validation.commands\uFF08\u963B\u65AD\u9879\uFF09\u2014\u2014 \u68C0\u6D4B\u5230 {a0} \u9879\u76EE\uFF0C\u5EFA\u8BAE\uFF1A{a1}": "validation.commands is not configured (blocking) \u2014 detected {a0} project; suggested: {a1}",
|
|
2758
2771
|
"\u672A\u914D\u7F6E validation.commands \u2014\u2014 \u672C\u8F6E\u65E0\u6CD5\u8BC1\u660E\u4FEE\u590D\uFF0C\u5C5E\u963B\u65AD\u9879": "validation.commands is not configured \u2014 the run cannot prove fixes (blocking)",
|
|
2759
2772
|
"Git \u4ED3\u5E93": "Git repository",
|
|
2760
2773
|
"\u5DF2\u68C0\u6D4B\u5230 Git \u4ED3\u5E93": "Git repository detected",
|
|
@@ -2771,6 +2784,7 @@ var EN = {
|
|
|
2771
2784
|
"\u5B58\u5728\u4E2D\u65AD\u65AD\u70B9\uFF08\u5DF2\u6062\u590D {a0} \u6B21\uFF09\u2014\u2014 \u6062\u590D\u6216\u6E05\u9664\u540E\u518D\u5F00\u8DD1": "An interrupted checkpoint is waiting (resumed {a0}x) \u2014 resume or clear it first",
|
|
2772
2785
|
\u65E0\u4E2D\u65AD\u65AD\u70B9: "No interrupted checkpoint",
|
|
2773
2786
|
\u914D\u7F6E\u9A8C\u8BC1\u547D\u4EE4: "Configure validation",
|
|
2787
|
+
\u5E94\u7528\u5EFA\u8BAE: "Apply suggestion",
|
|
2774
2788
|
\u6062\u590D\u65AD\u70B9: "Resume checkpoint",
|
|
2775
2789
|
"\u963B\u65AD {a0} \xB7 \u8B66\u544A {a1}": "Blocking {a0} \xB7 warnings {a1}",
|
|
2776
2790
|
"\u9884\u68C0\u5931\u8D25\uFF1A{a0}": "Preflight failed: {a0}",
|
|
@@ -5797,7 +5811,15 @@ function ObservatoryPanel(props) {
|
|
|
5797
5811
|
label: t("\u9A8C\u8BC1\u547D\u4EE4"),
|
|
5798
5812
|
detail: (d) => {
|
|
5799
5813
|
const n = Number(d.commandCount ?? 0);
|
|
5800
|
-
|
|
5814
|
+
if (n > 0) return tf("\u5DF2\u914D\u7F6E {a0} \u6761\u9A8C\u8BC1\u547D\u4EE4", { a0: n });
|
|
5815
|
+
const suggested = Array.isArray(d.suggestedCommands) ? d.suggestedCommands.map((c) => String(c)).filter((c) => c.length > 0) : [];
|
|
5816
|
+
if (suggested.length > 0) {
|
|
5817
|
+
return tf("\u672A\u914D\u7F6E validation.commands\uFF08\u963B\u65AD\u9879\uFF09\u2014\u2014 \u68C0\u6D4B\u5230 {a0} \u9879\u76EE\uFF0C\u5EFA\u8BAE\uFF1A{a1}", {
|
|
5818
|
+
a0: String(d.ecosystem || ""),
|
|
5819
|
+
a1: suggested.join(" \xB7 ")
|
|
5820
|
+
});
|
|
5821
|
+
}
|
|
5822
|
+
return t("\u672A\u914D\u7F6E validation.commands \u2014\u2014 \u672C\u8F6E\u65E0\u6CD5\u8BC1\u660E\u4FEE\u590D\uFF0C\u5C5E\u963B\u65AD\u9879");
|
|
5801
5823
|
}
|
|
5802
5824
|
},
|
|
5803
5825
|
git: {
|
|
@@ -5832,10 +5854,19 @@ function ObservatoryPanel(props) {
|
|
|
5832
5854
|
}
|
|
5833
5855
|
}
|
|
5834
5856
|
};
|
|
5835
|
-
const fixFor = (id) => {
|
|
5857
|
+
const fixFor = (id, detail) => {
|
|
5836
5858
|
switch (id) {
|
|
5837
|
-
case "validation":
|
|
5859
|
+
case "validation": {
|
|
5860
|
+
const suggested = Array.isArray(detail.suggestedCommands) ? detail.suggestedCommands.map((c) => String(c)).filter((c) => c.length > 0) : [];
|
|
5861
|
+
if (suggested.length > 0) {
|
|
5862
|
+
return {
|
|
5863
|
+
key: "preflight-fix-validation",
|
|
5864
|
+
label: t("\u5E94\u7528\u5EFA\u8BAE"),
|
|
5865
|
+
instruction: buildValidationApplyInstruction(suggested)
|
|
5866
|
+
};
|
|
5867
|
+
}
|
|
5838
5868
|
return { key: "preflight-fix-validation", label: t("\u914D\u7F6E\u9A8C\u8BC1\u547D\u4EE4"), instruction: buildConfigFieldInstruction("validation.commands") };
|
|
5869
|
+
}
|
|
5839
5870
|
case "stale-gate":
|
|
5840
5871
|
return { key: "preflight-fix-gate", label: t("\u6E05\u9664\u95E8\u7981"), instruction: buildQualityGateClearInstruction() };
|
|
5841
5872
|
case "interrupted-checkpoint":
|
|
@@ -5849,14 +5880,14 @@ function ObservatoryPanel(props) {
|
|
|
5849
5880
|
const status = String(c.status || "warn");
|
|
5850
5881
|
const meta = CHECK_META[id];
|
|
5851
5882
|
const label = meta ? meta.label : id || "?";
|
|
5852
|
-
const
|
|
5853
|
-
const fix = status !== "pass" ? fixFor(id) : null;
|
|
5883
|
+
const detailText = meta ? meta.detail(c.detail || {}) : "";
|
|
5884
|
+
const fix = status !== "pass" ? fixFor(id, c.detail || {}) : null;
|
|
5854
5885
|
return React.createElement(
|
|
5855
5886
|
"div",
|
|
5856
5887
|
{ key: `pf-${i}`, className: "iterate-obs-row" },
|
|
5857
5888
|
React.createElement("span", { className: "iterate-gate", "data-status": status }, status.toUpperCase()),
|
|
5858
5889
|
React.createElement("b", {}, label),
|
|
5859
|
-
React.createElement("span", { className: "iterate-obs-msg" },
|
|
5890
|
+
React.createElement("span", { className: "iterate-obs-msg" }, detailText),
|
|
5860
5891
|
fix ? React.createElement("button", {
|
|
5861
5892
|
className: "iterate-btn",
|
|
5862
5893
|
"data-copied": copiedKey === fix.key ? "" : void 0,
|
package/lib/parse.js
CHANGED
|
@@ -1954,6 +1954,30 @@ export function buildPreflightInstruction() {
|
|
|
1954
1954
|
)
|
|
1955
1955
|
}
|
|
1956
1956
|
|
|
1957
|
+
/**
|
|
1958
|
+
* `iterate_config` write instruction that installs the validation commands the
|
|
1959
|
+
* preflight panel detected. Copies the exact command set the user saw, so the
|
|
1960
|
+
* model applies it deterministically instead of re-detecting (and drifting).
|
|
1961
|
+
*
|
|
1962
|
+
* @param {string[]} commands
|
|
1963
|
+
* @returns {string}
|
|
1964
|
+
*/
|
|
1965
|
+
export function buildValidationApplyInstruction(commands) {
|
|
1966
|
+
const list = Array.isArray(commands)
|
|
1967
|
+
? commands.filter((c) => typeof c === 'string' && c.length > 0)
|
|
1968
|
+
: []
|
|
1969
|
+
const payload = {
|
|
1970
|
+
operation: 'write',
|
|
1971
|
+
updates: { validation: { commands: { default: list } } },
|
|
1972
|
+
}
|
|
1973
|
+
return (
|
|
1974
|
+
'请调用 `iterate_config` 把检测到的验证命令写入 `validation.commands.default`(会先备份配置):\n\n' +
|
|
1975
|
+
'```json\n' +
|
|
1976
|
+
`${JSON.stringify(payload, null, 2)}\n` +
|
|
1977
|
+
'```'
|
|
1978
|
+
)
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1957
1981
|
/**
|
|
1958
1982
|
* `iterate_rollback` payload for one applied fix id.
|
|
1959
1983
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.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",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"0.2.1-rc.2": "compatible",
|
|
55
55
|
"0.2.1-rc.3": "compatible",
|
|
56
56
|
"0.2.1-rc.4": "compatible",
|
|
57
|
+
"0.2.1-rc.5": "compatible",
|
|
57
58
|
"0.2.2-alpha.2": "compatible"
|
|
58
59
|
}
|
|
59
60
|
},
|
|
@@ -96,9 +97,9 @@
|
|
|
96
97
|
},
|
|
97
98
|
"peerDependencies": {
|
|
98
99
|
"@deepseek-ai/cordis": "^4.0.5-alpha.2 || ~4.0.6-alpha.1",
|
|
99
|
-
"@deepseek-ai/dsh-session": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 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",
|
|
100
|
-
"@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 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",
|
|
101
|
-
"@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 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"
|
|
100
|
+
"@deepseek-ai/dsh-session": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 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",
|
|
101
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 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",
|
|
102
|
+
"@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 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"
|
|
102
103
|
},
|
|
103
104
|
"peerDependenciesMeta": {
|
|
104
105
|
"@deepseek-ai/cordis": {
|
package/src/client/i18n.ts
CHANGED
|
@@ -460,6 +460,8 @@ export const EN: Record<string, string> = {
|
|
|
460
460
|
复制预检指令: 'Copy preflight instruction',
|
|
461
461
|
验证命令: 'Validation commands',
|
|
462
462
|
'已配置 {a0} 条验证命令': '{a0} validation command(s) configured',
|
|
463
|
+
'未配置 validation.commands(阻断项)—— 检测到 {a0} 项目,建议:{a1}':
|
|
464
|
+
'validation.commands is not configured (blocking) — detected {a0} project; suggested: {a1}',
|
|
463
465
|
'未配置 validation.commands —— 本轮无法证明修复,属阻断项':
|
|
464
466
|
'validation.commands is not configured — the run cannot prove fixes (blocking)',
|
|
465
467
|
'Git 仓库': 'Git repository',
|
|
@@ -481,6 +483,7 @@ export const EN: Record<string, string> = {
|
|
|
481
483
|
'An interrupted checkpoint is waiting (resumed {a0}x) — resume or clear it first',
|
|
482
484
|
无中断断点: 'No interrupted checkpoint',
|
|
483
485
|
配置验证命令: 'Configure validation',
|
|
486
|
+
应用建议: 'Apply suggestion',
|
|
484
487
|
恢复断点: 'Resume checkpoint',
|
|
485
488
|
'阻断 {a0} · 警告 {a1}': 'Blocking {a0} · warnings {a1}',
|
|
486
489
|
'预检失败:{a0}': 'Preflight failed: {a0}',
|
package/src/client/index.ts
CHANGED
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
scanSessionForDefenseEvents,
|
|
78
78
|
scanSessionForPreflight,
|
|
79
79
|
buildPreflightInstruction,
|
|
80
|
+
buildValidationApplyInstruction,
|
|
80
81
|
filterLiveEntries,
|
|
81
82
|
filterTimelineEntries,
|
|
82
83
|
serializeObservatoryExport,
|
|
@@ -3464,9 +3465,17 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3464
3465
|
label: t('验证命令'),
|
|
3465
3466
|
detail: (d) => {
|
|
3466
3467
|
const n = Number(d.commandCount ?? 0)
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3468
|
+
if (n > 0) return tf('已配置 {a0} 条验证命令', { a0: n })
|
|
3469
|
+
const suggested = Array.isArray(d.suggestedCommands)
|
|
3470
|
+
? d.suggestedCommands.map((c) => String(c)).filter((c) => c.length > 0)
|
|
3471
|
+
: []
|
|
3472
|
+
if (suggested.length > 0) {
|
|
3473
|
+
return tf('未配置 validation.commands(阻断项)—— 检测到 {a0} 项目,建议:{a1}', {
|
|
3474
|
+
a0: String(d.ecosystem || ''),
|
|
3475
|
+
a1: suggested.join(' · '),
|
|
3476
|
+
})
|
|
3477
|
+
}
|
|
3478
|
+
return t('未配置 validation.commands —— 本轮无法证明修复,属阻断项')
|
|
3470
3479
|
},
|
|
3471
3480
|
},
|
|
3472
3481
|
git: {
|
|
@@ -3505,10 +3514,21 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3505
3514
|
}
|
|
3506
3515
|
|
|
3507
3516
|
// Repair action per check id; null = guidance only (no tool can fix it).
|
|
3508
|
-
const fixFor = (id: string): { key: string; label: string; instruction: string } | null => {
|
|
3517
|
+
const fixFor = (id: string, detail: Record<string, unknown>): { key: string; label: string; instruction: string } | null => {
|
|
3509
3518
|
switch (id) {
|
|
3510
|
-
case 'validation':
|
|
3519
|
+
case 'validation': {
|
|
3520
|
+
const suggested = Array.isArray(detail.suggestedCommands)
|
|
3521
|
+
? detail.suggestedCommands.map((c) => String(c)).filter((c) => c.length > 0)
|
|
3522
|
+
: []
|
|
3523
|
+
if (suggested.length > 0) {
|
|
3524
|
+
return {
|
|
3525
|
+
key: 'preflight-fix-validation',
|
|
3526
|
+
label: t('应用建议'),
|
|
3527
|
+
instruction: buildValidationApplyInstruction(suggested),
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3511
3530
|
return { key: 'preflight-fix-validation', label: t('配置验证命令'), instruction: buildConfigFieldInstruction('validation.commands') }
|
|
3531
|
+
}
|
|
3512
3532
|
case 'stale-gate':
|
|
3513
3533
|
return { key: 'preflight-fix-gate', label: t('清除门禁'), instruction: buildQualityGateClearInstruction() }
|
|
3514
3534
|
case 'interrupted-checkpoint':
|
|
@@ -3523,12 +3543,12 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3523
3543
|
const status = String(c.status || 'warn')
|
|
3524
3544
|
const meta = CHECK_META[id]
|
|
3525
3545
|
const label = meta ? meta.label : id || '?'
|
|
3526
|
-
const
|
|
3527
|
-
const fix = status !== 'pass' ? fixFor(id) : null
|
|
3546
|
+
const detailText = meta ? meta.detail(c.detail || {}) : ''
|
|
3547
|
+
const fix = status !== 'pass' ? fixFor(id, c.detail || {}) : null
|
|
3528
3548
|
return React.createElement('div', { key: `pf-${i}`, className: 'iterate-obs-row' },
|
|
3529
3549
|
React.createElement('span', { className: 'iterate-gate', 'data-status': status }, status.toUpperCase()),
|
|
3530
3550
|
React.createElement('b', {}, label),
|
|
3531
|
-
React.createElement('span', { className: 'iterate-obs-msg' },
|
|
3551
|
+
React.createElement('span', { className: 'iterate-obs-msg' }, detailText),
|
|
3532
3552
|
fix
|
|
3533
3553
|
? React.createElement('button', {
|
|
3534
3554
|
className: 'iterate-btn', 'data-copied': copiedKey === fix.key ? '' : undefined,
|
package/src/tools/config.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from 'node:path'
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs'
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
4
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
5
|
-
import { defaultConfig, loadEffectiveConfig, validateConfig, resolveProjectRootForExec } from '../config-loader.ts'
|
|
5
|
+
import { defaultConfig, loadEffectiveConfig, validateConfig, resolveProjectRootForExec, flattenCommands } from '../config-loader.ts'
|
|
6
6
|
import {
|
|
7
7
|
applyConfigUpdates,
|
|
8
8
|
readRawConfig,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
writeConfigFile,
|
|
11
11
|
} from '../config-write.ts'
|
|
12
12
|
import { withProjectLock } from '../file-lock.ts'
|
|
13
|
+
import { buildValidationUpdates, detectValidationCommands } from '../validation-detect.ts'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Register the `iterate_config` tool.
|
|
@@ -21,16 +22,24 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
21
22
|
ctx.tools.register(
|
|
22
23
|
defineTool({
|
|
23
24
|
name: 'iterate_config',
|
|
24
|
-
// `read` never writes; `write` persists a config update
|
|
25
|
-
//
|
|
26
|
-
isConcurrencySafe: (args) =>
|
|
25
|
+
// `read` never writes; `write` persists a config update. `detect` is a
|
|
26
|
+
// read UNLESS it is asked to apply the detected commands.
|
|
27
|
+
isConcurrencySafe: (args) => {
|
|
28
|
+
const a = args as { operation?: unknown; apply?: unknown }
|
|
29
|
+
return a.operation !== 'write' && !(a.operation === 'detect' && a.apply === true)
|
|
30
|
+
},
|
|
27
31
|
// Pending-call card: a config read vs a validated write.
|
|
28
32
|
presentCall: (args) => {
|
|
29
|
-
const a = args as { operation?: unknown; section?: unknown }
|
|
30
|
-
const isWrite = a.operation === 'write'
|
|
33
|
+
const a = args as { operation?: unknown; section?: unknown; apply?: unknown }
|
|
34
|
+
const isWrite = a.operation === 'write' || (a.operation === 'detect' && a.apply === true)
|
|
35
|
+
const title = isWrite
|
|
36
|
+
? 'Update iterate.config.yaml'
|
|
37
|
+
: a.operation === 'detect'
|
|
38
|
+
? 'Detect validation commands'
|
|
39
|
+
: 'Read iterate.config.yaml'
|
|
31
40
|
return {
|
|
32
41
|
card: 'generic',
|
|
33
|
-
title
|
|
42
|
+
title,
|
|
34
43
|
kind: isWrite ? 'edit' : 'read',
|
|
35
44
|
...(typeof a.section === 'string' && a.section ? { rawInput: { section: a.section } } : {}),
|
|
36
45
|
}
|
|
@@ -39,13 +48,17 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
39
48
|
'Read or update the iterate.config.yaml configuration from the project root. ' +
|
|
40
49
|
'Returns the full parsed config, a specific section, or validation errors. ' +
|
|
41
50
|
'Use this to discover available dimensions, validation commands, git settings, and personalization rules, ' +
|
|
42
|
-
'or to write back validated changes (goal, dimensions, max_rounds, review, atomic, validation, git, etc.).'
|
|
51
|
+
'or to write back validated changes (goal, dimensions, max_rounds, review, atomic, validation, git, etc.). ' +
|
|
52
|
+
'operation "detect" inspects the project manifest (package.json scripts / pytest / Cargo / Go / Make) and ' +
|
|
53
|
+
'proposes a ready-to-apply validation.commands set; pass apply:true to install it in one call.',
|
|
43
54
|
|
|
44
55
|
parameters: {
|
|
45
56
|
operation: {
|
|
46
57
|
type: 'string',
|
|
47
|
-
description:
|
|
48
|
-
|
|
58
|
+
description:
|
|
59
|
+
'Default "read". "write" validates and applies a partial config update (backed up first). ' +
|
|
60
|
+
'"detect" proposes validation commands from the project manifest.',
|
|
61
|
+
enum: ['read', 'write', 'detect'],
|
|
49
62
|
},
|
|
50
63
|
path: {
|
|
51
64
|
type: 'string',
|
|
@@ -60,6 +73,16 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
60
73
|
type: 'boolean',
|
|
61
74
|
description: 'If true, validate the config schema and return any missing fields.',
|
|
62
75
|
},
|
|
76
|
+
apply: {
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
description:
|
|
79
|
+
'For operation "detect": when true, install the detected commands under validation.commands.default ' +
|
|
80
|
+
'(refused if commands are already configured unless force:true).',
|
|
81
|
+
},
|
|
82
|
+
force: {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
description: 'For operation "detect" with apply:true: replace an existing default command set.',
|
|
85
|
+
},
|
|
63
86
|
updates: {
|
|
64
87
|
type: 'json',
|
|
65
88
|
description:
|
|
@@ -83,6 +106,11 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
83
106
|
availableSections: { type: 'array', items: { type: 'string' } },
|
|
84
107
|
operation: { type: 'string' },
|
|
85
108
|
ok: { type: 'boolean' },
|
|
109
|
+
applied: { type: 'boolean' },
|
|
110
|
+
ecosystem: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
111
|
+
commands: { type: 'array', items: { type: 'string' } },
|
|
112
|
+
sources: { type: 'json' },
|
|
113
|
+
updates: { type: 'json' },
|
|
86
114
|
backupPath: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
87
115
|
error: { type: 'string' },
|
|
88
116
|
},
|
|
@@ -99,6 +127,81 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
99
127
|
}
|
|
100
128
|
const projectRoot = resolved.root
|
|
101
129
|
|
|
130
|
+
// ── Detect operation (validation-command auto-detection) ───────────
|
|
131
|
+
// Reads the project manifest and proposes a curated command set. With
|
|
132
|
+
// apply:true it installs the set under validation.commands.default in
|
|
133
|
+
// the same validated, backed-up, locked write path as operation:write.
|
|
134
|
+
if (args.operation === 'detect') {
|
|
135
|
+
const configPath = join(projectRoot, 'iterate.config.yaml')
|
|
136
|
+
const configExists = existsSync(configPath)
|
|
137
|
+
const detection = detectValidationCommands(projectRoot)
|
|
138
|
+
const updates = buildValidationUpdates(detection.commands)
|
|
139
|
+
const base = {
|
|
140
|
+
operation: 'detect',
|
|
141
|
+
found: configExists,
|
|
142
|
+
ecosystem: detection.ecosystem,
|
|
143
|
+
commands: detection.commands,
|
|
144
|
+
sources: detection.sources as unknown as JsonValue,
|
|
145
|
+
updates: updates as unknown as JsonValue,
|
|
146
|
+
}
|
|
147
|
+
if (detection.commands.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
ok: false,
|
|
151
|
+
applied: false,
|
|
152
|
+
error:
|
|
153
|
+
'no validation commands detected — the project does not advertise a test/typecheck/lint/build ' +
|
|
154
|
+
'command; configure validation.commands manually',
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (args.apply !== true) {
|
|
158
|
+
return { ...base, ok: true, applied: false }
|
|
159
|
+
}
|
|
160
|
+
// Refuse to silently clobber a hand-tuned command set.
|
|
161
|
+
const current = loadEffectiveConfig(projectRoot).config
|
|
162
|
+
const existing = flattenCommands(current.validation?.commands)
|
|
163
|
+
if (existing.length > 0 && args.force !== true) {
|
|
164
|
+
return {
|
|
165
|
+
...base,
|
|
166
|
+
ok: false,
|
|
167
|
+
applied: false,
|
|
168
|
+
error:
|
|
169
|
+
`validation.commands already has ${existing.length} command(s); ` +
|
|
170
|
+
'pass force:true to replace the "default" module',
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return withProjectLock(projectRoot, 'config-write', () => {
|
|
174
|
+
let cfgBase: Record<string, unknown>
|
|
175
|
+
try {
|
|
176
|
+
cfgBase = configExists
|
|
177
|
+
? readRawConfig(configPath)
|
|
178
|
+
: (defaultConfig() as unknown as Record<string, unknown>)
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return { ...base, ok: false, applied: false, error: `failed to read config: ${String(err)}` }
|
|
181
|
+
}
|
|
182
|
+
const merged = applyConfigUpdates(cfgBase, updates)
|
|
183
|
+
const schemaErrors = validateConfig(merged)
|
|
184
|
+
if (schemaErrors.length > 0) {
|
|
185
|
+
return {
|
|
186
|
+
...base,
|
|
187
|
+
ok: false,
|
|
188
|
+
applied: false,
|
|
189
|
+
errors: schemaErrors.map((e) => `missing/required field: ${e}`),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const result = writeConfigFile(projectRoot, merged)
|
|
193
|
+
if (!result.ok) return { ...base, ok: false, applied: false, error: result.error }
|
|
194
|
+
const { config } = loadEffectiveConfig(projectRoot)
|
|
195
|
+
return {
|
|
196
|
+
...base,
|
|
197
|
+
ok: true,
|
|
198
|
+
applied: true,
|
|
199
|
+
backupPath: result.backupPath ?? null,
|
|
200
|
+
config: config as unknown as JsonValue,
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
102
205
|
// ── Write operation ────────────────────────────────────────────────
|
|
103
206
|
if (args.operation === 'write') {
|
|
104
207
|
const updates = args.updates as Record<string, unknown> | undefined
|
package/src/tools/preflight.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { readCheckpoint, computeStatus } from './checkpoint.ts'
|
|
|
25
25
|
import { readDecisionEntries } from './decision-log.ts'
|
|
26
26
|
import { readRegistry } from './fix.ts'
|
|
27
27
|
import { resultHeadline } from './present.ts'
|
|
28
|
+
import { detectValidationCommands } from '../validation-detect.ts'
|
|
28
29
|
|
|
29
30
|
/** A single check's outcome. `fail` blocks a clean run; `warn` is advisory. */
|
|
30
31
|
export type PreflightStatus = 'pass' | 'warn' | 'fail'
|
|
@@ -65,6 +66,12 @@ export interface PreflightInput {
|
|
|
65
66
|
interrupted: boolean
|
|
66
67
|
/** How many times the leftover checkpoint has already been resumed. */
|
|
67
68
|
resumeCount: number
|
|
69
|
+
/** Commands auto-detected from the project manifest (only when none are configured). */
|
|
70
|
+
suggestedCommands?: string[]
|
|
71
|
+
/** Ecosystem that produced the suggestions (node/python/rust/go/make). */
|
|
72
|
+
ecosystem?: string | null
|
|
73
|
+
/** Manifest files the suggestions were derived from. */
|
|
74
|
+
detectedFrom?: string[]
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
/**
|
|
@@ -88,11 +95,23 @@ export interface PreflightInput {
|
|
|
88
95
|
* progressed; `warn` + resume or clear.
|
|
89
96
|
*/
|
|
90
97
|
export function buildPreflightReport(input: PreflightInput): PreflightReport {
|
|
98
|
+
const suggestions = Array.isArray(input.suggestedCommands) ? input.suggestedCommands.filter((c) => typeof c === 'string' && c.length > 0) : []
|
|
91
99
|
const checks: PreflightCheck[] = [
|
|
92
100
|
{
|
|
93
101
|
id: 'validation',
|
|
94
102
|
status: input.validationCommandCount > 0 ? 'pass' : 'fail',
|
|
95
|
-
detail: {
|
|
103
|
+
detail: {
|
|
104
|
+
commandCount: input.validationCommandCount,
|
|
105
|
+
// Only meaningful when the blocking case is hit; the client turns these
|
|
106
|
+
// into a one-click "apply detected commands" action.
|
|
107
|
+
...(input.validationCommandCount === 0 && suggestions.length > 0
|
|
108
|
+
? {
|
|
109
|
+
suggestedCommands: suggestions,
|
|
110
|
+
ecosystem: input.ecosystem ?? null,
|
|
111
|
+
detectedFrom: Array.isArray(input.detectedFrom) ? input.detectedFrom : [],
|
|
112
|
+
}
|
|
113
|
+
: {}),
|
|
114
|
+
},
|
|
96
115
|
},
|
|
97
116
|
{
|
|
98
117
|
id: 'git',
|
|
@@ -126,9 +145,17 @@ function describeCheck(id: string, detail: Record<string, unknown>): string {
|
|
|
126
145
|
switch (id) {
|
|
127
146
|
case 'validation': {
|
|
128
147
|
const n = Number(detail.commandCount ?? 0)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
if (n > 0) return `${n} validation command(s) configured`
|
|
149
|
+
const suggested = Array.isArray(detail.suggestedCommands)
|
|
150
|
+
? detail.suggestedCommands.map((c) => String(c))
|
|
151
|
+
: []
|
|
152
|
+
if (suggested.length > 0) {
|
|
153
|
+
return (
|
|
154
|
+
'no validation.commands configured — the run cannot prove fixes (blocking); ' +
|
|
155
|
+
`detected ${String(detail.ecosystem ?? 'project')} commands: ${suggested.join(', ')}`
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
return 'no validation.commands configured — the run cannot prove fixes (blocking)'
|
|
132
159
|
}
|
|
133
160
|
case 'git':
|
|
134
161
|
return detail.isRepo === true
|
|
@@ -229,6 +256,24 @@ export function registerPreflightTool(ctx: { tools: { register: (def: ReturnType
|
|
|
229
256
|
validationCommandCount = 0
|
|
230
257
|
}
|
|
231
258
|
|
|
259
|
+
// 1b. When nothing is configured, inspect the project manifest so the
|
|
260
|
+
// blocking check ships with an actionable, ready-to-apply set.
|
|
261
|
+
let suggestedCommands: string[] = []
|
|
262
|
+
let detectedEcosystem: string | null = null
|
|
263
|
+
let detectedFrom: string[] = []
|
|
264
|
+
if (validationCommandCount === 0) {
|
|
265
|
+
try {
|
|
266
|
+
const detection = detectValidationCommands(projectRoot)
|
|
267
|
+
suggestedCommands = detection.commands
|
|
268
|
+
detectedEcosystem = detection.ecosystem
|
|
269
|
+
detectedFrom = detection.sources.map((s) => s.file)
|
|
270
|
+
} catch {
|
|
271
|
+
suggestedCommands = []
|
|
272
|
+
detectedEcosystem = null
|
|
273
|
+
detectedFrom = []
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
232
277
|
// 2/3. git repo + dirty tree (best-effort; any failure degrades to unknown).
|
|
233
278
|
let isGitRepo = false
|
|
234
279
|
let changedFiles: number | null = null
|
|
@@ -281,6 +326,9 @@ export function registerPreflightTool(ctx: { tools: { register: (def: ReturnType
|
|
|
281
326
|
gateOverallStatus,
|
|
282
327
|
interrupted,
|
|
283
328
|
resumeCount,
|
|
329
|
+
suggestedCommands,
|
|
330
|
+
ecosystem: detectedEcosystem,
|
|
331
|
+
detectedFrom,
|
|
284
332
|
})
|
|
285
333
|
return { ...report, checks: report.checks as unknown as JsonValue }
|
|
286
334
|
},
|
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
/** Ecosystems this module knows how to read a manifest for. */
|
|
21
|
+
export type Ecosystem = 'node' | 'python' | 'rust' | 'go' | 'make'
|
|
22
|
+
|
|
23
|
+
/** One manifest fact that produced a command. */
|
|
24
|
+
export interface DetectionSource {
|
|
25
|
+
/** Manifest file, relative to the project root. */
|
|
26
|
+
file: string
|
|
27
|
+
/** Short machine-stable reason, e.g. `scripts.test`. */
|
|
28
|
+
reason: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The result of inspecting a project root. */
|
|
32
|
+
export interface ValidationDetection {
|
|
33
|
+
/** The ecosystem that produced the first command, or null when none matched. */
|
|
34
|
+
ecosystem: Ecosystem | null
|
|
35
|
+
/** Ordered, de-duplicated commands for `validation.commands.default`. */
|
|
36
|
+
commands: string[]
|
|
37
|
+
/** One entry per command, same order/length as `commands`. */
|
|
38
|
+
sources: DetectionSource[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Hard cap so a project with many scripts cannot flood the config. */
|
|
42
|
+
const MAX_COMMANDS = 6
|
|
43
|
+
|
|
44
|
+
function readText(path: string): string | null {
|
|
45
|
+
try {
|
|
46
|
+
return readFileSync(path, 'utf-8')
|
|
47
|
+
} catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readJsonObject(path: string): Record<string, unknown> | null {
|
|
53
|
+
const text = readText(path)
|
|
54
|
+
if (text === null) return null
|
|
55
|
+
try {
|
|
56
|
+
const parsed: unknown = JSON.parse(text)
|
|
57
|
+
// A JSON array / scalar is not a manifest object — treat it as absent.
|
|
58
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
59
|
+
? (parsed as Record<string, unknown>)
|
|
60
|
+
: null
|
|
61
|
+
} catch {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function existsFile(root: string, name: string): boolean {
|
|
67
|
+
try {
|
|
68
|
+
return existsSync(join(root, name))
|
|
69
|
+
} catch {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
75
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
76
|
+
? (value as Record<string, unknown>)
|
|
77
|
+
: {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type AddCommand = (command: string, file: string, reason: string) => void
|
|
81
|
+
|
|
82
|
+
/** Node: only scripts the manifest explicitly defines, in a curated order. */
|
|
83
|
+
function detectNode(root: string, add: AddCommand): boolean {
|
|
84
|
+
const pkg = readJsonObject(join(root, 'package.json'))
|
|
85
|
+
if (!pkg) return false
|
|
86
|
+
const scripts = asRecord(pkg.scripts)
|
|
87
|
+
const has = (name: string): boolean =>
|
|
88
|
+
typeof scripts[name] === 'string' && (scripts[name] as string).trim().length > 0
|
|
89
|
+
// test/typecheck/lint/build are the four signals that actually prove a change
|
|
90
|
+
// is sound. Arbitrary user scripts are intentionally NOT auto-run.
|
|
91
|
+
if (has('test')) add('npm test', 'package.json', 'scripts.test')
|
|
92
|
+
if (has('typecheck')) add('npm run typecheck', 'package.json', 'scripts.typecheck')
|
|
93
|
+
if (has('lint')) add('npm run lint', 'package.json', 'scripts.lint')
|
|
94
|
+
if (has('build')) add('npm run build', 'package.json', 'scripts.build')
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Python: pytest plus configured ruff / mypy. A bare pyproject is not pytest. */
|
|
99
|
+
function detectPython(root: string, add: AddCommand): boolean {
|
|
100
|
+
const pyproject = readText(join(root, 'pyproject.toml'))
|
|
101
|
+
const setupCfg = readText(join(root, 'setup.cfg'))
|
|
102
|
+
const pytestIni = existsFile(root, 'pytest.ini')
|
|
103
|
+
const toxIni = existsFile(root, 'tox.ini')
|
|
104
|
+
const testsDir = existsFile(root, 'tests')
|
|
105
|
+
const pyprojectPytest = pyproject !== null && /\[tool\.pytest/.test(pyproject)
|
|
106
|
+
const setupCfgPytest = setupCfg !== null && /\[tool:pytest\]/.test(setupCfg)
|
|
107
|
+
const matched = pyproject !== null || setupCfg !== null || pytestIni || toxIni || testsDir
|
|
108
|
+
if (!matched) return false
|
|
109
|
+
|
|
110
|
+
if (pytestIni) add('pytest -q', 'pytest.ini', 'pytest')
|
|
111
|
+
else if (toxIni) add('pytest -q', 'tox.ini', 'pytest')
|
|
112
|
+
else if (pyprojectPytest) add('pytest -q', 'pyproject.toml', 'tool.pytest')
|
|
113
|
+
else if (setupCfgPytest) add('pytest -q', 'setup.cfg', 'tool:pytest')
|
|
114
|
+
else if (testsDir) add('pytest -q', 'tests', 'tests-dir')
|
|
115
|
+
|
|
116
|
+
if (pyproject !== null && /\[tool\.ruff/.test(pyproject)) add('ruff check .', 'pyproject.toml', 'tool.ruff')
|
|
117
|
+
if (pyproject !== null && /\[tool\.mypy\]/.test(pyproject)) add('mypy .', 'pyproject.toml', 'tool.mypy')
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Rust: cargo test, plus clippy/rustfmt when the project configures them. */
|
|
122
|
+
function detectRust(root: string, add: AddCommand): boolean {
|
|
123
|
+
if (!existsFile(root, 'Cargo.toml')) return false
|
|
124
|
+
add('cargo test', 'Cargo.toml', 'cargo.test')
|
|
125
|
+
if (existsFile(root, 'clippy.toml') || existsFile(root, '.clippy.toml')) {
|
|
126
|
+
add('cargo clippy --all-targets -- -D warnings', 'clippy.toml', 'cargo.clippy')
|
|
127
|
+
}
|
|
128
|
+
if (existsFile(root, 'rustfmt.toml') || existsFile(root, '.rustfmt.toml')) {
|
|
129
|
+
add('cargo fmt --check', 'rustfmt.toml', 'cargo.fmt')
|
|
130
|
+
}
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Go: test + vet are the two checks a go module always supports. */
|
|
135
|
+
function detectGo(root: string, add: AddCommand): boolean {
|
|
136
|
+
if (!existsFile(root, 'go.mod')) return false
|
|
137
|
+
add('go test ./...', 'go.mod', 'go.test')
|
|
138
|
+
add('go vet ./...', 'go.mod', 'go.vet')
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Make: the generic fallback, only when no ecosystem-specific command exists. */
|
|
143
|
+
function detectMake(root: string, add: AddCommand): boolean {
|
|
144
|
+
const makefile = ['Makefile', 'makefile', 'GNUmakefile'].find((name) => existsFile(root, name))
|
|
145
|
+
if (!makefile) return false
|
|
146
|
+
const text = readText(join(root, makefile)) ?? ''
|
|
147
|
+
const targets = new Set<string>()
|
|
148
|
+
for (const line of text.split(/\r?\n/)) {
|
|
149
|
+
// `target:`, but NOT a `VAR := value` / `VAR = value` assignment. The
|
|
150
|
+
// lookahead has to skip the optional space before `=` (`test := nope`).
|
|
151
|
+
const match = /^([A-Za-z0-9_.-]+)\s*:(?!=|\s*=)/.exec(line)
|
|
152
|
+
if (match) targets.add(match[1] as string)
|
|
153
|
+
}
|
|
154
|
+
if (targets.has('test')) add('make test', makefile, 'target.test')
|
|
155
|
+
if (targets.has('check')) add('make check', makefile, 'target.check')
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Inspect `projectRoot` and return a curated, ordered validation-command set.
|
|
161
|
+
*
|
|
162
|
+
* Never throws: unreadable or malformed manifests simply contribute nothing.
|
|
163
|
+
* The returned `commands` and `sources` always have the same length.
|
|
164
|
+
*/
|
|
165
|
+
export function detectValidationCommands(projectRoot: string): ValidationDetection {
|
|
166
|
+
const commands: string[] = []
|
|
167
|
+
const sources: DetectionSource[] = []
|
|
168
|
+
const seen = new Set<string>()
|
|
169
|
+
const add: AddCommand = (command, file, reason) => {
|
|
170
|
+
if (commands.length >= MAX_COMMANDS) return
|
|
171
|
+
if (seen.has(command)) return
|
|
172
|
+
seen.add(command)
|
|
173
|
+
commands.push(command)
|
|
174
|
+
sources.push({ file, reason })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let ecosystem: Ecosystem | null = null
|
|
178
|
+
const run = (name: Ecosystem, detect: (add: AddCommand) => boolean): void => {
|
|
179
|
+
const before = commands.length
|
|
180
|
+
let matched = false
|
|
181
|
+
try {
|
|
182
|
+
matched = detect(add)
|
|
183
|
+
} catch {
|
|
184
|
+
matched = false
|
|
185
|
+
}
|
|
186
|
+
if (matched && commands.length > before && ecosystem === null) ecosystem = name
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
run('node', (a) => detectNode(projectRoot, a))
|
|
190
|
+
run('python', (a) => detectPython(projectRoot, a))
|
|
191
|
+
run('rust', (a) => detectRust(projectRoot, a))
|
|
192
|
+
run('go', (a) => detectGo(projectRoot, a))
|
|
193
|
+
// Make is the generic wrapper: only consult it when nothing else produced a
|
|
194
|
+
// command, so a Makefile that just calls `npm test` does not double-report.
|
|
195
|
+
if (commands.length === 0) run('make', (a) => detectMake(projectRoot, a))
|
|
196
|
+
|
|
197
|
+
return { ecosystem, commands, sources }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Build the partial config update that installs a detected command set under
|
|
202
|
+
* the `default` module. The config tool deep-merges this into an existing
|
|
203
|
+
* `validation.commands` map, so other modules are preserved.
|
|
204
|
+
*/
|
|
205
|
+
export function buildValidationUpdates(commands: string[]): Record<string, unknown> {
|
|
206
|
+
return { validation: { commands: { default: [...commands] } } }
|
|
207
|
+
}
|