@guanlin4924/devflow-pi 0.1.94 → 0.1.95

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 (238) hide show
  1. package/README.md +4 -4
  2. package/node_modules/@playwright/test/LICENSE +202 -0
  3. package/node_modules/@playwright/test/NOTICE +5 -0
  4. package/node_modules/@playwright/test/README.md +318 -0
  5. package/node_modules/@playwright/test/cli.js +19 -0
  6. package/node_modules/@playwright/test/index.d.ts +18 -0
  7. package/node_modules/@playwright/test/index.js +17 -0
  8. package/node_modules/@playwright/test/index.mjs +18 -0
  9. package/node_modules/@playwright/test/package.json +35 -0
  10. package/node_modules/@playwright/test/reporter.d.ts +17 -0
  11. package/node_modules/@playwright/test/reporter.js +17 -0
  12. package/node_modules/@playwright/test/reporter.mjs +17 -0
  13. package/node_modules/playwright/LICENSE +202 -0
  14. package/node_modules/playwright/NOTICE +5 -0
  15. package/node_modules/playwright/README.md +318 -0
  16. package/node_modules/playwright/ThirdPartyNotices.txt +14 -0
  17. package/node_modules/playwright/cli.js +19 -0
  18. package/node_modules/playwright/index.d.ts +17 -0
  19. package/node_modules/playwright/index.js +17 -0
  20. package/node_modules/playwright/index.mjs +18 -0
  21. package/node_modules/playwright/jsx-runtime.js +42 -0
  22. package/node_modules/playwright/jsx-runtime.mjs +21 -0
  23. package/node_modules/playwright/lib/agents/agentParser.js +89 -0
  24. package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +34 -0
  25. package/node_modules/playwright/lib/agents/generateAgents.js +395 -0
  26. package/node_modules/playwright/lib/agents/playwright-test-coverage.prompt.md +31 -0
  27. package/node_modules/playwright/lib/agents/playwright-test-generate.prompt.md +8 -0
  28. package/node_modules/playwright/lib/agents/playwright-test-generator.agent.md +88 -0
  29. package/node_modules/playwright/lib/agents/playwright-test-heal.prompt.md +6 -0
  30. package/node_modules/playwright/lib/agents/playwright-test-healer.agent.md +56 -0
  31. package/node_modules/playwright/lib/agents/playwright-test-plan.prompt.md +9 -0
  32. package/node_modules/playwright/lib/agents/playwright-test-planner.agent.md +74 -0
  33. package/node_modules/playwright/lib/cli/reportActions.js +78 -0
  34. package/node_modules/playwright/lib/cli/testActions.js +214 -0
  35. package/node_modules/playwright/lib/common/index.js +2923 -0
  36. package/node_modules/playwright/lib/common/index.js.txt +34 -0
  37. package/node_modules/playwright/lib/errorContext.js +129 -0
  38. package/node_modules/playwright/lib/globals.js +58 -0
  39. package/node_modules/playwright/lib/index.js +815 -0
  40. package/node_modules/playwright/lib/isomorphic.js +260 -0
  41. package/node_modules/playwright/lib/isomorphic.js.txt +9 -0
  42. package/node_modules/playwright/lib/loader/loaderProcessEntry.js +34 -0
  43. package/node_modules/playwright/lib/loader/loaderProcessEntry.js.txt +9 -0
  44. package/node_modules/playwright/lib/matchers/expect.js +13490 -0
  45. package/node_modules/playwright/lib/matchers/expect.js.LICENSE +640 -0
  46. package/node_modules/playwright/lib/matchers/expect.js.txt +67 -0
  47. package/node_modules/playwright/lib/mcp/test/browserBackend.js +135 -0
  48. package/node_modules/playwright/lib/mcp/test/generatorTools.js +122 -0
  49. package/node_modules/playwright/lib/mcp/test/plannerTools.js +150 -0
  50. package/node_modules/playwright/lib/mcp/test/seed.js +82 -0
  51. package/node_modules/playwright/lib/mcp/test/streams.js +44 -0
  52. package/node_modules/playwright/lib/mcp/test/testBackend.js +99 -0
  53. package/node_modules/playwright/lib/mcp/test/testContext.js +306 -0
  54. package/node_modules/playwright/lib/mcp/test/testTool.js +30 -0
  55. package/node_modules/playwright/lib/mcp/test/testTools.js +98 -0
  56. package/node_modules/playwright/lib/package.js +47 -0
  57. package/node_modules/playwright/lib/program.js +241 -0
  58. package/node_modules/playwright/lib/runner/index.js +8527 -0
  59. package/node_modules/playwright/lib/runner/index.js.txt +61 -0
  60. package/node_modules/playwright/lib/transform/babelBundle.js +72274 -0
  61. package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +2384 -0
  62. package/node_modules/playwright/lib/transform/babelBundle.js.txt +325 -0
  63. package/node_modules/playwright/lib/transform/esmLoader.js +8054 -0
  64. package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +355 -0
  65. package/node_modules/playwright/lib/transform/esmLoader.js.txt +60 -0
  66. package/node_modules/playwright/lib/util.js +373 -0
  67. package/node_modules/playwright/lib/worker/workerProcessEntry.js +1861 -0
  68. package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +19 -0
  69. package/node_modules/playwright/package.json +55 -0
  70. package/node_modules/playwright/test.d.ts +18 -0
  71. package/node_modules/playwright/test.js +24 -0
  72. package/node_modules/playwright/test.mjs +35 -0
  73. package/node_modules/playwright/types/test.d.ts +11074 -0
  74. package/node_modules/playwright/types/testReporter.d.ts +950 -0
  75. package/node_modules/playwright-core/LICENSE +202 -0
  76. package/node_modules/playwright-core/NOTICE +5 -0
  77. package/node_modules/playwright-core/README.md +3 -0
  78. package/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
  79. package/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
  80. package/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
  81. package/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
  82. package/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
  83. package/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
  84. package/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
  85. package/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
  86. package/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
  87. package/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
  88. package/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
  89. package/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
  90. package/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
  91. package/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
  92. package/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
  93. package/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
  94. package/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
  95. package/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
  96. package/node_modules/playwright-core/browsers.json +52 -0
  97. package/node_modules/playwright-core/cli.js +21 -0
  98. package/node_modules/playwright-core/index.d.ts +17 -0
  99. package/node_modules/playwright-core/index.js +17 -0
  100. package/node_modules/playwright-core/index.mjs +28 -0
  101. package/node_modules/playwright-core/lib/bootstrap.js +88 -0
  102. package/node_modules/playwright-core/lib/coreBundle.js +75813 -0
  103. package/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
  104. package/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
  105. package/node_modules/playwright-core/lib/entry/mcp.js +10 -0
  106. package/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
  107. package/node_modules/playwright-core/lib/package.js +50 -0
  108. package/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
  109. package/node_modules/playwright-core/lib/server/electron/loader.js +116 -0
  110. package/node_modules/playwright-core/lib/serverRegistry.js +1939 -0
  111. package/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +65 -0
  112. package/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
  113. package/node_modules/playwright-core/lib/tools/cli-client/cli.js +6 -0
  114. package/node_modules/playwright-core/lib/tools/cli-client/help.json +721 -0
  115. package/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
  116. package/node_modules/playwright-core/lib/tools/cli-client/output.js +351 -0
  117. package/node_modules/playwright-core/lib/tools/cli-client/program.js +418 -0
  118. package/node_modules/playwright-core/lib/tools/cli-client/registry.js +180 -0
  119. package/node_modules/playwright-core/lib/tools/cli-client/session.js +259 -0
  120. package/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
  121. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +425 -0
  122. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
  123. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
  124. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
  125. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
  126. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
  127. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
  128. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
  129. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
  130. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
  131. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +136 -0
  132. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
  133. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +89 -0
  134. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +69 -0
  135. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/typing.md +173 -0
  136. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +77 -0
  137. package/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +174 -0
  138. package/node_modules/playwright-core/lib/tools/utils/extension.js +112 -0
  139. package/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
  140. package/node_modules/playwright-core/lib/utilsBundle.js +85357 -0
  141. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +1890 -0
  142. package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css +1 -0
  143. package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js +32 -0
  144. package/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
  145. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
  146. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
  147. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
  148. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-AwgwcYie.css +1 -0
  149. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-hRvO4yU2.js +12 -0
  150. package/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
  151. package/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
  152. package/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
  153. package/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
  154. package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
  155. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
  156. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  157. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CVQWJZAA.js +32 -0
  158. package/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
  159. package/node_modules/playwright-core/lib/vite/recorder/assets/index-BZpYJZ-P.css +1 -0
  160. package/node_modules/playwright-core/lib/vite/recorder/assets/index-hrlLqLtq.js +129 -0
  161. package/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
  162. package/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
  163. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-BbkfBe3n.js +32 -0
  164. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Ds6CBOo0.js +182 -0
  165. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  166. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-DywYcAf8.js +7 -0
  167. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  168. package/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
  169. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.Bqk9acqE.css +1 -0
  170. package/node_modules/playwright-core/lib/vite/traceViewer/index.B4cLoZK3.js +1 -0
  171. package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  172. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
  173. package/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
  174. package/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
  175. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  176. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
  177. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
  178. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CU5KtEkS.js +5 -0
  179. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CyMfwkXJ.css +1 -0
  180. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
  181. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  182. package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  183. package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  184. package/node_modules/playwright-core/lib/xdg-open +1267 -0
  185. package/node_modules/playwright-core/package.json +34 -0
  186. package/node_modules/playwright-core/types/protocol.d.ts +24645 -0
  187. package/node_modules/playwright-core/types/structs.d.ts +52 -0
  188. package/node_modules/playwright-core/types/types.d.ts +26632 -0
  189. package/package.json +12 -2
  190. package/plugins/devflow-core/extensions/devflow-core.ts +431 -7
  191. package/plugins/devflow-core/runtime/hooks/hook_common.py +269 -0
  192. package/plugins/devflow-core/runtime/hooks/index_gate.py +29 -16
  193. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +407 -71
  194. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +51 -0
  195. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_pins.py +433 -0
  196. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_store.py +137 -0
  197. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_coverage.py +87 -0
  198. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +318 -0
  199. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/environment_profile.py +134 -0
  200. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +1 -0
  201. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_surface.py +138 -0
  202. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/playwright_adapter.py +515 -0
  203. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tc_hash.py +132 -0
  204. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +102 -8
  205. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +13 -0
  206. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +13 -34
  207. package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +3 -1
  208. package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +68 -140
  209. package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization_database.py +1 -1
  210. package/plugins/devflow-implementation/skills/workflow-execute-phase/references/execute-phase.md +3 -1
  211. package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +23 -4
  212. package/plugins/devflow-quality/runtime/playwright/devflow-test-dsl-v1.schema.json +872 -0
  213. package/plugins/devflow-quality/runtime/playwright/playwright.config.mjs +46 -0
  214. package/plugins/devflow-quality/runtime/playwright/reporter.mjs +311 -0
  215. package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +1308 -0
  216. package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +11 -0
  217. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +3 -0
  218. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +71 -58
  219. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/api-mainflow-rules.md +20 -15
  220. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/codebase-discovery.md +3 -1
  221. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +115 -192
  222. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/mainflow-selection.md +27 -23
  223. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/project-dsl-contract.md +78 -153
  224. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/ui-mainflow-rules.md +36 -83
  225. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +497 -556
  226. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/export_legacy_testagent.py +647 -0
  227. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/normalize_case_payloads.py +10 -3
  228. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/validate_case_payloads.py +165 -275
  229. package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +61 -0
  230. package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +4 -4
  231. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +15 -9
  232. package/src/approval-store.ts +340 -0
  233. package/src/constants.ts +1 -1
  234. package/src/doctor.ts +259 -2
  235. package/src/initialization-store.ts +48 -1
  236. package/src/paths.ts +12 -1
  237. package/src/project-context.ts +24 -1
  238. package/templates/omp/hook-config.json +6 -0
@@ -7,6 +7,8 @@ description: "Execute the frozen TC acceptance suite after code review. Use the
7
7
 
8
8
  此步骤在代码审查后运行。它的目标是执行冻结的 `TC-*`,而不是根据交付总结重新猜测要测什么。
9
9
 
10
+ 对 `execution_surface: ui|api_flow` 的自动化 TC,`workflow-generate-mainflow-cases` 是本步骤绑定阶段的**真实前置条件**(不是新的主链步骤,也不是可选的开发者同步):先用它为每个这类 TC 编译 `.devflow/planning/generated-cases/{feature}/TC-xxxx.json`、校验整个目录、独立 AI 审查并提交逐 TC `execution_plan` 批准。没有 DSL 或没有有效批准的 TC 只能 `blocked(dsl_not_approved)`,不得进入收口。
11
+
10
12
  ## 输入边界
11
13
 
12
14
  测试意图只能读取:
@@ -26,25 +28,29 @@ description: "Execute the frozen TC acceptance suite after code review. Use the
26
28
  ## 过程
27
29
 
28
30
  1. 用 `devflow_acceptance_tests(feature)` 检查测试集已由 CP-3 冻结。若有 stale 或候选 TC,停止并回到需求/测试设计。
29
- 2. 读取冻结 TC 与其 oracle、前置数据、身份和自动化模式。
31
+ 2. 读取冻结 TC 与其 oracle、前置数据、身份、自动化模式和 `execution_surface`。
30
32
  对 `api_refs`、`workload_refs` 和 `risk_refs` 指向的专项用例,保留其契约、性能/迁移或韧性义务;没有适配器或环境时必须写为 blocked,不能用普通主流程替代。
31
- 3. 读取代码审查报告、代码索引和实际源码,仅为每个 TC 绑定运行适配器;不得改变 TC 的断言、范围或预期。
33
+ 3. 读取代码审查报告、代码索引和实际源码,仅为每个 TC 绑定运行适配器;不得改变 TC 的断言、范围或预期。对每个自动化 `ui|api_flow` TC,若 `generated-cases/{feature}/TC-xxxx.json` 不存在或已过期(TC 重冻结、代码变更),先调用 `workflow-generate-mainflow-cases`;它无法生成的正向 UI/API TC(无稳定 locator、无法准备或隔离数据、纯视觉判断)按下一步降级,异常/边界/权限/性能专项 TC 也不由它生成。
32
34
  4. 写入 `.devflow/planning/features/{feature}/acceptance-test-bindings.json`:
33
35
  - 固定 `test_suite.graph_revision` 与 `fingerprint`;
34
36
  - 每个 TC 恰好一条绑定;
35
- - `mode` 必须与冻结 TC 的 `automation_mode` 一致;
36
- - 通用自动化用例使用 `{ "kind": "command", "argv": [ ... ] }`,不使用 shell 字符串;
37
+ - 默认 `mode` 与冻结 TC 的 `automation_mode` 一致。只允许单向降级:`automated → manual|blocked`、`manual → blocked`;`automated → manual` 必须先提交并由用户确认有效的 `mode_override` approval,其他降级必须保留 owner、reason 和 blocked_by;禁止升级;
38
+ - `execution_surface: ui|api_flow` 必须使用 `{ "kind": "devflow_playwright", "dsl_file": ".devflow/planning/generated-cases/{feature}/TC-xxxx.json", "environment_profile": "local", "timeout_seconds": 600, "artifacts": {"trace":"on_failure","screenshots":"on_failure","max_total_bytes":104857600} }`。绑定不得携带 argv、env、runner、config、result_file 或哈希;
39
+ - 每个 `devflow_playwright` TC 执行前必须存在有效的逐 TC `execution_plan` approval,profile(`.devflow/planning/test-environments/{profile}.json`)必须存在有效 `environment_profile` approval;缺失、DSL 或 profile 字节变化导致 pins 失效时只能 blocked(`dsl_not_approved` / `environment_profile_not_approved`),不得运行。Agent 用 `devflow_approval(action="propose", kind="environment_profile", items=[{"subject":"{profile}"}])` 提交 profile,用户在 `/devflow` → “待审批”确认;Agent 不能确认,也不能自写批准记录;
40
+ - `execution_surface: project_test` 的通用自动化用例使用 `{ "kind": "command", "argv": [ ... ] }`,不使用 shell 字符串。命令策略:`npm test` 要求项目已登记 `test` 脚本;`npm|pnpm|yarn run <name>` 要求 `<name>` 是初始化工具链已登记的脚本名,且 `run` 后必须紧跟脚本名(不得在 `run` 之前或之间放选项),额外参数只能放在 `--` 之后;argv 不得引用 `DEVFLOW_` 保留变量或 Devflow Playwright Runtime 路径(那属于 `devflow_playwright`);
37
41
  - Core 生成的 API TC 可直接使用 `{ "kind": "http", "base_url_env": "DEVFLOW_TEST_BASE_URL", "headers_env": { "Authorization": "DEVFLOW_TEST_AUTHORIZATION" } }`。请求、HTTP 状态和异常码断言来自冻结 TC 的 `execution`,绑定只能提供环境地址和鉴权,不得重写测试意图;若场景需要专用夹具,可额外配置 `{ "setup": { "argv": [ ... ] } }`,Core 会先记录并执行该受控命令,再发 HTTP 请求;
38
- - 人工或环境阻塞用例必须有 owner 与原因/阻塞源。
42
+ - 人工用例保留 owner 与执行说明;`human-acceptance-results.json` 仅是草稿和交接材料,最终结果必须来自有效的逐 TC `human_result` approval;
43
+ - 环境阻塞用例必须有 owner 与阻塞源。
39
44
  - `evidence_strategy=causal` 的自动用例额外写 `evidence_files`(项目根目录内的 HAR、控制台、DOM 快照、截图或日志文件相对路径)。Core 在执行前校验文件存在且未越出项目根目录,并把内容哈希写入结果;不能只填 Markdown 描述。
40
45
  - `evidence_strategy=observation` 只能保持 `blocked`,其观测计划是风险收口输入,不是测试通过证据。
41
- 5. 调用 `devflow_run_acceptance_tests(action="validate", feature=...)`。不通过时修正绑定,不得绕过。
42
- 6. 调用 `devflow_run_acceptance_tests(action="run", feature=...)`。该 Core 工具记录每个 TC 的命令哈希、退出码和输出摘要。
43
- 7. 失败、未执行或 blocked 的 TC 只能报告为 `blocked`/`failed`,不得进入版本收口。
46
+ 5. 调用 `devflow_run_acceptance_tests(action="validate", feature=...)`。它校验 adapter 与 `execution_surface` 的兼容矩阵、`devflow_playwright` 禁用键、DSL 存在且合法、`tc_sha256` 与覆盖。不通过时修正绑定,不得绕过。
47
+ 6. 对每个 manual TC,检查用户已在 `/devflow` → “待审批”中确认 `human_result`;Core 按当前 suite fingerprint、`tc_sha256` 和证据哈希重新校验。缺失或失效时保持 blocked。
48
+ 7. 调用 `devflow_run_acceptance_tests(action="run", feature=...)`。该 Core 工具执行 automated TC(`devflow_playwright` 在净化环境和独立运行目录中执行,并校验 Runtime 完整性与结果 nonce),合并已校验的 manual TC 结果,并记录命令/证据哈希、退出码、`dsl_approval`、`execution_surface`、`legacy_unclassified` 和降级记录。
49
+ 8. 失败、未执行或 blocked 的 TC 只能报告为 `blocked`/`failed`,不得进入版本收口。验收未全 passed 是硬门禁:即使默认 soft 模式,`run-acceptance-tests` 完成和 `release-closeout` 确认时 Core 也会重新校验结果并以 `acceptance_results_not_passed` 拒绝。可用 `devflow_run_acceptance_tests(action="validate_results", feature=...)` 预先检查。该校验会对照当前批准、DSL/profile 哈希与运行目录重新验证每条记录:运行后修改 DSL 或 profile、或批准被新的批准替换,都会使已记录的 passed 失效,必须重跑。
44
50
 
45
51
  ## 输出
46
52
 
47
53
  - `.devflow/planning/features/{feature}/acceptance-test-bindings.json`
48
54
  - `.devflow/planning/test-runs/{feature}/acceptance-results.json`
49
55
 
50
- `acceptance-results.json` 是 Core-owned 的执行事实;Markdown 结论不能替代它。
56
+ `acceptance-results.json` 是 Core 指定写入的本地执行缓存(Agent 不得写入,可读取用于展示);Markdown 结论不能替代它,收口以 Core 对它的重新校验(`validate_results`)为准。Hook 会拦截常规 Write/Edit/Bash 误写,但不是对同一系统用户下任意进程的安全隔离;未配置外部受保护信任根时,不得宣称结果在密码学意义上不可伪造。
@@ -0,0 +1,340 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { Database } from "bun:sqlite";
3
+
4
+ /**
5
+ * Core human-approval primitive (control.db, schema v3).
6
+ *
7
+ * `proposeApproval` may be reached from an agent tool. `confirmApproval` must
8
+ * only be called from the runtime extension's interactive UI callback: there
9
+ * is deliberately no CLI or agent-tool entry point for it.
10
+ */
11
+
12
+ export const APPROVAL_KINDS = ["execution_plan", "environment_profile", "mode_override", "human_result"] as const;
13
+ export type ApprovalKind = typeof APPROVAL_KINDS[number];
14
+ export const HUMAN_RESULT_STATUSES = ["passed", "failed", "blocked"] as const;
15
+ export type HumanResultStatus = typeof HUMAN_RESULT_STATUSES[number];
16
+ export type ProposalStatus = "proposed" | "consumed" | "rejected" | "expired";
17
+
18
+ /** An unconfirmed proposal older than this can no longer be confirmed. */
19
+ export const PROPOSAL_TTL_MS = 24 * 60 * 60 * 1000;
20
+ export const APPROVED_VIA = "runtime_ui_confirm";
21
+
22
+ export type ApprovalErrorCode =
23
+ | "approval_requires_database_authority"
24
+ | "invalid_proposal"
25
+ | "invalid_decision"
26
+ | "proposal_not_found"
27
+ | "proposal_not_pending"
28
+ | "proposal_expired"
29
+ | "pins_unavailable"
30
+ | "pins_mismatch"
31
+ | "state_revision_mismatch";
32
+
33
+ export class ApprovalError extends Error {
34
+ constructor(readonly code: ApprovalErrorCode, message: string) {
35
+ super(`${code}: ${message}`);
36
+ this.name = "ApprovalError";
37
+ }
38
+ }
39
+
40
+ export interface ProposalRecord {
41
+ id: string;
42
+ projectId: string;
43
+ feature: string;
44
+ kind: ApprovalKind;
45
+ subjectKey: string;
46
+ payload: Record<string, unknown>;
47
+ pins: Record<string, unknown>;
48
+ stateRevision: number;
49
+ status: ProposalStatus;
50
+ createdAt: string;
51
+ resolvedAt: string | null;
52
+ }
53
+
54
+ export interface ApprovalRecord {
55
+ id: string;
56
+ proposalId: string;
57
+ projectId: string;
58
+ feature: string;
59
+ kind: ApprovalKind;
60
+ subjectKey: string;
61
+ pins: Record<string, unknown>;
62
+ decision: Record<string, unknown>;
63
+ approvedVia: string;
64
+ approvedAt: string;
65
+ supersededAt: string | null;
66
+ }
67
+
68
+ export interface ProposeInput {
69
+ projectId: string;
70
+ feature: string;
71
+ kind: ApprovalKind;
72
+ subjectKey: string;
73
+ payload: Record<string, unknown>;
74
+ pins: Record<string, unknown>;
75
+ stateRevision: number;
76
+ }
77
+
78
+ export interface CurrentPins {
79
+ pins: Record<string, unknown>;
80
+ stateRevision: number;
81
+ }
82
+
83
+ export interface ConfirmInput {
84
+ projectId: string;
85
+ proposalId: string;
86
+ decision: Record<string, unknown>;
87
+ /**
88
+ * Recomputes the current pins for the stored proposal. It runs inside the
89
+ * write transaction (BEGIN IMMEDIATE), so it must be synchronous.
90
+ */
91
+ recompute: (proposal: ProposalRecord) => CurrentPins;
92
+ now?: Date;
93
+ }
94
+
95
+ export type ConfirmResult =
96
+ | { ok: true; approvalId: string; supersededIds: string[] }
97
+ | { ok: false; code: ApprovalErrorCode; message: string };
98
+
99
+ /** Deterministic key-sorted JSON used to compare pins. */
100
+ export function canonicalPinsJson(value: unknown): string {
101
+ if (Array.isArray(value)) return `[${value.map(canonicalPinsJson).join(",")}]`;
102
+ if (value && typeof value === "object") {
103
+ return `{${Object.keys(value as Record<string, unknown>).sort().map(key => `${JSON.stringify(key)}:${canonicalPinsJson((value as Record<string, unknown>)[key])}`).join(",")}}`;
104
+ }
105
+ if (value === undefined) return "null";
106
+ return JSON.stringify(value);
107
+ }
108
+
109
+ function isRecord(value: unknown): value is Record<string, unknown> {
110
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
111
+ }
112
+
113
+ function isKind(value: unknown): value is ApprovalKind {
114
+ return typeof value === "string" && (APPROVAL_KINDS as readonly string[]).includes(value);
115
+ }
116
+
117
+ const PIN_KEYS: Record<ApprovalKind, string[]> = {
118
+ execution_plan: ["dsl_sha256", "suite_fingerprint", "suite_graph_revision", "tc_sha256"],
119
+ environment_profile: ["profile_sha256"],
120
+ mode_override: ["binding_entry_sha256", "from", "suite_fingerprint", "tc_sha256", "to"],
121
+ human_result: ["evidence", "suite_fingerprint", "tc_sha256"],
122
+ };
123
+
124
+ /** Enforces the change-list §1.8 subject_key and pins_json shapes. */
125
+ export function validatePinsShape(kind: ApprovalKind, subjectKey: string, pins: Record<string, unknown>): string[] {
126
+ const errors: string[] = [];
127
+ const keys = Object.keys(pins).sort();
128
+ if (canonicalPinsJson(keys) !== canonicalPinsJson(PIN_KEYS[kind])) errors.push(`${kind} pins must contain exactly: ${PIN_KEYS[kind].join(", ")}`);
129
+ if (kind === "environment_profile") {
130
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(subjectKey)) errors.push("environment_profile subject_key must be a profile name");
131
+ } else if (!/^TC-[A-Za-z0-9_-]+$/.test(subjectKey)) {
132
+ errors.push(`${kind} subject_key must be a TC id`);
133
+ }
134
+ for (const key of keys) {
135
+ const value = pins[key];
136
+ if (key.endsWith("sha256") || key === "suite_fingerprint") {
137
+ if (typeof value !== "string" || value.length === 0) errors.push(`pin ${key} must be a non-empty string`);
138
+ }
139
+ }
140
+ if (kind === "human_result") {
141
+ const evidence = pins.evidence;
142
+ if (!Array.isArray(evidence) || evidence.some(item => !isRecord(item) || typeof item.path !== "string" || typeof item.sha256 !== "string")) {
143
+ errors.push("human_result evidence pins must be [{path, sha256}]");
144
+ }
145
+ }
146
+ return errors;
147
+ }
148
+
149
+ /** Validates a UI decision; human_result verdicts must come from the user. */
150
+ export function validateDecision(kind: ApprovalKind, decision: Record<string, unknown>, pins: Record<string, unknown>): string[] {
151
+ const errors: string[] = [];
152
+ if (kind === "human_result") {
153
+ const keys = Object.keys(decision).sort();
154
+ if (canonicalPinsJson(keys) !== canonicalPinsJson(["actual_result", "status"])) errors.push("human_result decision must contain exactly status and actual_result");
155
+ if (!(HUMAN_RESULT_STATUSES as readonly string[]).includes(String(decision.status))) errors.push("human_result status must be passed, failed, or blocked");
156
+ if (typeof decision.actual_result !== "string" || !decision.actual_result.trim()) errors.push("human_result actual_result is required");
157
+ if (decision.status === "passed" && (!Array.isArray(pins.evidence) || pins.evidence.length === 0)) errors.push("a passed human_result requires at least one pinned evidence file");
158
+ } else if (canonicalPinsJson(decision) !== canonicalPinsJson({ approved: true })) {
159
+ errors.push(`${kind} decision must be {approved: true}`);
160
+ }
161
+ return errors;
162
+ }
163
+
164
+ export function databaseAuthority(db: Database, projectId: string): string | null {
165
+ const row = db.query("SELECT authority FROM authority_state WHERE project_id=?").get(projectId) as { authority: string } | null;
166
+ return row?.authority ?? null;
167
+ }
168
+
169
+ function assertDatabaseAuthority(db: Database, projectId: string): void {
170
+ const authority = databaseAuthority(db, projectId);
171
+ if (authority !== "database") {
172
+ throw new ApprovalError("approval_requires_database_authority", `project authority is ${authority ?? "unregistered"}; approvals require database authority`);
173
+ }
174
+ }
175
+
176
+ function proposalFromRow(row: any): ProposalRecord {
177
+ return {
178
+ id: row.id, projectId: row.project_id, feature: row.feature, kind: row.kind, subjectKey: row.subject_key,
179
+ payload: JSON.parse(row.payload_json), pins: JSON.parse(row.pins_json), stateRevision: row.state_revision,
180
+ status: row.status, createdAt: row.created_at, resolvedAt: row.resolved_at ?? null,
181
+ };
182
+ }
183
+
184
+ function approvalFromRow(row: any): ApprovalRecord {
185
+ return {
186
+ id: row.id, proposalId: row.proposal_id, projectId: row.project_id, feature: row.feature, kind: row.kind,
187
+ subjectKey: row.subject_key, pins: JSON.parse(row.pins_json), decision: JSON.parse(row.decision_json),
188
+ approvedVia: row.approved_via, approvedAt: row.approved_at, supersededAt: row.superseded_at ?? null,
189
+ };
190
+ }
191
+
192
+ function validateProposeInput(input: ProposeInput): void {
193
+ if (!isKind(input.kind)) throw new ApprovalError("invalid_proposal", `unknown approval kind: ${String(input.kind)}`);
194
+ if (!input.feature || /[/\\\x00-\x1f\x7f]/.test(input.feature)) throw new ApprovalError("invalid_proposal", "feature must be a plain feature name");
195
+ if (!isRecord(input.payload)) throw new ApprovalError("invalid_proposal", "payload must be an object");
196
+ if (!isRecord(input.pins)) throw new ApprovalError("invalid_proposal", "pins must be an object");
197
+ if (!Number.isInteger(input.stateRevision) || input.stateRevision < 0) throw new ApprovalError("invalid_proposal", "state_revision must be a non-negative integer");
198
+ const shapeErrors = validatePinsShape(input.kind, input.subjectKey, input.pins);
199
+ if (shapeErrors.length) throw new ApprovalError("invalid_proposal", shapeErrors.join("; "));
200
+ }
201
+
202
+ /** Must run inside a write transaction; expires older open proposals for the subject, then inserts. */
203
+ function insertProposal(db: Database, input: ProposeInput, timestamp: string): { proposalId: string; expiredIds: string[] } {
204
+ assertDatabaseAuthority(db, input.projectId);
205
+ const proposalId = randomUUID();
206
+ const open = db.query(`SELECT id FROM approval_proposals
207
+ WHERE project_id=? AND feature=? AND kind=? AND subject_key=? AND status='proposed'`).all(input.projectId, input.feature, input.kind, input.subjectKey) as Array<{ id: string }>;
208
+ for (const item of open) {
209
+ db.query("UPDATE approval_proposals SET status='expired',resolved_at=? WHERE id=? AND status='proposed'").run(timestamp, item.id);
210
+ }
211
+ db.query(`INSERT INTO approval_proposals(id,project_id,feature,kind,subject_key,payload_json,pins_json,state_revision,status,created_at,resolved_at)
212
+ VALUES (?,?,?,?,?,?,?,?,'proposed',?,NULL)`).run(
213
+ proposalId, input.projectId, input.feature, input.kind, input.subjectKey,
214
+ canonicalPinsJson(input.payload), canonicalPinsJson(input.pins), input.stateRevision, timestamp,
215
+ );
216
+ return { proposalId, expiredIds: open.map(item => item.id) };
217
+ }
218
+
219
+ /**
220
+ * Records a proposal with the pins observed at proposal time. An older open
221
+ * proposal for the same feature/kind/subject is expired so only the newest
222
+ * proposal can be confirmed.
223
+ */
224
+ export function proposeApproval(db: Database, input: ProposeInput, options: { now?: Date } = {}): { proposalId: string; expiredIds: string[] } {
225
+ validateProposeInput(input);
226
+ const timestamp = (options.now ?? new Date()).toISOString();
227
+ return db.transaction(() => insertProposal(db, input, timestamp)).immediate();
228
+ }
229
+
230
+ /**
231
+ * All-or-nothing batch propose: every item is validated first, then all inserts and
232
+ * "expire older open proposal for the same subject" updates run in ONE immediate
233
+ * transaction that rolls back completely on any error.
234
+ */
235
+ export function proposeApprovals(db: Database, inputs: ProposeInput[], options: { now?: Date } = {}): Array<{ proposalId: string; expiredIds: string[] }> {
236
+ if (!Array.isArray(inputs) || inputs.length === 0) throw new ApprovalError("invalid_proposal", "at least one proposal is required");
237
+ for (const input of inputs) validateProposeInput(input);
238
+ const timestamp = (options.now ?? new Date()).toISOString();
239
+ return db.transaction(() => inputs.map(input => insertProposal(db, input, timestamp))).immediate();
240
+ }
241
+
242
+ export function getProposal(db: Database, projectId: string, proposalId: string): ProposalRecord | null {
243
+ const row = db.query("SELECT * FROM approval_proposals WHERE id=? AND project_id=?").get(proposalId, projectId);
244
+ return row ? proposalFromRow(row) : null;
245
+ }
246
+
247
+ export function listProposals(db: Database, projectId: string, filter: { feature?: string; status?: ProposalStatus; kind?: ApprovalKind } = {}): ProposalRecord[] {
248
+ const clauses = ["project_id=?"]; const params: Array<string> = [projectId];
249
+ if (filter.feature) { clauses.push("feature=?"); params.push(filter.feature); }
250
+ if (filter.status) { clauses.push("status=?"); params.push(filter.status); }
251
+ if (filter.kind) { clauses.push("kind=?"); params.push(filter.kind); }
252
+ return (db.query(`SELECT * FROM approval_proposals WHERE ${clauses.join(" AND ")} ORDER BY created_at, id`).all(...params) as any[]).map(proposalFromRow);
253
+ }
254
+
255
+ export function listApprovals(db: Database, projectId: string, filter: { feature?: string; kind?: ApprovalKind; includeSuperseded?: boolean } = {}): ApprovalRecord[] {
256
+ const clauses = ["project_id=?"]; const params: Array<string> = [projectId];
257
+ if (filter.feature) { clauses.push("feature=?"); params.push(filter.feature); }
258
+ if (filter.kind) { clauses.push("kind=?"); params.push(filter.kind); }
259
+ if (!filter.includeSuperseded) clauses.push("superseded_at IS NULL");
260
+ // approved_at has millisecond precision, so confirmations in one transaction
261
+ // window can tie. rowid preserves the actual append order; UUID ordering does not.
262
+ return (db.query(`SELECT * FROM approvals WHERE ${clauses.join(" AND ")} ORDER BY approved_at, rowid`).all(...params) as any[]).map(approvalFromRow);
263
+ }
264
+
265
+ /** The user declined the proposal in the UI; it can never be confirmed afterwards. */
266
+ export function rejectProposal(db: Database, projectId: string, proposalId: string, options: { now?: Date } = {}): boolean {
267
+ const timestamp = (options.now ?? new Date()).toISOString();
268
+ return db.query("UPDATE approval_proposals SET status='rejected',resolved_at=? WHERE id=? AND project_id=? AND status='proposed'")
269
+ .run(timestamp, proposalId, projectId).changes === 1;
270
+ }
271
+
272
+ export function proposalIsExpired(proposal: Pick<ProposalRecord, "createdAt">, now = new Date()): boolean {
273
+ const created = Date.parse(proposal.createdAt);
274
+ return !Number.isFinite(created) || now.getTime() - created > PROPOSAL_TTL_MS;
275
+ }
276
+
277
+ /**
278
+ * Confirms one proposal in a single IMMEDIATE transaction: re-checks database
279
+ * authority, proposal state and TTL, recomputes the pins (CAS), inserts the
280
+ * approval, consumes the proposal and supersedes the previous approval for the
281
+ * same feature/kind/subject. Stale or expired proposals are marked expired and
282
+ * the call fails; nothing else is written in that case.
283
+ */
284
+ export function confirmApproval(db: Database, input: ConfirmInput): ConfirmResult {
285
+ const now = input.now ?? new Date();
286
+ const timestamp = now.toISOString();
287
+ const expire = (proposalId: string) => {
288
+ db.query("UPDATE approval_proposals SET status='expired',resolved_at=? WHERE id=? AND status='proposed'").run(timestamp, proposalId);
289
+ };
290
+ const transaction = db.transaction((): ConfirmResult => {
291
+ const authority = databaseAuthority(db, input.projectId);
292
+ if (authority !== "database") {
293
+ return { ok: false, code: "approval_requires_database_authority", message: `project authority is ${authority ?? "unregistered"}` };
294
+ }
295
+ const row = db.query("SELECT * FROM approval_proposals WHERE id=? AND project_id=?").get(input.proposalId, input.projectId);
296
+ if (!row) return { ok: false, code: "proposal_not_found", message: `unknown proposal: ${input.proposalId}` };
297
+ const proposal = proposalFromRow(row);
298
+ if (proposal.status !== "proposed") return { ok: false, code: "proposal_not_pending", message: `proposal is ${proposal.status}` };
299
+ if (proposalIsExpired(proposal, now)) {
300
+ expire(proposal.id);
301
+ return { ok: false, code: "proposal_expired", message: "proposal is older than the confirmation window; propose again" };
302
+ }
303
+ const decisionErrors = isRecord(input.decision) ? validateDecision(proposal.kind, input.decision, proposal.pins) : ["decision must be an object"];
304
+ if (decisionErrors.length) return { ok: false, code: "invalid_decision", message: decisionErrors.join("; ") };
305
+ let current: CurrentPins;
306
+ try {
307
+ current = input.recompute(proposal);
308
+ } catch (error) {
309
+ return { ok: false, code: "pins_unavailable", message: error instanceof Error ? error.message : String(error) };
310
+ }
311
+ if (!current || !isRecord(current.pins) || canonicalPinsJson(current.pins) !== canonicalPinsJson(proposal.pins)) {
312
+ expire(proposal.id);
313
+ return { ok: false, code: "pins_mismatch", message: "suite, TC, binding, DSL, profile or evidence changed since the proposal; propose again" };
314
+ }
315
+ if (current.stateRevision !== proposal.stateRevision) {
316
+ expire(proposal.id);
317
+ return { ok: false, code: "state_revision_mismatch", message: `workflow state revision changed from ${proposal.stateRevision} to ${current.stateRevision}; propose again` };
318
+ }
319
+ const superseded = db.query(`SELECT id FROM approvals
320
+ WHERE project_id=? AND feature=? AND kind=? AND subject_key=? AND superseded_at IS NULL`).all(proposal.projectId, proposal.feature, proposal.kind, proposal.subjectKey) as Array<{ id: string }>;
321
+ for (const item of superseded) {
322
+ db.query("UPDATE approvals SET superseded_at=? WHERE id=? AND superseded_at IS NULL").run(timestamp, item.id);
323
+ }
324
+ const approvalId = randomUUID();
325
+ db.query(`INSERT INTO approvals(id,proposal_id,project_id,feature,kind,subject_key,pins_json,decision_json,approved_via,approved_at,superseded_at)
326
+ VALUES (?,?,?,?,?,?,?,?,?,?,NULL)`).run(
327
+ approvalId, proposal.id, proposal.projectId, proposal.feature, proposal.kind, proposal.subjectKey,
328
+ canonicalPinsJson(proposal.pins), canonicalPinsJson(input.decision), APPROVED_VIA, timestamp,
329
+ );
330
+ const consumed = db.query("UPDATE approval_proposals SET status='consumed',resolved_at=? WHERE id=? AND status='proposed'").run(timestamp, proposal.id);
331
+ if (consumed.changes !== 1) throw new ApprovalError("proposal_not_pending", "proposal was consumed concurrently");
332
+ return { ok: true, approvalId, supersededIds: superseded.map(item => item.id) };
333
+ });
334
+ try {
335
+ return transaction.immediate();
336
+ } catch (error) {
337
+ if (error instanceof ApprovalError) return { ok: false, code: error.code, message: error.message };
338
+ throw error;
339
+ }
340
+ }
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.94";
2
+ export const PRODUCT_VERSION = "0.1.95";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5