docs(#47): upgrade DevHarness baseline to bfdf648
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# 仓库内文本统一以 LF 存储,避免 Windows/WSL 混用产生全量行尾差异。
|
||||
# 行尾差异会淹没真实改动,也会让 gitea.env 之类的配置在 bash 中带上 \r,
|
||||
# 导致读取到的 URL 和令牌被截断。
|
||||
* text=auto eol=lf
|
||||
|
||||
*.py text eol=lf
|
||||
*.md text eol=lf
|
||||
*.json text eol=lf
|
||||
*.ps1 text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.zip binary
|
||||
*.har binary
|
||||
@@ -8,7 +8,7 @@
|
||||
## 依赖与并行
|
||||
|
||||
- 前置工单:无 / #编号
|
||||
- 是否允许与未完成的前置工单并行:是 / 否
|
||||
- 是否允许与前置工单并行:是 / 否
|
||||
- 原因:
|
||||
|
||||
## 子项目影响
|
||||
@@ -53,8 +53,10 @@
|
||||
|
||||
- 修改类型:纯显示文案 / 小范围 UI / 新组件 / 新页面或独立用户功能 / 重大交互或导航 / 非 UI / 恢复既有行为的 Bug
|
||||
- 所需设计证据:无 / 标注截图 / 低保真图 / 已确认原型 / 架构、API、数据、状态或流程设计 / 原设计或复现证据
|
||||
- 证据链接、Git 路径或事实来源:
|
||||
- App ID、版本、revision 或确认日期:
|
||||
- 可编辑设计源链接、版本或事实来源:
|
||||
- 本地 HTML 审核快照路径和版本(不适用时说明原因):
|
||||
- 本地浏览方式和资源完整性检查:
|
||||
- 版本、revision 或确认日期:
|
||||
- 状态:无 / 草稿 / 已确认 / 已废弃
|
||||
- 确认人、确认时间和覆盖范围:
|
||||
- 无需 UI 原型或无需任何原型的原因:
|
||||
@@ -65,6 +67,7 @@
|
||||
|
||||
- [ ] 不影响长期文档,原因:
|
||||
- [ ] 更新项目档案或本地开发与验证 Wiki
|
||||
- [ ] 更新常见修改或故障排查 Wiki
|
||||
- [ ] 更新架构与代码地图 Wiki
|
||||
- [ ] 更新业务规则与术语 Wiki
|
||||
- [ ] 更新 API 契约 Wiki
|
||||
|
||||
@@ -2,9 +2,27 @@
|
||||
|
||||
本仓库采用 DevHarness 工作流:Gitea 工单记录任务过程,Gitea Wiki 是长期开发文档和任务归档的事实来源,Git 保存源码、版本绑定资料和 Wiki 的本地镜像。`docs/` 中显式映射的 Markdown 是 Wiki 只读镜像;`docs/task/` 只保存用户明确要求导出的任务归档快照。
|
||||
|
||||
当前文档规则参考 DevHarness 提交 `b1f500128d6eb100985792d4a715db8b6b5ae203`,但所有模板内容都必须按 GoAuto 事实改写。开始工作前阅读 `docs/00-project-profile.md`、`docs/03-business-rules-and-glossary.md` 和当前工单。不同交付单元规则不同时,在 `server/`、`web/` 或 `android/` 下增加更具体的 `AGENTS.md`。
|
||||
当前文档规则参考 DevHarness 提交 `bfdf648962d11a8024f62768380d8571e1f45f68`,但所有模板内容都必须按 GoAuto 事实改写。开始工作前阅读任务涉及目录中的 `AGENTS.md`。不同交付单元规则不同时,在 `server/`、`web/` 或 `android/` 下增加更具体的 `AGENTS.md`;目录越深的规则越具体,但不得削弱上级安全规则。
|
||||
|
||||
## 永久规则
|
||||
[项目档案](docs/00-project-profile.md) 按需阅读,不作为每次任务的固定前置。出现下列情况之一时必须读:需要环境、配置或凭据来源;需要确认目录边界;需要判断子项目与交付单元划分;需要 DevHarness 来源与基线;需要项目专用验收要求。只为查命令不必打开项目档案。涉及采集、采购或设备行为时另读 `docs/03-business-rules-and-glossary.md` 和当前工单。
|
||||
|
||||
## 常用命令
|
||||
|
||||
所有命令默认从仓库根目录执行。
|
||||
|
||||
| 用途 | 命令 |
|
||||
|---|---|
|
||||
| 查看工作区 | `git status --short --branch` |
|
||||
| 检查模板结构 | `python dev_scripts/harness.py check --strict` |
|
||||
| 导出核心 Wiki 镜像 | `python dev_scripts/harness.py sync` |
|
||||
| 检查核心 Wiki 镜像 | `python dev_scripts/harness.py sync --check` |
|
||||
| 导出并完整校验 | `python dev_scripts/harness.py sync --verify` |
|
||||
| 创建任务归档 | `python dev_scripts/harness.py archive 123 "修复登录超时"` |
|
||||
| 增量导出任务归档 | `python dev_scripts/harness.py export` |
|
||||
| 全量导出任务归档 | `python dev_scripts/harness.py export --all` |
|
||||
| 服务端/Web/Android 验证 | `.\scripts\verify.ps1 -Component all` |
|
||||
|
||||
## 1. 永久规则
|
||||
|
||||
- 不把密码、Token、Cookie、私钥、PDD 账号凭据、个人数据或生产数据写入代码、日志、工单和文档。
|
||||
- 不执行付款;任何自动支付实现、入口或测试都禁止进入本项目。
|
||||
@@ -19,39 +37,66 @@
|
||||
- 重置采集任务保留 URL、goods_id、规则和设备快照,事务清空旧结果后重新进入 `pending`。
|
||||
- 保留与当前工单无关的工作区改动,不重置、不覆盖、不顺手修改。
|
||||
- 测试结果必须真实;未执行或无法覆盖的真机、多设备、云环境和高风险行为必须明确记录。
|
||||
- 高风险修改必须停止并等待人工确认:创建订单、权限、安全、并发、数据库迁移、删除数据、发布和其他不可逆操作。
|
||||
|
||||
## 工单门禁
|
||||
## 2. 哪些改动需要工单
|
||||
|
||||
- 新功能、缺陷修复、重构,以及接口、数据库、权限、并发、状态机、安全或用户界面变化必须先有单元工单。
|
||||
- 只改错别字、注释、文档措辞或不改变含义的纯显示文案时可以免工单;有任何行为、布局、状态或安全含义不确定时不得使用豁免。
|
||||
- Epic 和 MVP 只维护目标与子工单索引;单元任务是唯一实施单位。
|
||||
- 工单必须记录原始需求摘要、前置依赖、是否可并行、子项目影响、方案、设计证据、验收、验证、风险和文档影响。
|
||||
- 实施前检查依赖;真实依赖未满足且不允许并行时保持待实施。
|
||||
- 用户未明确验收前,工单保持待验收,不关闭。
|
||||
新功能、缺陷修复、重构,以及接口、数据库、权限、并发、状态机、安全或用户界面变化必须先有单元工单。
|
||||
|
||||
## 工单与设计证据双门禁
|
||||
以下小改动可以直接提交,不要求工单和任务归档:
|
||||
|
||||
- 纯显示文案只有在不改变业务含义、流程、权限、状态、接口、数据、安全、支付、金额、单位、程序标识符、布局和可访问性时才免原型,并执行最小界面检查。
|
||||
- 只改错别字、注释或文档措辞;
|
||||
- 只做格式化、导入排序或不跨文件的内部变量改名;
|
||||
- 补充类型标注或文档字符串且不改变行为;
|
||||
- 删除已经确认无人使用的死代码;
|
||||
- 只修改用户看到的界面显示文案,并且满足本文件「工单与设计证据双门禁」的全部豁免条件。
|
||||
|
||||
除严格符合界面显示文案豁免的修改外,只要涉及接口、数据库、状态、权限、安全、并发、用户界面,或者无法确定是否改变行为,就必须建工单。代码组件名、类名、变量、国际化键、API 字段和数据库字段不是显示文案,不适用豁免。
|
||||
|
||||
Epic 和 MVP 只维护目标与子工单索引;单元任务是唯一正式实施单位。
|
||||
|
||||
## 3. 需求到实施
|
||||
|
||||
1. 先确认目标、非目标和当前事实,区分代码事实、用户确认规则和假设。
|
||||
2. 工单必须记录原始需求摘要、前置依赖、是否可并行、子项目影响、方案、设计证据、验收、验证、风险和文档影响。
|
||||
3. 实施前检查依赖;真实依赖未满足且不允许并行时保持待实施。
|
||||
4. 只修改工单声明的交付单元和共享契约;新发现的相邻问题记录或另建工单,不混入当前任务。
|
||||
5. 共享 API 以 `docs/08-agent-api-contract.md` 为唯一事实来源。
|
||||
6. 数据库和接口变化必须同步更新架构、业务规则和 API 文档。
|
||||
7. 每个动作结果必须与 `taskId`、`deviceId` 和任务内的 `ruleSnapshot` 关联。
|
||||
8. Android Agent 必须使用任务租约和本地互斥锁保证串行。
|
||||
9. 服务端必须以最终包名、Activity 和页面证据验证动作,不只相信 Portal 的 success 响应。
|
||||
10. 执行与风险相称的测试,把实现、验证、未验证项和提交哈希回写工单。
|
||||
11. 长期核心文档必须先修改 Wiki、读取确认,再运行 `python dev_scripts/harness.py sync` 导出本地镜像;任务归档默认只更新 Wiki,不自动导出本地。不得直接编辑镜像后反向覆盖 Wiki。
|
||||
|
||||
Gitea 不可用时,输出完整工单草稿并说明阻塞。未经用户明确授权,不得默认绕过建单。
|
||||
|
||||
### 新项目 Wiki 初始化门禁
|
||||
|
||||
本项目 Wiki 已于 2026-08-17 初始化并启用 Wiki-first,本节适用于从本仓库派生新项目的场景。
|
||||
|
||||
- 从模板或本仓库创建新项目时,先创建 Gitea 远端仓库并启用工单和 Wiki,再配置 `wiki-docs.json`;不得把模板自带的本地 `docs/` 当作新项目 Wiki 已初始化的证据。
|
||||
- 优先使用项目已配置的 Gitea MCP 查询和写入 Wiki;MCP 不可用或不支持所需写操作时,才使用 Gitea API,并在初始化工单记录回退原因。凭据只从环境或 MCP 安全配置读取。
|
||||
- 先查询线上页面列表;`Home` 不存在时必须先创建 `Home`,在线回读正文并记录 revision,然后再逐页创建或更新其他核心映射页面。
|
||||
- 每个核心页面写入后必须在线回读并取得 revision。页面缺失、回读失败或没有 revision 时停止初始化,不得开始产品代码。
|
||||
- 产品编码前必须运行 `python dev_scripts/harness.py sync --verify`;全部成功才表示线上 Wiki 和核心镜像初始化完成。
|
||||
|
||||
### 工单与设计证据双门禁
|
||||
|
||||
- 纯显示文案只有在不改变业务含义、流程、权限、状态、接口、数据、安全、支付、金额、单位、程序标识符、布局和可访问性时才免原型,并执行最小界面检查。代码组件名、类名、变量、国际化键、API 字段和数据库字段不是显示文案,不适用豁免。
|
||||
- 现有界面的小范围样式或布局调整至少提供标注截图、低保真图或明确复用的现有规范。
|
||||
- 新组件记录正常、空、加载、失败、禁用和权限边界;按需提供低保真图。
|
||||
- 新页面、独立用户功能、重大交互或导航变化必须先制作 QuantUX 或其他可审阅原型;用户确认文字需求、原型和覆盖范围后才能编写生产代码。
|
||||
- 完整原型形成待审核版本后,必须在用户审核前生成本地可浏览 HTML 快照,保存到 `prototypes/<工单号>/<版本>/index.html`;版本目录内资源使用相对路径。可编辑设计源仍在 QuantUX,Git HTML 是版本化审核证据,Wiki 和工单只保存索引。
|
||||
- 已确认的 HTML 快照不得原位覆盖;页面结构、流程、状态、权限、异常处理或验收结果变化时,使用新版本目录重新导出并重新确认。提交审核前检查入口、主要交互和资源完整性,并删除凭据、账号、个人信息和生产数据。
|
||||
- 设计工具无法生成可用 HTML 时,在工单记录限制并停止审核,等待用户确认等效的本地可浏览方案;不得只保留难以访问的线上链接后直接编码。纯显示文案、小范围 UI、非 UI 需求和恢复既有行为的 Bug 不强制生成 HTML。
|
||||
- 后端、接口、数据和定时任务不强制 UI 原型,但必须先确认架构、API、数据、状态或流程设计。
|
||||
- 原型记录链接或 Git 路径、App ID/版本、草稿或已确认状态、确认人、确认时间和覆盖范围。草稿不能作为正式实现依据。
|
||||
- 页面结构、主要流程、状态、权限、异常处理或验收结果变化时,先更新设计证据并重新确认。
|
||||
|
||||
## 需求记录与实施
|
||||
> **存量原型的偏离说明(#47 确认)**:`prototypes/` 下已有的 `quantux-*.html` 平铺快照建立于本规则之前,保持原样不迁移,历史版本可在 Git 历史中查阅。工单号/版本目录结构自 #47 起对新增原型生效。
|
||||
|
||||
1. 先确认目标、非目标和当前事实,区分代码事实、用户确认规则和假设。
|
||||
2. 创建单元工单时记录来源、提出时间和表达目的所需的少量关键原话或脱敏摘要;不复制完整聊天或 Agent 内部推理。
|
||||
3. 只修改工单声明的交付单元和共享契约;新发现的相邻问题记录或另建工单,不混入当前任务。
|
||||
4. 共享 API 以 `docs/08-agent-api-contract.md` 为唯一事实来源。
|
||||
5. 数据库和接口变化必须同步更新架构、业务规则和 API 文档。
|
||||
6. 每个动作结果必须与 `taskId`、`deviceId` 和任务内的 `ruleSnapshot` 关联。
|
||||
7. Android Agent 必须使用任务租约和本地互斥锁保证串行。
|
||||
8. 服务端必须以最终包名、Activity 和页面证据验证动作,不只相信 Portal 的 success 响应。
|
||||
9. 执行与风险相称的测试,把实现、验证、未验证项和提交哈希回写工单。
|
||||
|
||||
## 自然语言快捷指令
|
||||
### 自然语言快捷指令
|
||||
|
||||
快捷指令只是本工作流的别名,不能绕过方案确认、前置依赖、安全规则、工单范围、必要验证或人工验收:
|
||||
|
||||
@@ -62,22 +107,71 @@
|
||||
- `继续工单 #N`:从首个未完成步骤继续,不重复仍然有效的检查。
|
||||
- `检查工单 #N`:只读核对范围、验收、测试和证据;不自动修复。
|
||||
- `同步文档`:读取 Wiki、导出核心 `docs/` 镜像并检查一致性;不修改 Wiki、不导出任务归档、不自动提交。
|
||||
- `导出任务归档`:仅在用户明确提出时增量导出 Wiki 任务归档;`导出全部任务归档` 才执行全量导出。
|
||||
- `导出任务归档`:人工触发 `python dev_scripts/harness.py export`,只导出新增或 revision 已变化的任务归档;`导出全部任务归档` 执行 `python dev_scripts/harness.py export --all`。
|
||||
- `#N 验收通过`:仅在用户明确验收后更新任务归档、同步必要镜像、关闭工单并同步父工单。
|
||||
|
||||
## 效率与停止条件
|
||||
### 需求记录与流转
|
||||
|
||||
- 优先执行能产生真实反馈的最小命令,采用“执行 → 首个真实错误 → 最小修复 → 继续”的闭环。
|
||||
- 同一任务和同一环境中已经验证的事实不重复检查;环境、配置、代码或关键前提变化后才重新验证。
|
||||
- 不主动增加与验收无关的文档、脚本、框架、重构或扩展性设计。
|
||||
- 涉及凭据、权限、安全、迁移、并发、删除、发布、创建订单或其他不可逆动作时,先完成相应门禁,不通过试错获取风险反馈。
|
||||
- 完成工单范围、必要测试、文档影响和证据回写后立即停止;未影响当前验收的相邻问题只提示或另建单。
|
||||
- 长期文档固定顺序为:修改 Wiki → 读取确认 → 导出核心 `docs/` → 检查一致性 → 提交镜像。不得直接编辑镜像后反向覆盖 Wiki。
|
||||
- 创建单元工单时记录来源、提出时间和表达目的所需的少量关键原话或脱敏摘要;不复制完整聊天或 Agent 内部推理。
|
||||
- 不得臆造用户原话;无法确认的表述记为假设并标注待确认。
|
||||
- Gitea 工单全文不导出到仓库;`docs/task/` 只是用户明确要求时导出的 Wiki 任务归档快照,可能不完整。
|
||||
- 需求变化时先更新工单与设计证据并重新确认,再继续实施。
|
||||
|
||||
## Git 与验收
|
||||
### 效率与范围控制
|
||||
|
||||
- 提交只包含当前工单内容,提交信息引用工单号。
|
||||
本节只用于减少无关工作和重复检查,不得削弱安全规则、已确认方案、工单范围、必要测试、Wiki 同步、Git 提交和验收归档要求。
|
||||
|
||||
#### 严格控制范围
|
||||
|
||||
- 默认严格按用户确认的目标和单元任务范围执行,不主动扩展相邻问题。
|
||||
- 除非任务目标、仓库强制规则或已发现的真实阻塞需要,不新增额外文档、辅助脚本、备份文件、框架、重构或扩展性设计。
|
||||
- 不执行与本次验收无关的验证;安全检查、受影响范围测试、回归测试和仓库规定的闭环验证不属于「额外验证」。
|
||||
- 新发现的相邻问题最多用一句话提示或记录到独立工单,不自动修复或混入当前提交。
|
||||
|
||||
#### 渐进执行和修复
|
||||
|
||||
- 完成已知必要的安全与前置检查后,优先执行能够产生真实反馈的最小命令。
|
||||
- 一次执行后先处理首个可定位、可行动的真实错误,不同时猜测并修改多个可能原因。
|
||||
- 采用「执行 → 查看错误 → 最小修复 → 从失败点继续或按需重跑」的闭环。
|
||||
- 不在真实证据出现前堆叠与已知风险无关的预防性检查。
|
||||
- 涉及凭据、权限、安全、数据、迁移、并发、删除、发布、创建订单或不可逆操作时,必须先完成相应前置检查,不得通过试错获取风险反馈。
|
||||
|
||||
#### 复用已验证事实
|
||||
|
||||
- 在同一任务和同一环境状态下,已经通过的路由、连接、恢复和环境检查不重复执行。
|
||||
- 只有会话、环境、代码、配置、依赖、凭据、远端状态或关键前提发生变化时才重新检查。
|
||||
- 代码修改后,受影响测试和最终验收必须重新执行;提交前工作区检查、推送前远端分支检查不得因为之前通过而省略。
|
||||
- 真机验证结论只在同一设备、同一 App 版本和同一规则快照下复用。
|
||||
|
||||
#### 明确停止条件
|
||||
|
||||
- 完成用户确认的验收标准和仓库规定的必要闭环后立即停止,不主动继续优化。
|
||||
- 「最小验收条件」包括当前工单要求的实现、必要测试、文档影响处理、Wiki 镜像检查、提交和证据回写,不等同于功能第一次运行成功。
|
||||
- 未影响当前验收的相邻问题只提示或建单,不顺手处理。
|
||||
|
||||
## 4. 工单层级
|
||||
|
||||
- Epic 维护长期目标与子工单索引,MVP 维护一次可交付范围,单元任务是唯一实施单位。
|
||||
- 单元任务通过后才做 MVP 集成验收;MVP 通过后才关闭 MVP;Epic 全部范围完成后才关闭 Epic。
|
||||
|
||||
## 5. Git 与验证
|
||||
|
||||
- 提交只包含当前工单相关文件,提交信息引用工单号。
|
||||
- 优先运行项目档案记录的格式、单元、契约和集成测试。
|
||||
- 涉及创建订单、权限、安全、并发、迁移和删除数据属于高风险,真机或正式实施前必须再次等待人工确认。
|
||||
- 完成后将实现、验证、遗留问题和提交哈希回写工单,等待用户验收。
|
||||
- 用户未明确要求时,不导出 `docs/task/`;任务归档默认只保存在 Wiki。
|
||||
|
||||
## 6. 完成、验收和归档
|
||||
|
||||
1. 逐项完成验收、测试和实现提交,并把最终方案、差异、结果、提交及遗留问题写回工单。
|
||||
2. 工单保持「待验收」,用户没有明确验收通过前不得关闭。
|
||||
3. 运行 `python dev_scripts/harness.py archive <编号> "<短标题>"` 创建 Wiki 任务归档,不登记或导出本地镜像。
|
||||
4. 读取确认 Wiki,运行 `python dev_scripts/harness.py sync --check` 检查核心镜像,并把任务归档页面、revision 和提交哈希写回工单。
|
||||
5. 用户验收通过后关闭单元工单,并同步更新 MVP 和 Epic。
|
||||
6. 用户未明确要求时,不导出 `docs/task/`;任务归档默认只保存在 Wiki。
|
||||
|
||||
## 7. 文档影响
|
||||
|
||||
- 每个单元任务必须在工单中选择「无长期文档影响并说明原因」或列出需要更新的 Wiki 页面。
|
||||
- 启动、测试、部署、排错命令,模块入口、目录职责、主要调用路径,配置、API、数据结构、状态、业务规则、安全边界、日志位置发生变化时,必须更新对应 Wiki。
|
||||
- 普通内部重构只有在入口、行为、配置和验证方式均未改变时,才可以记录为不影响长期文档。
|
||||
- 必需核心页面及结构以 `python dev_scripts/harness.py check --strict` 为准。
|
||||
|
||||
@@ -4,9 +4,33 @@
|
||||
|
||||
## Claude Code 专用说明
|
||||
|
||||
- `AGENTS.md` 是所有编码 Agent 的共同规则事实来源;本文件不重复业务规则。
|
||||
- 目标、范围和修改位置明确时使用常规开发模型完成分析、建单和实施。
|
||||
- 需求模糊、根因不明,或涉及跨模块架构、安全、权限、并发、迁移、创建订单和不可逆操作时,先使用更强推理模型制定方案并等待确认。
|
||||
- 轻量模型只处理范围明确的只读查找、文档读取和日志事实提取,不决定最终根因、风险等级、技术方案或验收结论。
|
||||
- Agent 交接必须包含目标、非目标、事实、假设、方案、修改范围、风险、回退和验收标准。
|
||||
- 只读子 Agent 必须通过工具权限保证只读;未配置只读权限时不得让它接触项目写操作。
|
||||
- 上方导入的 `AGENTS.md` 是所有编码 Agent 的共同规则事实来源,Claude Code 必须完整遵守。
|
||||
- 本文件只记录 Claude Code 特有的模型路由、工具和 Agent 协作规则。
|
||||
- 共同规则变化时只修改 `AGENTS.md`,不要在本文件重复维护。
|
||||
|
||||
## 模型路由
|
||||
|
||||
- 目标、范围和修改位置已经明确时,默认使用 Sonnet 分析、建单和实施。
|
||||
- 需求模糊、根因不明,或涉及跨模块架构、安全、权限、并发、迁移、创建订单和其他不可逆操作时,使用 Opus 或 `opusplan` 制定方案。
|
||||
- Opus 输出方案后必须等待用户确认;确认后由 Sonnet 根据方案创建工单并实施。
|
||||
- Haiku 只用于范围明确的只读任务,例如查找代码入口、读取项目文档、提取日志事实和整理调用关系。
|
||||
- 不让 Haiku 决定最终根因、技术方案、风险等级或验收结论。
|
||||
- 当前模型足以完成任务时不升级模型,也不为了形式固定依次调用三个模型。
|
||||
|
||||
## Agent 交接
|
||||
|
||||
- Opus 向 Sonnet 交接目标、非目标、事实、假设、方案、修改范围、风险、回退方式和验收标准。
|
||||
- Haiku 向主 Agent 交接结论、证据位置和仍不确定的内容,不返回与任务无关的大段原文。
|
||||
- Sonnet 严格按照已确认方案和工单实施;发现范围变化时返回主流程重新确认。
|
||||
- 不同 Agent 复用仍然有效的检查结果;会话、环境或关键前提变化时才重新检查。
|
||||
|
||||
## Haiku 只读约束
|
||||
|
||||
- Haiku 子 Agent 只允许使用读取、搜索和只读代码图谱工具。
|
||||
- 禁止 Haiku 修改文件、创建或更新工单、提交 Git、更新 Wiki 或执行具有副作用的命令。
|
||||
- 只读必须通过子 Agent 工具权限实现,不能只依赖提示语;未配置只读权限时不得调用 Haiku 处理项目内容。
|
||||
|
||||
## GoAuto 专用提醒
|
||||
|
||||
- 采购相关任务涉及创建 PDD 订单,属于不可逆操作;任何模型都不得跳过 `AGENTS.md` 的采购门禁,也不得实现支付。
|
||||
- 规格匹配的决策权在服务端(见 #46);Agent 本地不得猜测规格或点击相近候选。
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
"""把 Gitea Wiki 任务归档人工按需导出到 docs/task。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from new_task_archive import safe_title
|
||||
from wiki_docs import (
|
||||
DEFAULT_CONFIG,
|
||||
ROOT,
|
||||
WikiClient,
|
||||
WikiDocsError,
|
||||
dirty_paths,
|
||||
load_config,
|
||||
parse_mirror,
|
||||
write_mirror,
|
||||
)
|
||||
|
||||
|
||||
TASK_PAGE_PATTERN = re.compile(r"^Task-(?P<number>\d+)-(?P<title>.+)$")
|
||||
|
||||
|
||||
def task_revision(metadata: dict[str, Any], page_name: str) -> str:
|
||||
last_commit = metadata.get("last_commit")
|
||||
revision = last_commit.get("sha") if isinstance(last_commit, dict) else None
|
||||
if not isinstance(revision, str) or not revision:
|
||||
raise WikiDocsError(f"Wiki 页面缺少 revision:{page_name}")
|
||||
return revision
|
||||
|
||||
|
||||
def existing_task_mirrors(root: Path = ROOT) -> dict[str, Path]:
|
||||
"""按镜像头匹配已有文件,兼容历史自定义文件名。"""
|
||||
|
||||
mirrors: dict[str, Path] = {}
|
||||
task_dir = root / "docs" / "task"
|
||||
if not task_dir.is_dir():
|
||||
return mirrors
|
||||
for path in task_dir.glob("*.md"):
|
||||
try:
|
||||
metadata, _ = parse_mirror(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, WikiDocsError) as exc:
|
||||
raise WikiDocsError(f"已有任务镜像无效 {path.name}:{exc}") from exc
|
||||
page_name = metadata.get("wiki_page", "")
|
||||
if not TASK_PAGE_PATTERN.fullmatch(page_name):
|
||||
raise WikiDocsError(f"已有任务镜像页面名无效 {path.name}:{page_name}")
|
||||
if page_name in mirrors:
|
||||
raise WikiDocsError(f"任务页面存在重复本地镜像:{page_name}")
|
||||
mirrors[page_name] = path
|
||||
return mirrors
|
||||
|
||||
|
||||
def task_target(page_name: str, root: Path = ROOT) -> Path:
|
||||
match = TASK_PAGE_PATTERN.fullmatch(page_name)
|
||||
if match is None:
|
||||
raise WikiDocsError(f"不是任务归档页面:{page_name}")
|
||||
title = safe_title(match.group("title"))
|
||||
if not title:
|
||||
raise WikiDocsError(f"任务归档标题无效:{page_name}")
|
||||
return root / "docs" / "task" / f"{match.group('number')}-{title}.md"
|
||||
|
||||
|
||||
def export_task_archives(
|
||||
client: WikiClient, *, export_all: bool = False, root: Path = ROOT
|
||||
) -> list[str]:
|
||||
"""增量或全量读取任务归档;绝不删除本地文件。"""
|
||||
|
||||
dirty = dirty_paths(["docs/task"], root)
|
||||
if dirty:
|
||||
raise WikiDocsError(
|
||||
"本地任务镜像存在未提交改动,已停止以防覆盖:\n" + "\n".join(dirty)
|
||||
)
|
||||
|
||||
existing = existing_task_mirrors(root)
|
||||
pages = []
|
||||
for metadata in client.list_pages():
|
||||
title = metadata.get("title")
|
||||
if isinstance(title, str) and TASK_PAGE_PATTERN.fullmatch(title):
|
||||
pages.append((int(title.split("-", 2)[1]), title, metadata))
|
||||
pages.sort(key=lambda item: (item[0], item[1]))
|
||||
|
||||
messages: list[str] = []
|
||||
targets: set[Path] = set()
|
||||
for _, page_name, metadata in pages:
|
||||
target = existing.get(page_name, task_target(page_name, root))
|
||||
if target in targets:
|
||||
raise WikiDocsError(f"多个任务页面映射到同一本地路径:{target.name}")
|
||||
targets.add(target)
|
||||
revision = task_revision(metadata, page_name)
|
||||
if not export_all and target.is_file():
|
||||
local_metadata, _ = parse_mirror(target.read_text(encoding="utf-8"))
|
||||
if (
|
||||
local_metadata.get("wiki_page") == page_name
|
||||
and local_metadata.get("wiki_revision") == revision
|
||||
):
|
||||
messages.append(f"跳过:{target.relative_to(root)} <- {page_name}@{revision[:12]}")
|
||||
continue
|
||||
page = client.get_page_from_metadata(metadata, page_name)
|
||||
changed = write_mirror(target, page)
|
||||
action = "已导出" if changed else "无变化"
|
||||
messages.append(f"{action}:{target.relative_to(root)} <- {page_name}@{revision[:12]}")
|
||||
return messages
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="人工按需导出 Gitea Wiki 任务归档")
|
||||
parser.add_argument(
|
||||
"--all", action="store_true", help="全量读取全部线上任务归档;默认按 revision 增量"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config", default=str(DEFAULT_CONFIG), help="核心 Wiki 映射配置"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
config = load_config(Path(args.config).resolve())
|
||||
messages = export_task_archives(
|
||||
WikiClient(config), export_all=args.all
|
||||
)
|
||||
except WikiDocsError as exc:
|
||||
print(f"错误:{exc}")
|
||||
return 1
|
||||
for message in messages:
|
||||
print(message)
|
||||
print("任务归档全量导出完成" if args.all else "任务归档增量导出完成")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,656 @@
|
||||
"""DevHarness 单一命令行入口。
|
||||
|
||||
子命令:
|
||||
check 检查必需文件、核心文档和任务归档结构
|
||||
sync 从 Gitea Wiki 单向导出或校验核心 docs 镜像
|
||||
archive 在 Gitea Wiki 创建任务归档
|
||||
export 人工按需把 Wiki 任务归档导出到 docs/task
|
||||
|
||||
各子命令的实现逻辑取自原来的 check_harness.py、sync_wiki_docs.py、
|
||||
new_task_archive.py 和 export_task_archives.py,行为未改变。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from wiki_docs import (
|
||||
DEFAULT_CONFIG,
|
||||
WikiClient,
|
||||
WikiDocsError,
|
||||
dirty_paths,
|
||||
load_config,
|
||||
parse_mirror,
|
||||
sync_all,
|
||||
write_mirror,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 结构检查
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CORE_PAGE_PATHS = {
|
||||
"Home": "docs/README.md",
|
||||
"Project-Profile": "docs/00-project-profile.md",
|
||||
"Development-Workflow": "docs/01-workflow.md",
|
||||
"Architecture-and-Code-Map": "docs/02-architecture-and-code-map.md",
|
||||
"Business-Rules-and-Glossary": "docs/03-business-rules-and-glossary.md",
|
||||
"Local-Development-and-Verification": (
|
||||
"docs/04-local-development-and-verification.md"
|
||||
),
|
||||
"Common-Changes": "docs/05-common-changes.md",
|
||||
"Troubleshooting": "docs/06-troubleshooting.md",
|
||||
"Product-Requirements-Overview": "docs/07-mvp-requirements.md",
|
||||
"Android-Agent-API-Contract": "docs/08-agent-api-contract.md",
|
||||
"Delivery-Issues": "docs/09-delivery-issues.md",
|
||||
"OnePlus-Real-Device-Acceptance": "docs/10-real-device-acceptance.md",
|
||||
"PDD-Detail-Rule-Migration-Analysis": (
|
||||
"docs/11-pdd-detail-rule-migration-analysis.md"
|
||||
),
|
||||
"Task-Archive-Template": "docs/templates/task-archive.md",
|
||||
}
|
||||
# 按 GoAuto 实际文档结构定义,不照抄 DevHarness 模板章节名。
|
||||
# 只登记结构性、稳定的小标题;随业务演进的具体条目(如故障现象、单条业务规则)
|
||||
# 不纳入校验,避免文档正常更新即触发 check 失败。
|
||||
CORE_DOCUMENT_REQUIREMENTS = {
|
||||
"docs/README.md": (
|
||||
"## 建议阅读顺序",
|
||||
"## 事实来源",
|
||||
"## 五分钟检查",
|
||||
"## 同步与归档",
|
||||
),
|
||||
"docs/00-project-profile.md": (
|
||||
"## 基本信息",
|
||||
"## 建设基线",
|
||||
"## 交付单元",
|
||||
"## 文档事实来源",
|
||||
"## 环境与凭据",
|
||||
"## 当前阶段",
|
||||
),
|
||||
"docs/01-workflow.md": (
|
||||
"## 事实来源",
|
||||
"## 新项目 Wiki 初始化门禁",
|
||||
"## 工单与设计证据双门禁",
|
||||
"### 本地 HTML 审核快照",
|
||||
"## 任务层级与状态",
|
||||
"## 单元任务闭环",
|
||||
"## 需求记录与流转",
|
||||
"## 自然语言快捷指令",
|
||||
"## 效率与范围控制",
|
||||
"## 文档影响",
|
||||
),
|
||||
"docs/02-architecture-and-code-map.md": (
|
||||
"## MVP 架构",
|
||||
"## 最小闭环",
|
||||
"## 关键设计",
|
||||
"## 最小业务数据",
|
||||
"## 已建立的工程入口",
|
||||
),
|
||||
"docs/03-business-rules-and-glossary.md": (
|
||||
"## 当前范围",
|
||||
"## 自动化边界",
|
||||
"## 术语",
|
||||
),
|
||||
"docs/04-local-development-and-verification.md": (
|
||||
"## 通用检查",
|
||||
"## 服务端验证",
|
||||
"## Web 验证",
|
||||
"## Android 验证",
|
||||
"## 原型验证",
|
||||
),
|
||||
"docs/05-common-changes.md": (
|
||||
"## 风险分级",
|
||||
"## 更新文档",
|
||||
"## 验收 Agent 修改",
|
||||
),
|
||||
"docs/06-troubleshooting.md": (
|
||||
"## 排查顺序",
|
||||
),
|
||||
"docs/07-mvp-requirements.md": (
|
||||
"## 长期需求索引",
|
||||
),
|
||||
}
|
||||
REQUIRED_FILES = (
|
||||
"AGENTS.md",
|
||||
"CLAUDE.md",
|
||||
"README.md",
|
||||
"docs/00-project-profile.md",
|
||||
"docs/01-workflow.md",
|
||||
"docs/templates/task-archive.md",
|
||||
*CORE_DOCUMENT_REQUIREMENTS,
|
||||
"wiki-docs.json",
|
||||
"dev_scripts/wiki_docs.py",
|
||||
"dev_scripts/harness.py",
|
||||
".gitea/issue_template/epic.md",
|
||||
".gitea/issue_template/mvp.md",
|
||||
".gitea/issue_template/task.md",
|
||||
)
|
||||
ARCHIVE_HEADINGS = (
|
||||
"## 背景与目标",
|
||||
"## 最终方案",
|
||||
"## 修改文件",
|
||||
"## 验收结果",
|
||||
"## 测试",
|
||||
"## 相关提交",
|
||||
)
|
||||
|
||||
|
||||
def check_required_files(errors: list[str]) -> None:
|
||||
for relative_path in REQUIRED_FILES:
|
||||
if not (ROOT / relative_path).is_file():
|
||||
errors.append(f"缺少必需文件:{relative_path}")
|
||||
|
||||
|
||||
def check_project_profile(errors: list[str], warnings: list[str], strict: bool) -> None:
|
||||
profile = ROOT / "docs" / "00-project-profile.md"
|
||||
if not profile.is_file():
|
||||
return
|
||||
if "<填写" in profile.read_text(encoding="utf-8"):
|
||||
message = "项目档案仍有未填写内容"
|
||||
(errors if strict else warnings).append(message)
|
||||
|
||||
|
||||
def check_archives(errors: list[str]) -> None:
|
||||
task_dir = ROOT / "docs" / "task"
|
||||
for path in task_dir.glob("*.md"):
|
||||
if not re.match(r"^\d+-.+\.md$", path.name):
|
||||
errors.append(f"归档文件名不符合 <编号>-<标题>.md:{path.name}")
|
||||
content = path.read_text(encoding="utf-8")
|
||||
try:
|
||||
metadata, _ = parse_mirror(content)
|
||||
except WikiDocsError as exc:
|
||||
errors.append(f"{path.name} 的任务镜像无效:{exc}")
|
||||
continue
|
||||
if re.fullmatch(r"Task-\d+-.+", metadata.get("wiki_page", "")) is None:
|
||||
errors.append(f"{path.name} 的 wiki_page 不是任务归档页面")
|
||||
if re.fullmatch(r"[0-9a-f]{40,64}", metadata.get("wiki_revision", "")) is None:
|
||||
errors.append(f"{path.name} 的 wiki_revision 无效")
|
||||
for heading in ARCHIVE_HEADINGS:
|
||||
if heading not in content:
|
||||
errors.append(f"{path.name} 缺少章节:{heading}")
|
||||
if "**未验证部分**:" not in content:
|
||||
errors.append(f"{path.name} 没有记录未验证部分")
|
||||
|
||||
|
||||
def missing_sections(content: str, required: tuple[str, ...]) -> list[str]:
|
||||
return [section for section in required if section not in content]
|
||||
|
||||
|
||||
def check_core_documents(errors: list[str], root: Path = ROOT) -> None:
|
||||
"""检查初级维护者所需主题页的固定结构。"""
|
||||
|
||||
for relative_path, required in CORE_DOCUMENT_REQUIREMENTS.items():
|
||||
path = root / relative_path
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
_, body = parse_mirror(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, WikiDocsError):
|
||||
continue
|
||||
for section in missing_sections(body, required):
|
||||
errors.append(f"{relative_path} 缺少核心章节:{section}")
|
||||
|
||||
|
||||
def check_task_template(errors: list[str], root: Path = ROOT) -> None:
|
||||
path = root / ".gitea" / "issue_template" / "task.md"
|
||||
if not path.is_file():
|
||||
return
|
||||
content = path.read_text(encoding="utf-8")
|
||||
required = (
|
||||
"## 依赖与并行",
|
||||
"- 前置工单:无 / #编号",
|
||||
"- 是否允许与前置工单并行:是 / 否",
|
||||
"- 原因:",
|
||||
"## 子项目影响",
|
||||
"- 仅影响的子项目 / 交付单元:",
|
||||
"- 是否跨子项目:是 / 否",
|
||||
"- 是否修改共享接口或契约:是 / 否;唯一事实来源:",
|
||||
"- 各子项目需要执行的验证:",
|
||||
"## 原始需求",
|
||||
"- 来源:用户对话 / Gitea / 其他",
|
||||
"- 提出时间:",
|
||||
"- 关键原话或脱敏摘要:",
|
||||
"## 需求变化记录",
|
||||
"| 日期 | 变化内容 | 原因 | 用户确认 |",
|
||||
"## 设计与原型门禁",
|
||||
"- 修改类型:纯显示文案 / 小范围 UI / 新组件 / 新页面或独立用户功能 / 重大交互或导航 / 非 UI / 恢复既有行为的 Bug",
|
||||
"- 所需设计证据:无 / 标注截图 / 低保真图 / 已确认原型 / 架构、API、数据、状态或流程设计 / 原设计或复现证据",
|
||||
"- 可编辑设计源链接、版本或事实来源:",
|
||||
"- 本地 HTML 审核快照路径和版本(不适用时说明原因):",
|
||||
"- 本地浏览方式和资源完整性检查:",
|
||||
"- 版本、revision 或确认日期:",
|
||||
"- 状态:无 / 草稿 / 已确认 / 已废弃",
|
||||
"- 确认人、确认时间和覆盖范围:",
|
||||
"- 无需 UI 原型或无需任何原型的原因:",
|
||||
"## 文档影响",
|
||||
"- [ ] 不影响长期文档,原因:",
|
||||
"- [ ] 更新架构与代码地图",
|
||||
"- [ ] 更新业务规则与术语",
|
||||
"- [ ] 更新常见修改或故障排查",
|
||||
"## 交付文档影响",
|
||||
"- [ ] 无交付文档影响,原因:",
|
||||
"- [ ] 更新已有交付文档,受众与页面:",
|
||||
"- [ ] 新增交付文档,受众与页面:",
|
||||
"- [ ] 需要目标岗位或客户代表验证:是 / 否;验证方式:",
|
||||
)
|
||||
for section in missing_sections(content, required):
|
||||
errors.append(f"单元任务模板缺少:{section}")
|
||||
|
||||
|
||||
def check_agent_efficiency_rules(errors: list[str], root: Path = ROOT) -> None:
|
||||
path = root / "AGENTS.md"
|
||||
if not path.is_file():
|
||||
return
|
||||
content = path.read_text(encoding="utf-8")
|
||||
required = (
|
||||
"### 效率与范围控制",
|
||||
"#### 严格控制范围",
|
||||
"#### 渐进执行和修复",
|
||||
"#### 复用已验证事实",
|
||||
"#### 明确停止条件",
|
||||
"单元任务是唯一正式实施单位",
|
||||
"高风险修改必须停止",
|
||||
"用户没有明确验收通过前不得关闭",
|
||||
"长期核心文档必须先修改 Wiki",
|
||||
"### 新项目 Wiki 初始化门禁",
|
||||
"`Home` 不存在时必须先创建 `Home`",
|
||||
"不得把模板自带的本地 `docs/` 当作新项目 Wiki 已初始化的证据",
|
||||
"提交只包含当前工单相关文件",
|
||||
"### 工单与设计证据双门禁",
|
||||
"新页面、独立用户功能、重大交互或导航变化",
|
||||
"`prototypes/<工单号>/<版本>/index.html`",
|
||||
"已确认的 HTML 快照不得原位覆盖",
|
||||
"代码组件名、类名、变量、国际化键、API 字段和数据库字段不是显示文案",
|
||||
"### 自然语言快捷指令",
|
||||
"`只分析`",
|
||||
"`建工单`",
|
||||
"`执行工单 #N`",
|
||||
"`建工单并做`",
|
||||
"`继续工单 #N`",
|
||||
"`检查工单 #N`",
|
||||
"`同步文档`",
|
||||
"`导出任务归档`",
|
||||
"`导出全部任务归档`",
|
||||
"`#N 验收通过`",
|
||||
"### 需求记录与流转",
|
||||
"不得臆造用户原话",
|
||||
"不复制完整聊天",
|
||||
"Gitea 工单全文不导出到仓库",
|
||||
)
|
||||
for section in missing_sections(content, required):
|
||||
errors.append(f"AGENTS.md 缺少:{section}")
|
||||
|
||||
|
||||
def check_repository_readme(errors: list[str], root: Path = ROOT) -> None:
|
||||
"""检查产品 README 提供阅读入口与验证方式。
|
||||
|
||||
与 DevHarness 模板的差异(按 GoAuto 事实改写,见 #47):
|
||||
模板此处校验的是「新项目快速开始」中的线上 Wiki 初始化顺序,面向从模板
|
||||
派生新仓库的场景。GoAuto 是已建成的产品仓库,Wiki 已于 2026-08-17 初始化,
|
||||
其 README 面向本项目的使用者与维护者,写入建仓步骤会误导读者。
|
||||
因此本函数只校验产品 README 应有的结构;新项目 Wiki 初始化门禁改由
|
||||
AGENTS.md 与 docs/01-workflow.md 承载,两处仍由 check 强制校验。
|
||||
"""
|
||||
|
||||
path = root / "README.md"
|
||||
if not path.is_file():
|
||||
return
|
||||
content = path.read_text(encoding="utf-8")
|
||||
required = (
|
||||
"## 阅读入口",
|
||||
"## 当前状态",
|
||||
"## 验证",
|
||||
)
|
||||
for section in missing_sections(content, required):
|
||||
errors.append(f"README.md 缺少:{section}")
|
||||
|
||||
|
||||
def check_claude_code_entry(errors: list[str], root: Path = ROOT) -> None:
|
||||
"""检查 Claude Code 入口直接复用共同 Agent 规则。"""
|
||||
|
||||
path = root / "CLAUDE.md"
|
||||
if not path.is_file():
|
||||
return
|
||||
content = path.read_text(encoding="utf-8")
|
||||
lines = {line.strip() for line in content.splitlines()}
|
||||
if "@AGENTS.md" not in lines:
|
||||
errors.append("CLAUDE.md 缺少独立的 @AGENTS.md 导入")
|
||||
required = (
|
||||
"共同规则事实来源",
|
||||
"只记录 Claude Code 特有",
|
||||
"只修改 `AGENTS.md`",
|
||||
"## 模型路由",
|
||||
"## Agent 交接",
|
||||
"## Haiku 只读约束",
|
||||
"当前模型足以完成任务时不升级模型",
|
||||
"Opus 输出方案后必须等待用户确认",
|
||||
"不让 Haiku 决定最终根因",
|
||||
"只读必须通过子 Agent 工具权限实现",
|
||||
)
|
||||
for section in missing_sections(content, required):
|
||||
errors.append(f"CLAUDE.md 缺少:{section}")
|
||||
|
||||
|
||||
def core_mapping_errors(configured_mappings: dict[str, str]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for page, expected_path in CORE_PAGE_PATHS.items():
|
||||
if configured_mappings.get(page) != expected_path:
|
||||
errors.append(
|
||||
f"核心 Wiki 页面映射缺失或路径错误:{page} -> {expected_path}"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def check_wiki_mirrors(errors: list[str]) -> None:
|
||||
"""检查核心映射与镜像头;任务快照由 check_archives 单独检查。"""
|
||||
|
||||
try:
|
||||
config = load_config()
|
||||
except WikiDocsError as exc:
|
||||
errors.append(str(exc))
|
||||
return
|
||||
|
||||
configured_mappings = {mapping.page: mapping.path for mapping in config.mappings}
|
||||
errors.extend(core_mapping_errors(configured_mappings))
|
||||
|
||||
mapped_paths = {mapping.path for mapping in config.mappings}
|
||||
actual_paths = {
|
||||
path.relative_to(ROOT).as_posix() for path in (ROOT / "docs").rglob("*.md")
|
||||
if path.parent != ROOT / "docs" / "task"
|
||||
}
|
||||
for path in sorted(actual_paths - mapped_paths):
|
||||
errors.append(f"docs 中存在未登记的 Wiki 镜像:{path}")
|
||||
|
||||
for mapping in config.mappings:
|
||||
path = ROOT / mapping.path
|
||||
if not path.is_file():
|
||||
errors.append(f"缺少 Wiki 镜像:{mapping.path}")
|
||||
continue
|
||||
try:
|
||||
metadata, _ = parse_mirror(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, WikiDocsError) as exc:
|
||||
errors.append(f"Wiki 镜像无效 {mapping.path}:{exc}")
|
||||
continue
|
||||
if metadata.get("generated") != "true (请先修改 Gitea Wiki,禁止直接编辑本文件)":
|
||||
errors.append(f"{mapping.path} 没有只读镜像标记")
|
||||
if metadata.get("wiki_page") != mapping.page:
|
||||
errors.append(f"{mapping.path} 的 wiki_page 与映射不一致")
|
||||
revision = metadata.get("wiki_revision", "")
|
||||
if re.fullmatch(r"[0-9a-f]{40,64}", revision) is None:
|
||||
errors.append(f"{mapping.path} 的 wiki_revision 无效")
|
||||
if not metadata.get("synchronized_at"):
|
||||
errors.append(f"{mapping.path} 缺少 synchronized_at")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 任务归档
|
||||
|
||||
def safe_title(title: str) -> str:
|
||||
"""把标题转换为适合 Wiki 页面名和 Windows 文件名的短文本。"""
|
||||
|
||||
cleaned = re.sub(r'[<>:"/\\|?*]', "-", title.strip())
|
||||
cleaned = re.sub(r"\s+", "-", cleaned)
|
||||
cleaned = re.sub(r"-+", "-", cleaned)
|
||||
return cleaned.strip(".-")
|
||||
|
||||
|
||||
def build_archive(
|
||||
template: str,
|
||||
issue_number: str,
|
||||
title: str,
|
||||
page_name: str,
|
||||
issue_url: str,
|
||||
) -> str:
|
||||
content = template.replace("<工单号>", issue_number, 1)
|
||||
content = content.replace("<标题>", title.strip(), 1)
|
||||
content = content.replace("YYYY-MM-DD", date.today().isoformat(), 1)
|
||||
content = content.replace("<链接>", issue_url, 1)
|
||||
return content.replace("<页面名>", page_name, 1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 归档导出
|
||||
|
||||
TASK_PAGE_PATTERN = re.compile(r"^Task-(?P<number>\d+)-(?P<title>.+)$")
|
||||
|
||||
|
||||
def task_revision(metadata: dict[str, Any], page_name: str) -> str:
|
||||
last_commit = metadata.get("last_commit")
|
||||
revision = last_commit.get("sha") if isinstance(last_commit, dict) else None
|
||||
if not isinstance(revision, str) or not revision:
|
||||
raise WikiDocsError(f"Wiki 页面缺少 revision:{page_name}")
|
||||
return revision
|
||||
|
||||
|
||||
def existing_task_mirrors(root: Path = ROOT) -> dict[str, Path]:
|
||||
"""按镜像头匹配已有文件,兼容历史自定义文件名。"""
|
||||
|
||||
mirrors: dict[str, Path] = {}
|
||||
task_dir = root / "docs" / "task"
|
||||
if not task_dir.is_dir():
|
||||
return mirrors
|
||||
for path in task_dir.glob("*.md"):
|
||||
try:
|
||||
metadata, _ = parse_mirror(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, WikiDocsError) as exc:
|
||||
raise WikiDocsError(f"已有任务镜像无效 {path.name}:{exc}") from exc
|
||||
page_name = metadata.get("wiki_page", "")
|
||||
if not TASK_PAGE_PATTERN.fullmatch(page_name):
|
||||
raise WikiDocsError(f"已有任务镜像页面名无效 {path.name}:{page_name}")
|
||||
if page_name in mirrors:
|
||||
raise WikiDocsError(f"任务页面存在重复本地镜像:{page_name}")
|
||||
mirrors[page_name] = path
|
||||
return mirrors
|
||||
|
||||
|
||||
def task_target(page_name: str, root: Path = ROOT) -> Path:
|
||||
match = TASK_PAGE_PATTERN.fullmatch(page_name)
|
||||
if match is None:
|
||||
raise WikiDocsError(f"不是任务归档页面:{page_name}")
|
||||
title = safe_title(match.group("title"))
|
||||
if not title:
|
||||
raise WikiDocsError(f"任务归档标题无效:{page_name}")
|
||||
return root / "docs" / "task" / f"{match.group('number')}-{title}.md"
|
||||
|
||||
|
||||
def export_task_archives(
|
||||
client: WikiClient, *, export_all: bool = False, root: Path = ROOT
|
||||
) -> list[str]:
|
||||
"""增量或全量读取任务归档;绝不删除本地文件。"""
|
||||
|
||||
dirty = dirty_paths(["docs/task"], root)
|
||||
if dirty:
|
||||
raise WikiDocsError(
|
||||
"本地任务镜像存在未提交改动,已停止以防覆盖:\n" + "\n".join(dirty)
|
||||
)
|
||||
|
||||
existing = existing_task_mirrors(root)
|
||||
pages = []
|
||||
for metadata in client.list_pages():
|
||||
title = metadata.get("title")
|
||||
if isinstance(title, str) and TASK_PAGE_PATTERN.fullmatch(title):
|
||||
pages.append((int(title.split("-", 2)[1]), title, metadata))
|
||||
pages.sort(key=lambda item: (item[0], item[1]))
|
||||
|
||||
messages: list[str] = []
|
||||
targets: set[Path] = set()
|
||||
for _, page_name, metadata in pages:
|
||||
target = existing.get(page_name, task_target(page_name, root))
|
||||
if target in targets:
|
||||
raise WikiDocsError(f"多个任务页面映射到同一本地路径:{target.name}")
|
||||
targets.add(target)
|
||||
revision = task_revision(metadata, page_name)
|
||||
if not export_all and target.is_file():
|
||||
local_metadata, _ = parse_mirror(target.read_text(encoding="utf-8"))
|
||||
if (
|
||||
local_metadata.get("wiki_page") == page_name
|
||||
and local_metadata.get("wiki_revision") == revision
|
||||
):
|
||||
messages.append(f"跳过:{target.relative_to(root)} <- {page_name}@{revision[:12]}")
|
||||
continue
|
||||
page = client.get_page_from_metadata(metadata, page_name)
|
||||
changed = write_mirror(target, page)
|
||||
action = "已导出" if changed else "无变化"
|
||||
messages.append(f"{action}:{target.relative_to(root)} <- {page_name}@{revision[:12]}")
|
||||
return messages
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 子命令入口
|
||||
|
||||
|
||||
def run_check(args: argparse.Namespace) -> int:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
check_required_files(errors)
|
||||
check_project_profile(errors, warnings, args.strict)
|
||||
check_wiki_mirrors(errors)
|
||||
check_core_documents(errors)
|
||||
check_task_template(errors)
|
||||
check_agent_efficiency_rules(errors)
|
||||
check_repository_readme(errors)
|
||||
check_claude_code_entry(errors)
|
||||
check_archives(errors)
|
||||
|
||||
for warning in warnings:
|
||||
print(f"警告:{warning}")
|
||||
for error in errors:
|
||||
print(f"错误:{error}")
|
||||
|
||||
if errors:
|
||||
print(f"检查失败:{len(errors)} 个问题")
|
||||
return 1
|
||||
print("DevHarness 检查通过")
|
||||
return 0
|
||||
|
||||
|
||||
def run_sync(args: argparse.Namespace) -> int:
|
||||
"""--verify 依次执行导出、结构检查和一致性校验,替代原来的三条命令。"""
|
||||
|
||||
if args.verify:
|
||||
steps = (
|
||||
("同步", lambda: run_sync(
|
||||
argparse.Namespace(check=False, verify=False, config=args.config))),
|
||||
("结构检查", lambda: run_check(argparse.Namespace(strict=True))),
|
||||
("一致性校验", lambda: run_sync(
|
||||
argparse.Namespace(check=True, verify=False, config=args.config))),
|
||||
)
|
||||
for name, step in steps:
|
||||
code = step()
|
||||
if code != 0:
|
||||
print(f"错误:{name}未通过,已停止")
|
||||
return code
|
||||
return 0
|
||||
|
||||
try:
|
||||
config = load_config(Path(args.config).resolve())
|
||||
messages = sync_all(config, WikiClient(config), check=args.check)
|
||||
except WikiDocsError as exc:
|
||||
print(f"错误:{exc}")
|
||||
return 1
|
||||
for message in messages:
|
||||
print(message)
|
||||
print("Wiki 镜像检查通过" if args.check else "Wiki 镜像同步完成")
|
||||
return 0
|
||||
|
||||
|
||||
def run_archive(args: argparse.Namespace) -> int:
|
||||
short_title = safe_title(args.title)
|
||||
if not args.issue_number.isdigit():
|
||||
print("错误:工单号必须是数字")
|
||||
return 1
|
||||
if not short_title:
|
||||
print("错误:标题不能为空")
|
||||
return 1
|
||||
|
||||
try:
|
||||
config = load_config(Path(args.config).resolve())
|
||||
page_name = f"Task-{args.issue_number}-{short_title}"
|
||||
client = WikiClient(config)
|
||||
if any(item.get("title") == page_name for item in client.list_pages()):
|
||||
raise WikiDocsError(f"任务归档已经存在:{page_name}")
|
||||
template = client.get_page("Task-Archive-Template").text
|
||||
issue_url = (
|
||||
f"{config.gitea_url}/{config.owner}/{config.repository}/issues/"
|
||||
f"{args.issue_number}"
|
||||
)
|
||||
content = build_archive(
|
||||
template, args.issue_number, args.title, page_name, issue_url
|
||||
)
|
||||
page = client.create_page(
|
||||
page_name,
|
||||
content,
|
||||
f"docs: 创建任务 #{args.issue_number} 归档草稿",
|
||||
)
|
||||
except WikiDocsError as exc:
|
||||
print(f"错误:{exc}")
|
||||
return 1
|
||||
|
||||
print(f"已创建 Wiki:{page.html_url}")
|
||||
print("未导出本地任务归档;需要时运行 harness.py export")
|
||||
return 0
|
||||
|
||||
|
||||
def run_export(args: argparse.Namespace) -> int:
|
||||
try:
|
||||
config = load_config(Path(args.config).resolve())
|
||||
messages = export_task_archives(WikiClient(config), export_all=args.all)
|
||||
except WikiDocsError as exc:
|
||||
print(f"错误:{exc}")
|
||||
return 1
|
||||
for message in messages:
|
||||
print(message)
|
||||
print("任务归档全量导出完成" if args.all else "任务归档增量导出完成")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="DevHarness 检查、同步与归档工具")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_check = sub.add_parser("check", help="检查 DevHarness 项目结构")
|
||||
p_check.add_argument(
|
||||
"--strict", action="store_true", help="项目档案有占位内容时返回失败"
|
||||
)
|
||||
p_check.set_defaults(func=run_check)
|
||||
|
||||
p_sync = sub.add_parser("sync", help="从 Gitea Wiki 单向同步核心 docs 镜像")
|
||||
p_sync.add_argument(
|
||||
"--check", action="store_true", help="只检查 Wiki 与镜像是否一致,不写文件"
|
||||
)
|
||||
p_sync.add_argument(
|
||||
"--verify",
|
||||
action="store_true",
|
||||
help="依次执行导出、check --strict 和一致性校验",
|
||||
)
|
||||
p_sync.add_argument(
|
||||
"--config", default=str(DEFAULT_CONFIG), help="Wiki 页面映射 JSON 文件"
|
||||
)
|
||||
p_sync.set_defaults(func=run_sync)
|
||||
|
||||
p_archive = sub.add_parser("archive", help="在 Gitea Wiki 创建任务归档")
|
||||
p_archive.add_argument("issue_number", help="Gitea 工单号,例如 123")
|
||||
p_archive.add_argument("title", help="简短任务标题")
|
||||
p_archive.add_argument(
|
||||
"--config", default=str(DEFAULT_CONFIG), help="Wiki 映射配置"
|
||||
)
|
||||
p_archive.set_defaults(func=run_archive)
|
||||
|
||||
p_export = sub.add_parser("export", help="人工按需导出 Gitea Wiki 任务归档")
|
||||
p_export.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="全量读取全部线上任务归档;默认按 revision 增量",
|
||||
)
|
||||
p_export.add_argument(
|
||||
"--config", default=str(DEFAULT_CONFIG), help="核心 Wiki 映射配置"
|
||||
)
|
||||
p_export.set_defaults(func=run_export)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,87 +0,0 @@
|
||||
"""只在 Gitea Wiki 创建任务归档;本地镜像由人工按需导出。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from wiki_docs import (
|
||||
DEFAULT_CONFIG,
|
||||
WikiClient,
|
||||
WikiDocsError,
|
||||
load_config,
|
||||
)
|
||||
|
||||
|
||||
def safe_title(title: str) -> str:
|
||||
"""把标题转换为适合 Wiki 页面名和 Windows 文件名的短文本。"""
|
||||
|
||||
cleaned = re.sub(r'[<>:"/\\|?*]', "-", title.strip())
|
||||
cleaned = re.sub(r"\s+", "-", cleaned)
|
||||
cleaned = re.sub(r"-+", "-", cleaned)
|
||||
return cleaned.strip(".-")
|
||||
|
||||
|
||||
def build_archive(
|
||||
template: str,
|
||||
issue_number: str,
|
||||
title: str,
|
||||
page_name: str,
|
||||
issue_url: str,
|
||||
) -> str:
|
||||
content = template.replace("<工单号>", issue_number, 1)
|
||||
content = content.replace("<标题>", title.strip(), 1)
|
||||
content = content.replace("YYYY-MM-DD", date.today().isoformat(), 1)
|
||||
content = content.replace("<链接>", issue_url, 1)
|
||||
return content.replace("<页面名>", page_name, 1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="在 Gitea Wiki 创建任务归档,不自动导出本地镜像"
|
||||
)
|
||||
parser.add_argument("issue_number", help="Gitea 工单号,例如 123")
|
||||
parser.add_argument("title", help="简短任务标题")
|
||||
parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Wiki 映射配置")
|
||||
args = parser.parse_args()
|
||||
|
||||
short_title = safe_title(args.title)
|
||||
if not args.issue_number.isdigit():
|
||||
print("错误:工单号必须是数字")
|
||||
return 1
|
||||
if not short_title:
|
||||
print("错误:标题不能为空")
|
||||
return 1
|
||||
|
||||
try:
|
||||
config = load_config(Path(args.config).resolve())
|
||||
page_name = f"Task-{args.issue_number}-{short_title}"
|
||||
client = WikiClient(config)
|
||||
if any(item.get("title") == page_name for item in client.list_pages()):
|
||||
raise WikiDocsError(f"任务归档已经存在:{page_name}")
|
||||
template = client.get_page("Task-Archive-Template").text
|
||||
issue_url = (
|
||||
f"{config.gitea_url}/{config.owner}/{config.repository}/issues/"
|
||||
f"{args.issue_number}"
|
||||
)
|
||||
content = build_archive(
|
||||
template, args.issue_number, args.title, page_name, issue_url
|
||||
)
|
||||
page = client.create_page(
|
||||
page_name,
|
||||
content,
|
||||
f"docs: 创建任务 #{args.issue_number} 归档草稿",
|
||||
)
|
||||
except WikiDocsError as exc:
|
||||
print(f"错误:{exc}")
|
||||
return 1
|
||||
|
||||
print(f"已创建 Wiki:{page.html_url}")
|
||||
print("未导出本地任务归档;需要时运行 export_task_archives.py")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,33 +0,0 @@
|
||||
"""从 Gitea Wiki 单向导出配置中的核心 docs 镜像。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from wiki_docs import DEFAULT_CONFIG, WikiClient, WikiDocsError, load_config, sync_all
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="从 Gitea Wiki 单向同步核心 docs 镜像")
|
||||
parser.add_argument(
|
||||
"--check", action="store_true", help="只检查 Wiki 与镜像是否一致,不写文件"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config", default=str(DEFAULT_CONFIG), help="Wiki 页面映射 JSON 文件"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
config = load_config(Path(args.config).resolve())
|
||||
messages = sync_all(config, WikiClient(config), check=args.check)
|
||||
except WikiDocsError as exc:
|
||||
print(f"错误:{exc}")
|
||||
return 1
|
||||
for message in messages:
|
||||
print(message)
|
||||
print("Wiki 镜像检查通过" if args.check else "Wiki 镜像同步完成")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+23
-2
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Development-Workflow
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Development-Workflow.-
|
||||
wiki_revision: c62e9f4fbdc34036dda37a0298e0ae1ff12d7c94
|
||||
synchronized_at: 2026-08-17T03:27:49Z
|
||||
wiki_revision: d22ebd3709589b5defdf0abd705e76db6d0f1977
|
||||
synchronized_at: 2026-08-19T01:17:00Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 开发工作流
|
||||
@@ -18,6 +18,16 @@ synchronized_at: 2026-08-17T03:27:49Z
|
||||
|
||||
核心页面通过 `wiki-docs.json` 显式映射,固定执行 Wiki → `docs/` 单向同步。页面删除、重命名、映射变化或事实来源反向切换必须另行确认。
|
||||
|
||||
## 新项目 Wiki 初始化门禁
|
||||
|
||||
本项目 Wiki 已于 2026-08-17 初始化并启用 Wiki-first,本节适用于从本仓库派生新项目的场景。
|
||||
|
||||
- 先创建 Gitea 远端仓库并启用工单和 Wiki,再配置 `wiki-docs.json`;不得把本地 `docs/` 当作线上 Wiki 已初始化的证据。
|
||||
- 优先使用已配置的 Gitea MCP 查询和写入 Wiki;MCP 不可用或不支持所需写操作时才使用 Gitea API,并在初始化工单记录回退原因。凭据只从环境或 MCP 安全配置读取。
|
||||
- 先查询线上页面列表;`Home` 不存在时必须先创建 `Home`,在线回读正文并记录 revision,再逐页创建或更新其他核心映射页面。
|
||||
- 每个核心页面写入后必须在线回读并取得 revision。页面缺失、回读失败或没有 revision 时停止初始化,不得开始产品代码。
|
||||
- 产品编码前必须运行 `python dev_scripts/harness.py sync --verify`,全部成功才表示初始化完成。
|
||||
|
||||
## 工单与设计证据双门禁
|
||||
|
||||
正式实施前依次判断是否需要工单,以及需要什么设计证据。工单不能替代原型确认,原型也不能替代技术方案、安全检查和单元工单。
|
||||
@@ -39,6 +49,17 @@ synchronized_at: 2026-08-17T03:27:49Z
|
||||
|
||||
设计证据记录链接或路径、App ID/版本、草稿/已确认/已废弃状态、确认人、确认时间和覆盖范围。页面结构、主要流程、状态、权限、异常处理或验收结果变化时必须重新确认。
|
||||
|
||||
### 本地 HTML 审核快照
|
||||
|
||||
- 完整原型形成待审核版本后,必须在用户审核前生成本地可浏览 HTML 快照,保存到 `prototypes/<工单号>/<版本>/index.html`;版本目录内资源使用相对路径。
|
||||
- 可编辑设计源仍在 QuantUX,Git 中的 HTML 是版本化审核证据,Wiki 和工单只保存索引。
|
||||
- 已确认的 HTML 快照不得原位覆盖。页面结构、流程、状态、权限、异常处理或验收结果变化时,使用新版本目录重新导出并重新确认。
|
||||
- 提交审核前检查入口、主要交互和资源完整性,并删除凭据、账号、个人信息和生产数据。
|
||||
- QuantUX 无法生成可用 HTML 时,在工单记录限制并停止审核,等待用户确认等效的本地可浏览方案;不得只保留线上链接后直接编码。
|
||||
- 纯显示文案、小范围 UI、非 UI 需求和恢复既有行为的 Bug 不强制生成 HTML。
|
||||
|
||||
**存量偏离(#47 确认)**:`prototypes/` 下已有的 `quantux-*.html` 平铺快照建立于本规则之前,保持原样不迁移,历史版本可在 Git 历史中查阅;工单号/版本目录结构自 #47 起对新增原型生效。
|
||||
|
||||
## 任务层级与状态
|
||||
|
||||
```text
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Troubleshooting
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Troubleshooting
|
||||
wiki_revision: 779ea2d826cb6f09b7a3df98f243a22bdd8bf6f5
|
||||
synchronized_at: 2026-08-17T03:28:14Z
|
||||
wiki_revision: 6c4352598e1152ed562e998aa641f38415ac71c7
|
||||
synchronized_at: 2026-08-19T01:17:13Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 故障排查
|
||||
|
||||
## 排查顺序
|
||||
|
||||
1. 先确认现象可复现,并记录设备、App 版本、任务号和规则快照。
|
||||
2. 从服务端结构化任务日志与错误码入手,不先猜测 Android 端实现。
|
||||
3. 再看设备心跳、租约与任务状态是否自洽。
|
||||
4. 最后才进入 Android 侧控件与页面证据排查。
|
||||
5. 涉及创建订单、权限、凭据或数据删除时立即停止并等待人工确认,不通过重试获取反馈。
|
||||
|
||||
以下按现象归类,均已实测确认。
|
||||
|
||||
## 设备显示离线
|
||||
|
||||
1. 检查服务器 API 地址和 HTTPS 证书。
|
||||
|
||||
Reference in New Issue
Block a user