diff --git a/AGENTS.md b/AGENTS.md index 8172a58..78cbe86 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,11 +4,11 @@ ## 全局约束(照 spec 决策表) -- 命名:仓库 `flowguard-plugin`、name `flowguard`、displayName「研发流程门禁」、i18n en "FlowGuard: R&D Process Gate"、命令前缀 `/flowguard-*`。十阶段产物只写 `docs/project/` 与 `docs/features//`;新项目不得创建 `.flowguard/`。 +- 命名:仓库 `flowguard-plugin`、name `flowguard`、displayName「研发流程门禁」、i18n en "FlowGuard: R&D Process Gate"、命令前缀 `/flowguard-*`。十阶段产物只写 `docs/project/` 与 `docs/features//`;仓库内不得出现 `.flowguard/`。 - Python 仅标准库;测试用 `python3 -m unittest discover -s tests`(不是 pytest)。 - SKILL.md ≤ 500 行;frontmatter 必含 `name`(kebab,与目录同名)/ `license: Apache-2.0` / `description`(含触发词与负面边界)/ `compatibility`。 - 跨技能引用只用「技能名 + `npx skills add / --skill `」,禁止 `../` 相对路径指向其它技能。 -- 十阶段由智能体推进,Hook 只校验与拦截;门禁无 strict_mode 软化开关。用户批准与验收不得由 agent 伪造;旧状态仅供迁移和兼容读取。 +- 十阶段由智能体推进,Hook 只校验与拦截;门禁无 strict_mode 软化开关。用户批准与验收不得由 agent 伪造。 - REQ-ID 全局唯一,格式 `/REQ-`;feature-id/模块名 kebab `^[a-z0-9]+(?:-[a-z0-9]+)*$`。 - 门禁/校验输出统一诊断信封基础字段 `{severity, code, message, fix}`;新治理拒绝可追加 `missing/allowed_actions`。 - 四宿主 manifest 版本字段必须一致(`.codex-plugin/plugin.json` 例外:`+codex.`);`.zcode-plugin/plugin.json` 不得含 `hooks` 键;不得含占位 `mcpServers`。 @@ -37,5 +37,5 @@ - 当前规格(单一权威):`docs/superpowers/specs/2026-09-23-flowguard-docs-ten-stage-governance.md` - 当前实施计划:`docs/superpowers/plans/2026-09-23-flowguard-agent-driven-sdd-governance.md` -- v0.2 的“十阶段仅兼容”决策已被当前规格取代;旧 `.flowguard/` 仅是迁移输入,不得作为新任务默认产物。 +- v0.2 的“十阶段仅兼容”决策已被当前规格取代;`.flowguard/` 兼容/迁移线已于 v0.4.0(2026-09-23)下线,旧命令面一并移除。 - 钩子协议:`hooks/__protocol__.md`(改协议必须同 commit 更新契约与测试) diff --git a/PRIVACY.md b/PRIVACY.md index 09d4f8b..9659e3f 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -3,7 +3,7 @@ flowguard-plugin(研发流程门禁)完全在本地运行: - 不采集、不上传任何用户数据、代码内容或流程产物; -- 流程状态(`.flowguard/`)与审计日志(`.flowguard/journal/`)仅存在于用户项目仓库内; +- 流程状态(`docs/` 阶段文档与证据登记)仅存在于用户项目仓库内;会话缓存(上下文/锁)仅存在于本机宿主状态目录; - 插件不包含任何遥测、统计或网络上报代码。 如插件分发渠道另有要求,以渠道政策与本文件中更严格者为准。 diff --git a/README.md b/README.md index 4df81fc..d3b011b 100644 --- a/README.md +++ b/README.md @@ -84,16 +84,16 @@ Denials use exit code 2 and return `code / message / fix / missing / allowed_act See [hooks/__protocol__.md](hooks/__protocol__.md). -## Document locations and legacy migration +## Document locations -Project stages 02/07/10 live in `docs/project/`; feature stages 01/03/04/05/06/08/09 live in `docs/features//`. New projects do not create `.flowguard/`; session cache lives outside the repository. For existing projects, run `migrate --dry-run`, resolve conflicts, then `migrate --apply`; retain the original data until verified. If migration stops midway, newly created documents are listed and kept for inspection rather than deleting files another process may have edited. `legacy-init` is only for old-command compatibility. +Project stages 02/07/10 live in `docs/project/`; feature stages 01/03/04/05/06/08/09 live in `docs/features//`. Session cache lives outside the repository in the host state directory. The legacy `.flowguard/` layout and its migration commands were removed in v0.4.0; `docs/legacy-flowguard/` in this repository is a read-only historical archive. ## Documentation - [Current architecture](docs/FlowGuard-Architecture.zh_CN.md) - [Ten-stage docs governance specification](docs/superpowers/specs/2026-09-23-flowguard-docs-ten-stage-governance.md) - [Implementation plan](docs/superpowers/plans/2026-09-23-flowguard-agent-driven-sdd-governance.md) -- [Legacy artifact contract](docs/FLOWGUARD_ARTIFACT_SPEC.md) +- [Artifact contract](docs/FLOWGUARD_ARTIFACT_SPEC.md) - [Roadmap](docs/roadmap.md) ## Verification diff --git a/README.zh-CN.md b/README.zh-CN.md index 72a3529..37fd12a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -91,16 +91,16 @@ python3 scripts/flowguard_state.py governance \ 协议见 [hooks/__protocol__.md](hooks/__protocol__.md)。 -## 文档位置与旧项目迁移 +## 文档位置 -项目级 02/07/10 放在 `docs/project/`,功能级 01/03/04/05/06/08/09 放在 `docs/features//`。新项目不创建 `.flowguard/`;会话缓存保存在宿主状态目录。旧项目先运行 `migrate --dry-run`,确认无冲突后再 `migrate --apply`,核对完成前保留旧数据。迁移中途失败时,已创建文档保留并在错误中列出,需人工核对;不会为了回滚而删除可能已被他人修改的文件。`legacy-init` 仅供旧命令兼容。 +项目级 02/07/10 放在 `docs/project/`,功能级 01/03/04/05/06/08/09 放在 `docs/features//`。会话缓存保存在仓库外的宿主状态目录。旧 `.flowguard/` 布局及其迁移命令已在 v0.4.0 下线;本仓 `docs/legacy-flowguard/` 仅是只读历史归档。 ## 文档 - [FlowGuard-Architecture.zh_CN.md](docs/FlowGuard-Architecture.zh_CN.md) — 当前架构、运行流、可信边界和风险 - [十阶段 docs 治理规格](docs/superpowers/specs/2026-09-23-flowguard-docs-ten-stage-governance.md) - [实施计划](docs/superpowers/plans/2026-09-23-flowguard-agent-driven-sdd-governance.md) -- [旧产物兼容契约](docs/FLOWGUARD_ARTIFACT_SPEC.md) +- [产物格式契约](docs/FLOWGUARD_ARTIFACT_SPEC.md) - [路线图](docs/roadmap.md) ## 验证 diff --git a/commands/flowguard-advance.json b/commands/flowguard-advance.json deleted file mode 100644 index eaf2f32..0000000 --- a/commands/flowguard-advance.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "flowguard-advance", - "description": "按 docs/ 阶段文档显式推进;验收必须有真实用户确认", - "prompt": "先运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" stage status --task-id --json`。展示指定阶段文档的完成内容、机械校验结果、测试与审查证据、尚存风险;明确请求用户确认。用户未说「验收通过/确认」时不得把阶段推进为 accepted。收到明确确认后,运行 `... flowguard_state.py stage advance --task-id --stage <01-requirements|...|10-release> --status accepted --approval-ref <真实批准依据> --json`,再读 stage status 核对。不要使用旧 advance 命令写 .flowguard 状态;仅凭 --approval-ref 字符串不足以证明不可伪造的用户回执。" -} diff --git a/commands/flowguard-context.json b/commands/flowguard-context.json index baed466..fc89760 100644 --- a/commands/flowguard-context.json +++ b/commands/flowguard-context.json @@ -1,5 +1,5 @@ { "name": "flowguard-context", "description": "绑定会话、worktree、任务与原生规格,并创建 docs/ 阶段文档", - "prompt": "先运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" discover --json`,再根据真实任务运行 `... flowguard_state.py context bind --session --task-id --task-type --spec-system [--spec-ref ] [--parent-id ] --json`。可用 `context show` 或 `context list` 查看。绑定可写任务会在 docs/project/ 与 docs/features// 创建缺失的十阶段文档,不创建 .flowguard/。仅在用户明确确认后使用 `context approve --context-id --approval scope_approved --actor user --json`;actor 文本不是可信用户回执,不得编造批准。" + "prompt": "先运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" discover --json`,再根据真实任务运行 `... flowguard_state.py context bind --session --task-id --task-type --spec-system [--spec-ref ] [--parent-id ] --json`。可用 `context show` 或 `context list` 查看。绑定可写任务会在 docs/project/ 与 docs/features// 创建缺失的十阶段文档。仅在用户明确确认后使用 `context approve --context-id --approval scope_approved --actor user --json`;actor 文本不是可信用户回执,不得编造批准。" } diff --git a/commands/flowguard-discover.json b/commands/flowguard-discover.json index 33b9d5e..38f21f8 100644 --- a/commands/flowguard-discover.json +++ b/commands/flowguard-discover.json @@ -1,5 +1,5 @@ { "name": "flowguard-discover", "description": "只读发现目标 Git 项目、原生 SDD 体系、CLI 可用性与冲突,不初始化任何工具", - "prompt": "执行 FlowGuard 的只读 SDD 发现:\n\n1. 确认用户指定的真实仓库或模块路径,运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" discover --json`\n2. 报告 Git/worktree、项目类型、`.specify/`、`openspec/`、Superpowers 产物、CLI 可用性和旧 FlowGuard 状态\n3. 明确区分:CLI 已安装、Skill 已安装、项目已初始化\n4. 若状态为 `choice_required`,停止创建规格并请求用户选择事实源\n5. 若状态为 `assessment_required`,由智能体结合用户任务分类 read_only/simple_change/important_change/incident;不要静默初始化\n\n本命令严格只读,不运行 specify init、openspec init、安装或迁移。" + "prompt": "执行 FlowGuard 的只读 SDD 发现:\n\n1. 确认用户指定的真实仓库或模块路径,运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" discover --json`\n2. 报告 Git/worktree、项目类型、`.specify/`、`openspec/`、Superpowers 产物与 CLI 可用性\n3. 明确区分:CLI 已安装、Skill 已安装、项目已初始化\n4. 若状态为 `choice_required`,停止创建规格并请求用户选择事实源\n5. 若状态为 `assessment_required`,由智能体结合用户任务分类 read_only/simple_change/important_change/incident;不要静默初始化\n\n本命令严格只读,不运行 specify init、openspec init 或安装。" } diff --git a/commands/flowguard-feature.json b/commands/flowguard-feature.json deleted file mode 100644 index 765f54a..0000000 --- a/commands/flowguard-feature.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "flowguard-feature", - "description": "旧十阶段兼容功能管理;新任务使用 flowguard-context 的父子任务与依赖", - "prompt": "管理 flowguard 功能流水线。先确认用户想做哪个子命令:\n\n**new --modules <模块...> [--title 标题]**\n1. feature-id 必须是 kebab 格式(小写字母/数字/连字符)\n2. --modules 必须是 project.json.modules 中已注册的模块(可多个,决定写码门禁的归属判定)\n3. 运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" feature new --modules <模块...> --json`\n4. 报告生成的 7 份功能产物与下一步(/flowguard-next)\n\n**list**:运行 `... feature list --json` 并渲染功能清单(状态/涉及模块)。\n\n**done **:先检查是否全部阶段收敛(未收敛会报未验收阶段清单);如用户坚持放弃收口,引导用 drop 并写理由。\n\n**drop --reason <理由>**:理由必填,留痕 journal。\n\n禁止跳过 CLI 直接手改 .flowguard/ 下的状态文件。" -} diff --git a/commands/flowguard-gate.json b/commands/flowguard-gate.json deleted file mode 100644 index 39337af..0000000 --- a/commands/flowguard-gate.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "flowguard-gate", - "description": "旧十阶段兼容门禁;新任务使用 flowguard-governance 检查写码/提交/发布", - "prompt": "运行 flowguard 门禁自检:\n\n1. 运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" gate --json` 查看四类动作整体放行状态\n2. 对被阻塞的动作,逐条运行带 `--action <动作> --path <路径>` 的定向检查拿到诊断信封(severity/code/message/fix)\n3. 把每个阻塞渲染成「动作 | code | 原因 | 解锁命令」表格\n4. 运行 `... validate --json` 附带产物完整性问题(格式/追溯矩阵/测试文件存在性/Tier2 技能缺失)\n\n只读操作:本命令绝不修改状态。被阻塞时给出最小解锁路径(通常 = 完成并验收某阶段),提醒用户 override 是留痕逃生口而非常规手段。" -} diff --git a/commands/flowguard-init.json b/commands/flowguard-init.json index e716b4c..9f0a501 100644 --- a/commands/flowguard-init.json +++ b/commands/flowguard-init.json @@ -1,5 +1,5 @@ { "name": "flowguard-init", - "description": "只创建 docs/project/ 中的项目级十阶段文档;不创建 .flowguard/", - "prompt": "先运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" discover --json` 做只读检查。若用户已要求初始化 FlowGuard 文档且目标仓库明确,运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" init --json`。只创建 docs/project/ 下的 02/07/10;功能文档由 context bind 创建。不得自动执行 specify init、openspec init 或 legacy-init。" + "description": "只创建 docs/project/ 中的项目级十阶段文档", + "prompt": "先运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" discover --json` 做只读检查。若用户已要求初始化 FlowGuard 文档且目标仓库明确,运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" init --json`。只创建 docs/project/ 下的 02/07/10;功能文档由 context bind 创建。不得自动执行 specify init 或 openspec init。" } diff --git a/commands/flowguard-next.json b/commands/flowguard-next.json deleted file mode 100644 index 408fedd..0000000 --- a/commands/flowguard-next.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "flowguard-next", - "description": "由智能体判断十阶段的下一步,不由 Hook 自动推进", - "prompt": "运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" stage status --task-id --json`,读取 docs/ 中 01—10 状态和原生规格引用。智能体结合用户目标、依赖与现有产物决定当前阶段;读取对应 flowguard-* 阶段技能,补充文档和验证证据。用 `... flowguard_state.py stage advance --task-id --stage <01-requirements|...|10-release> --status in_progress --json` 标记开始。不要调用旧 next 命令推进新任务,不得代替用户验收。" -} diff --git a/commands/flowguard-override.json b/commands/flowguard-override.json deleted file mode 100644 index 7b019a0..0000000 --- a/commands/flowguard-override.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "flowguard-override", - "description": "旧十阶段兼容逃生通道;必须用户主动发起、填写理由并留痕", - "prompt": "这是硬门禁的唯一逃生口,**只能由用户主动发起**;如果 agent 是自己想跳过阶段,立即停止。\n\n1. 与用户确认三件事:要跳过的阶段(--stage,缺省=当前功能首个未收敛阶段)、理由(必填)、作用于哪个功能(--feature,缺省=当前功能)或项目级\n2. 向用户复述后果:overridden 状态 + journal 留痕 + 后续审计可见\n3. 用户确认后运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" override --reason <理由> [--stage <阶段>] [--feature ] --json`\n4. 运行 `... status --json` 报告跳过后的流程状态\n\n禁止:用 override 替代正常验收流程;为用户编造理由;连续跳过多个阶段而不逐个留痕。" -} diff --git a/commands/flowguard-stage.json b/commands/flowguard-stage.json index b563bcf..6510665 100644 --- a/commands/flowguard-stage.json +++ b/commands/flowguard-stage.json @@ -1,5 +1,5 @@ { "name": "flowguard-stage", "description": "查看和推进 docs/ 中的十阶段文档", - "prompt": "运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" stage status --task-id --json` 查看阶段。写入 docs/project/ 或 docs/features// 对应文档并自检后,可运行 `... flowguard_state.py stage advance --task-id --stage <阶段文件名> --status in_progress --json`;只有真实用户批准或可审计依据才可将阶段标记 accepted/inherited/skipped。不能用模型自述伪造批准;旧 .flowguard/ 仅是迁移输入。" + "prompt": "运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" stage status --task-id --json` 查看阶段。写入 docs/project/ 或 docs/features// 对应文档并自检后,可运行 `... flowguard_state.py stage advance --task-id --stage <阶段文件名> --status in_progress --json`;只有真实用户批准或可审计依据才可将阶段标记 accepted/inherited/skipped。不能用模型自述伪造批准。" } diff --git a/commands/flowguard-status.json b/commands/flowguard-status.json deleted file mode 100644 index 197f5be..0000000 --- a/commands/flowguard-status.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "flowguard-status", - "description": "读取 docs/ 十阶段项目与功能状态", - "prompt": "运行 `python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/flowguard_state.py\" stage status --task-id --json`。展示 01—10 阶段状态、文档路径、失效原因和下一步;项目级 02/07/10 从 docs/project/ 读取,功能级七阶段从 docs/features// 读取。不要用旧 status/current_feature 作为新任务的唯一事实源。" -} diff --git a/docs/FLOWGUARD_ARTIFACT_SPEC.md b/docs/FLOWGUARD_ARTIFACT_SPEC.md index e26ba05..9e8b2d5 100644 --- a/docs/FLOWGUARD_ARTIFACT_SPEC.md +++ b/docs/FLOWGUARD_ARTIFACT_SPEC.md @@ -19,7 +19,7 @@ ## 1. 通用规则 - **元信息头**:产物文件首行注释块 ``,模板自带,勿删。 -- **阶段信息**:`### 1.3 FlowGuard 阶段信息` 表记录任务、父任务、阶段、状态、规格事实源、原生产物、批准依据、前置指纹与验收指纹;新项目不创建 `.flowguard/`。 +- **阶段信息**:`### 1.3 FlowGuard 阶段信息` 表记录任务、父任务、阶段、状态、规格事实源、原生产物、批准依据、前置指纹与验收指纹。 - **占位符规则**:含 `<占位符>`(尖括号)的块视为**模板未填写示例**,不参与机械检查。真实内容不得包含 `<...>` 形式文本。 - **代码围栏掩码**:``` 围栏内的 `###`/`####` 头不参与解析。 - **追加式**(项目级产物 02/07/10):增补条目一律追加并标注来源(`feature: `),禁止改写既有正文;确需修改走回改降级流程。 diff --git a/docs/FlowGuard-Architecture.zh_CN.md b/docs/FlowGuard-Architecture.zh_CN.md index c1a06b1..054f66e 100644 --- a/docs/FlowGuard-Architecture.zh_CN.md +++ b/docs/FlowGuard-Architecture.zh_CN.md @@ -47,7 +47,7 @@ flowchart TB Proof --> Docs ``` -十阶段文档不写进 `.flowguard/`。项目级 02 架构、07 规范、10 发布位于 `docs/project/`;功能级 01 需求、03 方案、04 用例、05 概要设计、06 详细设计、08 审查、09 文档位于 `docs/features//`。独立子功能有自己的功能级文档,通过父任务引用继承项目级约束。普通实现步骤仍留在原生 tasks 中。 +十阶段文档只写 `docs/`。项目级 02 架构、07 规范、10 发布位于 `docs/project/`;功能级 01 需求、03 方案、04 用例、05 概要设计、06 详细设计、08 审查、09 文档位于 `docs/features//`。独立子功能有自己的功能级文档,通过父任务引用继承项目级约束。普通实现步骤仍留在原生 tasks 中。 `context.py` 将会话选择、worktree 和任务关系缓存于宿主状态目录;项目可用 Git、`docs/` 和原生规格重建流程事实。`stage_docs.py` 解析阶段状态和验收指纹,`evidence.py` 在阶段文档中登记检查结果,`governance.py` 提供唯一动作裁决;Hooks 与 CLI 仅为适配层。 @@ -66,9 +66,9 @@ flowchart LR 读取、澄清、补规格、补测试始终是解除阻断的路径。写业务代码要求 01—07 阶段满足;提交还要求 08—09 及当前代码的测试、静态分析、语义审查证据;发布还要求 10、发布就绪、用户验收与必要子任务完成。重要变更另须有效原生规格引用和明确范围批准。阶段正文变化使该阶段原验收失效;证据出现更新的失败或代码指纹变化时不得沿用旧 PASS。 -## 4. 迁移与兼容 +## 4. 兼容与历史 -旧 `.flowguard/` 只作为已有项目的迁移输入。`migrate --dry-run` 检查映射与冲突;`migrate --apply` 无覆盖地复制十阶段正文到 `docs/`,将旧验收状态转为待复核。核对完成前保留旧数据。新项目的 `init` 只创建 `docs/project/`;`legacy-init` 明确限定旧命令兼容。 +`.flowguard/` 兼容/迁移线已随 v0.4.0(2026-09-23)下线:不再提供 `migrate`/`legacy-init`,旧状态机命令与旧 gate 解锁矩阵一并移除。本仓 `docs/legacy-flowguard/` 仅是迁移前状态的只读历史归档,不是事实源,也不构成迁移承诺。新项目的 `init` 只创建 `docs/project/`。 ## 5. 可信边界与当前限制 diff --git a/docs/architecture.md b/docs/architecture.md index 21f8688..dd5b717 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,4 +2,4 @@ > 本文件保留为历史稳定链接。当前权威架构文档见 [FlowGuard-Architecture.zh_CN.md](FlowGuard-Architecture.zh_CN.md)。 -当前架构保留强制十阶段,由智能体判断和推进;FlowGuard 校验 `docs/` 产物与证据,Hook 阻止绕过。旧版全局 `current_feature` 和 `.flowguard/` 状态仅作迁移输入。 +当前架构保留强制十阶段,由智能体判断和推进;FlowGuard 校验 `docs/` 产物与证据,Hook 阻止绕过。旧版全局 `current_feature` 和 `.flowguard/` 状态模型已随 v0.4.0 下线。 diff --git a/docs/legacy-flowguard/README.md b/docs/legacy-flowguard/README.md index 27cf015..fe1b2d2 100644 --- a/docs/legacy-flowguard/README.md +++ b/docs/legacy-flowguard/README.md @@ -1,6 +1,6 @@ # 旧 FlowGuard 状态快照 -> **文档说明**:保留本仓从旧 `.flowguard/` 迁移前的状态和审计记录,供逐项核对;不是当前流程事实源。 +> **文档说明**:保留本仓从旧 `.flowguard/` 迁移前的状态和审计记录,供逐项核对。纯历史归档:既不是流程事实源,也不构成迁移承诺或兼容支持。 > **版本**:v1.0 > **最后更新**:2026-09-23 @@ -10,7 +10,7 @@ 当前项目不应把这里的旧状态当作可继续推进的状态机;阶段状态以 `docs/` 中的新文档为准。迁移后旧验收不自动继承,须重新核对。 -旧 `journal-restore` 的五个跳过用例原样归档为 `test_recover.py`,它们要求重建已废弃的 `.flowguard/project.json` 和 `state.json`,不属于现行十阶段验收。现行恢复行为由 `tests/test_docs_pipeline.py` 的缓存丢失/SessionStart 恢复测试与 `tests/test_docs_migration.py` 的 dry-run、冲突保护测试验证;旧 journal 追加事件的要求不迁移为 `docs/` 流程要求。 +旧 `journal-restore` 的五个跳过用例原样归档为 `test_recover.py`,它们要求重建已废弃的 `.flowguard/project.json` 和 `state.json`,不属于现行十阶段验收。现行恢复行为由 `tests/test_docs_pipeline.py` 的缓存丢失/SessionStart 恢复测试验证;旧 journal 追加事件的要求不迁移为 `docs/` 流程要求。(迁移器与旧命令面已随 v0.4.0 下线。) --- diff --git a/docs/roadmap.md b/docs/roadmap.md index 957ad70..16e87ec 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,7 +1,7 @@ # FlowGuard 技术路线图 > 当前规格:`docs/superpowers/specs/2026-09-23-flowguard-docs-ten-stage-governance.md`。 -> 十阶段仍是强制流程骨架,由智能体推进;文档放在 `docs/`,旧 `.flowguard/` 只作为迁移输入。 +> 十阶段仍是强制流程骨架,由智能体推进;文档放在 `docs/`。`.flowguard/` 兼容/迁移线已于 v0.4.0(2026-09-23)下线。 ## v0.2 —— 智能体驱动 SDD 治理 @@ -14,14 +14,12 @@ | 六类动作门禁 | ✅ | `governance.py` + commit 证据矩阵 | | 五类 Hooks | ✅ | 子进程协议测试 | | 主技能改为智能体循环 | ✅ | 生成 parity 测试 | -| 旧十阶段兼容 | ✅ | 原有回归测试 | ## 下一阶段 —— 十阶段 docs 收敛与可信门禁 - 完成 `docs/` 阶段文档的内容/前置条件/受影响下游失效校验;不把模型自述当验收。 - 使代码、原生规格和检查生产者变化能够使相关验收与证据过期。 - 建立不可伪造的用户确认回执与 Git/CI 独立门禁,并验证 Codex、ZCode、Kimi 的真实 Hook 触发。 -- 核对本仓 `docs/legacy-flowguard/` 归档与迁移文档,形成可审计迁移报告。 ## v0.3 —— 原生工具状态适配 @@ -41,7 +39,6 @@ - 仓库级风险策略:公共 API、数据库、权限、安全、发布动作的差异化门槛。 - 可选/必要子任务和父级集成验收。 -- 旧 `.flowguard/` 到 `docs/` 的可核对迁移器;迁移前不删除旧数据。 - 四宿主真实安装/加载/回执矩阵和性能预算。 ## 开放问题 @@ -50,4 +47,4 @@ 2. CodeReview 插件的报告 Schema、模型置信度和失败策略如何定义? 3. 大型 monorepo 的代码指纹应按上下文文件集还是整个 worktree 计算? 4. incident 恢复后补规格的最大时间窗口和发布限制如何配置? -5. 旧十阶段兼容层何时满足移除条件? +5. ~~旧十阶段兼容层何时满足移除条件?~~ 已决(2026-09-23):v0.4.0 全面下线。 diff --git a/docs/superpowers/plans/2026-09-23-flowguard-agent-driven-sdd-governance.md b/docs/superpowers/plans/2026-09-23-flowguard-agent-driven-sdd-governance.md index d311f55..1cf356c 100644 --- a/docs/superpowers/plans/2026-09-23-flowguard-agent-driven-sdd-governance.md +++ b/docs/superpowers/plans/2026-09-23-flowguard-agent-driven-sdd-governance.md @@ -27,12 +27,12 @@ 8. 更新架构、README、Hook 协议和路线图。 9. 执行目标测试、全量 unittest、生成 parity、插件验证和 `git diff --check`。 -## 3. 兼容策略 +## 3. 兼容策略(2026-09-23 更新:兼容线已下线) -- 不删除或重写现有 `.flowguard` 产物。 +- ~~不删除或重写现有 `.flowguard` 产物。~~ v0.4.0 起 `.flowguard` 兼容/迁移线全面下线,旧命令面移除;`docs/legacy-flowguard/` 保留为只读历史归档。 - 不把旧 accepted 状态迁移为新用户批准或新证据。 -- 旧命令本版本继续可用,但输出兼容提示;新任务文档只推荐治理命令。 -- 版本按仓库发布规则升级;本轮不提交、不推送、不创建 Release,除非用户后续明确要求。 +- ~~旧命令本版本继续可用,但输出兼容提示;~~ 新任务只使用治理命令(init/discover/context/evidence/governance/stage/validate)。 +- 版本按仓库发布规则升级。 ## 4. 完成定义 diff --git a/docs/superpowers/specs/2026-09-23-flowguard-docs-ten-stage-governance.md b/docs/superpowers/specs/2026-09-23-flowguard-docs-ten-stage-governance.md index 6d1341c..9e87ce5 100644 --- a/docs/superpowers/specs/2026-09-23-flowguard-docs-ten-stage-governance.md +++ b/docs/superpowers/specs/2026-09-23-flowguard-docs-ten-stage-governance.md @@ -43,9 +43,9 @@ - Hooks 负责发现、提醒、校验、拦截和回报,不自动决定语义阶段或替用户验收。 - 通用 Shell 写入、不同宿主的 Hook 协议、安装后的实际触发均属于生产验收范围;仅通过单元测试或清单校验不足以证明完整强制门禁。 -## 5. 迁移与兼容 +## 5. 迁移与兼容(已下线) -对已有 `.flowguard/` 项目提供只读预检和显式迁移。迁移复制十阶段正文、状态与审计引用到 `docs/`,逐项核对后才能移走旧目录;遇到目标文档冲突必须停止,不覆盖用户内容。旧命令只保留读取或给出迁移提示,不能在新项目生成 `.flowguard/`。 +**变更(2026-09-23,v0.4.0)**:`.flowguard/` 兼容/迁移线全面下线——不再提供 `migrate`/`legacy-init`,旧状态机命令(feature/next/advance/override/status/gate)与旧 gate 解锁矩阵一并移除,仓库内不得出现 `.flowguard/`。`docs/legacy-flowguard/` 仅是本仓迁移前状态的只读历史归档,不构成迁移承诺。 ## 6. 验收行为 @@ -54,7 +54,7 @@ 3. 01—07 未满足时阻断业务写入;08/09 未满足或证据过期时阻断提交;10 和用户验收未满足时阻断发布。 4. 十阶段文档按 full-stack-doc 结构生成,原生 SDD 正文只被引用。 5. 测试、CodeGuard、CodeReview 结果进入对应阶段文档;最新失败不能被旧 PASS 掩盖。 -6. 旧 `.flowguard/` 迁移可预检、无覆盖、可核对,迁移前不破坏旧数据。 +6. 旧 `.flowguard/` 命令面不存在,仓库内不出现 `.flowguard/` 目录。 7. Codex、ZCode、Kimi 至少各有一个真实安装与 Hook 触发回执;缺失的宿主能力必须明确为未验证或降级,而非宣称强制门禁。 --- diff --git a/hooks/__protocol__.md b/hooks/__protocol__.md index 20da2a0..6ab0820 100644 --- a/hooks/__protocol__.md +++ b/hooks/__protocol__.md @@ -29,8 +29,8 @@ |:---|:---|:---|:---|:---:| | `flowguard_status_summary.py` | SessionStart | 只读发现 SDD、恢复上下文、提示冲突/待分类 | stdout 摘要 | 0 | | `flowguard_prompt_guard.py` | UserPromptSubmit | 提醒智能体重新判断任务、范围与事实源 | stdout 提醒 | 0 | -| `flowguard_gate.py` | PreToolUse | 校验业务写入、Git commit、发布;保护治理状态 | stderr 诊断 | 0 / 2 | -| `flowguard_artifact_check.py` | PostToolUse | 观察 `docs/` 阶段文档是否失效;在有明确 exit_code 时记录检查结果;兼容旧产物降级 | stdout JSON `systemMessage`;stderr 兼容提示 | 0 | +| `flowguard_gate.py` | PreToolUse | 校验业务写入、Git commit、发布;拒绝越出 worktree 的写入 | stderr 诊断 | 0 / 2 | +| `flowguard_artifact_check.py` | PostToolUse | 观察 `docs/` 阶段文档是否失效;在有明确 exit_code 时记录检查结果 | stdout JSON `systemMessage`;stderr 兼容提示 | 0 | | `flowguard_artifact_check.py` | Kimi PostToolUseFailure(Shell) | 明确的测试工具失败记 FAIL,覆盖同一指纹的旧 PASS;不把错误文本当成功摘要 | stdout JSON `systemMessage` | 0 | | `flowguard_stage_summary.py` | Stop | 汇总上下文、缺失提交证据和下一步 | stdout JSON `systemMessage` | 0 | @@ -54,9 +54,9 @@ | `mcp__codeguard__list_languages`、`mcp__codeguard__analyze_java_impact` | 已核对为只读,放行;仅匹配 MCP 服务器名为 `codeguard` 的精确工具名 | | `mcp__codeguard__check_code_style` | `test_write`;必须显式传入属于当前 Git worktree 的 `path`;仅在 PostToolUse 逐语言结构化检查全部 PASS 时登记静态分析 PASS | | `mcp__codeguard__auto_fix` | `code_write`;同样要求显式同 worktree `path`,再校验 01—07 阶段 | -| 未分类的本地或 MCP 工具 | Git/旧 FlowGuard 项目中拒绝;先补工具副作用分类及回归测试,不能按名称猜测其只读性 | +| 未分类的本地或 MCP 工具 | Git 项目中拒绝;先补工具副作用分类及回归测试,不能按名称猜测其只读性 | -`.flowguard/contexts`、`.flowguard/evidence`、journal、`project.json` 禁止通过文件编辑工具直接修改,返回 `governance_state_protected`。 +宿主会话缓存(上下文/锁)位于仓库外的宿主状态目录,写入目标越出当前 Git worktree 返回 `governance_write_target_mismatch`;`docs/` 阶段机器区(阶段状态表、证据表)只能经 FlowGuard CLI 变更,直接手改会因指纹不匹配而失效。 CodeGuard MCP 的宿主配置若使用其他服务器名,本表不自动匹配;需按实际工具名另做审计和测试。精确名字分类只是副作用路由,不是 MCP 来源认证或结果可信度证明。 拒绝输出至少包含: diff --git a/hooks/flowguard_artifact_check.py b/hooks/flowguard_artifact_check.py index d1dd709..3c7973f 100644 --- a/hooks/flowguard_artifact_check.py +++ b/hooks/flowguard_artifact_check.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""PostToolUse:证据采集/失效 + 旧产物校验与降级(恒 exit 0)。""" +"""PostToolUse:证据采集与阶段文档失效提示(恒 exit 0)。""" import hashlib import json import os @@ -11,7 +11,7 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) -from flowguard_lib import codereview_evidence, context, evidence, journal, stage_docs, state, tool_scope, validation # noqa: E402 +from flowguard_lib import codereview_evidence, context, evidence, stage_docs, tool_scope # noqa: E402 # artifact 文件名 → 阶段(用于降级与校验路由) ARTIFACT_STAGE = { @@ -176,20 +176,6 @@ def _parse_artifact(path): if len(parts) == 2 and parts[0] == "project": stage = ARTIFACT_STAGE.get(Path(parts[1]).stem) return ("docs_project", None, stage) if stage else None - marker = ".flowguard/" - if marker not in p or not p.endswith(".md"): - return None - rel = p.split(marker, 1)[1] - if rel.startswith("features/"): - parts = rel.split("/") - if len(parts) >= 3 and parts[2] == "artifacts": - stem = parts[3][:-3] if parts[3].endswith(".md") else parts[3] - stage = ARTIFACT_STAGE.get(stem) - return ("feature", parts[1], stage) if stage else None - if rel.startswith("project/"): - stem = Path(rel).stem - stage = ARTIFACT_STAGE.get(stem) - return ("project", None, stage) if stage else None return None @@ -222,50 +208,16 @@ def _check_artifact(cwd, file_path, active, notices): info = _parse_artifact(file_path) if not info: return - scope, fid, stage = info - if scope.startswith("docs_"): - task_id = fid or (active or {}).get("task_id") or "project" - try: - phase = stage_docs.read(cwd, task_id, next( - aid for aid, item in stage_docs.registry.ARTIFACTS.items() if item["stage"] == stage - )) - if phase["status"] == "invalidated": - _notice(notices, f"[flowguard] 阶段文档已失效: {phase['path']};请复核后重新申请验收") - except Exception as error: - _notice(notices, f"[flowguard] 阶段文档待修复: {error}") - return + _scope, fid, stage = info + task_id = fid or (active or {}).get("task_id") or "project" try: - if scope == "feature": - owner = state.load_feature(cwd, fid) - jscope = f"feature:{fid}" - else: - owner = state.load_project(cwd) - jscope = "project" - except Exception: - return - - degraded = state.degrade_from(owner, stage) - if degraded: - if scope == "feature": - state.save_feature(cwd, owner) - else: - state.save_project(cwd, owner) - journal.append(cwd, jscope, "artifact_rework_degrade", - {"artifact": Path(file_path).name, "degraded": degraded}) - _notice(notices, f"[flowguard] 产物回改,已降级阶段: {', '.join(degraded)}") - - try: - text = (cwd / file_path).read_text(encoding="utf-8") - issues = [] - if stage == "requirements" and fid: - issues = validation.validate_requirements(text, fid) - elif stage == "review" and fid: - issues = validation.validate_review(text) - for issue in issues: - if issue["level"] in ("ERROR", "WARNING"): - _notice(notices, f"[flowguard] {issue['level']}: {issue['message']} → {issue['fix']}") - except Exception: - pass + phase = stage_docs.read(cwd, task_id, next( + aid for aid, item in stage_docs.registry.ARTIFACTS.items() if item["stage"] == stage + )) + if phase["status"] == "invalidated": + _notice(notices, f"[flowguard] 阶段文档已失效: {phase['path']};请复核后重新申请验收") + except Exception as error: + _notice(notices, f"[flowguard] 阶段文档待修复: {error}") def main(): diff --git a/hooks/flowguard_gate.py b/hooks/flowguard_gate.py index 3ce7238..0d4d958 100644 --- a/hooks/flowguard_gate.py +++ b/hooks/flowguard_gate.py @@ -10,7 +10,7 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) -from flowguard_lib import discovery, gate, governance, registry, tool_scope # noqa: E402 +from flowguard_lib import discovery, governance, registry, tool_scope # noqa: E402 CODEGUARD_MCP_ACTIONS = { "mcp__codeguard__list_languages": "read", @@ -68,10 +68,6 @@ def _write_action(cwd, path): rel = _relative(cwd, path) if rel.startswith((".specify/", "openspec/", "docs/")): return "spec_write" - if rel.startswith(".flowguard/") and ( - "/artifacts/" in rel or rel.startswith(".flowguard/project/") - ): - return "spec_write" # 旧产物兼容:允许补规格以解除阻断 parts = Path(rel).parts name = Path(rel).name.lower() if ( @@ -85,13 +81,6 @@ def _write_action(cwd, path): return "code_write" -def _protected_governance_path(cwd, path): - rel = _relative(cwd, path) - return rel.startswith(( - ".flowguard/contexts/", ".flowguard/evidence/", ".flowguard/journal/", - )) or rel in (".flowguard/project.json",) - - def _patch_paths(command): if not isinstance(command, str) or not command.startswith("*** Begin Patch"): return [] @@ -294,27 +283,11 @@ def main(): if tool == "apply_patch": paths = _patch_paths(tool_input.get("command")) write_targets = paths - if any(_protected_governance_path(cwd, item) for item in paths): - _print_denial({ - "code": "governance_state_protected", - "message": "FlowGuard 治理状态禁止通过补丁直接修改", - "fix": "使用 FlowGuard CLI 变更状态,以保留校验和审计记录", - }) - return 2 actions = {_write_action(cwd, item) for item in paths} if paths else {"code_write"} action = next(item for item in ("code_write", "test_write", "spec_write") if item in actions) elif tool in ("Write", "Edit", "MultiEdit", "WriteFile", "StrReplaceFile"): path = tool_input.get("file_path") write_targets = [path] if isinstance(path, str) and path else [] - if isinstance(path, str) and path and _protected_governance_path(cwd, path): - _print_denial({ - "code": "governance_state_protected", - "message": "FlowGuard 治理状态禁止通过文件编辑工具直接修改", - "fix": "使用 context/evidence/legacy CLI 变更状态,以保留校验和审计记录", - "missing": [], - "allowed_actions": ["read", "spec_write"], - }) - return 2 action = _write_action(cwd, path) if isinstance(path, str) and path else "code_write" elif tool in ("Bash", "Shell"): command = tool_input.get("command") @@ -408,16 +381,6 @@ def main(): "fix": "进入目标 worktree,执行 SDD 发现与任务绑定后再提交", }) return 2 - elif (Path(cwd) / ".flowguard" / "project.json").exists(): - if action == "unclassified": - _print_denial({ - "code": "governance_unclassified_tool", - "message": f"旧 FlowGuard 项目无法判定工具 {tool!r} 的副作用", - "fix": "改用已识别工具,或为该工具添加明确的风险分类", - }) - return 2 - legacy_action = "build_release" if action == "release" else "write_code" - res = gate.check_action(Path(cwd), legacy_action, path=path) else: return 0 except Exception as e: # 钩子自身崩溃 = 放行 + 警告 diff --git a/hooks/flowguard_stage_summary.py b/hooks/flowguard_stage_summary.py index b4f46e6..97f0a5a 100644 --- a/hooks/flowguard_stage_summary.py +++ b/hooks/flowguard_stage_summary.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Stop:汇总治理上下文、缺失证据和兼容状态,不把会话结束当作任务完成。""" +"""Stop:汇总治理上下文与缺失证据,不把会话结束当作任务完成。""" import json import os import sys @@ -8,10 +8,7 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) -from flowguard_lib import context, discovery, evidence, governance, stage_docs, state # noqa: E402 - -FEATURE_STAGES = ("requirements", "solution", "testcases", "hld", "lld", "review", "docs") -OK = ("accepted", "skipped", "overridden") +from flowguard_lib import context, discovery, evidence, governance, stage_docs # noqa: E402 def _emit(lines): @@ -75,32 +72,6 @@ def main(): lines.append("[flowguard] 提交证据已齐;提交前仍需运行 governance --action git_commit 复核") else: lines.append("[flowguard] 本轮尚未绑定治理上下文;下轮先完成任务分类与 context bind") - try: - project = state.load_project(cwd) - except Exception: - project = None - if not project: - _emit(lines) - return 0 - fid = project.get("current_feature") - if fid: - try: - f = state.load_feature(cwd, fid) - nxt = next((s for s in FEATURE_STAGES if f["stages"][s]["status"] not in OK), None) - if f.get("status") == "active": - if nxt: - lines.append(f"[flowguard] 兼容功能 {fid} 下一步: 阶段 {nxt}(/flowguard-next)") - else: - lines.append(f"[flowguard] 兼容功能 {fid} 全阶段已收敛,可 /flowguard-feature done {fid}") - except Exception: - pass - pending = [s for s in ("architecture", "standards", "release") - if project["stages"][s]["status"] not in OK] - if pending: - lines.append(f"[flowguard] 兼容项目级待推进: {', '.join(pending)}(/flowguard-next --stage {pending[0]})") - active = [k for k, v in (project.get("features") or {}).items() if v.get("status") == "active"] - if not active and not pending: - lines.append("[flowguard] 兼容十阶段全部收敛;新任务仍须以治理上下文和当前证据复核") _emit(lines) return 0 diff --git a/hooks/flowguard_status_summary.py b/hooks/flowguard_status_summary.py index de158aa..57e593f 100644 --- a/hooks/flowguard_status_summary.py +++ b/hooks/flowguard_status_summary.py @@ -8,9 +8,7 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) -from flowguard_lib import context, discovery, stage_docs, state # noqa: E402 - -FEATURE_STAGES = ("requirements", "solution", "testcases", "hld", "lld", "review", "docs") +from flowguard_lib import context, discovery, stage_docs # noqa: E402 def main(): @@ -65,22 +63,6 @@ def main(): f"| parent={task['parent_task_id'] or '-'} " f"| 下一步={task['next_stage'] or '全部满足'}" ) - try: - project = state.load_project(cwd) - except Exception: - project = None - if project: - lines.append( - f"[flowguard] 兼容状态: 项目 {project['project']} | " - f"legacy current_feature={project.get('current_feature') or '未设定'}" - ) - for fid in (project.get("features") or {}): - try: - f = state.load_feature(cwd, fid) - except Exception: - continue - sts = " ".join(f"{s[0]}={f['stages'][s]['status']}" for s in FEATURE_STAGES) - lines.append(f"[flowguard] 兼容功能 {fid}({f.get('status')}): {sts}") if lines: print("\n".join(lines)) return 0 diff --git a/kimi-commands/flowguard-advance.md b/kimi-commands/flowguard-advance.md deleted file mode 100644 index 041f087..0000000 --- a/kimi-commands/flowguard-advance.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: flowguard-advance -description: "按 docs/ 阶段文档显式推进;验收必须有真实用户确认" ---- - -执行前确认已启用的 FlowGuard 插件根目录。若 Shell 环境未提供 `KIMI_PLUGIN_ROOT`,通过 `/plugins info flowguard` 定位安装目录并将下方路径改为绝对路径;不要在目标 Git 项目中猜测或执行同名脚本。 - -先运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" stage status --task-id --json`。展示指定阶段文档的完成内容、机械校验结果、测试与审查证据、尚存风险;明确请求用户确认。用户未说「验收通过/确认」时不得把阶段推进为 accepted。收到明确确认后,运行 `... flowguard_state.py stage advance --task-id --stage <01-requirements|...|10-release> --status accepted --approval-ref <真实批准依据> --json`,再读 stage status 核对。不要使用旧 advance 命令写 .flowguard 状态;仅凭 --approval-ref 字符串不足以证明不可伪造的用户回执。 diff --git a/kimi-commands/flowguard-context.md b/kimi-commands/flowguard-context.md index 87596ad..cb450f0 100644 --- a/kimi-commands/flowguard-context.md +++ b/kimi-commands/flowguard-context.md @@ -5,4 +5,4 @@ description: "绑定会话、worktree、任务与原生规格,并创建 docs/ 执行前确认已启用的 FlowGuard 插件根目录。若 Shell 环境未提供 `KIMI_PLUGIN_ROOT`,通过 `/plugins info flowguard` 定位安装目录并将下方路径改为绝对路径;不要在目标 Git 项目中猜测或执行同名脚本。 -先运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" discover --json`,再根据真实任务运行 `... flowguard_state.py context bind --session --task-id --task-type --spec-system [--spec-ref ] [--parent-id ] --json`。可用 `context show` 或 `context list` 查看。绑定可写任务会在 docs/project/ 与 docs/features// 创建缺失的十阶段文档,不创建 .flowguard/。仅在用户明确确认后使用 `context approve --context-id --approval scope_approved --actor user --json`;actor 文本不是可信用户回执,不得编造批准。 +先运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" discover --json`,再根据真实任务运行 `... flowguard_state.py context bind --session --task-id --task-type --spec-system [--spec-ref ] [--parent-id ] --json`。可用 `context show` 或 `context list` 查看。绑定可写任务会在 docs/project/ 与 docs/features// 创建缺失的十阶段文档。仅在用户明确确认后使用 `context approve --context-id --approval scope_approved --actor user --json`;actor 文本不是可信用户回执,不得编造批准。 diff --git a/kimi-commands/flowguard-discover.md b/kimi-commands/flowguard-discover.md index 0c8c2c0..99e6379 100644 --- a/kimi-commands/flowguard-discover.md +++ b/kimi-commands/flowguard-discover.md @@ -8,9 +8,9 @@ description: "只读发现目标 Git 项目、原生 SDD 体系、CLI 可用性 执行 FlowGuard 的只读 SDD 发现: 1. 确认用户指定的真实仓库或模块路径,运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" discover --json` -2. 报告 Git/worktree、项目类型、`.specify/`、`openspec/`、Superpowers 产物、CLI 可用性和旧 FlowGuard 状态 +2. 报告 Git/worktree、项目类型、`.specify/`、`openspec/`、Superpowers 产物与 CLI 可用性 3. 明确区分:CLI 已安装、Skill 已安装、项目已初始化 4. 若状态为 `choice_required`,停止创建规格并请求用户选择事实源 5. 若状态为 `assessment_required`,由智能体结合用户任务分类 read_only/simple_change/important_change/incident;不要静默初始化 -本命令严格只读,不运行 specify init、openspec init、安装或迁移。 +本命令严格只读,不运行 specify init、openspec init 或安装。 diff --git a/kimi-commands/flowguard-feature.md b/kimi-commands/flowguard-feature.md deleted file mode 100644 index 498e71a..0000000 --- a/kimi-commands/flowguard-feature.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: flowguard-feature -description: "旧十阶段兼容功能管理;新任务使用 flowguard-context 的父子任务与依赖" ---- - -执行前确认已启用的 FlowGuard 插件根目录。若 Shell 环境未提供 `KIMI_PLUGIN_ROOT`,通过 `/plugins info flowguard` 定位安装目录并将下方路径改为绝对路径;不要在目标 Git 项目中猜测或执行同名脚本。 - -管理 flowguard 功能流水线。先确认用户想做哪个子命令: - -**new --modules <模块...> [--title 标题]** -1. feature-id 必须是 kebab 格式(小写字母/数字/连字符) -2. --modules 必须是 project.json.modules 中已注册的模块(可多个,决定写码门禁的归属判定) -3. 运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" feature new --modules <模块...> --json` -4. 报告生成的 7 份功能产物与下一步(/flowguard-next) - -**list**:运行 `... feature list --json` 并渲染功能清单(状态/涉及模块)。 - -**done **:先检查是否全部阶段收敛(未收敛会报未验收阶段清单);如用户坚持放弃收口,引导用 drop 并写理由。 - -**drop --reason <理由>**:理由必填,留痕 journal。 - -禁止跳过 CLI 直接手改 .flowguard/ 下的状态文件。 diff --git a/kimi-commands/flowguard-gate.md b/kimi-commands/flowguard-gate.md deleted file mode 100644 index 290ef64..0000000 --- a/kimi-commands/flowguard-gate.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: flowguard-gate -description: "旧十阶段兼容门禁;新任务使用 flowguard-governance 检查写码/提交/发布" ---- - -执行前确认已启用的 FlowGuard 插件根目录。若 Shell 环境未提供 `KIMI_PLUGIN_ROOT`,通过 `/plugins info flowguard` 定位安装目录并将下方路径改为绝对路径;不要在目标 Git 项目中猜测或执行同名脚本。 - -运行 flowguard 门禁自检: - -1. 运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" gate --json` 查看四类动作整体放行状态 -2. 对被阻塞的动作,逐条运行带 `--action <动作> --path <路径>` 的定向检查拿到诊断信封(severity/code/message/fix) -3. 把每个阻塞渲染成「动作 | code | 原因 | 解锁命令」表格 -4. 运行 `... validate --json` 附带产物完整性问题(格式/追溯矩阵/测试文件存在性/Tier2 技能缺失) - -只读操作:本命令绝不修改状态。被阻塞时给出最小解锁路径(通常 = 完成并验收某阶段),提醒用户 override 是留痕逃生口而非常规手段。 diff --git a/kimi-commands/flowguard-init.md b/kimi-commands/flowguard-init.md index 08206fc..181bec4 100644 --- a/kimi-commands/flowguard-init.md +++ b/kimi-commands/flowguard-init.md @@ -1,8 +1,8 @@ --- name: flowguard-init -description: "只创建 docs/project/ 中的项目级十阶段文档;不创建 .flowguard/" +description: "只创建 docs/project/ 中的项目级十阶段文档" --- 执行前确认已启用的 FlowGuard 插件根目录。若 Shell 环境未提供 `KIMI_PLUGIN_ROOT`,通过 `/plugins info flowguard` 定位安装目录并将下方路径改为绝对路径;不要在目标 Git 项目中猜测或执行同名脚本。 -先运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" discover --json` 做只读检查。若用户已要求初始化 FlowGuard 文档且目标仓库明确,运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" init --json`。只创建 docs/project/ 下的 02/07/10;功能文档由 context bind 创建。不得自动执行 specify init、openspec init 或 legacy-init。 +先运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" discover --json` 做只读检查。若用户已要求初始化 FlowGuard 文档且目标仓库明确,运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" init --json`。只创建 docs/project/ 下的 02/07/10;功能文档由 context bind 创建。不得自动执行 specify init 或 openspec init。 diff --git a/kimi-commands/flowguard-next.md b/kimi-commands/flowguard-next.md deleted file mode 100644 index c1827e0..0000000 --- a/kimi-commands/flowguard-next.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: flowguard-next -description: "由智能体判断十阶段的下一步,不由 Hook 自动推进" ---- - -执行前确认已启用的 FlowGuard 插件根目录。若 Shell 环境未提供 `KIMI_PLUGIN_ROOT`,通过 `/plugins info flowguard` 定位安装目录并将下方路径改为绝对路径;不要在目标 Git 项目中猜测或执行同名脚本。 - -运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" stage status --task-id --json`,读取 docs/ 中 01—10 状态和原生规格引用。智能体结合用户目标、依赖与现有产物决定当前阶段;读取对应 flowguard-* 阶段技能,补充文档和验证证据。用 `... flowguard_state.py stage advance --task-id --stage <01-requirements|...|10-release> --status in_progress --json` 标记开始。不要调用旧 next 命令推进新任务,不得代替用户验收。 diff --git a/kimi-commands/flowguard-override.md b/kimi-commands/flowguard-override.md deleted file mode 100644 index a79d0d6..0000000 --- a/kimi-commands/flowguard-override.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: flowguard-override -description: "旧十阶段兼容逃生通道;必须用户主动发起、填写理由并留痕" ---- - -执行前确认已启用的 FlowGuard 插件根目录。若 Shell 环境未提供 `KIMI_PLUGIN_ROOT`,通过 `/plugins info flowguard` 定位安装目录并将下方路径改为绝对路径;不要在目标 Git 项目中猜测或执行同名脚本。 - -这是硬门禁的唯一逃生口,**只能由用户主动发起**;如果 agent 是自己想跳过阶段,立即停止。 - -1. 与用户确认三件事:要跳过的阶段(--stage,缺省=当前功能首个未收敛阶段)、理由(必填)、作用于哪个功能(--feature,缺省=当前功能)或项目级 -2. 向用户复述后果:overridden 状态 + journal 留痕 + 后续审计可见 -3. 用户确认后运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" override --reason <理由> [--stage <阶段>] [--feature ] --json` -4. 运行 `... status --json` 报告跳过后的流程状态 - -禁止:用 override 替代正常验收流程;为用户编造理由;连续跳过多个阶段而不逐个留痕。 diff --git a/kimi-commands/flowguard-stage.md b/kimi-commands/flowguard-stage.md index bfc9a02..95fc7ad 100644 --- a/kimi-commands/flowguard-stage.md +++ b/kimi-commands/flowguard-stage.md @@ -5,4 +5,4 @@ description: "查看和推进 docs/ 中的十阶段文档" 执行前确认已启用的 FlowGuard 插件根目录。若 Shell 环境未提供 `KIMI_PLUGIN_ROOT`,通过 `/plugins info flowguard` 定位安装目录并将下方路径改为绝对路径;不要在目标 Git 项目中猜测或执行同名脚本。 -运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" stage status --task-id --json` 查看阶段。写入 docs/project/ 或 docs/features// 对应文档并自检后,可运行 `... flowguard_state.py stage advance --task-id --stage <阶段文件名> --status in_progress --json`;只有真实用户批准或可审计依据才可将阶段标记 accepted/inherited/skipped。不能用模型自述伪造批准;旧 .flowguard/ 仅是迁移输入。 +运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" stage status --task-id --json` 查看阶段。写入 docs/project/ 或 docs/features// 对应文档并自检后,可运行 `... flowguard_state.py stage advance --task-id --stage <阶段文件名> --status in_progress --json`;只有真实用户批准或可审计依据才可将阶段标记 accepted/inherited/skipped。不能用模型自述伪造批准。 diff --git a/kimi-commands/flowguard-status.md b/kimi-commands/flowguard-status.md deleted file mode 100644 index 634b63d..0000000 --- a/kimi-commands/flowguard-status.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: flowguard-status -description: "读取 docs/ 十阶段项目与功能状态" ---- - -执行前确认已启用的 FlowGuard 插件根目录。若 Shell 环境未提供 `KIMI_PLUGIN_ROOT`,通过 `/plugins info flowguard` 定位安装目录并将下方路径改为绝对路径;不要在目标 Git 项目中猜测或执行同名脚本。 - -运行 `python3 "${KIMI_PLUGIN_ROOT}/scripts/flowguard_state.py" stage status --task-id --json`。展示 01—10 阶段状态、文档路径、失效原因和下一步;项目级 02/07/10 从 docs/project/ 读取,功能级七阶段从 docs/features// 读取。不要用旧 status/current_feature 作为新任务的唯一事实源。 diff --git a/scripts/flowguard_lib/detect.py b/scripts/flowguard_lib/detect.py index 07b04b9..c94c85a 100644 --- a/scripts/flowguard_lib/detect.py +++ b/scripts/flowguard_lib/detect.py @@ -73,10 +73,3 @@ def _walk(base): yield Path(dirpath), dirnames, filenames -def init_project(root): - """建 .flowguard/ 骨架(实现在 templates/artifacts.py,此处委托保持单入口)。""" - try: - from ..templates.artifacts import init_project as _init - except ImportError: # pragma: no cover 脚本态 - from templates.artifacts import init_project as _init - return _init(root) diff --git a/scripts/flowguard_lib/discovery.py b/scripts/flowguard_lib/discovery.py index 963da1f..3b71195 100644 --- a/scripts/flowguard_lib/discovery.py +++ b/scripts/flowguard_lib/discovery.py @@ -100,7 +100,6 @@ def discover(start): "markers": markers, "artifacts": sorted(set(artifacts)), "conflicts": conflicts, - "legacy_flowguard": (root / ".flowguard" / "project.json").is_file(), }, "tools": { "specify_cli": bool(shutil.which("specify")), diff --git a/scripts/flowguard_lib/evidence.py b/scripts/flowguard_lib/evidence.py index 4b52664..f8b1ad8 100644 --- a/scripts/flowguard_lib/evidence.py +++ b/scripts/flowguard_lib/evidence.py @@ -85,7 +85,7 @@ def code_fingerprint(root, ctx): digest.update(head.stdout.strip().encode("utf-8")) diff = _git(root, "diff", "--binary", "HEAD", "--", ".", ":(exclude)docs/features", ":(exclude)docs/project", - ":(exclude)docs/legacy-flowguard", ":(exclude).flowguard", + ":(exclude)docs/legacy-flowguard", ":(exclude).specify", ":(exclude)openspec", ":(exclude)docs/superpowers") if diff.returncode: @@ -96,7 +96,7 @@ def code_fingerprint(root, ctx): raise GitStateError("无法读取 Git 未跟踪文件") for raw in sorted(item for item in others.stdout.split(b"\0") if item): rel = raw.decode("utf-8", errors="surrogateescape") - if rel.startswith(("docs/features/", "docs/project/", "docs/legacy-flowguard/", ".flowguard/", *NATIVE_SPEC_DIRS)): + if rel.startswith(("docs/features/", "docs/project/", "docs/legacy-flowguard/", *NATIVE_SPEC_DIRS)): continue digest.update(raw) path = root / rel @@ -110,7 +110,7 @@ def code_fingerprint(root, ctx): raise GitStateError("无法读取初始 Git 工作树文件") for raw in sorted(set(item for item in files.stdout.split(b"\0") if item)): rel = raw.decode("utf-8", errors="surrogateescape") - if rel.startswith(("docs/features/", "docs/project/", "docs/legacy-flowguard/", ".flowguard/", *NATIVE_SPEC_DIRS)): + if rel.startswith(("docs/features/", "docs/project/", "docs/legacy-flowguard/", *NATIVE_SPEC_DIRS)): continue digest.update(raw) path = root / rel diff --git a/scripts/flowguard_lib/gate.py b/scripts/flowguard_lib/gate.py deleted file mode 100644 index b786910..0000000 --- a/scripts/flowguard_lib/gate.py +++ /dev/null @@ -1,37 +0,0 @@ -"""门禁判定链入口:路径分类 + 动作解锁(spec §5)。 - -原则:未初始化的 .flowguard 一律放行(防误伤);门禁拒绝时产出诊断信封。 -""" -from . import registry, state - - -def _classify(project, path): - if not path: - return "other" - p = str(path).replace("\\", "/") - if p.startswith(".flowguard/") or p == ".flowguard": - return "artifact" - m = registry.match_module(project, p) - return f"module:{m}" if m else "other" - - -def classify_path(root, path): - try: - project = state.load_project(root) - except state.StateError: - return "other" - return _classify(project, path) - - -def check_action(root, action, *, path=None, feature=None): - """返回 {"allowed": bool, "envelope": dict|None};任何状态读取异常都放行(防误伤)。""" - try: - project = state.load_project(root) - fid = feature or project.get("current_feature") - fstate = state.load_feature(root, fid) if fid else None - except state.StateError: - return {"allowed": True, "envelope": None} - if action == "write_code" and _classify(project, path) == "artifact": - return {"allowed": True, "envelope": None} # 产物类放行(spec §5.3) - allowed, env = registry.unlock_check(action, project, fstate, path=path) - return {"allowed": allowed, "envelope": env} diff --git a/scripts/flowguard_lib/instructions.py b/scripts/flowguard_lib/instructions.py deleted file mode 100644 index 5aef35a..0000000 --- a/scripts/flowguard_lib/instructions.py +++ /dev/null @@ -1,51 +0,0 @@ -"""instructions 命令面:给编排技能的机读指令(context/rules 来自 config.yaml,照 OpenSpec)。""" -from pathlib import Path - -from . import registry, yamlmini - -_DEFAULT_CONFIG = {"context": "", "rules": {}} - - -def load_config(root): - p = Path(root) / ".flowguard" / "config.yaml" - if not p.exists(): - return dict(_DEFAULT_CONFIG) - data = yamlmini.load(p.read_text(encoding="utf-8")) - return {"context": data.get("context", ""), "rules": data.get("rules", {})} - - -def build(root, artifact_id, feature=None): - art = registry.ARTIFACTS.get(artifact_id) - if art is None: - raise KeyError(f"未知 artifact: {artifact_id}") - cfg = load_config(root) - stage = art["stage"] - template = f"skills/flowguard-{stage}/references/templates/{_TEMPLATE_NAMES[artifact_id]}" - tier2 = [(skill, pkg, registry.install_cmd(skill, pkg)) - for skill, pkg in registry.TIER2_REFS.get(stage, [])] - return { - "artifact": artifact_id, - "scope": art["scope"], - "stage": stage, - "context": cfg["context"], - "rules": cfg["rules"], - "template": template, - "requires": list(art["requires"]), - "unlocks": [nid for nid, a in registry.ARTIFACTS.items() if artifact_id in a["requires"]], - "tier2": tier2, - } - - -# artifact id → 产物文件名(模板文件与产物同名,便于机械定位) -_TEMPLATE_NAMES = { - "01-requirements": "01-requirements.md", - "02-architecture": "02-architecture.md", - "03-solution": "03-solution.md", - "04-testcases": "04-testcases.md", - "05-hld": "05-hld.md", - "06-lld": "06-lld.md", - "07-standards": "07-standards.md", - "08-review": "08-review.md", - "09-docs": "09-docs.md", - "10-release": "10-release.md", -} diff --git a/scripts/flowguard_lib/journal.py b/scripts/flowguard_lib/journal.py deleted file mode 100644 index d846e27..0000000 --- a/scripts/flowguard_lib/journal.py +++ /dev/null @@ -1,28 +0,0 @@ -"""追加式审计日志:.flowguard/journal/events.jsonl,每行 {ts, scope, event, detail}。""" -import datetime -import json -from pathlib import Path - - -def _file(root): - d = Path(root) / ".flowguard" / "journal" - d.mkdir(parents=True, exist_ok=True) - return d / "events.jsonl" - - -def append(root, scope, event, detail=None): - rec = { - "ts": datetime.datetime.now(datetime.timezone.utc).isoformat(), - "scope": scope, - "event": event, - "detail": detail or {}, - } - with _file(root).open("a", encoding="utf-8") as fh: - fh.write(json.dumps(rec, ensure_ascii=False) + "\n") - - -def read(root): - f = _file(root) - if not f.exists(): - return [] - return [json.loads(line) for line in f.read_text(encoding="utf-8").splitlines() if line.strip()] diff --git a/scripts/flowguard_lib/migration.py b/scripts/flowguard_lib/migration.py deleted file mode 100644 index 2553230..0000000 --- a/scripts/flowguard_lib/migration.py +++ /dev/null @@ -1,99 +0,0 @@ -"""把旧阶段正文复制到 docs/,保留旧数据以供核对。""" -import json -import re -import stat -from pathlib import Path - -from . import ids, registry, stage_docs, state - - -class MigrationError(Exception): - pass - - -def _legacy_status(root, task_id, stage): - if registry.ARTIFACTS[stage]["scope"] == "project": - path = Path(root) / ".flowguard" / "project.json" - else: - path = Path(root) / ".flowguard" / "features" / task_id / "state.json" - if not path.is_file(): - return "pending" - try: - data = json.loads(path.read_text(encoding="utf-8")) - return data.get("stages", {}).get(registry.ARTIFACTS[stage]["stage"], {}).get("status", "pending") - except (OSError, json.JSONDecodeError) as error: - raise MigrationError(f"旧状态不可读: {path}") from error - - -def preview(root): - root = Path(root).resolve() - legacy = root / ".flowguard" - entries = [] - for stage, art in registry.ARTIFACTS.items(): - if art["scope"] == "project": - sources = [("project", legacy / "project" / f"{stage}.md")] - else: - sources = [(path.name, path / "artifacts" / f"{stage}.md") - for path in sorted((legacy / "features").glob("*")) if path.is_dir()] - for task_id, source in sources: - if not source.is_file(): - continue - if not ids.is_kebab(task_id): - raise MigrationError(f"旧功能标识非法,需人工处理: {task_id}") - destination = stage_docs.path_for(root, task_id, stage) - entries.append({ - "source": str(source.relative_to(root)), - "destination": str(destination.relative_to(root)), - "task_id": task_id, "stage": stage, - "legacy_status": _legacy_status(root, task_id, stage), - }) - conflicts = [item["destination"] for item in entries if (root / item["destination"]).exists()] - return {"entries": entries, "conflicts": conflicts, "created": []} - - -def _with_metadata(text, item): - source = item["source"] - old = item["legacy_status"] - new_status = "in_progress" if old == "in_progress" else "pending_acceptance" if old in ( - "accepted", "skipped", "overridden", "pending_acceptance", - ) else "pending" - ctx = {"task_id": item["task_id"], "spec_system": "none", "spec_ref": None} - metadata = stage_docs._metadata(ctx, item["stage"]) - metadata = metadata.replace("| 阶段状态 | pending |", f"| 阶段状态 | {new_status} |") - metadata = metadata.replace("| 批准依据 | - |", f"| 批准依据 | 待复核;旧状态 {old} |") - metadata += f"| 迁移来源 | {source} |\n\n" - match = re.search(r"^## 2\.", text, flags=re.MULTILINE) - if match: - return text[:match.start()] + metadata + text[match.start():] - return text.rstrip() + "\n\n" + metadata - - -def apply(root): - with state.state_lock(root): - return _apply_unlocked(root) - - -def _apply_unlocked(root): - root = Path(root).resolve() - plan = preview(root) - if plan["conflicts"]: - raise MigrationError("目标文档冲突,未写入任何文件: " + ", ".join(plan["conflicts"])) - created = [] - try: - for item in plan["entries"]: - source = root / item["source"] - target = root / item["destination"] - target.parent.mkdir(parents=True, exist_ok=True) - text = _with_metadata(source.read_text(encoding="utf-8"), item) - state.atomic_create_text( - target, text, mode=stat.S_IMODE(source.stat().st_mode), - ) - created.append(target) - except Exception as error: - kept = [str(target.relative_to(root)) for target in created] - detail = "、".join(kept) if kept else "无" - raise MigrationError( - f"迁移中断;已创建文档保留供核对: {detail};未移除旧数据。原因: {error}" - ) from error - plan["created"] = [str(target.relative_to(root)) for target in created] - return plan diff --git a/scripts/flowguard_lib/registry.py b/scripts/flowguard_lib/registry.py index 6220c58..775055f 100644 --- a/scripts/flowguard_lib/registry.py +++ b/scripts/flowguard_lib/registry.py @@ -1,43 +1,30 @@ -"""阶段/artifact DAG 注册表 + 动作解锁表(十阶段,spec v3.1 §2.1/§3.1/§5.1)。 +"""阶段/artifact DAG 注册表(十阶段,spec v3.1 §2.1/§3.1)。 -跨 scope requires 语义(台账裁定):项目级 artifact 依赖功能级 artifact 时, -「所有 active 功能的对应 artifact 均 done|skipped|overridden」即满足;无 active 功能时空满足。 +requires 语义:项目级 artifact 依赖功能级 artifact 时,前置满足以 docs/ +阶段文档状态为准(stage_docs 按声明序检查全部前置)。 """ -from pathlib import Path - -from .diag import envelope from .state import StateError # noqa: F401 (re-export 供测试/调用方) ARTIFACTS = { "01-requirements": {"scope": "feature", "stage": "requirements", - "rel_tpl": "features/{feature}/artifacts/01-requirements.md", "requires": ()}, "02-architecture": {"scope": "project", "stage": "architecture", - "rel_tpl": "project/02-architecture.md", "requires": ()}, "03-solution": {"scope": "feature", "stage": "solution", - "rel_tpl": "features/{feature}/artifacts/03-solution.md", "requires": ("01-requirements", "02-architecture")}, "04-testcases": {"scope": "feature", "stage": "testcases", - "rel_tpl": "features/{feature}/artifacts/04-testcases.md", "requires": ("03-solution",)}, "05-hld": {"scope": "feature", "stage": "hld", - "rel_tpl": "features/{feature}/artifacts/05-hld.md", "requires": ("04-testcases",)}, "06-lld": {"scope": "feature", "stage": "lld", - "rel_tpl": "features/{feature}/artifacts/06-lld.md", "requires": ("05-hld",)}, "07-standards": {"scope": "project", "stage": "standards", - "rel_tpl": "project/07-standards.md", "requires": ()}, "08-review": {"scope": "feature", "stage": "review", - "rel_tpl": "features/{feature}/artifacts/08-review.md", "requires": ("06-lld", "07-standards")}, "09-docs": {"scope": "feature", "stage": "docs", - "rel_tpl": "features/{feature}/artifacts/09-docs.md", "requires": ("08-review",)}, "10-release": {"scope": "project", "stage": "release", - "rel_tpl": "project/10-release.md", "requires": ("07-standards", "09-docs")}, } @@ -60,8 +47,6 @@ "release": [("easy4j-deploy", "java-skills"), ("fw-release-gate", "firmware-skills")], } -OK_STATUSES = ("accepted", "skipped", "overridden") - # 发布命令的兼容文本模式;直接 CLI 的带选项调用由 Hook 词法分类补充。 RELEASE_CMD_PATTERNS = ( "mvn deploy", "mvn release", "gradle publish", "gradle release", @@ -123,134 +108,3 @@ def topo_order(): if indeg[d] == 0: ready.append(d) return out - - -def _owner_stages(aid, project, feature): - art = ARTIFACTS[aid] - if art["scope"] == "project": - return project["stages"] - return feature["stages"] if feature else {} - - -def _artifact_file(aid, project, feature, root): - art = ARTIFACTS[aid] - rel = art["rel_tpl"].format(feature=(feature or {}).get("feature", "_")) - return Path(root) / ".flowguard" / rel - - -def artifact_status(aid, project, feature, root=None): - """DAG 派生子状态:done|skipped|ready|blocked(+ missing deps)。""" - art = ARTIFACTS[aid] - st = _owner_stages(aid, project, feature).get(art["stage"], {}).get("status", "pending") - if st in ("skipped", "overridden"): - return "skipped", [] - if root is not None and _artifact_file(aid, project, feature, root).exists(): - return "done", [] - missing = [] - for dep in art["requires"]: - if not _dep_ok(dep, project, feature, root): - missing.append(dep) - return ("ready", []) if not missing else ("blocked", missing) - - -def _dep_ok(dep, project, feature, root): - dep_art = ARTIFACTS[dep] - if dep_art["scope"] == "project" or feature is not None: - stages = _owner_stages(dep, project, feature) - if stages.get(dep_art["stage"], {}).get("status") in OK_STATUSES: - return True - st, _ = artifact_status(dep, project, feature, root) - return st in ("done", "skipped") - # 项目级 artifact 依赖功能级 artifact:所有 active 功能均满足才放行(台账裁定) - active = {fid: f for fid, f in project.get("features", {}).items() if f.get("status") == "active"} - if not active: - return True - for fid in active: - f = {"feature": fid, "stages": _load_active_feature_stages(project, fid)} - st, _ = artifact_status(dep, project, f, root) - if st not in ("done", "skipped"): - return False - return True - - -def _load_active_feature_stages(project, fid): - """活动功能的阶段状态直接取索引;完整 state.json 由调用方持有(此处尽力而为)。""" - # project.json 不冗余存 stages;若需精确状态,调用方应传 feature。此处按文件存在性判定由 - # artifact_status(root) 完成;无 root 时保守视为未满足。 - return {} - - -def match_module(project, path): - """路径 → 模块名;不在任何 src_roots 返回 None。src_roots 为 "." 表示整仓。""" - if not path: - return None - p = str(path).replace("\\", "/") - for name, mod in (project.get("modules") or {}).items(): - for sr in mod.get("src_roots", []): - if sr == ".": - return name - if p == sr or p.startswith(sr.rstrip("/") + "/"): - return name - return None - - -def unlock_check(action, project, feature, path=None): - """动作解锁表(spec §5.1)。返回 (allowed, envelope|None)。""" - if action == "write_code": - m = match_module(project, path) - if m is None: - return True, None - if not feature or feature.get("status") != "active": - return False, envelope("ERROR", "gate_write_code_no_module", - f"路径属于模块 {m},但当前无 active 功能上下文", - "用 /flowguard-next --feature X 进入功能,或 /flowguard-feature new 声明涉及模块") - if m not in (feature.get("modules") or []): - return False, envelope("ERROR", "gate_write_code_feature_mismatch", - f"模块 {m} 不属于当前功能 {feature.get('feature')}", - f"在功能 {feature.get('feature')} 中补声明模块 {m},或切换 current_feature") - notok = [s for s in ("requirements", "solution", "testcases", "hld", "lld") - if feature["stages"].get(s, {}).get("status") not in OK_STATUSES] - if notok: - return False, envelope("ERROR", "gate_write_code_tdd", - f"写码前置阶段未验收: {', '.join(notok)}", - f"先完成并验收 {notok[0]}(/flowguard-advance --feature {feature.get('feature')})") - std = project["stages"].get("standards", {}).get("status", "pending") - if std == "pending": - return False, envelope("ERROR", "gate_write_code_no_standards", - "项目编码规范(07-standards)尚未生成", - "先推进 standards 阶段生成规范集") - return True, None - - if action == "write_review": - notok = [s for s in ("testcases", "hld", "lld") - if feature["stages"].get(s, {}).get("status") not in OK_STATUSES] - if notok: - return False, envelope("ERROR", "gate_review_needs_testcases", - f"审查前置阶段未验收: {', '.join(notok)}", - f"先验收 {notok[0]}") - if project["stages"].get("standards", {}).get("status") not in OK_STATUSES: - return False, envelope("ERROR", "gate_review_needs_standards", - "项目编码规范未验收,审查缺少规范依据", - "先验收 07-standards") - return True, None - - if action == "write_docs": - if feature["stages"].get("review", {}).get("status") not in OK_STATUSES: - return False, envelope("ERROR", "gate_docs_needs_review", - "代码审查未验收,文档阶段未解锁", - "先完成并验收 08-review") - return True, None - - if action == "build_release": - if project["stages"].get("standards", {}).get("status") not in OK_STATUSES: - return False, envelope("ERROR", "gate_review_needs_standards", - "项目编码规范未验收,不可发布", - "先验收 07-standards") - active = [fid for fid, f in (project.get("features") or {}).items() if f.get("status") == "active"] - if active: - return False, envelope("ERROR", "gate_release_active_features", - f"仍有 active 功能: {', '.join(active)}", - "将各功能 done(/flowguard-advance)或 drop(留痕)后交付") - return True, None - - raise ValueError(f"未知动作: {action}") diff --git a/scripts/flowguard_lib/state.py b/scripts/flowguard_lib/state.py index 43014f6..53d75c3 100644 --- a/scripts/flowguard_lib/state.py +++ b/scripts/flowguard_lib/state.py @@ -1,38 +1,11 @@ -"""三级结构状态机:project.json + features//state.json,合法迁移表单源。""" +"""状态锁与原子写工具;项目流程事实保存在 docs/(十阶段文档为唯一事实源)。""" import contextlib -import datetime import fcntl -import json import os import pathlib import stat import tempfile -STAGE_STATUSES = ("pending", "in_progress", "pending_acceptance", "accepted", "skipped", "overridden") -FEATURE_STATUSES = ("active", "done", "dropped") - -# 合法迁移(唯一事实源;测试做全矩阵断言) -LEGAL = { - "pending": ("in_progress",), - "in_progress": ("pending_acceptance",), - "pending_acceptance": ("accepted", "skipped", "in_progress"), - "accepted": ("in_progress",), - "skipped": ("in_progress",), - "overridden": ("in_progress",), -} - -# 下游依赖:某阶段产物回改时须一并降级的阶段(spec §4.4 降级瀑布) -DOWNSTREAM = { - "requirements": ("testcases", "review", "docs"), - "solution": ("testcases", "review", "docs"), - "testcases": ("review", "docs"), - "hld": ("lld", "review", "docs"), - "lld": ("review", "docs"), - "review": ("docs",), - "architecture": ("requirements", "solution", "testcases", "hld", "lld", "review", "docs"), - "standards": ("review", "docs"), -} - class StateError(Exception): pass @@ -56,21 +29,6 @@ def state_lock(root): fh.close() -def _flowguard_dir(root, *, create=False): - d = pathlib.Path(root) / ".flowguard" - if create: - d.mkdir(parents=True, exist_ok=True) - return d - - -def _atomic_write(path, data): - path.parent.mkdir(parents=True, exist_ok=True) - fd, tmp = tempfile.mkstemp(dir=str(path.parent)) - with os.fdopen(fd, "w", encoding="utf-8") as fh: - json.dump(data, fh, ensure_ascii=False, indent=2) - os.replace(tmp, path) - - def atomic_write_text(path, content): """同目录临时文件替换文档;写入或替换失败时保留旧正文。""" path = pathlib.Path(path) @@ -93,7 +51,7 @@ def atomic_create_text(path, content, *, mode=None): """原子创建新文档,不覆盖并发创建的目标,也不留下半写文件。""" path = pathlib.Path(path) path.parent.mkdir(parents=True, exist_ok=True) - fd, temporary = tempfile.mkstemp(prefix=".flowguard-", dir=str(path.parent)) + fd, temporary = tempfile.mkstemp(prefix=".fg-atomic-", dir=str(path.parent)) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: handle.write(content) @@ -104,64 +62,3 @@ def atomic_create_text(path, content, *, mode=None): os.link(temporary, path) finally: pathlib.Path(temporary).unlink(missing_ok=True) - - -def load_project(root): - p = _flowguard_dir(root) / "project.json" - if not p.exists(): - raise StateError("缺少 .flowguard/project.json,请先运行 /flowguard-init") - return json.loads(p.read_text(encoding="utf-8")) - - -def save_project(root, data): - _atomic_write(_flowguard_dir(root, create=True) / "project.json", data) - - -def _feature_path(root, feature_id, *, create=False): - return _flowguard_dir(root, create=create) / "features" / feature_id / "state.json" - - -def load_feature(root, feature_id): - p = _feature_path(root, feature_id) - if not p.exists(): - raise StateError(f"功能不存在: {feature_id}") - return json.loads(p.read_text(encoding="utf-8")) - - -def save_feature(root, data): - _atomic_write(_feature_path(root, data["feature"], create=True), data) - - -def transition_stage(owner, stage, target, *, reason="", evidence=None): - """单一状态迁移入口;非法迁移/缺失理由抛 StateError。""" - cur = owner["stages"][stage]["status"] - if target not in LEGAL.get(cur, ()): - raise StateError(f"非法状态迁移 {cur} → {target}(合法目标: {LEGAL.get(cur, ())})") - if target in ("skipped", "overridden") and not reason: - raise StateError(f"{target} 必须填写理由") - owner["stages"][stage]["status"] = target - if reason: - owner["stages"][stage]["reason"] = reason - if target == "accepted": - owner["stages"][stage]["accepted_at"] = datetime.datetime.now(datetime.timezone.utc).isoformat() - if evidence: - owner["stages"][stage]["evidence"] = evidence - - -def transition_override(owner, stage, *, reason): - """override 逃生口:任意态 → overridden(spec §4.4),必须用户发起 + 理由。""" - if not reason: - raise StateError("override 必须填写理由") - owner["stages"][stage]["status"] = "overridden" - owner["stages"][stage]["reason"] = reason - - -def degrade_from(owner, stage): - """stage 及其下游中已 accepted/pending_acceptance 的阶段降回 in_progress,返回被降级列表。""" - chain = [stage] + [s for s in DOWNSTREAM.get(stage, ()) if s in owner["stages"]] - hit = [] - for s in chain: - if owner["stages"][s]["status"] in ("accepted", "pending_acceptance"): - owner["stages"][s]["status"] = "in_progress" - hit.append(s) - return hit diff --git a/scripts/flowguard_lib/validation.py b/scripts/flowguard_lib/validation.py index fc97912..5f73f14 100644 --- a/scripts/flowguard_lib/validation.py +++ b/scripts/flowguard_lib/validation.py @@ -37,15 +37,16 @@ def _mask_code_fences(lines): def requirement_ids(text): - """抽取各 requirement 正文首行反引号内的 REQ-ID。""" + """抽取各 requirement 正文首行反引号内的 REQ-ID;含占位符的模板示例块不参与机械校验。""" lines = text.splitlines() masked = _mask_code_fences(lines) - found, current_req = [], False + found, current_req, placeholder = [], False, False for ln, hide in zip(lines, masked): if hide: continue if re.match(r"^### Requirement:", ln): current_req, body_seen = True, False + placeholder = ("<" in ln) or ("{{" in ln) continue if current_req: if re.match(r"^#### Scenario:", ln): @@ -53,8 +54,10 @@ def requirement_ids(text): continue if not body_seen and ln.strip(): body_seen = True + if "<" in ln or "{{" in ln: + placeholder = True m = re.search(r"`([^`]+)`", ln) - if m and ids.parse_req_id(m.group(1)): + if m and ids.parse_req_id(m.group(1)) and not placeholder: found.append(m.group(1)) return found @@ -64,22 +67,24 @@ def validate_requirements(text, feature): masked = _mask_code_fences(lines) issues = [] seen_names = {} - in_req, req_name, req_line, body_lines, scenario_count = False, "", 0, [], 0 + in_req, req_name, req_line, body_lines, scenario_count, placeholder = False, "", 0, [], 0, False def flush(): - nonlocal in_req, req_name, req_line, body_lines, scenario_count + nonlocal in_req, req_name, req_line, body_lines, scenario_count, placeholder if not in_req: return - body = "\n".join(body_lines) - if not re.search(r"\b(SHALL|MUST)\b", body): - issues.append(_issue("WARNING", "01-requirements.md", - f"requirement「{req_name}」正文缺 SHALL/MUST", - HINTS["shall"], line=req_line)) - if scenario_count < 1: - issues.append(_issue("WARNING", "01-requirements.md", - f"requirement「{req_name}」无 Scenario", - HINTS["scenario_count"], line=req_line)) + if not placeholder: + body = "\n".join(body_lines) + if not re.search(r"\b(SHALL|MUST)\b", body): + issues.append(_issue("WARNING", "01-requirements.md", + f"requirement「{req_name}」正文缺 SHALL/MUST", + HINTS["shall"], line=req_line)) + if scenario_count < 1: + issues.append(_issue("WARNING", "01-requirements.md", + f"requirement「{req_name}」无 Scenario", + HINTS["scenario_count"], line=req_line)) in_req = False + placeholder = False for no, (ln, hide) in enumerate(zip(lines, masked), 1): if hide: @@ -92,6 +97,7 @@ def flush(): req_name = m_req.group(1).strip() req_line = no in_req, body_lines, scenario_count = True, [], 0 + placeholder = ("<" in ln) or ("{{" in ln) folded = ids.fold_name(req_name) if folded in seen_names: issues.append(_issue("WARNING", "01-requirements.md", @@ -115,6 +121,8 @@ def flush(): HINTS["scenario_header"], line=no)) continue if in_req: + if "<" in ln or "{{" in ln: + placeholder = True body_lines.append(ln) flush() diff --git a/scripts/flowguard_lib/yamlmini.py b/scripts/flowguard_lib/yamlmini.py deleted file mode 100644 index 3a4e5e4..0000000 --- a/scripts/flowguard_lib/yamlmini.py +++ /dev/null @@ -1,112 +0,0 @@ -"""config.yaml 子集解析器(stdlib-only,不引 PyYAML)。 - -支持的形状(超出即 ValueError): -- 顶层标量:`key: value` -- 顶层字面块:`key: |` + 两空格缩进行 -- 两层嵌套映射:`key:` 空值,随后两空格 `subkey: value` -- 列表:顶层 `key:` + 两空格 `- item`;或 `key:` + `subkey:` + 四空格 `- item` -""" - - -def load(text): - root = {} - cur_top = None # 当前顶层键 - cur_sub = None # 当前两层映射的子键 - block_key = None # 字面块收集中的顶层键 - block_lines = [] - - def _end_block(): - nonlocal block_key - if block_key is not None: - root[block_key] = "\n".join(block_lines).rstrip("\n") - block_key = None - - for lineno, raw in enumerate(text.splitlines(), 1): - line = raw.rstrip("\n") - stripped = line.strip() - indent = len(line) - len(line.lstrip(" ")) - - if stripped == "" or stripped.startswith("#"): - if block_key is not None: - block_lines.append("") - continue - - if block_key is not None: - if indent >= 2: - block_lines.append(line[2:]) - continue - _end_block() # 缩进回 0:块结束,本行继续按常规解析 - - if stripped.startswith("- "): - item = stripped[2:].strip().strip('"') - if indent == 4 and cur_top is not None and cur_sub is not None: - slot = root[cur_top].get(cur_sub) - if slot is None or slot == {}: - slot = [] - elif not isinstance(slot, list): - raise ValueError(f"unsupported yaml shape at line {lineno}: {raw!r}") - slot.append(item) - root[cur_top][cur_sub] = slot - elif indent == 2 and cur_top is not None and cur_sub is None: - slot = root.get(cur_top) - if slot is None or slot == {}: - slot = [] - elif not isinstance(slot, list): - raise ValueError(f"unsupported yaml shape at line {lineno}: {raw!r}") - slot.append(item) - root[cur_top] = slot - else: - raise ValueError(f"unsupported yaml shape at line {lineno}: {raw!r}") - continue - - key, sep, val = stripped.partition(":") - if not sep: - raise ValueError(f"unsupported yaml shape at line {lineno}: {raw!r}") - key, val = key.strip(), val.strip() - - if indent == 0: - cur_top, cur_sub = key, None - if val == "|": - block_key, block_lines = key, [] - root[key] = "" # 块结束时覆盖 - elif val == "": - root[key] = {} # 占位:后续按列表/映射实化 - else: - root[key] = val.strip('"') - elif indent == 2: - if not isinstance(root.get(cur_top), dict): - raise ValueError(f"unsupported yaml shape at line {lineno}: {raw!r}") - if val == "|": - raise ValueError(f"unsupported yaml shape at line {lineno}: 字面块仅支持顶层") - cur_sub = key - if val == "": - root[cur_top].setdefault(key, {}) - else: - root[cur_top][key] = val.strip('"') - else: - raise ValueError(f"unsupported yaml shape at line {lineno}: {raw!r}") - - _end_block() - return root - - -def dump(data): - lines = [] - for key, val in data.items(): - if isinstance(val, str) and "\n" in val: - lines.append(f"{key}: |") - lines.extend(f" {ln}" for ln in val.splitlines()) - elif isinstance(val, dict): - lines.append(f"{key}:") - for sub, sval in val.items(): - if isinstance(sval, list): - lines.append(f" {sub}:") - lines.extend(f" - {item}" for item in sval) - else: - lines.append(f" {sub}: {sval}") - elif isinstance(val, list): - lines.append(f"{key}:") - lines.extend(f" - {item}" for item in val) - else: - lines.append(f"{key}: {val}") - return "\n".join(lines) + "\n" diff --git a/scripts/flowguard_state.py b/scripts/flowguard_state.py index 40f9c2a..5e42eb6 100644 --- a/scripts/flowguard_state.py +++ b/scripts/flowguard_state.py @@ -11,16 +11,10 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) from flowguard_lib import ( # noqa: E402 - context, detect, discovery, evidence, gate, governance, ids, instructions, - journal, migration, registry, stage_docs, state, validation, + context, discovery, evidence, governance, registry, + stage_docs, state, validation, ) from flowguard_lib.diag import emit as emit_diag, envelope as mk_env # noqa: E402 -from templates import artifacts # noqa: E402 - -FEATURE_STAGES = ("requirements", "solution", "testcases", "hld", "lld", "review", "docs") -PROJECT_STAGES = ("architecture", "standards", "release") -OK_STATUSES = ("accepted", "skipped", "overridden") -FEATURE_ARTIFACTS = [aid for aid, a in registry.ARTIFACTS.items() if a["scope"] == "feature"] class Parser(argparse.ArgumentParser): @@ -57,28 +51,12 @@ def _read(path): return "" -def _artifact_path(root, rel): - return root / ".flowguard" / rel - - def cmd_init(args): root = Path.cwd() created = stage_docs.ensure_project(root) _out({"initialized": True, "root": str(root), "created": created}, args.json) -def cmd_legacy_init(args): - root = Path.cwd() - if not (root / ".flowguard").is_dir(): - _die(mk_env("ERROR", "legacy_state_required", - "新项目不创建 .flowguard/;legacy-init 仅用于已有旧项目", - "运行 discover/context bind 创建 docs/ 阶段文档;旧项目先运行 migrate --dry-run")) - project = artifacts.init_project(root) - journal.append(root, "project", "init", {"modules": list(project["modules"])}) - _out({"initialized": True, "project": project["project"], - "modules": list(project["modules"]), "stack": project["stack"]}, args.json) - - def cmd_discover(args): """只读发现 Git、原生 SDD 标识、CLI 可用性和冲突。""" _out(discovery.discover(Path.cwd()), args.json) @@ -159,241 +137,35 @@ def cmd_stage(args): _out(data, args.json) -def cmd_migrate(args): - data = migration.apply(Path.cwd()) if args.apply else migration.preview(Path.cwd()) - _out(data, args.json) - - -def cmd_status(args): - root = Path.cwd() - project = state.load_project(root) - scope = list((project.get("features") or {}).keys()) - if getattr(args, "feature", None): - if args.feature not in scope: - _die(mk_env("ERROR", "unknown_feature", f"功能不存在: {args.feature}", - "用 /flowguard-feature list 查看功能清单")) - scope = [args.feature] - out = {"project": project["project"], "current_feature": project.get("current_feature"), - "project_stages": {s: project["stages"][s]["status"] for s in PROJECT_STAGES}, - "features": {}} - for fid in scope: - try: - f = state.load_feature(root, fid) - except state.StateError: - continue - req_text = _read(_artifact_path( - root, f"features/{fid}/" + f["stages"]["requirements"].get("artifact", "artifacts/01-requirements.md"))) - reqs = validation.requirement_ids(req_text) - tc_text = _read(_artifact_path( - root, f"features/{fid}/" + f["stages"]["testcases"].get("artifact", "artifacts/04-testcases.md"))) - cov = sum(1 for r in reqs if f"- REQ: {r}" in tc_text) - out["features"][fid] = {"status": f.get("status"), "modules": f.get("modules"), - "stages": {s: f["stages"][s]["status"] for s in FEATURE_STAGES}, - "req_total": len(reqs), "req_covered": cov} - _out(out, args.json) - - -def cmd_feature(args): - root = Path.cwd() - project = state.load_project(root) - if args.action == "new": - if not ids.is_kebab(args.id): - _die(mk_env("ERROR", "bad_feature_id", f"功能 id 非法: {args.id}", ids.KEBAB_FIX)) - unknown = [m for m in args.modules if m not in project["modules"]] - if unknown: - _die(mk_env("ERROR", "unknown_module", f"模块未注册: {', '.join(unknown)}", - "先在 project.json.modules 登记(/flowguard-init 自动探测)")) - if args.id in (project.get("features") or {}): - _die(mk_env("ERROR", "feature_exists", f"功能已存在: {args.id}", "换一个 id")) - fdir = f"features/{args.id}" - (root / ".flowguard" / fdir / "artifacts").mkdir(parents=True, exist_ok=True) - stages = {s: {"status": "pending"} for s in FEATURE_STAGES} - for aid in FEATURE_ARTIFACTS: - rel = registry.ARTIFACTS[aid]["rel_tpl"].format(feature=args.id) - stages[registry.ARTIFACTS[aid]["stage"]]["artifact"] = rel.removeprefix(f"{fdir}/") - p = _artifact_path(root, rel) - if not p.exists(): - p.parent.mkdir(parents=True, exist_ok=True) - p.write_text(artifacts.render(aid, { - "feature": args.id, "title": args.title or args.id, - "modules": ",".join(args.modules), - "req_prefix": "/REQ"}), encoding="utf-8") - feature = {"version": 1, "feature": args.id, "title": args.title or args.id, - "modules": args.modules, "status": "active", "stages": stages} - state.save_feature(root, feature) - project.setdefault("features", {})[args.id] = {"status": "active", "path": fdir} - if not project.get("current_feature"): - project["current_feature"] = args.id - state.save_project(root, project) - journal.append(root, f"feature:{args.id}", "feature_new", {"modules": args.modules}) - _out({"feature": args.id, "status": "active", "artifacts": len(FEATURE_ARTIFACTS)}, args.json) - elif args.action == "list": - _out(project.get("features") or {}, args.json) - elif args.action in ("done", "drop"): - f = state.load_feature(root, args.id) - if args.action == "drop": - if not getattr(args, "reason", ""): - _die(mk_env("ERROR", "reason_required", "drop 必须填写理由", "--reason <理由>")) - f["status"] = "dropped" - f["drop_reason"] = args.reason - else: - notok = [s for s in FEATURE_STAGES if f["stages"][s]["status"] not in OK_STATUSES] - if notok: - _die(mk_env("ERROR", "feature_not_done", f"未验收阶段: {', '.join(notok)}", - f"先验收 {notok[0]}(/flowguard-advance --feature {args.id})")) - f["status"] = "done" - state.save_feature(root, f) - project["features"][args.id]["status"] = f["status"] - state.save_project(root, project) - journal.append(root, f"feature:{args.id}", f"feature_{args.action}", - {"reason": getattr(args, "reason", "")}) - _out({"feature": args.id, "status": f["status"]}, args.json) - - -def cmd_next(args): - root = Path.cwd() - project = state.load_project(root) - fid = args.feature or project.get("current_feature") - if fid and args.stage in (None, *FEATURE_STAGES): - f = state.load_feature(root, fid) - if f.get("status") != "active": - _die(mk_env("ERROR", "feature_inactive", f"功能 {fid} 状态 {f.get('status')}", - "选择 active 功能或新建功能")) - project["current_feature"] = fid - state.save_project(root, project) - stage = next((s for s in FEATURE_STAGES if f["stages"][s]["status"] == "pending"), None) - if stage: - state.transition_stage(f, stage, "in_progress") - state.save_feature(root, f) - aid = next((a for a in FEATURE_ARTIFACTS if registry.ARTIFACTS[a]["stage"] == stage), - "01-requirements") - ins = instructions.build(root, aid, feature=fid) - journal.append(root, f"feature:{fid}", "next", {"stage": stage}) - _out({"feature": fid, "stage": stage, "instructions": ins}, args.json) - elif args.stage: - if args.stage not in PROJECT_STAGES: - _die(mk_env("ERROR", "bad_stage", f"项目级阶段非法: {args.stage}", f"可选 {PROJECT_STAGES}")) - if project["stages"][args.stage]["status"] == "pending": - state.transition_stage(project, args.stage, "in_progress") - state.save_project(root, project) - aid = next(a for a, v in registry.ARTIFACTS.items() if v["stage"] == args.stage) - journal.append(root, "project", "next", {"stage": args.stage}) - _out({"stage": args.stage, "instructions": instructions.build(root, aid)}, args.json) - else: - _die(mk_env("ERROR", "usage", "next 需要 --feature 或 --stage", "见 --help")) - - -def cmd_advance(args): - """用户验收入口:accepted 的唯一写入点。""" - root = Path.cwd() - project = state.load_project(root) - with state.state_lock(root): - fid = args.feature or project.get("current_feature") - use_feature = bool(fid) and args.stage in (None, *FEATURE_STAGES) - if use_feature: - f = state.load_feature(root, fid) - stage = args.stage or next( - (s for s in FEATURE_STAGES - if f["stages"][s]["status"] in ("in_progress", "pending_acceptance")), None) - if stage is None: - nxt = next((s for s in FEATURE_STAGES if f["stages"][s]["status"] == "pending"), None) - _die(mk_env("ERROR", "stage_not_started", - f"无可验收阶段(下一阶段 {nxt} 尚未开始)", - f"先用 /flowguard-next --feature {fid} 开始")) - if f["stages"][stage]["status"] == "pending": - _die(mk_env("ERROR", "stage_not_started", f"阶段 {stage} 尚未开始", - "先用 /flowguard-next 开始该阶段")) - if f["stages"][stage]["status"] == "in_progress": - state.transition_stage(f, stage, "pending_acceptance") - state.transition_stage(f, stage, "accepted", reason="用户验收", evidence=args.evidence) - feature_done = all(f["stages"][s]["status"] in OK_STATUSES for s in FEATURE_STAGES) - if feature_done: - f["status"] = "done" - project["features"][fid]["status"] = "done" - state.save_feature(root, f) - state.save_project(root, project) - journal.append(root, f"feature:{fid}", "accepted_by_user", {"stage": stage}) - _out({"feature": fid, "stage": stage, "status": "accepted", - "feature_status": f["status"]}, args.json) - else: - stage = args.stage or next( - (s for s in PROJECT_STAGES - if project["stages"][s]["status"] in ("in_progress", "pending_acceptance")), None) - if stage is None or project["stages"][stage]["status"] == "pending": - _die(mk_env("ERROR", "stage_not_started", f"项目级阶段 {stage} 尚未开始", - "先用 /flowguard-next --stage X 开始")) - if project["stages"][stage]["status"] == "in_progress": - state.transition_stage(project, stage, "pending_acceptance") - state.transition_stage(project, stage, "accepted", - reason="用户验收", evidence=args.evidence) - state.save_project(root, project) - journal.append(root, "project", "accepted_by_user", {"stage": stage}) - _out({"stage": stage, "status": "accepted"}, args.json) - - -def cmd_override(args): - root = Path.cwd() - project = state.load_project(root) - with state.state_lock(root): - fid = args.feature or project.get("current_feature") - use_feature = bool(fid) and args.stage in (None, *FEATURE_STAGES) - if use_feature: - f = state.load_feature(root, fid) - stage = args.stage or next( - (s for s in FEATURE_STAGES if f["stages"][s]["status"] not in OK_STATUSES), None) - if stage is None: - _die(mk_env("ERROR", "usage", "功能所有阶段已收敛", "无需 override")) - state.transition_override(f, stage, reason=args.reason) - state.save_feature(root, f) - journal.append(root, f"feature:{fid}", "override", - {"stage": stage, "reason": args.reason}) - _out({"feature": fid, "stage": stage, "status": "overridden"}, args.json) - else: - stage = args.stage or next( - (s for s in PROJECT_STAGES if project["stages"][s]["status"] not in OK_STATUSES), None) - if stage is None: - _die(mk_env("ERROR", "usage", "项目所有阶段已收敛", "无需 override")) - state.transition_override(project["stages"], stage, reason=args.reason) - state.save_project(root, project) - journal.append(root, "project", "override", {"stage": stage, "reason": args.reason}) - _out({"stage": stage, "status": "overridden"}, args.json) - - -def cmd_gate(args): - root = Path.cwd() - if args.action: - res = gate.check_action(root, args.action, path=args.path, feature=args.feature) - if res["allowed"]: - _out({"allowed": True, "action": args.action}, args.json) - return - _die(res["envelope"], code=2, as_json=args.json) - project = state.load_project(root) - checks = {} - for action in ("write_code", "write_review", "write_docs", "build_release"): - res = gate.check_action(root, action, path=args.path, feature=args.feature) - checks[action] = res["allowed"] - _out({"project": project["project"], "current_feature": project.get("current_feature"), - "actions": checks}, args.json) - - def cmd_validate(args): + """产物内容校验(REQ/追溯/结论)+ 下一阶段 Tier2 安装检查;范围 docs/features//。""" root = Path.cwd() - project = state.load_project(root) + tasks = stage_docs.recoverable(root)["tasks"] + if args.task_id: + tasks = [t for t in tasks if t.get("task_id") == args.task_id] + if not tasks: + _die(mk_env("ERROR", "unknown_task", f"功能文档不存在: {args.task_id}", + "先用 context bind 创建 docs/features//"), as_json=args.json) issues = [] - scope = [args.feature] if args.feature else list((project.get("features") or {}).keys()) - for fid in scope: - f = state.load_feature(root, fid) - adir = root / ".flowguard" / "features" / fid / "artifacts" - req_text = _read(adir / "01-requirements.md") + for task in tasks: + task_id = task["task_id"] + if task.get("error"): + issues.append({"level": "ERROR", "path": f"docs/features/{task_id}", + "message": task["error"], "fix": "修复阶段文档元信息表"}) + continue + req_text = _read(stage_docs.path_for(root, task_id, "01-requirements")) reqs = validation.requirement_ids(req_text) - issues += validation.validate_requirements(req_text, fid) - issues += validation.validate_testcases(_read(adir / "04-testcases.md"), reqs, root=root) - issues += validation.validate_review(_read(adir / "08-review.md")) - stage_now = next((s for s in FEATURE_STAGES if f["stages"][s]["status"] == "in_progress"), None) - if stage_now: + issues += validation.validate_requirements(req_text, task_id) + issues += validation.validate_testcases( + _read(stage_docs.path_for(root, task_id, "04-testcases")), reqs, root=root) + issues += validation.validate_review( + _read(stage_docs.path_for(root, task_id, "08-review"))) + next_stage = task.get("next_stage") + if next_stage: + stage = registry.ARTIFACTS[next_stage]["stage"] issues += validation.missing_tier2( - [(sk, pkg, registry.install_cmd(sk, pkg)) - for sk, pkg in registry.TIER2_REFS.get(stage_now, [])], root=root) + [(skill, pkg, registry.install_cmd(skill, pkg)) + for skill, pkg in registry.TIER2_REFS.get(stage, [])], root=root) has_error = any(i["level"] == "ERROR" for i in issues) if args.json: print(json.dumps({"issues": issues, "ok": not has_error}, ensure_ascii=False, indent=2)) @@ -404,15 +176,6 @@ def cmd_validate(args): sys.exit(3 if has_error else 0) -def cmd_instructions(args): - try: - ins = instructions.build(Path.cwd(), args.artifact, feature=args.feature) - except KeyError: - _die(mk_env("ERROR", "unknown_artifact", f"未知 artifact: {args.artifact}", - "artifact 取值为 01-requirements ~ 10-release")) - print(json.dumps(ins, ensure_ascii=False, indent=2)) - - def main(argv=None): p = Parser(prog="flowguard_state") sub = p.add_subparsers(dest="cmd", required=True) @@ -420,9 +183,6 @@ def main(argv=None): sp = sub.add_parser("init", help="在 docs/project/ 创建项目级十阶段文档(幂等)") sp.set_defaults(fn=cmd_init) - sp = sub.add_parser("legacy-init", help="仅旧项目兼容:初始化 .flowguard/ 状态") - sp.set_defaults(fn=cmd_legacy_init) - sp = sub.add_parser("discover", help="只读发现 Git / SDD / 工具状态,不执行初始化") sp.set_defaults(fn=cmd_discover) @@ -469,58 +229,11 @@ def main(argv=None): sp.add_argument("--reason") sp.set_defaults(fn=cmd_stage) - sp = sub.add_parser("migrate", help="预检或复制旧 .flowguard 产物至 docs/") - mode = sp.add_mutually_exclusive_group(required=True) - mode.add_argument("--dry-run", action="store_true") - mode.add_argument("--apply", action="store_true") - sp.set_defaults(fn=cmd_migrate) - - sp = sub.add_parser("status", help="流程看板") - sp.add_argument("--feature") - sp.set_defaults(fn=cmd_status) - - sp = sub.add_parser("feature", help="功能生命周期 new|list|done|drop") - sp.add_argument("action", choices=["new", "list", "done", "drop"]) - sp.add_argument("id", nargs="?") - sp.add_argument("--modules", nargs="+") - sp.add_argument("--title") - sp.add_argument("--reason") - sp.set_defaults(fn=cmd_feature) - - sp = sub.add_parser("next", help="开始阶段(设 current_feature / 项目阶段 in_progress)") - sp.add_argument("--feature") - sp.add_argument("--stage") - sp.set_defaults(fn=cmd_next) - - sp = sub.add_parser("advance", help="用户验收:accepted 唯一写入点") - sp.add_argument("--feature") - sp.add_argument("--stage") - sp.add_argument("--evidence", nargs="*") - sp.set_defaults(fn=cmd_advance) - - sp = sub.add_parser("override", help="逃生通道(必须 --reason,留痕)") - sp.add_argument("--feature") - sp.add_argument("--stage") - sp.add_argument("--reason", required=True) - sp.set_defaults(fn=cmd_override) - - sp = sub.add_parser("gate", help="门禁自检") - sp.add_argument("--feature") - sp.add_argument("--action", choices=["write_code", "write_review", "write_docs", "build_release"]) - sp.add_argument("--path") - sp.set_defaults(fn=cmd_gate) - - sp = sub.add_parser("validate", help="产物校验(含追溯/执行证据/Tier2 缺失)") - sp.add_argument("--feature") + sp = sub.add_parser("validate", help="产物校验(内容/追溯/结论/Tier2 缺失)") + sp.add_argument("--task-id") sp.set_defaults(fn=cmd_validate) - sp = sub.add_parser("instructions", help="机读指令(context/rules/template/requires/unlocks/tier2)") - sp.add_argument("artifact") - sp.add_argument("--feature") - sp.set_defaults(fn=cmd_instructions) - - for name in ("init", "legacy-init", "discover", "context", "evidence", "governance", "stage", "migrate", "status", - "feature", "next", "advance", "override", "gate", "validate", "instructions"): + for name in ("init", "discover", "context", "evidence", "governance", "stage", "validate"): sub.choices[name].add_argument("--json", action="store_true") args = p.parse_args(argv) @@ -529,8 +242,8 @@ def main(argv=None): try: args.fn(args) except (state.StateError, context.ContextError, evidence.EvidenceError, - stage_docs.StageDocError, migration.MigrationError) as e: - _die(mk_env("ERROR", "state_error", str(e), "检查 docs/ 阶段文档、宿主会话缓存或旧 journal")) + stage_docs.StageDocError) as e: + _die(mk_env("ERROR", "state_error", str(e), "检查 docs/ 阶段文档或宿主会话缓存")) if __name__ == "__main__": diff --git a/scripts/templates/artifacts.py b/scripts/templates/artifacts.py index b7c1d0e..cd7194c 100644 --- a/scripts/templates/artifacts.py +++ b/scripts/templates/artifacts.py @@ -304,46 +304,3 @@ def render(artifact_id, ctx): """ -def init_project(root): - """建 .flowguard/ 骨架 + config.yaml 默认 + 项目级产物模板;幂等(已初始化则原样返回)。""" - import json - root = Path(root) - fg = root / ".flowguard" - for sub in ("project", "features", "journal"): - (fg / sub).mkdir(parents=True, exist_ok=True) - existing = fg / "project.json" - if existing.exists(): - return json.loads(existing.read_text(encoding="utf-8")) - try: - from ..flowguard_lib import detect as _detect - except ImportError: # pragma: no cover 脚本态 - from flowguard_lib import detect as _detect - try: - from ..flowguard_lib import yamlmini - except ImportError: # pragma: no cover - from flowguard_lib import yamlmini - cfg_path = fg / "config.yaml" - if not cfg_path.exists(): - cfg_path.write_text(yamlmini.dump( - {"schema": "flowguard", "context": "", "rules": {}}), encoding="utf-8") - - det = _detect.detect(root) - project = { - "version": 1, - "project": root.resolve().name, - "stack": det["stack"], - "modules": det["modules"], - "current_feature": None, - "stages": {art["stage"]: {"status": "pending", "artifact": art["rel_tpl"]} - for art in registry.ARTIFACTS.values() if art["scope"] == "project"}, - "features": {}, - } - if not existing.exists(): - existing.write_text(json.dumps(project, ensure_ascii=False, indent=2), encoding="utf-8") - - for aid in ("02-architecture", "07-standards", "10-release"): - p = fg / registry.ARTIFACTS[aid]["rel_tpl"] - if not p.exists(): - p.parent.mkdir(parents=True, exist_ok=True) - p.write_text(render(aid, {"title": project["project"], "modules": "-"}), encoding="utf-8") - return project diff --git a/scripts/templates/workflows.py b/scripts/templates/workflows.py index fa8590a..6b7e67b 100644 --- a/scripts/templates/workflows.py +++ b/scripts/templates/workflows.py @@ -27,7 +27,7 @@ "name": "flowguard-requirements", "scope": "feature", "stage": "requirements", "artifact": "01-requirements", "goal": "产出功能需求产物 01-requirements.md:用户故事 + /REQ-n + 可验收标准。", "description": ("需求分析阶段的编排技能。当用户要写需求/用户故事/功能规格、或把模糊想法变成可验收需求时使用;" - "以 current_feature 为工作对象,产出 Requirement/Scenario 结构化需求。" + "以已绑定的 task-id 为工作对象,产出 Requirement/Scenario 结构化需求。" "不要用它做架构选型或写测试用例(后续阶段职责)。"), "routes": [ "ddd-domain-designer → 领域建模深水区(npx skills add full-stack-skills/ddd-skills --skill ddd-domain-designer)", @@ -160,7 +160,7 @@ def render_skill(spec): 十阶段是强制流程骨架,不是由 Hook 自动推进的固定脚本。智能体判断任务及下一步,Spec Kit/OpenSpec 管正式规格,Superpowers 管工程方法;FlowGuard 校验 `docs/` 中的阶段产物、批准和证据,并阻止绕过。 -项目级文档位于 `docs/project/`(02、07、10);功能及独立子功能位于 `docs/features//`(01、03、04、05、06、08、09)。新项目不创建 `.flowguard/`;原生规格只在阶段文档中引用,不复制正文。 +项目级文档位于 `docs/project/`(02、07、10);功能及独立子功能位于 `docs/features//`(01、03、04、05、06、08、09)。原生规格只在阶段文档中引用,不复制正文。 ## 30 秒开始 @@ -216,9 +216,6 @@ def render_skill(spec): - CodeReview:基于正式规格和代码上下文输出语义风险,作为 `semantic_review` 证据。 - 机器 PASS 不自动成为用户验收;代码变化使可过期证据失效。 -## 旧项目迁移 - -发现旧 `.flowguard/` 时先运行 `migrate --dry-run`,经用户确认再 `migrate --apply`,核对 `docs/` 后才考虑移走旧数据;冲突时停止,不能覆盖现有文档。`legacy-init` 仅用于旧流程兼容,新项目不得使用。 """ @@ -243,7 +240,7 @@ def render_skill(spec): name: {name} license: Apache-2.0 description: {spec['description']} -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # {name} —— {spec['goal']} diff --git a/skills/flowguard-architecture/SKILL.md b/skills/flowguard-architecture/SKILL.md index c6bf420..ea7ad57 100644 --- a/skills/flowguard-architecture/SKILL.md +++ b/skills/flowguard-architecture/SKILL.md @@ -2,7 +2,7 @@ name: flowguard-architecture license: Apache-2.0 description: 架构设计阶段(项目级,走一次)。当用户要定技术选型、写 ADR、划模块边界时使用;ADR 一律追加式并标注来源功能,禁止改写既有条目。不要用它写单个功能的技术方案(那是 flowguard-solution)。 -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # flowguard-architecture —— 产出项目架构产物 02-architecture.md:选型 / ADR 追加式列表 / 模块边界。 diff --git a/skills/flowguard-docs/SKILL.md b/skills/flowguard-docs/SKILL.md index a7267a8..5a3df95 100644 --- a/skills/flowguard-docs/SKILL.md +++ b/skills/flowguard-docs/SKILL.md @@ -2,7 +2,7 @@ name: flowguard-docs license: Apache-2.0 description: 文档生成阶段(薄)。当用户要为功能补 API/用户/运维文档并记录生成情况时使用。不要用它写发布清单(flowguard-release)。 -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # flowguard-docs —— 产出功能文档清单与生成记录 09-docs.md。 diff --git a/skills/flowguard-hld/SKILL.md b/skills/flowguard-hld/SKILL.md index d3ba41d..d3ffaa8 100644 --- a/skills/flowguard-hld/SKILL.md +++ b/skills/flowguard-hld/SKILL.md @@ -2,7 +2,7 @@ name: flowguard-hld license: Apache-2.0 description: 概要设计阶段(薄)。当用户要划分模块/服务、描述交互与数据流时使用。不要用它写类级明细(flowguard-lld)。 -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # flowguard-hld —— 产出概要设计 05-hld.md:模块/服务划分与交互。 diff --git a/skills/flowguard-lld/SKILL.md b/skills/flowguard-lld/SKILL.md index 734d026..71b440e 100644 --- a/skills/flowguard-lld/SKILL.md +++ b/skills/flowguard-lld/SKILL.md @@ -2,7 +2,7 @@ name: flowguard-lld license: Apache-2.0 description: 详细设计阶段(薄)。当用户要定类职责、表结构、接口字段时使用。不要用它重复概要设计的模块划分。 -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # flowguard-lld —— 产出详细设计 06-lld.md:类/表/接口明细与异常边界。 diff --git a/skills/flowguard-release/SKILL.md b/skills/flowguard-release/SKILL.md index 61cfc2f..43fbe08 100644 --- a/skills/flowguard-release/SKILL.md +++ b/skills/flowguard-release/SKILL.md @@ -2,7 +2,7 @@ name: flowguard-release license: Apache-2.0 description: 部署交付阶段(项目级收口)。当所有功能 done 后做交付收口、写发布清单时使用。不要在还有 active 功能时尝试发布(门禁会阻断)。 -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # flowguard-release —— 产出发布清单 10-release.md:列出本次交付功能及各自 09 文档状态,登记版本/校验和/回滚方案/证据;列入功能的文档变化后重新验收发布阶段。 diff --git a/skills/flowguard-requirements/SKILL.md b/skills/flowguard-requirements/SKILL.md index 6373ea4..038deb8 100644 --- a/skills/flowguard-requirements/SKILL.md +++ b/skills/flowguard-requirements/SKILL.md @@ -1,8 +1,8 @@ --- name: flowguard-requirements license: Apache-2.0 -description: 需求分析阶段的编排技能。当用户要写需求/用户故事/功能规格、或把模糊想法变成可验收需求时使用;以 current_feature 为工作对象,产出 Requirement/Scenario 结构化需求。不要用它做架构选型或写测试用例(后续阶段职责)。 -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +description: 需求分析阶段的编排技能。当用户要写需求/用户故事/功能规格、或把模糊想法变成可验收需求时使用;以已绑定的 task-id 为工作对象,产出 Requirement/Scenario 结构化需求。不要用它做架构选型或写测试用例(后续阶段职责)。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # flowguard-requirements —— 产出功能需求产物 01-requirements.md:用户故事 + /REQ-n + 可验收标准。 diff --git a/skills/flowguard-review/SKILL.md b/skills/flowguard-review/SKILL.md index dcd9272..48c5df3 100644 --- a/skills/flowguard-review/SKILL.md +++ b/skills/flowguard-review/SKILL.md @@ -2,7 +2,7 @@ name: flowguard-review license: Apache-2.0 description: 代码审查阶段(薄)。当用户要求审查某功能的代码变更、或汇总审查发现项时使用;每条发现项必须带证据与结论。不要用它跑静态检查工具(codeguard-plugin 职责)。 -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # flowguard-review —— 产出证据化代码审查报告 08-review.md:发现项(证据)+ 结论(fix/wontfix/deferred)。 diff --git a/skills/flowguard-solution/SKILL.md b/skills/flowguard-solution/SKILL.md index 84313ab..feb5427 100644 --- a/skills/flowguard-solution/SKILL.md +++ b/skills/flowguard-solution/SKILL.md @@ -2,7 +2,7 @@ name: flowguard-solution license: Apache-2.0 description: 技术方案阶段。当用户为某功能定实现方案、接口契约或风险预案时使用。不要用它做全项目架构决策(flowguard-architecture)。 -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # flowguard-solution —— 产出功能技术方案 03-solution.md:实现选型 / 接口契约 / 风险清单。 diff --git a/skills/flowguard-standards/SKILL.md b/skills/flowguard-standards/SKILL.md index bdc692c..2b3cf75 100644 --- a/skills/flowguard-standards/SKILL.md +++ b/skills/flowguard-standards/SKILL.md @@ -2,7 +2,7 @@ name: flowguard-standards license: Apache-2.0 description: 编码规范阶段(项目级,厚)。当用户要定编码规范、按栈选规范来源、或增补项目规约时使用;规范集生成后,写码门禁才解锁。不要用它执行 lint(那是 codeguard-plugin)。 -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # flowguard-standards —— 产出项目编码规范集 07-standards.md:按模块栈路由规范来源,追加式增补。 diff --git a/skills/flowguard-testcases/SKILL.md b/skills/flowguard-testcases/SKILL.md index 779d046..9b3d60a 100644 --- a/skills/flowguard-testcases/SKILL.md +++ b/skills/flowguard-testcases/SKILL.md @@ -2,7 +2,7 @@ name: flowguard-testcases license: Apache-2.0 description: 测试用例阶段(厚阶段)。当用户要写测试用例、建追溯矩阵、或在写码前定测试计划时使用;每条用例必须标注 REQ 与测试文件(TDD 门槛的机械检查依据)。不要用它实际运行测试或写业务代码。 -compatibility: Python 3 标准库;十阶段文档位于 docs/,无需项目 .flowguard/ 目录。 +compatibility: Python 3 标准库;十阶段文档位于 docs/。 --- # flowguard-testcases —— 产出测试用例 04-testcases.md:用例 + 追溯矩阵(REQ↔用例↔测试文件)。 diff --git a/skills/flowguard/SKILL.md b/skills/flowguard/SKILL.md index 840cdf5..793312c 100644 --- a/skills/flowguard/SKILL.md +++ b/skills/flowguard/SKILL.md @@ -9,7 +9,7 @@ compatibility: Python 3 标准库;Git 项目无需预先初始化 FlowGuard。 十阶段是强制流程骨架,不是由 Hook 自动推进的固定脚本。智能体判断任务及下一步,Spec Kit/OpenSpec 管正式规格,Superpowers 管工程方法;FlowGuard 校验 `docs/` 中的阶段产物、批准和证据,并阻止绕过。 -项目级文档位于 `docs/project/`(02、07、10);功能及独立子功能位于 `docs/features//`(01、03、04、05、06、08、09)。新项目不创建 `.flowguard/`;原生规格只在阶段文档中引用,不复制正文。 +项目级文档位于 `docs/project/`(02、07、10);功能及独立子功能位于 `docs/features//`(01、03、04、05、06、08、09)。原生规格只在阶段文档中引用,不复制正文。 ## 30 秒开始 @@ -65,6 +65,3 @@ python3 "${FLOWGUARD_PLUGIN_ROOT:?}/scripts/flowguard_state.py" governance --ses - CodeReview:基于正式规格和代码上下文输出语义风险,作为 `semantic_review` 证据。 - 机器 PASS 不自动成为用户验收;代码变化使可过期证据失效。 -## 旧项目迁移 - -发现旧 `.flowguard/` 时先运行 `migrate --dry-run`,经用户确认再 `migrate --apply`,核对 `docs/` 后才考虑移走旧数据;冲突时停止,不能覆盖现有文档。`legacy-init` 仅用于旧流程兼容,新项目不得使用。 diff --git a/tests/test_cli.py b/tests/test_cli.py index f4c4e24..f410b4d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,8 @@ -import json, subprocess, sys, tempfile, unittest +import json +import subprocess +import sys +import tempfile +import unittest from pathlib import Path REPO = Path(__file__).resolve().parents[1] @@ -10,75 +14,48 @@ def run(root, *argv): capture_output=True, text=True) -class CliTest(unittest.TestCase): +class ValidateCliTest(unittest.TestCase): + """validate 面向 docs/features//:内容校验、追溯矩阵与 Tier2 检查。""" + def setUp(self): self.root = Path(tempfile.mkdtemp()) - (self.root / "pom.xml").write_text("", encoding="utf-8") - (self.root / ".flowguard").mkdir() # 显式旧项目夹具 - - def test_full_walk(self): - # init 幂等 - r = run(self.root, "legacy-init", "--json") - self.assertEqual(r.returncode, 0, r.stderr) - self.assertEqual(json.loads(r.stdout)["modules"], ["app"]) - self.assertEqual(run(self.root, "legacy-init").returncode, 0) - - # 功能创建 + 非法 id - r = run(self.root, "feature", "new", "order-refund", "--modules", "app", "--json") - self.assertEqual(r.returncode, 0, r.stderr) - r = run(self.root, "feature", "new", "Bad_ID", "--modules", "app") - self.assertEqual(r.returncode, 3) - r = run(self.root, "feature", "new", "order-refund", "--modules", "app") - self.assertEqual(r.returncode, 3) - - # 未开始阶段:写码被门禁拒绝(exit 2,信封 code) - r = run(self.root, "gate", "--action", "write_code", "--path", "app/A.java", "--json") - self.assertEqual(r.returncode, 2) - self.assertIn("gate_write_code", r.stdout) - - # 五个功能级阶段 next → advance(advance = 用户验收) - for _ in range(5): - self.assertEqual(run(self.root, "next", "--json").returncode, 0) - r = run(self.root, "advance", "--evidence", "用户确认", "--json") - self.assertEqual(r.returncode, 0, r.stderr) + self.task = "order-refund" + bound = run(self.root, "context", "bind", "--session", "s", "--task-id", self.task, + "--task-type", "simple_change", "--spec-system", "none", "--json") + self.assertEqual(bound.returncode, 0, bound.stderr) - # 项目级规范阶段(advance 必须显式 --stage,否则默认作用于 current_feature) - self.assertEqual(run(self.root, "next", "--stage", "standards", "--json").returncode, 0) - r = run(self.root, "advance", "--stage", "standards", "--evidence", "用户确认", "--json") - self.assertEqual(r.returncode, 0, r.stderr) - - # 写码放行 - r = run(self.root, "gate", "--action", "write_code", "--path", "app/A.java", "--json") + def test_template_state_passes_with_tier2_warning_only(self): + r = run(self.root, "validate", "--json") self.assertEqual(r.returncode, 0, r.stdout + r.stderr) - - # 看板 - r = run(self.root, "status", "--json") - self.assertEqual(r.returncode, 0) - st = json.loads(r.stdout) - self.assertEqual(st["features"]["order-refund"]["stages"]["requirements"], "accepted") - self.assertEqual(st["current_feature"], "order-refund") - - # override 留痕(review 阶段还在 pending) - r = run(self.root, "override", "--stage", "review", "--reason", "hotfix 需求", "--json") - self.assertEqual(r.returncode, 0, r.stderr) - - # validate:全模板态(示例块含占位符)→ 不参与机械检查,validate 通过 + out = json.loads(r.stdout) + self.assertTrue(out["ok"]) + self.assertTrue(all(i["level"] != "ERROR" for i in out["issues"])) + + def test_wrong_feature_req_id_is_error(self): + path = self.root / "docs" / "features" / self.task / "01-requirements.md" + path.write_text( + "### 1.3 FlowGuard 阶段信息\n\n" + "| 字段 | 值 |\n|:---|:---|\n" + f"| 任务 | {self.task} |\n| 父任务 | - |\n| 阶段 | 01-requirements |\n" + "| 阶段状态 | pending |\n| 规格事实源 | none |\n| 原生产物 | - |\n" + "| 批准依据 | - |\n| 前置指纹 | - |\n| 验收指纹 | - |\n\n" + "## 3. 用户故事与验收标准\n\n" + "### Requirement: 退款接口\n`other/REQ-1` SHALL 提供退款。\n\n#### Scenario: 成功\n", + encoding="utf-8") r = run(self.root, "validate", "--json") - self.assertEqual(r.returncode, 0, r.stdout) - self.assertTrue(json.loads(r.stdout)["ok"]) - - # 未初始化目录:gate 放行 - empty = Path(tempfile.mkdtemp()) - r = run(empty, "gate", "--action", "write_code", "--path", "src/A.java", "--json") - self.assertEqual(r.returncode, 0) + self.assertEqual(r.returncode, 3) + out = json.loads(r.stdout) + self.assertFalse(out["ok"]) + self.assertTrue(any("other/REQ-1" in i["message"] for i in out["issues"])) + + def test_task_id_filter_and_unknown_task(self): + ok = run(self.root, "validate", "--task-id", self.task, "--json") + self.assertEqual(ok.returncode, 0, ok.stdout) + missing = run(self.root, "validate", "--task-id", "nope", "--json") + self.assertEqual(missing.returncode, 3) + self.assertEqual(json.loads(missing.stdout)["code"], "unknown_task") + self.assertFalse((self.root / ".flowguard").exists()) - def test_instructions_json(self): - self.assertEqual(run(self.root, "legacy-init", "--json").returncode, 0) - r = run(self.root, "instructions", "01-requirements", "--json") - self.assertEqual(r.returncode, 0) - ins = json.loads(r.stdout) - for key in ("artifact", "scope", "stage", "context", "rules", "template", "requires", "unlocks", "tier2"): - self.assertIn(key, ins) if __name__ == "__main__": unittest.main() diff --git a/tests/test_commands.py b/tests/test_commands.py index 083cae3..101cc8f 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -9,8 +9,7 @@ EXPECTED = { "flowguard-discover", "flowguard-context", "flowguard-evidence", "flowguard-governance", - "flowguard-init", "flowguard-feature", "flowguard-status", "flowguard-next", - "flowguard-advance", "flowguard-gate", "flowguard-override", "flowguard-stage", + "flowguard-init", "flowguard-stage", } class CommandsTest(unittest.TestCase): @@ -43,15 +42,6 @@ def test_prompt_cli_subcommands_all_exist(self): for m in re.finditer(r"flowguard_state\.py\"\s+([a-z]+)", prompt): self.assertIn(m.group(1), registered, f"{p.stem}: 子命令 {m.group(1)} 未注册") - def test_advance_requires_user_confirmation(self): - d = json.loads((CMD / "flowguard-advance.json").read_text(encoding="utf-8")) - self.assertIn("确认", d["prompt"]) - self.assertIn("不得", d["prompt"]) - - def test_override_user_initiated_only(self): - d = json.loads((CMD / "flowguard-override.json").read_text(encoding="utf-8")) - self.assertIn("用户", d["prompt"]) - self.assertIn("理由", d["prompt"]) if __name__ == "__main__": unittest.main() diff --git a/tests/test_detect.py b/tests/test_detect.py index 80745e4..8bb4d18 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -63,21 +63,5 @@ def test_testcases_template_has_traceability_fields(self): self.assertIn("- REQ:", text) self.assertIn("- 测试文件:", text) -class InitProjectTest(unittest.TestCase): - def test_init_creates_skeleton(self): - root = Path(tempfile.mkdtemp()) - touch(root / "pom.xml") - proj = detect.init_project(root) - self.assertTrue((root / ".flowguard" / "project.json").exists()) - self.assertTrue((root / ".flowguard" / "config.yaml").exists()) - self.assertTrue((root / ".flowguard" / "project" / "02-architecture.md").exists()) - self.assertTrue((root / ".flowguard" / "project" / "07-standards.md").exists()) - self.assertTrue((root / ".flowguard" / "project" / "10-release.md").exists()) - self.assertEqual(proj["modules"]["app"]["stack"], "java-spring") - self.assertIsNone(proj["current_feature"]) - # 幂等 - proj2 = detect.init_project(root) - self.assertEqual(proj2, proj) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_docs_migration.py b/tests/test_docs_migration.py deleted file mode 100644 index 2981ca9..0000000 --- a/tests/test_docs_migration.py +++ /dev/null @@ -1,113 +0,0 @@ -import json -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path -from unittest import mock - -REPO = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO / "scripts")) - -from flowguard_lib import stage_docs # noqa: E402 - - -def run(root, *args): - return subprocess.run([sys.executable, str(REPO / "scripts/flowguard_state.py"), *args], - cwd=root, capture_output=True, text=True) - - -class MigrationTest(unittest.TestCase): - def setUp(self): - self.root = Path(tempfile.mkdtemp()) - subprocess.run(["git", "init", "-q"], cwd=self.root, check=True) - source = self.root / ".flowguard/features/refund/artifacts/01-requirements.md" - source.parent.mkdir(parents=True) - source.write_text( - "# 退款需求\n\n## 1. 文档信息\n\n## 2. 需求\n\n" - "### Requirement: 幂等退款\n`refund/REQ-1` 系统 SHALL 拒绝重复退款。\n\n" - "#### Scenario: 重复通知\n- WHEN 收到重复通知\n- THEN 不重复退款\n", - encoding="utf-8", - ) - state = self.root / ".flowguard/features/refund/state.json" - state.write_text(json.dumps({ - "feature": "refund", "stages": {"requirements": {"status": "accepted"}}, - }), encoding="utf-8") - - def test_preview_is_read_only_and_apply_preserves_legacy_content_without_trusting_old_acceptance(self): - result = run(self.root, "migrate", "--dry-run", "--json") - self.assertEqual(result.returncode, 0, result.stderr) - plan = json.loads(result.stdout) - self.assertEqual(plan["conflicts"], []) - self.assertEqual(len(plan["entries"]), 1) - self.assertFalse((self.root / "docs").exists()) - - applied_result = run(self.root, "migrate", "--apply", "--json") - self.assertEqual(applied_result.returncode, 0, applied_result.stderr) - applied = json.loads(applied_result.stdout) - self.assertEqual(len(applied["created"]), 1) - migrated = self.root / "docs/features/refund/01-requirements.md" - self.assertIn("拒绝重复退款", migrated.read_text(encoding="utf-8")) - self.assertEqual(stage_docs.read(self.root, "refund", "01-requirements")["status"], "pending_acceptance") - self.assertTrue((self.root / ".flowguard/features/refund/artifacts/01-requirements.md").is_file()) - - def test_destination_conflict_stops_all_writes(self): - destination = self.root / "docs/features/refund/01-requirements.md" - destination.parent.mkdir(parents=True) - destination.write_text("# user content\n", encoding="utf-8") - - result = run(self.root, "migrate", "--apply", "--json") - self.assertNotEqual(result.returncode, 0) - self.assertIn("冲突", result.stdout + result.stderr) - self.assertEqual(destination.read_text(encoding="utf-8"), "# user content\n") - - def test_failed_atomic_creation_leaves_legacy_and_destination_intact(self): - destination = self.root / "docs/features/refund/01-requirements.md" - source = self.root / ".flowguard/features/refund/artifacts/01-requirements.md" - before = source.read_bytes() - from flowguard_lib import migration - - with mock.patch("os.link", side_effect=OSError("link failed")): - with self.assertRaises(migration.MigrationError): - migration.apply(self.root) - self.assertEqual(source.read_bytes(), before) - self.assertFalse(destination.exists()) - self.assertEqual(list(destination.parent.glob(".flowguard-*")), []) - - def test_migration_cannot_write_while_stage_state_lock_is_held(self): - from flowguard_lib import state - destination = self.root / "docs/features/refund/01-requirements.md" - with state.state_lock(self.root): - result = run(self.root, "migrate", "--apply", "--json") - self.assertNotEqual(result.returncode, 0, result.stdout) - self.assertFalse(destination.exists()) - - def test_failed_later_migration_does_not_delete_externally_changed_document(self): - from flowguard_lib import migration, state - second = self.root / ".flowguard/project/02-architecture.md" - second.parent.mkdir(parents=True) - second.write_text("# 旧架构\n", encoding="utf-8") - first_target = self.root / "docs/features/refund/01-requirements.md" - second_target = self.root / "docs/project/02-architecture.md" - original_create = state.atomic_create_text - calls = 0 - - def create_then_external_edit(path, content, *, mode=None): - nonlocal calls - calls += 1 - if calls == 2: - first_target.write_text("# 用户并发修改\n", encoding="utf-8") - raise OSError("second creation failed") - return original_create(path, content, mode=mode) - - with mock.patch.object(state, "atomic_create_text", side_effect=create_then_external_edit): - with self.assertRaises(migration.MigrationError) as failure: - migration.apply(self.root) - self.assertIn("docs/features/refund/01-requirements.md", str(failure.exception)) - self.assertTrue(first_target.exists(), "迁移回滚删除了用户刚修改的文档") - self.assertEqual(first_target.read_text(encoding="utf-8"), "# 用户并发修改\n") - self.assertFalse(second_target.exists()) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_docs_pipeline.py b/tests/test_docs_pipeline.py index 87a52d5..ac22efa 100644 --- a/tests/test_docs_pipeline.py +++ b/tests/test_docs_pipeline.py @@ -67,7 +67,7 @@ def test_binding_creation_failure_does_not_leave_partial_stage_document(self): with self.assertRaises(OSError): self.bind() self.assertFalse(target.exists()) - self.assertEqual(list(target.parent.glob(".flowguard-*")), []) + self.assertEqual(list(target.parent.glob(".fg-atomic-*")), []) self.assertIsNone(context.active(self.root, "s")) def test_project_init_creation_failure_does_not_leave_partial_document(self): @@ -77,7 +77,7 @@ def test_project_init_creation_failure_does_not_leave_partial_document(self): with self.assertRaises(OSError): stage_docs.ensure_project(self.root) self.assertFalse(target.exists()) - self.assertEqual(list(target.parent.glob(".flowguard-*")), []) + self.assertEqual(list(target.parent.glob(".fg-atomic-*")), []) def test_project_init_cannot_create_docs_while_state_lock_is_held(self): from flowguard_lib import state diff --git a/tests/test_e2e.py b/tests/test_e2e.py deleted file mode 100644 index c576a29..0000000 --- a/tests/test_e2e.py +++ /dev/null @@ -1,117 +0,0 @@ -"""端到端验收:≥2 模块 × ≥2 功能,双功能交错推进到部署交付(spec §12 验收标准)。""" -import json, shutil, subprocess, sys, tempfile, unittest -from pathlib import Path - -REPO = Path(__file__).resolve().parents[1] -CLI = REPO / "scripts" / "flowguard_state.py" -FIXTURE = REPO / "tests" / "fixtures" / "multi_feature" - -REQ_A = """# 订单退款 —— 需求分析 - -### Requirement: 退款申请 -`order-refund/REQ-1` 用户 SHALL 能对已完成订单发起退款申请。 - -#### Scenario: 正常申请 -- **WHEN** 用户提交退款 -- **THEN** 生成退款单 -""" - -TC_A = """# 订单退款 —— 测试用例 - -### 用例 TC-1: 正常退款 -- REQ: order-refund/REQ-1 -- 测试文件: tests/test_refund.py -- 步骤: 提交退款 -- 预期: 生成退款单 -""" - - -class EndToEndTest(unittest.TestCase): - def setUp(self): - self.root = Path(tempfile.mkdtemp()) - shutil.copytree(FIXTURE, self.root, dirs_exist_ok=True) - (self.root / ".flowguard").mkdir() # 显式旧项目夹具 - - def cli(self, *argv): - p = subprocess.run([sys.executable, str(CLI), *argv], cwd=self.root, - capture_output=True, text=True) - return p - - def advance(self, *extra): - p = self.cli("advance", "--evidence", "用户确认", *extra, "--json") - self.assertEqual(p.returncode, 0, p.stdout + p.stderr) - - def test_two_features_two_modules_full_walk(self): - # init:探测双模块 - r = self.cli("legacy-init", "--json") - self.assertEqual(r.returncode, 0, r.stderr) - init = json.loads(r.stdout) - self.assertEqual(sorted(init["modules"]), ["order", "web"]) - self.assertEqual(init["stack"]["backend"], "java-spring") - self.assertEqual(init["stack"]["frontend"], "vue3") - - # 功能 A(跨双模块)+ 功能 B - self.assertEqual(self.cli("feature", "new", "order-refund", - "--modules", "order", "web", "--json").returncode, 0) - self.assertEqual(self.cli("feature", "new", "user-export", - "--modules", "web", "--json").returncode, 0) - - # 项目级:架构 + 规范 - self.assertEqual(self.cli("next", "--stage", "architecture").returncode, 0) - self.advance("--stage", "architecture") - self.assertEqual(self.cli("next", "--stage", "standards").returncode, 0) - self.advance("--stage", "standards") - - # A:真实内容写入需求与用例(含测试文件),五阶段逐个验收 - adir = self.root / ".flowguard" / "features" / "order-refund" / "artifacts" - (adir / "01-requirements.md").write_text(REQ_A, encoding="utf-8") - (adir / "04-testcases.md").write_text(TC_A, encoding="utf-8") - tests_dir = self.root / "tests" - tests_dir.mkdir(exist_ok=True) - (tests_dir / "test_refund.py").write_text("def test_refund():\n assert True\n", encoding="utf-8") - for _ in range(5): - self.assertEqual(self.cli("next", "--feature", "order-refund").returncode, 0) - self.advance("--feature", "order-refund") - - # TDD 门槛生效:current=A,A 写码放行(含跨模块 web) - r = self.cli("gate", "--action", "write_code", "--path", "server/order/A.java", "--json") - self.assertEqual(r.returncode, 0, r.stdout + r.stderr) - r = self.cli("gate", "--action", "write_code", "--path", "web/src/Pay.vue", "--json") - self.assertEqual(r.returncode, 0, r.stdout + r.stderr) - - # A:validate 全绿(REQ 覆盖 + 测试文件存在) - r = self.cli("validate", "--feature", "order-refund", "--json") - self.assertEqual(r.returncode, 0, r.stdout) - - # B:只推进了需求(in_progress)→ current 切到 B 后写码被 TDD 拒绝 - self.assertEqual(self.cli("next", "--feature", "user-export").returncode, 0) - r = self.cli("gate", "--action", "write_code", "--path", "web/src/E.vue", "--json") - self.assertEqual(r.returncode, 2) - self.assertIn("gate_write_code_tdd", r.stdout) - - # A:审查 + 文档验收 → done - self.assertEqual(self.cli("next", "--feature", "order-refund").returncode, 0) - self.advance("--feature", "order-refund") # review - self.assertEqual(self.cli("next", "--feature", "order-refund").returncode, 0) - self.advance("--feature", "order-refund") # docs → done - st = json.loads(self.cli("status", "--json").stdout) - self.assertEqual(st["features"]["order-refund"]["status"], "done") - - # B:留痕放弃 - self.assertEqual(self.cli("feature", "drop", "user-export", - "--reason", "需求撤销", "--json").returncode, 0) - - # 交付收口 - self.assertEqual(self.cli("next", "--stage", "release").returncode, 0) - self.advance("--stage", "release") - self.assertTrue((self.root / ".flowguard" / "project" / "10-release.md").exists()) - r = self.cli("gate", "--action", "build_release", "--json") - self.assertEqual(r.returncode, 0, r.stdout + r.stderr) - - # journal 全程留痕 - jl = (self.root / ".flowguard" / "journal" / "events.jsonl").read_text(encoding="utf-8") - for ev in ("init", "feature_new", "accepted_by_user", "feature_drop"): - self.assertIn(f'"event": "{ev}"', jl) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_error_paths.py b/tests/test_error_paths.py index db9bdee..b9543ed 100644 --- a/tests/test_error_paths.py +++ b/tests/test_error_paths.py @@ -1,5 +1,10 @@ """错误路径与诊断信封契约:一切 CLI/hook 错误必须走 {severity,code,message,fix},不裸崩。""" -import json, subprocess, sys, tempfile, unittest +import json +import os +import subprocess +import sys +import tempfile +import unittest from pathlib import Path REPO = Path(__file__).resolve().parents[1] @@ -15,29 +20,37 @@ def run(root, *argv): class ErrorPathTest(unittest.TestCase): def setUp(self): self.root = Path(tempfile.mkdtemp()) - (self.root / "pom.xml").write_text("", encoding="utf-8") - (self.root / ".flowguard").mkdir() # 显式旧项目夹具 - self.assertEqual(run(self.root, "legacy-init", "--json").returncode, 0) - self.assertEqual(run(self.root, "feature", "new", "f1", "--modules", "app", "--json").returncode, 0) + bound = run(self.root, "context", "bind", "--session", "s", "--task-id", "f1", + "--task-type", "simple_change", "--spec-system", "none", "--json") + self.assertEqual(bound.returncode, 0, bound.stderr) - def test_unknown_artifact_gets_envelope(self): - r = run(self.root, "instructions", "99-bogus", "--json") - self.assertEqual(r.returncode, 3) - env = json.loads(r.stdout) - self.assertEqual(env["code"], "unknown_artifact") + def _envelope(self, result): + self.assertEqual(result.returncode, 3, result.stdout + result.stderr) + env = json.loads(result.stdout) self.assertEqual(set(env), {"severity", "code", "message", "fix"}) - self.assertNotIn("Traceback", r.stderr) - - def test_status_unknown_feature_gets_envelope(self): - r = run(self.root, "status", "--feature", "nope", "--json") - self.assertEqual(r.returncode, 3) - env = json.loads(r.stdout) - self.assertEqual(env["code"], "unknown_feature") - - def test_status_feature_filters(self): - r = run(self.root, "status", "--feature", "f1", "--json") - self.assertEqual(r.returncode, 0, r.stdout) - self.assertEqual(list(json.loads(r.stdout)["features"]), ["f1"]) + self.assertNotIn("Traceback", result.stderr) + return env + + def test_unknown_task_gets_envelope(self): + env = self._envelope(run(self.root, "stage", "status", "--task-id", "nope", "--json")) + self.assertEqual(env["code"], "state_error") + + def test_stage_advance_requires_approval_ref(self): + env = self._envelope(run( + self.root, "stage", "advance", "--task-id", "f1", + "--stage", "01-requirements", "--status", "accepted", "--json")) + self.assertIn("批准", env["message"]) + + def test_evidence_record_unknown_context_gets_envelope(self): + env = self._envelope(run( + self.root, "evidence", "record", "--context-id", "missing", + "--kind", "tests", "--producer", "t", "--result", "pass", + "--summary", "s", "--source-ref", "r", "--json")) + self.assertEqual(env["code"], "state_error") + + def test_context_show_unbound_gets_envelope(self): + env = self._envelope(run(self.root, "context", "show", "--json")) + self.assertEqual(env["code"], "context_not_bound") def test_usage_error_exit3_with_envelope(self): r = run(self.root, "bogus-cmd") @@ -49,12 +62,6 @@ def test_usage_error_exit3_with_envelope(self): self.assertEqual(json.loads(r2.stdout)["code"], "usage") self.assertNotIn("Traceback", r.stderr + r2.stderr) - def test_drop_without_reason(self): - r = run(self.root, "feature", "drop", "f1", "--json") - self.assertEqual(r.returncode, 3) - env = json.loads(r.stdout) - self.assertIn("reason", env["code"].lower() + env["message"].lower()) - class HookErrorPathTest(unittest.TestCase): def test_gate_half_valid_write_json_is_denied_inside_git_repo(self): @@ -80,14 +87,26 @@ def test_summary_bad_cwd_silent(self): def test_governance_summary_hooks_fail_open_on_corrupt_context_index(self): root = Path(tempfile.mkdtemp()) subprocess.run(["git", "init", "-q"], cwd=root, check=True) - index = root / ".flowguard" / "contexts" / "index.json" - index.parent.mkdir(parents=True) + state_home = Path(tempfile.mkdtemp()) + old_home = os.environ.get("FLOWGUARD_STATE_HOME") + os.environ["FLOWGUARD_STATE_HOME"] = str(state_home) + try: + sys.path.insert(0, str(REPO / "scripts")) + from flowguard_lib import runtime + index = runtime.repository_state_dir(root) / "contexts" / "index.json" + finally: + if old_home is None: + os.environ.pop("FLOWGUARD_STATE_HOME", None) + else: + os.environ["FLOWGUARD_STATE_HOME"] = old_home + index.parent.mkdir(parents=True, exist_ok=True) index.write_text("{broken", encoding="utf-8") + env = dict(os.environ, FLOWGUARD_STATE_HOME=str(state_home)) for hook in ("flowguard_status_summary.py", "flowguard_prompt_guard.py", "flowguard_stage_summary.py"): p = subprocess.run( [sys.executable, str(HOOKS / hook)], input=json.dumps({"cwd": str(root), "session_id": "s"}), - capture_output=True, text=True, + capture_output=True, text=True, env=env, ) self.assertEqual(p.returncode, 0, f"{hook}: {p.stderr}") self.assertNotIn("Traceback", p.stderr) diff --git a/tests/test_gate.py b/tests/test_gate.py deleted file mode 100644 index a11921e..0000000 --- a/tests/test_gate.py +++ /dev/null @@ -1,93 +0,0 @@ -import json, tempfile, unittest -from pathlib import Path -from scripts.flowguard_lib import gate - -def write(path, data): - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") - -def mk_project(**kw): - proj = { - "version": 1, - "modules": {"order": {"src_roots": ["server/order"], "stack": "java"}, - "web": {"src_roots": ["web/src"], "stack": "vue3"}}, - "current_feature": "order-refund", - "stages": {"architecture": {"status": "accepted"}, - "standards": {"status": kw.get("std", "in_progress")}, - "release": {"status": "pending"}}, - "features": kw.get("features", {"order-refund": {"status": "active", "path": "features/order-refund"}}), - } - return proj - -def mk_feature(stages=None): - base = {s: {"status": "accepted"} for s in ("requirements", "solution", "testcases", "hld", "lld")} - base["review"] = {"status": "pending"} - base["docs"] = {"status": "pending"} - if stages: - base.update({k: {"status": v} for k, v in stages.items()}) - return {"version": 1, "feature": "order-refund", "modules": ["order"], "status": "active", "stages": base} - -class GateTest(unittest.TestCase): - def setUp(self): - self.root = Path(tempfile.mkdtemp()) - - def _init(self, project=None, feature=None): - write(self.root / ".flowguard" / "project.json", project or mk_project()) - write(self.root / ".flowguard" / "features" / "order-refund" / "state.json", feature or mk_feature()) - - def test_classify_path(self): - self._init() - self.assertEqual(gate.classify_path(self.root, ".flowguard/artifacts/01-requirements.md"), "artifact") - self.assertEqual(gate.classify_path(self.root, "server/order/A.java"), "module:order") - self.assertEqual(gate.classify_path(self.root, "README.md"), "other") - self.assertEqual(gate.classify_path(self.root, "web/src/App.vue"), "module:web") - - def test_check_action_matrix(self): - self._init() - rows = [ - ("write_code", "server/order/A.java", True, None), - ("write_code", "web/src/App.vue", False, "gate_write_code_feature_mismatch"), - ("write_code", "README.md", True, None), - ("write_code", ".flowguard/artifacts/01-requirements.md", True, None), - ] - for action, path, allowed, code in rows: - res = gate.check_action(self.root, action, path=path) - self.assertEqual(set(res), {"allowed", "envelope"}) - self.assertEqual(res["allowed"], allowed, f"{action} {path}: {res}") - got = (res["envelope"] or {}).get("code") - self.assertEqual(got, code, f"{action} {path}") - - def test_tdd_gate(self): - self._init(feature=mk_feature({"testcases": "in_progress"})) - res = gate.check_action(self.root, "write_code", path="server/order/A.java") - self.assertFalse(res["allowed"]) - self.assertEqual(res["envelope"]["code"], "gate_write_code_tdd") - - def test_no_current_feature(self): - proj = mk_project() - proj.pop("current_feature") - write(self.root / ".flowguard" / "project.json", proj) - write(self.root / ".flowguard" / "features" / "order-refund" / "state.json", mk_feature()) - res = gate.check_action(self.root, "write_code", path="server/order/A.java") - self.assertFalse(res["allowed"]) - self.assertEqual(res["envelope"]["code"], "gate_write_code_no_module") - - def test_review_docs_release_chain(self): - self._init() - self.assertFalse(gate.check_action(self.root, "write_docs")["allowed"]) - self.assertFalse(gate.check_action(self.root, "build_release")["allowed"]) - f = mk_feature({"review": "accepted", "docs": "accepted"}) - write(self.root / ".flowguard" / "features" / "order-refund" / "state.json", f) - self.assertTrue(gate.check_action(self.root, "write_docs")["allowed"]) - proj = mk_project(std="accepted", - features={"order-refund": {"status": "done", "path": "features/order-refund"}}) - write(self.root / ".flowguard" / "project.json", proj) - self.assertTrue(gate.check_action(self.root, "build_release")["allowed"]) - - def test_uninitialized_flowguard_allows(self): - res = gate.check_action(self.root, "write_code", path="server/order/A.java") - self.assertTrue(res["allowed"]) - self.assertIsNone(res["envelope"]) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_governance_cli.py b/tests/test_governance_cli.py index c93e618..af53883 100644 --- a/tests/test_governance_cli.py +++ b/tests/test_governance_cli.py @@ -89,10 +89,10 @@ def test_stage_command_reads_and_advances_docs_without_private_directory(self): self.assertEqual(json.loads(started.stdout)["status"], "in_progress") self.assertFalse((self.root / ".flowguard").exists()) - def test_legacy_init_cannot_create_private_directory_in_new_project(self): + def test_legacy_commands_are_gone_and_never_create_private_directory(self): result = run(self.root, "legacy-init", "--json") self.assertEqual(result.returncode, 3) - self.assertEqual(json.loads(result.stdout)["code"], "legacy_state_required") + self.assertEqual(json.loads(result.stdout)["code"], "usage") self.assertFalse((self.root / ".flowguard").exists()) def test_init_creates_only_project_stage_documents(self): diff --git a/tests/test_hooks.py b/tests/test_hooks.py index e88aa50..b18ca7d 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -15,11 +15,6 @@ def run_hook(name, payload): return p -def write(path, data): - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") - - def mk_git_repo(): root = Path(tempfile.mkdtemp()) subprocess.run(["git", "init", "-q"], cwd=root, check=True) @@ -66,175 +61,7 @@ def codereview_evidence_fixture(root, *, findings=None, fingerprint=None): "user_disposition": None, "disposition_source": None, "skip_reason": None} -def mk_project(current_feature="order-refund", std="pending", features=None): - return {"version": 1, "project": "demo", "modules": {"app": {"src_roots": ["."], "stack": "java-spring"}}, - "current_feature": current_feature, - "stages": {"architecture": {"status": "pending"}, "standards": {"status": std}, - "release": {"status": "pending"}}, - "features": features if features is not None else - {"order-refund": {"status": "active", "path": "features/order-refund"}}} - - -def mk_feature(stages=None): - base = {s: {"status": "pending"} for s in - ("requirements", "solution", "testcases", "hld", "lld", "review", "docs")} - if stages: - base.update({k: {"status": v} for k, v in stages.items()}) - return {"version": 1, "feature": "order-refund", "modules": ["app"], "status": "active", "stages": base} - - class GateHookTest(unittest.TestCase): - def setUp(self): - self.root = Path(tempfile.mkdtemp()) - write(self.root / ".flowguard" / "project.json", mk_project()) - write(self.root / ".flowguard" / "features" / "order-refund" / "state.json", mk_feature()) - - def test_block_write_with_envelope(self): - p = run_hook("flowguard_gate.py", {"tool_name": "Write", - "tool_input": {"file_path": "app/A.java"}, "cwd": str(self.root)}) - self.assertEqual(p.returncode, 2) - self.assertIn("ERROR:", p.stderr) - self.assertIn("Fix:", p.stderr) - self.assertIn("gate_write_code", p.stderr) - - def test_artifact_path_allowed(self): - p = run_hook("flowguard_gate.py", {"tool_name": "Edit", - "tool_input": {"file_path": ".flowguard/features/order-refund/artifacts/01-requirements.md"}, - "cwd": str(self.root)}) - self.assertEqual(p.returncode, 0) - - def test_malformed_stdin_allowed(self): - p = subprocess.run([sys.executable, str(HOOKS / "flowguard_gate.py")], - input="not json", capture_output=True, text=True) - self.assertEqual(p.returncode, 0) - self.assertIn("WARNING", p.stderr) - - def test_uninitialized_allowed(self): - empty = Path(tempfile.mkdtemp()) - p = run_hook("flowguard_gate.py", {"tool_name": "Write", - "tool_input": {"file_path": "src/A.java"}, "cwd": str(empty)}) - self.assertEqual(p.returncode, 0) - - def test_bash_release_blocked(self): - write(self.root / ".flowguard" / "project.json", mk_project(std="accepted")) - p = run_hook("flowguard_gate.py", {"tool_name": "Bash", - "tool_input": {"command": "mvn deploy -q"}, "cwd": str(self.root)}) - self.assertEqual(p.returncode, 2) - self.assertIn("gate_release_active_features", p.stderr) - - def test_bash_plain_allowed(self): - p = run_hook("flowguard_gate.py", {"tool_name": "Bash", - "tool_input": {"command": "ls -la"}, "cwd": str(self.root)}) - self.assertEqual(p.returncode, 0) - - def test_kimi_shell_read_is_not_rejected_as_unknown_tool(self): - root = mk_git_repo() - p = run_hook("flowguard_gate.py", {"tool_name": "Shell", - "tool_input": {"command": "git status --short"}, - "cwd": str(root), "session_id": "s"}) - self.assertEqual(p.returncode, 0, p.stderr) - - def test_relative_traversal_cannot_disguise_business_write_as_docs_or_tests(self): - root = mk_git_repo() - for file_path in ("docs/../src/app.py", "tests/../src/app.py"): - with self.subTest(file_path=file_path): - result = run_hook("flowguard_gate.py", { - "tool_name": "Write", "tool_input": {"file_path": file_path}, - "cwd": str(root), "session_id": "s", - }) - self.assertEqual(result.returncode, 2, result.stderr) - self.assertIn("governance_context_required", result.stderr) - - def test_symlinked_docs_path_cannot_disguise_business_write(self): - root = mk_git_repo() - (root / "docs").mkdir() - (root / "docs" / "bridge").symlink_to(root / "src", target_is_directory=True) - result = run_hook("flowguard_gate.py", { - "tool_name": "Write", - "tool_input": {"file_path": "docs/bridge/app.py"}, - "cwd": str(root), "session_id": "s", - }) - self.assertEqual(result.returncode, 2, result.stderr) - self.assertIn("governance_context_required", result.stderr) - - def test_file_write_to_another_worktree_is_rejected_before_current_context_gate(self): - root = mk_git_repo() - other = mk_git_repo() - result = run_hook("flowguard_gate.py", { - "tool_name": "Write", - "tool_input": {"file_path": str(other / "src" / "app.py")}, - "cwd": str(root), "session_id": "s", - }) - self.assertEqual(result.returncode, 2, result.stderr) - self.assertIn("governance_write_target_mismatch", result.stderr) - - def test_kimi_read_tools_are_allowed_without_task_binding(self): - root = mk_git_repo() - for tool_name in ("ReadFile", "ReadMediaFile", "SetTodoList"): - with self.subTest(tool_name=tool_name): - result = run_hook("flowguard_gate.py", { - "tool_name": tool_name, "tool_input": {}, - "cwd": str(root), "session_id": "s", - }) - self.assertEqual(result.returncode, 0, result.stderr) - - def test_kimi_file_tools_route_docs_and_code_separately(self): - root = mk_git_repo() - for tool_name in ("WriteFile", "StrReplaceFile"): - with self.subTest(tool_name=tool_name): - docs = run_hook("flowguard_gate.py", { - "tool_name": tool_name, - "tool_input": {"file_path": "docs/features/fix/01-requirements.md"}, - "cwd": str(root), "session_id": "s", - }) - code = run_hook("flowguard_gate.py", { - "tool_name": tool_name, "tool_input": {"file_path": "src/app.py"}, - "cwd": str(root), "session_id": "s", - }) - self.assertEqual(docs.returncode, 0, docs.stderr) - self.assertEqual(code.returncode, 2, code.stderr) - self.assertNotIn("governance_unclassified_tool", code.stderr) - - def test_kimi_shell_commit_is_classified_as_commit(self): - root = mk_git_repo() - p = run_hook("flowguard_gate.py", {"tool_name": "Shell", - "tool_input": {"command": "git commit -m fix"}, - "cwd": str(root), "session_id": "s"}) - self.assertEqual(p.returncode, 2, p.stderr) - self.assertNotIn("governance_unclassified_tool", p.stderr) - - def test_tdd_gate_after_accept(self): - f = mk_feature({s: "accepted" for s in - ("requirements", "solution", "testcases", "hld", "lld")}) - write(self.root / ".flowguard" / "features" / "order-refund" / "state.json", f) - write(self.root / ".flowguard" / "project.json", mk_project(std="accepted")) - p = run_hook("flowguard_gate.py", {"tool_name": "Write", - "tool_input": {"file_path": "app/A.java"}, "cwd": str(self.root)}) - self.assertEqual(p.returncode, 0) - -class ArtifactCheckHookTest(unittest.TestCase): - def setUp(self): - self.root = Path(tempfile.mkdtemp()) - f = mk_feature({s: "accepted" for s in - ("requirements", "solution", "testcases", "hld", "lld", "review", "docs")}) - write(self.root / ".flowguard" / "project.json", mk_project()) - write(self.root / ".flowguard" / "features" / "order-refund" / "state.json", f) - art = self.root / ".flowguard" / "features" / "order-refund" / "artifacts" / "01-requirements.md" - art.parent.mkdir(parents=True, exist_ok=True) - art.write_text("# 需求", encoding="utf-8") - - def test_rework_degrades_downstream(self): - p = run_hook("flowguard_artifact_check.py", - {"tool_name": "Edit", - "tool_input": {"file_path": ".flowguard/features/order-refund/artifacts/01-requirements.md"}, - "cwd": str(self.root)}) - self.assertEqual(p.returncode, 0) - f = json.loads((self.root / ".flowguard" / "features" / "order-refund" / "state.json").read_text()) - self.assertEqual(f["stages"]["requirements"]["status"], "in_progress") - self.assertEqual(f["stages"]["testcases"]["status"], "in_progress") - self.assertEqual(f["stages"]["docs"]["status"], "in_progress") - self.assertIn("artifact_rework_degrade", - (self.root / ".flowguard" / "journal" / "events.jsonl").read_text()) def test_post_hook_ignores_valid_json_that_is_not_an_object(self): for payload in ([], "text", 42, None): @@ -244,12 +71,14 @@ def test_post_hook_ignores_valid_json_that_is_not_an_object(self): self.assertEqual(json.loads(result.stdout), {}) class SummaryHooksTest(unittest.TestCase): - def test_status_summary_initialized(self): - root = Path(tempfile.mkdtemp()) - write(root / ".flowguard" / "project.json", mk_project()) - p = run_hook("flowguard_status_summary.py", {"cwd": str(root)}) - self.assertEqual(p.returncode, 0) + def test_status_summary_shows_bound_context(self): + root = mk_git_repo() + context.bind(root, session_id="s", task_id="fix", task_type="simple_change", + spec_system="none", spec_ref=None) + p = run_hook("flowguard_status_summary.py", {"cwd": str(root), "session_id": "s"}) + self.assertEqual(p.returncode, 0, p.stderr) self.assertIn("[flowguard]", p.stdout) + self.assertIn("fix", p.stdout) def test_status_summary_silent_when_uninitialized(self): p = run_hook("flowguard_status_summary.py", {"cwd": str(Path(tempfile.mkdtemp()))}) @@ -257,12 +86,12 @@ def test_status_summary_silent_when_uninitialized(self): self.assertEqual(p.stdout, "") def test_stage_summary_next_step(self): - root = Path(tempfile.mkdtemp()) - write(root / ".flowguard" / "project.json", mk_project()) - write(root / ".flowguard" / "features" / "order-refund" / "state.json", mk_feature()) - p = run_hook("flowguard_stage_summary.py", {"cwd": str(root)}) - self.assertEqual(p.returncode, 0) - self.assertIn("requirements", p.stdout) + root = mk_git_repo() + context.bind(root, session_id="s", task_id="fix", task_type="simple_change", + spec_system="none", spec_ref=None) + p = run_hook("flowguard_stage_summary.py", {"cwd": str(root), "session_id": "s"}) + self.assertEqual(p.returncode, 0, p.stderr) + self.assertIn("01-requirements", p.stdout) class GovernanceHookTest(unittest.TestCase): @@ -808,16 +637,17 @@ def test_codex_post_apply_patch_reports_invalidated_stage_as_json(self): self.assertEqual(stage_docs.read(root, "fix", "01-requirements")["status"], "invalidated") self.assertIn("阶段文档已失效", json.loads(result.stdout)["systemMessage"]) - def test_direct_governance_state_tampering_is_blocked(self): + def test_host_side_state_tampering_is_rejected_outside_worktree(self): root = mk_git_repo() + home = Path(tempfile.mkdtemp()) p = run_hook( "flowguard_gate.py", {"tool_name": "Edit", - "tool_input": {"file_path": ".flowguard/contexts/fake.json"}, + "tool_input": {"file_path": str(home / "contexts" / "fake.json")}, "cwd": str(root), "session_id": "s"}, ) self.assertEqual(p.returncode, 2) - self.assertIn("governance_state_protected", p.stderr) + self.assertIn("governance_write_target_mismatch", p.stderr) def test_git_commit_requires_evidence_for_bound_context(self): root = mk_git_repo() diff --git a/tests/test_instructions.py b/tests/test_instructions.py deleted file mode 100644 index e3d1f95..0000000 --- a/tests/test_instructions.py +++ /dev/null @@ -1,39 +0,0 @@ -import tempfile, unittest -from pathlib import Path -from scripts.flowguard_lib import instructions, yamlmini - -CONFIG = """\ -schema: flowguard -context: | - Tech stack: Python + Vue -rules: - specs: - - 每条 REQ 必须带验收标准 -""" - -class InstructionsTest(unittest.TestCase): - def setUp(self): - self.root = Path(tempfile.mkdtemp()) - (self.root / ".flowguard").mkdir() - - def test_injects_config_context_and_rules(self): - (self.root / ".flowguard" / "config.yaml").write_text(CONFIG, encoding="utf-8") - ins = instructions.build(self.root, "01-requirements", feature="order-refund") - self.assertIn("Tech stack: Python + Vue", ins["context"]) - self.assertEqual(ins["rules"]["specs"][0], "每条 REQ 必须带验收标准") - self.assertEqual(ins["scope"], "feature") - self.assertEqual(ins["stage"], "requirements") - self.assertTrue(ins["template"].endswith("01-requirements.md")) - self.assertTrue(any("feature-design" == t[0] for t in ins["tier2"])) - for _skill, _pkg, cmd in ins["tier2"]: - self.assertTrue(cmd.startswith("npx skills add full-stack-skills/")) - - def test_missing_config_gives_empty(self): - ins = instructions.build(self.root, "02-architecture") - self.assertEqual(ins["context"], "") - self.assertEqual(ins["rules"], {}) - self.assertEqual(ins["scope"], "project") - self.assertEqual(yamlmini.load.__module__, "scripts.flowguard_lib.yamlmini") - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_journal.py b/tests/test_journal.py deleted file mode 100644 index 382daa2..0000000 --- a/tests/test_journal.py +++ /dev/null @@ -1,27 +0,0 @@ -import datetime, tempfile, unittest -from pathlib import Path -from scripts.flowguard_lib import journal - -class JournalTest(unittest.TestCase): - def setUp(self): - self.root = Path(tempfile.mkdtemp()) - - def test_append_and_read_order(self): - journal.append(self.root, "feature:order-refund", "gate_block", {"code": "x"}) - journal.append(self.root, "project", "accepted", {"stage": "requirements"}) - recs = journal.read(self.root) - self.assertEqual(len(recs), 2) - self.assertEqual(recs[0]["scope"], "feature:order-refund") - self.assertEqual(recs[0]["event"], "gate_block") - self.assertEqual(recs[1]["detail"]["stage"], "requirements") - - def test_timestamp_iso(self): - journal.append(self.root, "project", "init", {}) - ts = journal.read(self.root)[0]["ts"] - datetime.datetime.fromisoformat(ts) # 不抛即可 - - def test_read_empty(self): - self.assertEqual(journal.read(self.root), []) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_registry.py b/tests/test_registry.py index 2984a11..5386ede 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -1,19 +1,6 @@ import unittest from scripts.flowguard_lib import registry -def mk_project(std="in_progress", arch="accepted", features=None): - return {"version": 1, "modules": {"order": {"src_roots": ["server/order"], "stack": "java"}, - "web": {"src_roots": ["web/src"], "stack": "vue3"}}, - "stages": {"architecture": {"status": arch}, "standards": {"status": std}, "release": {"status": "pending"}}, - "features": features or {}} - -def mk_feature(fid="order-refund", modules=("order",), status="active", stages=None): - base = {s: {"status": "accepted"} for s in ("requirements", "solution", "testcases", "hld", "lld")} - base["review"] = {"status": "pending"} - base["docs"] = {"status": "pending"} - if stages: - base.update(stages) - return {"version": 1, "feature": fid, "modules": list(modules), "status": status, "stages": base} class StructureTest(unittest.TestCase): def test_ten_artifacts(self): @@ -34,97 +21,6 @@ def test_topo_requirements_before_testcases(self): self.assertLess(order.index("01-requirements"), order.index("04-testcases")) self.assertLess(order.index("05-hld"), order.index("06-lld")) -class ArtifactStatusTest(unittest.TestCase): - def test_blocked_and_ready(self): - proj = mk_project() - feat = mk_feature(stages={"solution": {"status": "pending"}, - "requirements": {"status": "pending"}}) - # 03-solution requires 01-requirements(文件存在) + 02-architecture(项目阶段 accepted) - status, missing = registry.artifact_status("03-solution", proj, feat) - self.assertEqual(status, "blocked") - self.assertIn("01-requirements", missing) - feat["stages"]["requirements"]["status"] = "accepted" - (self # 占位保持可读 - ) - import pathlib, tempfile - root = pathlib.Path(tempfile.mkdtemp()) - (root / ".flowguard" / "features" / "order-refund" / "artifacts").mkdir(parents=True) - (root / ".flowguard" / "features" / "order-refund" / "artifacts" / "01-requirements.md").write_text("x") - status, missing = registry.artifact_status("03-solution", proj, feat, root=root) - self.assertEqual(status, "ready", missing) - - def test_done_and_skipped(self): - import pathlib, tempfile - proj = mk_project() - feat = mk_feature() - root = pathlib.Path(tempfile.mkdtemp()) - d = root / ".flowguard" / "features" / "order-refund" / "artifacts" - d.mkdir(parents=True) - (d / "01-requirements.md").write_text("x") - status, _ = registry.artifact_status("01-requirements", proj, feat, root=root) - self.assertEqual(status, "done") - feat2 = mk_feature(stages={"hld": {"status": "skipped", "reason": "r"}}) - status, _ = registry.artifact_status("05-hld", proj, feat2, root=root) - self.assertEqual(status, "skipped") - -class UnlockTest(unittest.TestCase): - def test_write_code_happy(self): - ok, env = registry.unlock_check("write_code", mk_project(), mk_feature(), path="server/order/A.java") - self.assertTrue(ok, env) - - def test_write_code_other_path_allowed(self): - ok, env = registry.unlock_check("write_code", mk_project(), mk_feature(), path="README.md") - self.assertTrue(ok) - self.assertIsNone(env) - - def test_write_code_no_feature(self): - ok, env = registry.unlock_check("write_code", mk_project(), None, path="server/order/A.java") - self.assertFalse(ok) - self.assertEqual(env["code"], "gate_write_code_no_module") - - def test_write_code_module_mismatch(self): - ok, env = registry.unlock_check("write_code", mk_project(), mk_feature(), path="web/src/App.vue") - self.assertFalse(ok) - self.assertEqual(env["code"], "gate_write_code_feature_mismatch") - - def test_write_code_tdd_gate(self): - f = mk_feature(stages={"testcases": {"status": "in_progress"}}) - ok, env = registry.unlock_check("write_code", mk_project(), f, path="server/order/A.java") - self.assertFalse(ok) - self.assertEqual(env["code"], "gate_write_code_tdd") - - def test_write_code_needs_standards(self): - ok, env = registry.unlock_check("write_code", mk_project(std="pending"), mk_feature(), path="server/order/A.java") - self.assertFalse(ok) - self.assertEqual(env["code"], "gate_write_code_no_standards") - - def test_write_review_chain(self): - f = mk_feature(stages={"lld": {"status": "in_progress"}}) - ok, env = registry.unlock_check("write_review", mk_project(), f) - self.assertFalse(ok) - self.assertEqual(env["code"], "gate_review_needs_testcases") - ok, env = registry.unlock_check("write_review", mk_project(std="pending"), mk_feature()) - self.assertFalse(ok) - self.assertEqual(env["code"], "gate_review_needs_standards") - ok, _ = registry.unlock_check("write_review", mk_project(arch="accepted", std="accepted"), mk_feature()) - self.assertTrue(ok) - - def test_write_docs_needs_review(self): - ok, env = registry.unlock_check("write_docs", mk_project(), mk_feature()) - self.assertFalse(ok) - self.assertEqual(env["code"], "gate_docs_needs_review") - f = mk_feature(stages={"review": {"status": "accepted"}}) - ok, _ = registry.unlock_check("write_docs", mk_project(), f) - self.assertTrue(ok) - - def test_build_release_needs_no_active(self): - proj = mk_project(std="accepted", features={"order-refund": {"status": "active", "path": "features/order-refund"}}) - ok, env = registry.unlock_check("build_release", proj, None) - self.assertFalse(ok) - self.assertEqual(env["code"], "gate_release_active_features") - proj["features"]["order-refund"]["status"] = "done" - ok, _ = registry.unlock_check("build_release", proj, None) - self.assertTrue(ok) if __name__ == "__main__": unittest.main() diff --git a/tests/test_state.py b/tests/test_state.py deleted file mode 100644 index 9a080ba..0000000 --- a/tests/test_state.py +++ /dev/null @@ -1,96 +0,0 @@ -import tempfile, unittest -from pathlib import Path -from scripts.flowguard_lib import state - -STATUSES = state.STAGE_STATUSES -LEGAL = { - ("pending", "in_progress"), - ("in_progress", "pending_acceptance"), - ("pending_acceptance", "accepted"), - ("pending_acceptance", "skipped"), - ("pending_acceptance", "in_progress"), - ("accepted", "in_progress"), - ("skipped", "in_progress"), - ("overridden", "in_progress"), -} - -class TransitionMatrixTest(unittest.TestCase): - def test_full_6x6_matrix(self): - for frm in STATUSES: - for to in STATUSES: - owner = {"stages": {"s": {"status": frm}}} - should = (frm, to) in LEGAL - if should: - state.transition_stage(owner, "s", to, reason="r") - self.assertEqual(owner["stages"]["s"]["status"], to) - else: - with self.assertRaises(state.StateError, msg=f"{frm}->{to} 应非法"): - state.transition_stage(owner, "s", to, reason="r") - - def test_reason_required_for_optout(self): - owner = {"stages": {"s": {"status": "pending_acceptance"}}} - for to in ("skipped", "overridden"): - with self.assertRaises(state.StateError): - state.transition_stage(owner, "s", to, reason="") - - def test_accepted_writes_accepted_at_and_evidence(self): - owner = {"stages": {"s": {"status": "pending_acceptance"}}} - state.transition_stage(owner, "s", "accepted", evidence=["证据1"]) - self.assertIn("accepted_at", owner["stages"]["s"]) - self.assertEqual(owner["stages"]["s"]["evidence"], ["证据1"]) - -class DegradeTest(unittest.TestCase): - def test_cascade_full(self): - owner = {"stages": {s: {"status": "accepted"} for s in - ("requirements", "testcases", "review", "docs")}} - down = state.degrade_from(owner, "requirements") - self.assertEqual(sorted(down), ["docs", "requirements", "review", "testcases"]) - for s in down: - self.assertEqual(owner["stages"][s]["status"], "in_progress") - - def test_degrade_only_accepted_or_pending(self): - owner = {"stages": {"requirements": {"status": "accepted"}, - "testcases": {"status": "in_progress"}, - "review": {"status": "pending"}, - "docs": {"status": "pending_acceptance"}}} - down = state.degrade_from(owner, "requirements") - self.assertEqual(sorted(down), ["docs", "requirements"]) - -class OverrideTest(unittest.TestCase): - def test_any_status_to_overridden(self): - for frm in STATUSES: - owner = {"stages": {"s": {"status": frm}}} - state.transition_override(owner, "s", reason="hotfix 需求") - self.assertEqual(owner["stages"]["s"]["status"], "overridden") - - def test_reason_required(self): - owner = {"stages": {"s": {"status": "pending"}}} - with self.assertRaises(state.StateError): - state.transition_override(owner, "s", reason="") - -class LockAndIOTest(unittest.TestCase): - def setUp(self): - self.root = Path(tempfile.mkdtemp()) - - def test_lock_mutex(self): - with state.state_lock(self.root): - with self.assertRaises(state.StateError): - with state.state_lock(self.root): - pass - - def test_project_roundtrip(self): - with self.assertRaises(state.StateError): - state.load_project(self.root) # 未初始化 - data = {"version": 1, "project": "demo", "stages": {}} - state.save_project(self.root, data) - self.assertEqual(state.load_project(self.root), data) - - def test_feature_roundtrip(self): - with self.assertRaises(state.StateError): - state.load_feature(self.root, "nope") - data = {"version": 1, "feature": "f1", "status": "active", "stages": {}} - state.save_feature(self.root, data) - self.assertEqual(state.load_feature(self.root, "f1"), data) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_yamlmini.py b/tests/test_yamlmini.py deleted file mode 100644 index e979256..0000000 --- a/tests/test_yamlmini.py +++ /dev/null @@ -1,39 +0,0 @@ -import unittest -from scripts.flowguard_lib import yamlmini - -SAMPLE = """\ -schema: flowguard -context: | - Tech stack: Python - Team: 3 people -rules: - specs: - - 每条 REQ 必须带验收标准 - - 中文书写 -tasks: - - 可勾选 -""" - -class YamlMiniTest(unittest.TestCase): - def test_load_sample(self): - data = yamlmini.load(SAMPLE) - self.assertEqual(data["schema"], "flowguard") - self.assertIn("Tech stack: Python", data["context"]) - self.assertIn("Team: 3 people", data["context"]) - self.assertEqual(data["rules"]["specs"][0], "每条 REQ 必须带验收标准") - self.assertEqual(data["tasks"], ["可勾选"]) - - def test_roundtrip(self): - data = yamlmini.load(SAMPLE) - self.assertEqual(yamlmini.load(yamlmini.dump(data)), data) - - def test_rejects_unknown_shape(self): - with self.assertRaises(ValueError): - yamlmini.load("a:\n b:\n c:\n d: too-deep\n") - - def test_rejects_scalar_list_confusion(self): - with self.assertRaises(ValueError): - yamlmini.load("a: 1\n - item\n") - -if __name__ == "__main__": - unittest.main()