@michaelxwb/code-flow 0.6.4 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +47 -3
  2. package/docs/USAGE.md +1529 -0
  3. package/docs/acceptance-contract.md +217 -0
  4. package/docs/ci/acceptance-evidence.md +77 -0
  5. package/docs/migrations/runtime-0.7.0.md +41 -0
  6. package/docs/migrations/runtime-0.8.0.md +154 -0
  7. package/docs/migrations/runtime-0.9.0.md +40 -0
  8. package/docs/spec-injection-hardening.md +54 -0
  9. package/docs/spec-injection-performance.json +73 -0
  10. package/docs/spec-optimization.md +12 -0
  11. package/package.json +18 -3
  12. package/src/adapters/claude/CLAUDE.md +1 -1
  13. package/src/adapters/claude/commands/cf-init.md +1 -3
  14. package/src/adapters/claude/commands/cf-spec.md +23 -3
  15. package/src/adapters/claude/commands/cf-stats.md +11 -1
  16. package/src/adapters/claude/commands/cf-sync.md +20 -6
  17. package/src/adapters/claude/commands/cf-task/align.md +25 -3
  18. package/src/adapters/claude/commands/cf-task/archive.md +36 -40
  19. package/src/adapters/claude/commands/cf-task/block.md +15 -2
  20. package/src/adapters/claude/commands/cf-task/graph.md +13 -1
  21. package/src/adapters/claude/commands/cf-task/note.md +13 -1
  22. package/src/adapters/claude/commands/cf-task/plan.md +137 -23
  23. package/src/adapters/claude/commands/cf-task/prd.md +13 -2
  24. package/src/adapters/claude/commands/cf-task/start.md +67 -35
  25. package/src/adapters/claude/commands/cf-task/status.md +12 -0
  26. package/src/adapters/claude/commands/cf-task/verify-e2e.md +72 -34
  27. package/src/adapters/claude/commands/cf-validate.md +30 -5
  28. package/src/adapters/codex/AGENTS.md +1 -1
  29. package/src/adapters/codex/hooks.json +14 -10
  30. package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
  31. package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
  32. package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
  33. package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
  34. package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
  35. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -40
  36. package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
  37. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
  38. package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
  39. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
  40. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
  41. package/src/adapters/codex/skills/cf-task-start/SKILL.md +67 -35
  42. package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
  43. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +73 -34
  44. package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
  45. package/src/adapters/commands.json +115 -0
  46. package/src/adapters/costrict/CLAUDE.md +1 -1
  47. package/src/adapters/costrict/commands/cf-init.md +1 -3
  48. package/src/adapters/costrict/commands/cf-spec.md +23 -3
  49. package/src/adapters/costrict/commands/cf-stats.md +11 -1
  50. package/src/adapters/costrict/commands/cf-sync.md +20 -6
  51. package/src/adapters/costrict/commands/cf-task/align.md +25 -3
  52. package/src/adapters/costrict/commands/cf-task/archive.md +36 -40
  53. package/src/adapters/costrict/commands/cf-task/block.md +15 -2
  54. package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
  55. package/src/adapters/costrict/commands/cf-task/note.md +13 -1
  56. package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
  57. package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
  58. package/src/adapters/costrict/commands/cf-task/start.md +67 -35
  59. package/src/adapters/costrict/commands/cf-task/status.md +12 -0
  60. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +72 -34
  61. package/src/adapters/costrict/commands/cf-validate.md +30 -5
  62. package/src/adapters/install-plan.json +100 -0
  63. package/src/adapters/opencode/AGENTS.md +1 -1
  64. package/src/adapters/opencode/commands/cf-init.md +7 -7
  65. package/src/adapters/opencode/commands/cf-learn.md +6 -6
  66. package/src/adapters/opencode/commands/cf-spec.md +34 -2
  67. package/src/adapters/opencode/commands/cf-stats.md +17 -7
  68. package/src/adapters/opencode/commands/cf-sync.md +21 -6
  69. package/src/adapters/opencode/commands/cf-task/align.md +25 -8
  70. package/src/adapters/opencode/commands/cf-task/archive.md +36 -40
  71. package/src/adapters/opencode/commands/cf-task/block.md +16 -2
  72. package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
  73. package/src/adapters/opencode/commands/cf-task/note.md +14 -1
  74. package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
  75. package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
  76. package/src/adapters/opencode/commands/cf-task/start.md +68 -41
  77. package/src/adapters/opencode/commands/cf-task/status.md +13 -0
  78. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +72 -34
  79. package/src/adapters/opencode/commands/cf-validate.md +35 -9
  80. package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
  81. package/src/adapters/platform-bindings.json +370 -0
  82. package/src/cli.js +369 -316
  83. package/src/core/code-flow/config.yml +4 -1
  84. package/src/core/code-flow/runtime-commands.json +259 -0
  85. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
  86. package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
  87. package/src/core/code-flow/scripts/cf_audit.py +10 -0
  88. package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
  89. package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
  90. package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
  91. package/src/core/code-flow/scripts/cf_feedback.py +5 -51
  92. package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
  93. package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
  94. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
  95. package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
  96. package/src/core/code-flow/scripts/cf_spec_context.py +5 -1714
  97. package/src/core/code-flow/scripts/cf_spec_gate.py +5 -257
  98. package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
  99. package/src/core/code-flow/scripts/cf_stats.py +5 -501
  100. package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
  101. package/src/core/code-flow/scripts/cf_sync.py +5 -135
  102. package/src/core/code-flow/scripts/cf_task_index.py +5 -143
  103. package/src/core/code-flow/scripts/cf_task_parallel.py +5 -380
  104. package/src/core/code-flow/scripts/cf_task_workflow.py +6 -103
  105. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
  106. package/src/core/code-flow/scripts/cf_validation.py +6 -77
  107. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  108. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  109. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
  110. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
  111. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
  112. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
  113. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
  114. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
  115. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
  116. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
  117. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
  118. package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
  119. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
  120. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
  121. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
  122. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
  123. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  124. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
  125. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
  126. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
  127. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
  128. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
  129. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
  130. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
  131. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
  132. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
  133. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
  134. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
  135. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
  136. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
  137. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
  138. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
  139. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
  140. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  141. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
  142. package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
  143. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
  144. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
  145. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
  146. package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
  147. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
  148. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
  149. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
  150. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
  151. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
  152. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
  153. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
  154. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
  155. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  156. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
  157. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  158. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
  159. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
  160. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
  161. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
  162. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
  163. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
  164. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  165. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
  166. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
  167. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
  168. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
  169. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
  170. package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
  171. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
  172. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
  173. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
  174. package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +37 -3
  175. package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
  176. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
  177. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
  178. package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
  179. package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
  180. package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +252 -26
  181. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
  182. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
  183. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
  184. package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
  185. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  186. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
  187. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
  188. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
  189. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
  190. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
  191. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
  192. package/src/core/code-flow/scripts/runtime-resources.json +29 -0
  193. package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
  194. package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
  195. package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
  196. package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
  197. package/src/core/code-flow/validation.yml +9 -3
  198. package/src/migrate/runtime.js +213 -0
  199. package/src/migrate/spec-workflow.js +16 -1
  200. package/src/runtime.js +219 -0
  201. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  202. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  203. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  204. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -299
  205. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  206. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  207. /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
@@ -1,5 +1,7 @@
1
1
  # {模块名称} 前端模块需求与设计简报
2
2
 
3
+ > **生成约定**:产出文档的 frontmatter 声明 `design_template: frontend`。浏览器 E2E 必须有匹配的 env/components/boundary,expected 逐条用 expected_refs 关联步骤或断言;deferred 补原因、owner 与 target_phase。API/CLI/后台全链路用 system,容量用 capacity;非 E2E 不写 why_e2e。完成后运行 scenarios parse --quality 与 design lint;计划再校验真实测试登记/收集。完整示例见工具文档 `docs/acceptance-contract.md`。
4
+
3
5
  > **文档编号**: FE-{模块代号}-{版本号}
4
6
  > **文档版本**: v1.0
5
7
  > **创建日期**: YYYY-MM-DD
@@ -24,6 +26,7 @@
24
26
  - [2.2 功能方案](#22-功能方案-必填)
25
27
  - [2.3 范围与边界](#23-范围与边界-必填)
26
28
  - [2.4 验收条件](#24-验收条件-必填)
29
+ - [2.4.1 验证环境假设](#241-验证环境假设-必填)
27
30
  - [3. 前端技术设计](#3-前端技术设计)
28
31
  - [3.1 技术选型](#31-技术选型-必填)
29
32
  - [3.2 页面与路由结构](#32-页面与路由结构-必填)
@@ -90,25 +93,25 @@
90
93
 
91
94
  ### 2.4 验收条件 [必填]
92
95
 
93
- > 交互层面的可验证场景,写到可转 E2E / 组件交互测试断言的粒度(明确操作与可观测 UI 结果)。场景 ID 即测试用例来源。测试层级只能填 `unit` / `integration` / `E2E` / `manual`;跨路由、服务请求、状态更新和最终 UI 的用户流程必须标为 `E2E`。关键真实边界列出不得 mock 的浏览器、路由、状态或服务边界。
94
-
95
- **正常场景**
96
-
97
- | 场景ID | 功能ID | 优先级 | 测试层级 | 关键真实边界 | 操作步骤 | 预期 UI 结果 |
98
- |--------|--------|--------|---------|-------------|---------|-------------|
99
- | S-01 | FEAT-01 | P0 | E2E | Browser → Router → Service → UI | 1. <操作> | <可见结果> |
96
+ > **场景唯一源**:验收场景定义在 `<需求>.scenarios.md`。本节只保留场景引用和前端设计影响,不复制完整场景表;优先级、执行方式、环境、输入等执行元数据以 scenarios 源为准;没有可执行命令的场景必须显式标为 aspirational;plan 阶段投影到 manifest check;测试层级(unit / component / contract / integration / system / capacity / E2E / manual)与关键真实边界定义在 scenarios 源中。
97
+ >
98
+ > **前端 E2E 精度**:表单交互、布局、文案、字段顺序、空态和错误提示用 component + mock API;E2E 只保留真实浏览器 + 真实后端 + 鉴权/DB 的关键用户旅程。E2E 必须有 `why_e2e`、非 smoke 的 `trigger_paths` 和可观察结果,禁止源码字符串/CSS/宽度断言。
100
99
 
101
- **异常场景**
100
+ | 场景ID | 前端设计影响 / 备注 |
101
+ |--------|--------------------|
102
+ | S-01 | 正常交互路径;涉及的页面、组件与真实边界 |
103
+ | E-01 | 异常/空态路径;涉及的组件状态与用户可见反馈 |
102
104
 
103
- | 场景ID | 功能ID | 测试层级 | 关键真实边界 | 触发条件 | UI 表现 |
104
- |--------|--------|---------|-------------|---------|---------|
105
- | E-01 | FEAT-01 | integration | Service → UI | (接口失败 / 空数据 / 校验不通过) | (错误提示 / 空态 / 字段定位) |
105
+ #### 2.4.1 验证环境假设 [必填]
106
106
 
107
- **非功能指标** [按需]
107
+ > 验收场景依赖的容量、硬件、外部依赖与 profile 参数必须在设计阶段写清;执行期缺失时应返回 `env_blocked` 并列出 owner,不得伪装成产品失败。
108
108
 
109
- | 指标ID | 指标名称 | 目标值 | 测量方法 |
110
- |--------|---------|-------|---------|
111
- | NFR-PERF-01 | 首屏 / 交互响应 | 待定 | Lighthouse / 实测 |
109
+ | 维度 | 假设 | 验证方式 / 参数 | 不满足时 |
110
+ |------|------|----------------|---------|
111
+ | 容量 | (页面/组件规模、并发用户、浏览器矩阵;无实测填"待定") | Playwright projects / 基准 fixture | 标记 `incomplete` 或拆分批次 |
112
+ | 硬件 | (CI runner / 设备 / 视口规格) | preflight 检查 | 返回 `env_blocked` |
113
+ | 依赖 | (后端 API / 浏览器版本 / 测试账号 / 字体与静态资源) | `preflight` 命令(health check) | 返回 `env_blocked` 并列出缺失项 |
114
+ | profile 参数 | (smoke / changed / full、浏览器矩阵、重试与超时) | `verify --layers e2e --profile ...` | 显式报 `incomplete`,不自动降级 |
112
115
 
113
116
  ---
114
117
 
@@ -1,5 +1,7 @@
1
1
  # {模块名称} 模块需求与设计一体化文档
2
2
 
3
+ > **生成约定**:产出文档的 frontmatter 声明 `design_template: full`。浏览器 E2E 必须有匹配的 env/components/boundary,expected 逐条用 expected_refs 关联步骤或断言;deferred 补原因、owner 与 target_phase。API/CLI/后台全链路用 system,容量用 capacity;非 E2E 不写 why_e2e。完成后运行 scenarios parse --quality 与 design lint;计划再校验真实测试登记/收集。完整示例见工具文档 `docs/acceptance-contract.md`。
4
+
3
5
  > **文档编号**: MOD-{模块代号}-{版本号}
4
6
  > **文档版本**: v1.0
5
7
  > **创建日期**: YYYY-MM-DD
@@ -28,6 +30,7 @@
28
30
  - [2.3 功能方案](#23-功能方案-必填)
29
31
  - [2.4 范围与边界](#24-范围与边界-必填)
30
32
  - [2.5 验收条件](#25-验收条件-必填)
33
+ - [2.5.4 验证环境假设](#254-验证环境假设-必填)
31
34
  - [3. 技术设计](#3-技术设计)
32
35
  - [3.1 方案选型](#31-方案选型-必填)
33
36
  - [3.2 架构设计](#32-架构设计-必填)
@@ -131,6 +134,8 @@
131
134
 
132
135
  ### 2.5 验收条件 [必填]
133
136
 
137
+ > **场景唯一源**:验收场景定义在 `<需求>.scenarios.md`。本节只保留业务规则、设计影响和场景引用;不要在此复制完整全局场景表。
138
+
134
139
  #### 2.5.1 业务规则与约束
135
140
 
136
141
  | ID | 类型 | 描述 | 验证场景 |
@@ -140,27 +145,17 @@
140
145
 
141
146
  #### 2.5.2 功能验收场景
142
147
 
143
- > 场景写到可转自动化测试断言的粒度(明确前置、输入、可观测输出)。场景 ID(S-/E-/B-)即测试用例来源,§6 追溯矩阵直接引用这些场景 ID。
148
+ > **场景唯一源**:验收场景定义在 `<需求>.scenarios.md`。设计文档只引用全局场景 ID,并说明设计影响,不复制完整场景字段;优先级、执行方式、环境、输入等执行元数据以 scenarios 源为准;没有可执行命令的场景必须显式标为 aspirational;plan 阶段投影到 manifest check;测试层级(unit / component / contract / integration / system / capacity / E2E / manual)与关键真实边界定义在 scenarios 源中。
144
149
  >
145
- > **测试层级**只能填 `unit` / `integration` / `E2E` / `manual`。跨 API、存储、运行时生成、渲染或用户可见结果等多个边界的场景必须标为 `E2E`;`manual` 仅限无法自动化的外部条件,并记录原因。**关键真实边界**列出测试中不得 mock 的组件或最终可观测面,编码阶段不得自行降级。
146
-
147
- **正常场景**
148
-
149
- | 场景ID | 功能ID | 优先级 | 测试层级 | 关键真实边界 | 前置条件 | 操作步骤 | 预期结果 |
150
- |--------|--------|--------|---------|-------------|---------|---------|---------|
151
- | S-01 | FEAT-01 | P0 | E2E | API → Store → 最终输出 | | | |
152
-
153
- **异常场景**
154
-
155
- | 场景ID | 功能ID | 测试层级 | 关键真实边界 | 触发条件 | 系统行为 | 用户感知 |
156
- |--------|--------|---------|-------------|---------|---------|---------|
157
- | E-01 | FEAT-01 | integration | Service → Store | | | |
158
-
159
- **边界场景** [按需]
150
+ > **E2E 精度**:E2E 只用于 P0/P1 关键用户旅程,且必须有 `why_e2e`(低层测试为什么不够)、非 smoke 的 `trigger_paths`(哪些源码变化才触发)和可观察结果。UI 布局/文案/字段顺序用 component,DTO/错误码用 contract,鉴权/事务/模块边界用 integration;不要用 E2E 覆盖所有用户可见结果。
151
+ >
152
+ > ✅ CAS 409:contract 验响应、component 验表单保留、integration 用真实存储验并发;聚合补零与关系绑定同样优先采用相应低层组合。Cookie 隔离和 SSE 恢复保留必要的跨边界关键验证。❌ 设固定 E2E 数量或比例;比例和任务耗时仅作诊断,零 E2E 的完整覆盖合法。
160
153
 
161
- | 场景ID | 测试层级 | 关键真实边界 | 字段/条件 | 边界值 | 预期行为 |
162
- |--------|---------|-------------|----------|--------|---------|
163
- | B-01 | unit | 校验函数 | | | |
154
+ | 场景ID | 设计影响 / 备注 |
155
+ |--------|----------------|
156
+ | S-01 | 正常路径;涉及的设计组件与真实边界 |
157
+ | E-01 | 异常路径;涉及的设计组件与失败表现 |
158
+ | B-01 | 边界条件;按需 |
164
159
 
165
160
  #### 2.5.3 非功能指标 [按需]
166
161
 
@@ -184,6 +179,17 @@
184
179
  | NFR-SEC-01 | 认证鉴权 | 未授权用户无法访问 |
185
180
  | NFR-SEC-02 | 数据加密 | 敏感字段不可明文 |
186
181
 
182
+ #### 2.5.4 验证环境假设 [必填]
183
+
184
+ > 验收场景依赖的容量、硬件、外部依赖与 profile 参数必须在设计阶段写清;执行期缺失时应由 runner 返回 `env_blocked` 并列出 owner,不得伪装成产品失败。
185
+
186
+ | 维度 | 假设 | 验证方式 / 参数 | 不满足时 |
187
+ |------|------|----------------|---------|
188
+ | 容量 | (数据量、并发数、场景规模,如 119 任务 / 62 E2E;无实测填"待定") | 基准 fixture / 压测命令 | 标记 `incomplete` 或拆分批次 |
189
+ | 硬件 | (CPU / 内存 / 磁盘 / 网络规格,如 8C16G) | preflight 检查 | 返回 `env_blocked` 并列出缺失项 |
190
+ | 依赖 | (数据库 / 浏览器 / API / CI runner / 端口 / 测试账号) | `preflight` 命令(health check、端口探测) | 返回 `env_blocked` 并列出 owner |
191
+ | profile 参数 | (smoke / changed / full 选择、deadline、并行度、重试与超时) | `verify --layers ... --profile ... --deadline ...` | 显式报 `incomplete`,不自动降级 |
192
+
187
193
  ---
188
194
 
189
195
  ## 3. 技术设计
@@ -465,11 +471,15 @@ flowchart TD
465
471
 
466
472
  ---
467
473
 
468
- ## 6. 需求追溯矩阵
474
+ ## 6. 需求追溯矩阵(生成式)
475
+
476
+ > 本节由 `cf_traceability.py --design <design.md> --manifest <.acceptance-manifest.json>` 从 §2.5 与 manifest **生成,不手工维护**;不要直接编辑下面的表格,提交前用生成结果替换。
477
+ >
478
+ > 生成矩阵会列出每个场景的优先级、测试层级、执行方式、环境、输入、追溯 check、执行命令、owner 与状态;没有 check 且未显式标为 `aspirational` 的场景会让生成命令返回 block。
469
479
 
470
- | 用户故事 | 功能ID | 接口ID | 测试用例ID | 测试层级 | 状态 |
471
- |---------|--------|--------|-----------|---------|------|
472
- | US-01 | FEAT-01 | API-01 | S-01 | E2E | 待实现 |
480
+ | 用户故事 | 功能ID | 接口ID | 测试用例ID | 测试层级 | 执行方式 | 追溯 check | 状态 |
481
+ |---------|--------|--------|-----------|---------|---------|-----------|------|
482
+ | US-01 | FEAT-01 | API-01 | S-01 | E2E | auto | CHK-xxxx | 待实现 |
473
483
 
474
484
  > 用户故事一列引用 PRD 中的 US-XX(无 PRD 时填"需求描述");测试用例 ID 列引用 §2.5.2 的场景 ID(S-/E-/B-),不要另造编号。RULE 与高影响 RISK 必须映射到场景;无法自动化时写明手动验证原因,不得留空。
475
485
 
@@ -1,5 +1,7 @@
1
1
  # {模块名称} 模块需求与设计简报
2
2
 
3
+ > **生成约定**:产出文档的 frontmatter 声明 `design_template: lite`。浏览器 E2E 必须有匹配的 env/components/boundary,expected 逐条用 expected_refs 关联步骤或断言;deferred 补原因、owner 与 target_phase。API/CLI/后台全链路用 system,容量用 capacity;非 E2E 不写 why_e2e。完成后运行 scenarios parse --quality 与 design lint;计划再校验真实测试登记/收集。完整示例见工具文档 `docs/acceptance-contract.md`。
4
+
3
5
  > **文档编号**: MOD-{模块代号}-{版本号}
4
6
  > **文档版本**: v1.0
5
7
  > **创建日期**: YYYY-MM-DD
@@ -29,6 +31,7 @@
29
31
  - [2.2 功能方案](#22-功能方案-必填)
30
32
  - [2.3 范围与边界](#23-范围与边界-必填)
31
33
  - [2.4 验收条件](#24-验收条件-必填)
34
+ - [2.4.3 验证环境假设](#243-验证环境假设-必填)
32
35
  - [3. 技术设计](#3-技术设计)
33
36
  - [3.1 技术选型](#31-技术选型-必填)
34
37
  - [3.2 架构设计](#32-架构设计-必填)
@@ -106,23 +109,19 @@
106
109
 
107
110
  ### 2.4 验收条件 [必填]
108
111
 
112
+ > **场景唯一源**:验收场景定义在 `<需求>.scenarios.md`。本节只保留业务规则、设计影响和场景引用;不要在此复制完整全局场景表。
113
+
109
114
  #### 2.4.1 功能验收场景
110
115
 
111
- > 每个 P0/P1 FEAT 至少 1 条正常 + 1 条异常场景;"操作步骤/预期结果"写到可直接转为自动化测试断言的粒度(明确输入与可观测输出)。场景 ID(S-/E-/B-)即测试用例编号,供 plan 拆解引用。
116
+ > **场景唯一源**:验收场景定义在 `<需求>.scenarios.md`。设计文档只引用全局场景 ID,并说明设计影响,不复制完整场景字段;优先级、执行方式、环境、输入等执行元数据以 scenarios 源为准;没有可执行命令的场景必须显式标为 aspirational;plan 阶段投影到 manifest check;测试层级(unit / component / contract / integration / system / capacity / E2E / manual)与关键真实边界定义在 scenarios 源中。
112
117
  >
113
- > **测试层级**只能填 `unit` / `integration` / `E2E` / `manual`。跨 API、存储、运行时生成、渲染或用户可见结果等多个边界的场景必须标为 `E2E`;`manual` 仅限无法自动化的外部条件,并记录原因。**关键真实边界**列出测试中不得 mock 的组件或最终可观测面,编码阶段不得自行降级。
114
-
115
- **正常场景**
116
-
117
- | 场景ID | 功能ID | 优先级 | 测试层级 | 关键真实边界 | 操作步骤 | 预期结果 |
118
- |--------|--------|--------|---------|-------------|---------|---------|
119
- | S-01 | FEAT-01 | P0 | E2E | API → Store → 最终输出 | 1. <步骤><br>2. <步骤> | <结果> |
118
+ > **E2E 精度**:E2E 只用于 P0/P1 关键用户旅程,且必须有 `why_e2e`、非 smoke 的 `trigger_paths` 和可观察结果。UI 布局/文案/字段顺序用 component,DTO/错误码用 contract,鉴权/事务/模块边界用 integration。
120
119
 
121
- **异常场景**
122
-
123
- | 场景ID | 功能ID | 测试层级 | 关键真实边界 | 触发条件 | 系统行为 |
124
- |--------|--------|---------|-------------|---------|---------|
125
- | E-01 | FEAT-01 | integration | Service → Store | | |
120
+ | 场景ID | 设计影响 / 备注 |
121
+ |--------|----------------|
122
+ | S-01 | 正常路径;涉及的设计组件与真实边界 |
123
+ | E-01 | 异常路径;涉及的设计组件与失败表现 |
124
+ | B-01 | 边界条件;按需 |
126
125
 
127
126
  #### 2.4.2 非功能指标 [按需]
128
127
 
@@ -131,6 +130,17 @@
131
130
  | NFR-PERF-01 | 响应时间(P95) | ≤200ms | APM监控 |
132
131
  | NFR-REL-01 | 可用性(SLA) | ≥99.9% | 年度可用时长 |
133
132
 
133
+ #### 2.4.3 验证环境假设 [必填]
134
+
135
+ > 验收场景依赖的容量、硬件、外部依赖与 profile 参数必须在设计阶段写清;执行期缺失时应返回 `env_blocked` 并列出 owner,不得伪装成产品失败。
136
+
137
+ | 维度 | 假设 | 验证方式 / 参数 | 不满足时 |
138
+ |------|------|----------------|---------|
139
+ | 容量 | (数据量、并发数、场景规模;无实测填"待定") | 基准 fixture / 压测命令 | 标记 `incomplete` 或拆分批次 |
140
+ | 硬件 | (CPU / 内存 / 磁盘 / 网络规格) | preflight 检查 | 返回 `env_blocked` |
141
+ | 依赖 | (数据库 / 浏览器 / API / 端口 / 测试账号) | `preflight` 命令 | 返回 `env_blocked` 并列出缺失项 |
142
+ | profile 参数 | (smoke / changed / full、deadline、并行度、重试) | `verify --profile ... --deadline ...` | 显式报 `incomplete`,不自动降级 |
143
+
134
144
  ---
135
145
 
136
146
  ## 3. 技术设计
@@ -0,0 +1,55 @@
1
+ ---
2
+ id: scenarios-template
3
+ description: Scenario-first acceptance source template (not auto-injected)
4
+ stages: [design]
5
+ enforcement: advisory
6
+ ---
7
+
8
+ # {需求名} 验收场景
9
+
10
+ > 本文件是验收场景的唯一源。design 文档只写实现逻辑并引用场景 ID;任务文件只登记 owner 与执行命令。
11
+ >
12
+ > 以下是可通过场景质量检查的订单示例,使用时替换为本需求的真实行为与路径。将 YAML 放到 `<需求>.scenarios.md` 的 frontmatter 或作为整个 YAML 文件保存;不要把模板自身的 Spec 元数据当成场景源。`checks` 在 plan 生成真实 check ID 后绑定,空列表不能通过需要测试登记的 verify-plan。
13
+
14
+ ```yaml
15
+ schema: 1
16
+ requirement: 订单创建
17
+ scenarios:
18
+ - id: S-01
19
+ title: 登录用户通过浏览器创建订单
20
+ priority: P0 # P0 / P1 / P2;E2E 只允许 P0/P1 关键旅程
21
+ level: e2e # unit / component / contract / integration / system / capacity / e2e / manual
22
+ exec: deferred # auto / deferred / manual / aspirational
23
+ env: browser # none / local / local-api / browser / external / staging / capacity / ci / production
24
+ components: [browser, frontend, api, db]
25
+ boundary: 真实浏览器 → 订单 API → 数据库
26
+ preconditions: [用户已登录,购物车含一件库存充足的商品]
27
+ steps: [在浏览器点击提交订单]
28
+ expected: [订单列表新增一条状态为 created 的订单]
29
+ expected_refs: [{expected: 1, steps: [1]}] # 从 1 开始,逐个 expected 引用步骤或已声明断言 ID
30
+ why_e2e: 真实浏览器登录态与订单持久化跨前后端,需要联合验证
31
+ trigger_paths: [src/frontend/*, src/orders/*]
32
+ smoke: true # check 与收集到的测试都必须登记 smoke marker
33
+ defer_reason: 在需求终验阶段运行真实浏览器链路
34
+ owner: TASK-001
35
+ target_phase: review # plan / code / review / ci / release
36
+ features: [FEAT-01]
37
+ design_refs: [API-01] # 对应设计必须定义 FEAT-01 / API-01
38
+ rules: []
39
+ checks: [] # plan 阶段绑定;一个场景可多个 check
40
+ ```
41
+
42
+ > **测试层级决策**:UI 交互/布局/字段顺序 → `component`;DTO/OpenAPI/错误码 →
43
+ > `contract`;鉴权/事务/模块边界/进程内 API → `integration`;API/CLI/后台全链路 →
44
+ > `system`;容量 → `capacity` 或 capacity 标签;只有真实浏览器 +
45
+ > 真实后端 + 真实 DB/鉴权的关键旅程才用 `E2E`。E2E 不是“用户可见结果”的默认层。
46
+
47
+ > CAS 409 示例:contract 检查冲突响应,component 检查表单保留,integration 用真实存储验证并发写入;分别登记 check,按 expected 映射必要成员。低层覆盖完整时可以不新增 E2E;仅为剩余跨边界接线风险选择 E2E,说明独特故障、已有低层覆盖及剩余风险,由设计评审判断理由。
48
+ >
49
+ > ✅ CAS 用 contract + component + 真实存储 integration;聚合补零用数据 contract + component;关系绑定用真实存储 integration。Cookie 身份隔离与 SSE 断线恢复仍可保留针对性的浏览器 E2E,验证低层无法证明的跨边界接线。
50
+ >
51
+ > ❌ 因用户看得到结果就为每个表单、聚合或关系操作新增一条 E2E;也不要只改旧 E2E 的层级后继承历史通过证据。
52
+ >
53
+ > **质量门禁**:E2E 必须声明浏览器 env/组件、匹配的 boundary、why_e2e、步骤和具体预期;非 smoke 必须有 trigger_paths。每个 expected 必须引用存在的步骤或断言,deferred 必须有原因、owner 与 target_phase。
54
+ >
55
+ > `scenarios parse --quality` 检查语义,`design lint` 检查设计 ID 闭环,`acceptance manifest --verify-plan` 还检查 Spec Gate 与真实测试登记/收集。start 按当前 TASK 的 E2E 与全局 smoke 生成 e2e_plan,不额外扩大本任务验收范围。收集通过不能替代真实执行证据;配置示例见工具文档 `docs/acceptance-contract.md`。
@@ -1,6 +1,7 @@
1
1
  # validators 在每次 Stop 按“会话内编辑过的文件”匹配执行(quality_loop.stop_check)。
2
- # 全量测试 / e2e 等慢命令标 `heavy: true`:Stop 每轮跳过,任务 finish 自动执行
3
- # 一次,也可显式运行 /cf-validate;避免每个任务都重跑多遍。
2
+ # 全量测试 / e2e 等慢命令标 `heavy: true`:Stop 与任务 finish 都跳过(deferred_heavy),
3
+ # 只由需求终验 verify-e2e / 归档与显式 /cf-validate 全量执行一次;结果按工作树内容指纹
4
+ # 缓存,同一内容不会重复执行。
4
5
  validators:
5
6
  - name: "Python 语法检查"
6
7
  trigger: "**/*.py"
@@ -12,24 +13,28 @@ validators:
12
13
  trigger: "**/*.{ts,tsx}"
13
14
  command: "npx tsc --noEmit"
14
15
  timeout: 30000
16
+ skip_if_missing: true # 未安装 typescript 时跳过(degrade 记录),不阻断
15
17
  on_fail: "检查类型定义,参见 specs/frontend/quality-standards.md"
16
18
 
17
19
  - name: "Vue 类型检查"
18
20
  trigger: "**/*.vue"
19
21
  command: "npx vue-tsc --noEmit"
20
22
  timeout: 30000
23
+ skip_if_missing: true
21
24
  on_fail: "检查 Vue 组件类型定义,参见 specs/frontend/component-specs.md"
22
25
 
23
26
  - name: "ESLint"
24
27
  trigger: "**/*.{ts,tsx,js,jsx,vue}"
25
28
  command: "npx eslint {files}"
26
29
  timeout: 15000
30
+ skip_if_missing: true
27
31
  on_fail: "运行 npx eslint --fix 自动修复,参见 specs/frontend/quality-standards.md"
28
32
 
29
33
  - name: "Stylelint"
30
34
  trigger: "**/*.{css,scss,less}"
31
35
  command: "npx stylelint {files}"
32
36
  timeout: 15000
37
+ skip_if_missing: true
33
38
  on_fail: "运行 npx stylelint --fix 自动修复,参见 specs/frontend/component-specs.md"
34
39
 
35
40
  - name: "前端单元测试"
@@ -43,11 +48,12 @@ validators:
43
48
  trigger: "**/*.py"
44
49
  command: "python3 -m mypy {files}"
45
50
  timeout: 30000
51
+ skip_if_missing: true # 未安装 mypy 时跳过(degrade 记录),新项目不再被阻断
46
52
  on_fail: "检查类型注解,参见 specs/backend/code-quality-performance.md"
47
53
 
48
54
  - name: "Pytest"
49
55
  trigger: "**/*.py"
50
56
  command: "python3 -m pytest --tb=short -q"
51
- timeout: 60000
57
+ timeout: 600000
52
58
  heavy: true
53
59
  on_fail: "测试失败,检查断言和 mock 是否需要更新"
@@ -0,0 +1,213 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const runtime = require('../runtime');
7
+ const pkg = require('../../package.json');
8
+
9
+ function guardVersion(root) {
10
+ const version = fs.readFileSync(path.join(root, '.code-flow/.version'), 'utf8').trim();
11
+ if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error(`Invalid installed runtime version: ${version}`);
12
+ const installed = version.split('.').map(Number);
13
+ const target = pkg.version.split('.').map(Number);
14
+ for (let index = 0; index < 3; index++) {
15
+ if (installed[index] > target[index]) throw new Error(`Refusing runtime downgrade from ${version} to ${pkg.version}; update the CLI.`);
16
+ if (installed[index] < target[index]) return;
17
+ }
18
+ }
19
+
20
+ function walk(root, directory = '') {
21
+ const target = path.join(root, directory);
22
+ if (!fs.existsSync(target)) return [];
23
+ return fs.readdirSync(target, { withFileTypes: true }).flatMap(entry => {
24
+ if (entry.name === '__pycache__') return [];
25
+ const relative = path.join(directory, entry.name);
26
+ return entry.isDirectory() ? walk(root, relative) : [relative];
27
+ });
28
+ }
29
+
30
+ function platforms(root) {
31
+ const markers = { claude: '.claude/commands/cf-init.md', codex: '.agents/skills/cf-init/SKILL.md',
32
+ costrict: '.costrict/commands/cf-init.md', opencode: '.opencode/commands/cf-init.md' };
33
+ const found = Object.entries(markers).filter(([, marker]) => fs.existsSync(path.join(root, marker))).map(([name]) => name);
34
+ if (!found.length) throw new Error('No installed platform found; run code-flow init first.');
35
+ return found;
36
+ }
37
+
38
+ function inputs(root, installed) {
39
+ const core = path.join(__dirname, '../core/code-flow');
40
+ const files = [...Object.keys(runtime.artifactSources(installed)), ...walk(core).map(file => `.code-flow/${file}`),
41
+ '.code-flow/scripts/cf_codex_user_prompt_hook.py',
42
+ '.code-flow/.version', '.code-flow/.adapter-versions.json', '.code-flow/.runtime-install.json',
43
+ '.code-flow/.gitignore', 'AGENTS.md', 'CLAUDE.md', 'opencode.json', '.codex/config.toml',
44
+ ...Object.keys(runtime.hookContracts(installed))];
45
+ return [...new Set(files)].filter(file => fs.existsSync(path.join(root, file)));
46
+ }
47
+
48
+ function copyFile(root, target, relative) {
49
+ const destination = path.join(target, relative);
50
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
51
+ fs.copyFileSync(path.join(root, relative), destination);
52
+ }
53
+
54
+ function writeJson(file, value) {
55
+ fs.writeFileSync(`${file}.tmp`, JSON.stringify(value, null, 2) + '\n');
56
+ fs.renameSync(`${file}.tmp`, file);
57
+ }
58
+
59
+ function staleScripts(root, installed) {
60
+ // 0.9 moved the flat cf_*.py implementation modules under codeflow/{hooks,cli};
61
+ // upgrades must remove the old flat files, keeping the 22 path-frozen shims.
62
+ const current = new Set(Object.keys(runtime.artifactSources(installed)));
63
+ return walk(root, '.code-flow/scripts')
64
+ .filter(file => file.endsWith('.py'))
65
+ .filter(file => !current.has(file.replace(/\\/g, '/')));
66
+ }
67
+
68
+ function prepare(root, locked = false) {
69
+ guardVersion(root);
70
+ const installed = platforms(root);
71
+ const id = `runtime-${Date.now()}-${process.pid}`;
72
+ const workspace = path.join(root, '.code-flow/migrations', id);
73
+ const staging = path.join(workspace, 'staging');
74
+ fs.mkdirSync(staging, { recursive: true });
75
+ fs.mkdirSync(path.join(workspace, 'backup'));
76
+ const stale = staleScripts(root, installed);
77
+ const original = [...new Set([...inputs(root, installed), ...stale])];
78
+ const sources = Object.fromEntries(original.map(file => [file, runtime.digest(path.join(root, file))]));
79
+ const journal = { schema_version: 1, id, root, platforms: installed,
80
+ status: 'staging', sources, hashes: {}, deletions: [], written: [] };
81
+ writeJson(path.join(workspace, 'journal.json'), journal);
82
+ if (locked) writeJson(path.join(root, '.code-flow/.runtime-migration.lock/owner.json'), { pid: process.pid, id });
83
+ for (const relative of original) {
84
+ copyFile(root, staging, relative);
85
+ copyFile(root, path.join(workspace, 'backup'), relative);
86
+ }
87
+ writeJson(path.join(staging, '.runtime-stage.json'), { root, id });
88
+ for (const platform of installed) {
89
+ const result = spawnSync(process.execPath, [path.join(__dirname, '../cli.js'), 'init', `--platform=${platform}`], {
90
+ cwd: staging, encoding: 'utf8', env: { ...process.env, CODE_FLOW_RUNTIME_STAGE: staging },
91
+ });
92
+ if (result.error || result.status !== 0) throw new Error(`Runtime staging failed: ${result.error || result.stderr || result.stdout}`);
93
+ }
94
+ for (const relative of stale) fs.rmSync(path.join(staging, relative), { force: true });
95
+ runtime.verify(staging);
96
+ const targets = walk(staging).filter(file => file !== '.runtime-stage.json' && !file.startsWith('.code-flow/backups/'));
97
+ const hashes = Object.fromEntries(targets.map(file => [file, runtime.digest(path.join(staging, file))]));
98
+ journal.deletions = original.filter(file => !fs.existsSync(path.join(staging, file)));
99
+ journal.hashes = hashes;
100
+ journal.status = 'prepared';
101
+ writeJson(path.join(workspace, 'journal.json'), journal);
102
+ return { workspace, journal, staging };
103
+ }
104
+
105
+ function restore(workspace, journal) {
106
+ const order = [...journal.written].reverse();
107
+ for (const relative of order) {
108
+ const target = path.join(journal.root, relative);
109
+ const backup = path.join(workspace, 'backup', relative);
110
+ if (fs.existsSync(backup)) copyFile(path.join(workspace, 'backup'), journal.root, relative);
111
+ else fs.rmSync(target, { force: true });
112
+ }
113
+ journal.status = 'rolled_back';
114
+ writeJson(path.join(workspace, 'journal.json'), journal);
115
+ return { status: journal.status, migration_id: journal.id };
116
+ }
117
+
118
+ function apply(root) {
119
+ let repairReason;
120
+ try {
121
+ runtime.verify(root, true);
122
+ return { status: 'already_migrated' };
123
+ } catch (error) {
124
+ repairReason = error.message;
125
+ }
126
+ const { workspace, journal, staging } = prepare(root, true);
127
+ try {
128
+ for (const [relative, hash] of Object.entries(journal.sources)) {
129
+ if (runtime.digest(path.join(root, relative)) !== hash) throw new Error(`Source changed during staging: ${relative}`);
130
+ }
131
+ journal.status = 'applying';
132
+ writeJson(path.join(workspace, 'journal.json'), journal);
133
+ const targets = Object.keys(journal.hashes).filter(file => file !== '.code-flow/.version');
134
+ for (const relative of journal.deletions) {
135
+ journal.written.push(relative);
136
+ writeJson(path.join(workspace, 'journal.json'), journal);
137
+ fs.rmSync(path.join(root, relative));
138
+ }
139
+ targets.push('.code-flow/.version');
140
+ for (const relative of targets) {
141
+ if (!journal.sources[relative] && fs.existsSync(path.join(root, relative))) throw new Error(`New target appeared: ${relative}`);
142
+ if (runtime.digest(path.join(staging, relative)) !== journal.hashes[relative]) throw new Error(`Staging drift: ${relative}`);
143
+ journal.written.push(relative);
144
+ writeJson(path.join(workspace, 'journal.json'), journal);
145
+ copyFile(staging, root, relative);
146
+ }
147
+ runtime.verify(root, true);
148
+ journal.status = 'committed';
149
+ writeJson(path.join(workspace, 'journal.json'), journal);
150
+ return { status: journal.status, migration_id: journal.id, platforms: journal.platforms, repair_reason: repairReason };
151
+ } catch (error) {
152
+ const result = restore(workspace, journal);
153
+ return { ...result, error: error.message };
154
+ }
155
+ }
156
+
157
+ function rollback(root, id) {
158
+ if (!/^runtime-\d+-\d+$/.test(id)) throw new Error('Invalid runtime migration id.');
159
+ const workspace = path.join(root, '.code-flow/migrations', id);
160
+ const journal = JSON.parse(fs.readFileSync(path.join(workspace, 'journal.json'), 'utf8'));
161
+ if (journal.root !== root) throw new Error('Migration belongs to a different project.');
162
+ if (journal.status === 'rolled_back') return { status: 'rolled_back', migration_id: id };
163
+ if (journal.status === 'committed') {
164
+ for (const [relative, hash] of Object.entries(journal.hashes)) {
165
+ if (runtime.digest(path.join(root, relative)) !== hash) throw new Error(`Rollback would overwrite subsequent changes: ${relative}`);
166
+ }
167
+ }
168
+ return restore(workspace, journal);
169
+ }
170
+
171
+ function withLock(root, values) {
172
+ const lock = path.join(root, '.code-flow/.runtime-migration.lock');
173
+ try {
174
+ fs.mkdirSync(lock);
175
+ } catch (error) {
176
+ if (error.code !== 'EEXIST') throw error;
177
+ const owner = JSON.parse(fs.readFileSync(path.join(lock, 'owner.json'), 'utf8'));
178
+ let live = true;
179
+ try { process.kill(owner.pid, 0); } catch (signalError) {
180
+ if (signalError.code !== 'ESRCH') throw signalError;
181
+ live = false;
182
+ }
183
+ if (live) throw new Error('Another runtime migration is running.');
184
+ if (values[0] !== '--rollback' || values[1] !== owner.id) {
185
+ throw new Error(`Interrupted migration; run code-flow migrate --runtime --rollback ${owner.id}`);
186
+ }
187
+ fs.rmSync(lock, { recursive: true });
188
+ fs.mkdirSync(lock);
189
+ }
190
+ writeJson(path.join(lock, 'owner.json'), { pid: process.pid, id: '' });
191
+ try {
192
+ return values[0] === '--apply' ? apply(root) : rollback(root, values[1]);
193
+ } finally {
194
+ fs.rmSync(lock, { recursive: true });
195
+ }
196
+ }
197
+
198
+ function run(root, values) {
199
+ if (values.length === 1 && values[0] === '--dry-run') {
200
+ const { workspace, journal } = prepare(root);
201
+ fs.rmSync(path.join(workspace, 'staging'), { recursive: true });
202
+ fs.rmSync(path.join(workspace, 'backup'), { recursive: true });
203
+ journal.status = 'previewed';
204
+ writeJson(path.join(workspace, 'journal.json'), journal);
205
+ return { status: journal.status, migration_id: journal.id, platforms: journal.platforms,
206
+ files: Object.keys(journal.hashes), apply: 'code-flow migrate --runtime --apply' };
207
+ }
208
+ if (values.length === 1 && values[0] === '--apply') return withLock(root, values);
209
+ if (values.length === 2 && values[0] === '--rollback') return withLock(root, values);
210
+ throw new Error('Choose --dry-run, --apply, or --rollback <id>.');
211
+ }
212
+
213
+ module.exports = { run };
@@ -5,7 +5,7 @@ const crypto = require("node:crypto");
5
5
  const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
 
8
- const PREFLIGHT = path.join(__dirname, "../core/code-flow/scripts/cf_spec_migrate.py");
8
+ const PREFLIGHT = path.join(__dirname, "../core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py");
9
9
 
10
10
  function walkFiles(root, directory = root) {
11
11
  const files = [];
@@ -172,7 +172,12 @@ function rollback(planPath, error = "requested") {
172
172
  const journal = readJson(locations.journal);
173
173
  try {
174
174
  restoreBackup(plan, locations, journal.manifest || { targets: [] });
175
+ // A rolled-back plan must be re-appliable: stale staging content makes the
176
+ // next `stage` refuse ("staging root must be a pre-created empty directory").
177
+ if (fs.existsSync(locations.staging)) fs.rmSync(locations.staging, { recursive: true, force: true });
178
+ fs.mkdirSync(locations.staging, { recursive: true });
175
179
  journal.error = String(error);
180
+ journal.operations = [];
176
181
  persistJournal(locations.journal, journal, "rolled_back");
177
182
  return { status: "rolled_back", migration_id: plan.migration_id };
178
183
  } catch (restoreError) {
@@ -204,6 +209,16 @@ function recoverCommitting(plan, journal, locations, options) {
204
209
  function apply(planPath, options = {}) {
205
210
  const plan = readJson(planPath);
206
211
  const locations = journalPaths(planPath);
212
+ // Guard: a plan carries an absolute project_root; applying a plan from a
213
+ // different project (e.g. a copy or a stale path) would hash-check and
214
+ // mutate that other project instead of cwd. The CLI always passes
215
+ // projectRoot; direct library callers keep the previous behavior.
216
+ if (options.projectRoot) {
217
+ const expectedRoot = fs.realpathSync(options.projectRoot);
218
+ if (fs.realpathSync(plan.project_root) !== expectedRoot) {
219
+ throw new Error(`migration plan targets ${plan.project_root}, not ${expectedRoot}`);
220
+ }
221
+ }
207
222
  const versionPath = path.join(plan.project_root, ".code-flow/.version");
208
223
  if (fs.existsSync(versionPath) && fs.readFileSync(versionPath, "utf8").trim() === "0.6.0") return { status: "already_migrated", migration_id: plan.migration_id };
209
224
  const journal = readJson(locations.journal);