@michaelxwb/code-flow 0.8.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +73 -152
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +156 -1417
  5. package/docs/v1-design.md +1858 -0
  6. package/docs/v1-implementation.md +186 -0
  7. package/lib/adapters.js +126 -0
  8. package/lib/cli.js +258 -0
  9. package/lib/config.js +85 -0
  10. package/lib/doctor.js +283 -0
  11. package/lib/frontmatter.js +42 -0
  12. package/lib/fs.js +141 -0
  13. package/lib/glob.js +23 -0
  14. package/lib/hook.js +99 -0
  15. package/lib/init.js +132 -0
  16. package/lib/learn.js +170 -0
  17. package/lib/locks.js +52 -0
  18. package/lib/markdown.js +13 -0
  19. package/lib/process.js +116 -0
  20. package/lib/render.js +115 -0
  21. package/lib/repo.js +48 -0
  22. package/lib/runs.js +155 -0
  23. package/lib/session.js +40 -0
  24. package/lib/spec.js +80 -0
  25. package/lib/task.js +258 -0
  26. package/lib/taskdoc.js +308 -0
  27. package/lib/toml.js +140 -0
  28. package/lib/verify.js +168 -0
  29. package/lib/worktree.js +930 -0
  30. package/package.json +18 -21
  31. package/templates/adapters/opencode.mjs +65 -0
  32. package/templates/config.json +39 -0
  33. package/templates/instructions.md +13 -0
  34. package/templates/launcher/bridge.cjs +295 -0
  35. package/templates/launcher/hook.cmd +15 -0
  36. package/templates/launcher/hook.ps1 +10 -0
  37. package/templates/launcher/hook.sh +20 -0
  38. package/templates/skills/cf-check.md +19 -0
  39. package/templates/skills/cf-init.md +16 -0
  40. package/templates/skills/cf-spec.md +20 -0
  41. package/templates/skills/cf-task-align.md +19 -0
  42. package/templates/skills/cf-task-archive.md +20 -0
  43. package/templates/skills/cf-task-plan.md +31 -0
  44. package/templates/skills/cf-task-prd.md +14 -0
  45. package/templates/skills/cf-task-start.md +41 -0
  46. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +12 -12
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +17 -19
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +12 -12
  49. package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
  50. package/templates/workflow/project-guidelines.md +48 -0
  51. package/templates/workflow/scenarios.md +67 -0
  52. package/templates/workflow/task.md +56 -0
  53. package/docs/acceptance-contract.md +0 -217
  54. package/docs/ci/acceptance-evidence.md +0 -77
  55. package/docs/migrations/runtime-0.7.0.md +0 -41
  56. package/docs/migrations/runtime-0.8.0.md +0 -154
  57. package/docs/migrations/runtime-0.9.0.md +0 -40
  58. package/docs/migrations/spec-workflow-0.6.0.md +0 -42
  59. package/docs/spec-injection-hardening.md +0 -54
  60. package/docs/spec-injection-performance.json +0 -73
  61. package/docs/spec-optimization.md +0 -12
  62. package/src/adapters/claude/CLAUDE.md +0 -45
  63. package/src/adapters/claude/commands/cf-init.md +0 -262
  64. package/src/adapters/claude/commands/cf-learn.md +0 -291
  65. package/src/adapters/claude/commands/cf-spec.md +0 -87
  66. package/src/adapters/claude/commands/cf-stats.md +0 -96
  67. package/src/adapters/claude/commands/cf-sync.md +0 -39
  68. package/src/adapters/claude/commands/cf-task/align.md +0 -331
  69. package/src/adapters/claude/commands/cf-task/archive.md +0 -155
  70. package/src/adapters/claude/commands/cf-task/block.md +0 -49
  71. package/src/adapters/claude/commands/cf-task/graph.md +0 -92
  72. package/src/adapters/claude/commands/cf-task/note.md +0 -248
  73. package/src/adapters/claude/commands/cf-task/plan.md +0 -477
  74. package/src/adapters/claude/commands/cf-task/prd.md +0 -173
  75. package/src/adapters/claude/commands/cf-task/start.md +0 -317
  76. package/src/adapters/claude/commands/cf-task/status.md +0 -71
  77. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -148
  78. package/src/adapters/claude/commands/cf-validate.md +0 -100
  79. package/src/adapters/claude/settings.local.json +0 -50
  80. package/src/adapters/codex/AGENTS.md +0 -45
  81. package/src/adapters/codex/config.toml +0 -3
  82. package/src/adapters/codex/hooks.json +0 -54
  83. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  84. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  85. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -88
  86. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -97
  87. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -45
  88. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -332
  89. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -156
  90. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -50
  91. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -93
  92. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -245
  93. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -478
  94. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -174
  95. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -318
  96. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -72
  97. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -145
  98. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -102
  99. package/src/adapters/commands.json +0 -115
  100. package/src/adapters/costrict/CLAUDE.md +0 -45
  101. package/src/adapters/costrict/commands/cf-init.md +0 -262
  102. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  103. package/src/adapters/costrict/commands/cf-spec.md +0 -87
  104. package/src/adapters/costrict/commands/cf-stats.md +0 -96
  105. package/src/adapters/costrict/commands/cf-sync.md +0 -39
  106. package/src/adapters/costrict/commands/cf-task/align.md +0 -331
  107. package/src/adapters/costrict/commands/cf-task/archive.md +0 -155
  108. package/src/adapters/costrict/commands/cf-task/block.md +0 -49
  109. package/src/adapters/costrict/commands/cf-task/graph.md +0 -92
  110. package/src/adapters/costrict/commands/cf-task/note.md +0 -248
  111. package/src/adapters/costrict/commands/cf-task/plan.md +0 -477
  112. package/src/adapters/costrict/commands/cf-task/prd.md +0 -173
  113. package/src/adapters/costrict/commands/cf-task/start.md +0 -317
  114. package/src/adapters/costrict/commands/cf-task/status.md +0 -71
  115. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -148
  116. package/src/adapters/costrict/commands/cf-validate.md +0 -100
  117. package/src/adapters/costrict/settings.local.json +0 -50
  118. package/src/adapters/install-plan.json +0 -100
  119. package/src/adapters/opencode/AGENTS.md +0 -45
  120. package/src/adapters/opencode/commands/cf-init.md +0 -281
  121. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  122. package/src/adapters/opencode/commands/cf-spec.md +0 -91
  123. package/src/adapters/opencode/commands/cf-stats.md +0 -100
  124. package/src/adapters/opencode/commands/cf-sync.md +0 -44
  125. package/src/adapters/opencode/commands/cf-task/align.md +0 -331
  126. package/src/adapters/opencode/commands/cf-task/archive.md +0 -160
  127. package/src/adapters/opencode/commands/cf-task/block.md +0 -54
  128. package/src/adapters/opencode/commands/cf-task/graph.md +0 -97
  129. package/src/adapters/opencode/commands/cf-task/note.md +0 -253
  130. package/src/adapters/opencode/commands/cf-task/plan.md +0 -477
  131. package/src/adapters/opencode/commands/cf-task/prd.md +0 -178
  132. package/src/adapters/opencode/commands/cf-task/start.md +0 -317
  133. package/src/adapters/opencode/commands/cf-task/status.md +0 -76
  134. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -148
  135. package/src/adapters/opencode/commands/cf-validate.md +0 -105
  136. package/src/adapters/opencode/opencode.json +0 -3
  137. package/src/adapters/opencode/plugins/code-flow/index.js +0 -162
  138. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  139. package/src/adapters/platform-bindings.json +0 -370
  140. package/src/cli.js +0 -1076
  141. package/src/core/code-flow/config.yml +0 -161
  142. package/src/core/code-flow/runtime-commands.json +0 -259
  143. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -10
  144. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -10
  145. package/src/core/code-flow/scripts/cf_audit.py +0 -10
  146. package/src/core/code-flow/scripts/cf_codex_hook.py +0 -10
  147. package/src/core/code-flow/scripts/cf_design_lint.py +0 -10
  148. package/src/core/code-flow/scripts/cf_evidence_import.py +0 -10
  149. package/src/core/code-flow/scripts/cf_feedback.py +0 -10
  150. package/src/core/code-flow/scripts/cf_opencode_event.py +0 -10
  151. package/src/core/code-flow/scripts/cf_post_hook.py +0 -11
  152. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -11
  153. package/src/core/code-flow/scripts/cf_scenarios.py +0 -10
  154. package/src/core/code-flow/scripts/cf_spec_context.py +0 -10
  155. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -10
  156. package/src/core/code-flow/scripts/cf_spec_session.py +0 -10
  157. package/src/core/code-flow/scripts/cf_stats.py +0 -10
  158. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -11
  159. package/src/core/code-flow/scripts/cf_sync.py +0 -10
  160. package/src/core/code-flow/scripts/cf_task_index.py +0 -10
  161. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -10
  162. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -10
  163. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -11
  164. package/src/core/code-flow/scripts/cf_validation.py +0 -10
  165. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  166. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  167. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +0 -347
  168. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +0 -660
  169. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +0 -202
  170. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +0 -60
  171. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +0 -73
  172. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +0 -282
  173. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +0 -266
  174. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +0 -172
  175. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +0 -326
  176. package/src/core/code-flow/scripts/codeflow/acceptance/cf_checks.py +0 -333
  177. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +0 -197
  178. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +0 -174
  179. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +0 -300
  180. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +0 -502
  181. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  182. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +0 -347
  183. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +0 -1490
  184. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +0 -533
  185. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +0 -172
  186. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +0 -607
  187. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +0 -53
  188. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +0 -58
  189. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +0 -1175
  190. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +0 -60
  191. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +0 -46
  192. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +0 -540
  193. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +0 -144
  194. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +0 -177
  195. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +0 -1142
  196. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +0 -742
  197. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +0 -31
  198. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  199. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +0 -67
  200. package/src/core/code-flow/scripts/codeflow/core/cf_core.py +0 -609
  201. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +0 -325
  202. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +0 -57
  203. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +0 -92
  204. package/src/core/code-flow/scripts/codeflow/core/cf_log.py +0 -160
  205. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +0 -77
  206. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +0 -26
  207. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +0 -33
  208. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +0 -32
  209. package/src/core/code-flow/scripts/codeflow/core/cf_session_state.py +0 -37
  210. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +0 -53
  211. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +0 -9
  212. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +0 -408
  213. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +0 -253
  214. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  215. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +0 -527
  216. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  217. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +0 -95
  218. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +0 -98
  219. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +0 -30
  220. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +0 -41
  221. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +0 -292
  222. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +0 -161
  223. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  224. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +0 -412
  225. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +0 -301
  226. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +0 -149
  227. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +0 -111
  228. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +0 -114
  229. package/src/core/code-flow/scripts/codeflow/spec/cf_scan.py +0 -367
  230. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +0 -55
  231. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +0 -477
  232. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +0 -119
  233. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_metadata.py +0 -387
  234. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py +0 -529
  235. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +0 -193
  236. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +0 -187
  237. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_resolver.py +0 -369
  238. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_router.py +0 -304
  239. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_verify.py +0 -687
  240. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +0 -330
  241. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +0 -83
  242. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +0 -755
  243. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_transaction.py +0 -110
  244. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  245. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +0 -867
  246. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +0 -178
  247. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +0 -635
  248. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +0 -251
  249. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +0 -570
  250. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +0 -73
  251. package/src/core/code-flow/scripts/runtime-resources.json +0 -29
  252. package/src/core/code-flow/specs/backend/_map.md +0 -56
  253. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  254. package/src/core/code-flow/specs/backend/database.md +0 -51
  255. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  256. package/src/core/code-flow/specs/backend/logging.md +0 -50
  257. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  258. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  259. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  260. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  261. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  262. package/src/core/code-flow/specs/shared/_map.md +0 -57
  263. package/src/core/code-flow/specs/shared/scenarios-template.md +0 -55
  264. package/src/core/code-flow/tasks/.gitkeep +0 -0
  265. package/src/core/code-flow/validation.yml +0 -59
  266. package/src/migrate/runtime.js +0 -213
  267. package/src/migrate/spec-workflow.js +0 -253
  268. package/src/runtime.js +0 -219
@@ -1,330 +0,0 @@
1
- """Stage Gate decisions and plan coverage for Spec Context."""
2
- from __future__ import annotations
3
-
4
- from dataclasses import dataclass
5
- from datetime import datetime, timezone
6
- from pathlib import Path
7
- import re
8
- from typing import Mapping, Optional
9
-
10
- from codeflow.spec.cf_spec_context_model import Decision, RuleBinding, RuleStageStatus, SpecBinding, SpecContext
11
- from codeflow.task.cf_task_doc import (
12
- TaskDocumentError, TaskSection, parse_task_document, task_rule_owner_refs, task_spec_refs,
13
- )
14
-
15
-
16
- @dataclass(frozen=True)
17
- class GateIssue:
18
- code: str
19
- spec_id: str
20
- rule_ref: str
21
- stage: str
22
- message: str
23
-
24
-
25
- @dataclass(frozen=True)
26
- class GateResult:
27
- decision: str
28
- errors: tuple[GateIssue, ...]
29
- warnings: tuple[GateIssue, ...]
30
- affected_refs: tuple[str, ...]
31
-
32
-
33
- def _parse_time(value: str) -> Optional[datetime]:
34
- try:
35
- parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
36
- except ValueError:
37
- return None
38
- return parsed if parsed.tzinfo is not None else None
39
-
40
-
41
- def _decision_valid(decision: Optional[Decision]) -> bool:
42
- if decision is None:
43
- return False
44
- return all((decision.reason, decision.confirmed_by, decision.confirmed_at, decision.source))
45
-
46
-
47
- def _waiver_issue(decision: Optional[Decision], now: datetime) -> Optional[str]:
48
- if not _decision_valid(decision) or decision is None or not decision.expires_at:
49
- return "waiver_invalid"
50
- expires = _parse_time(decision.expires_at)
51
- if expires is None:
52
- return "waiver_invalid"
53
- return "waiver_expired" if expires <= now else None
54
-
55
-
56
- def _evidence_issue(rule: RuleBinding, status: RuleStageStatus, diff_sha256: Optional[str]) -> Optional[str]:
57
- if not status.evidence:
58
- return "evidence_missing"
59
- for evidence in status.evidence:
60
- if evidence.get("status") != "verified":
61
- continue
62
- if evidence.get("rule_text_sha256") != rule.text_sha256:
63
- continue
64
- evidence_diff = evidence.get("diff_sha256")
65
- if evidence_diff is not None and diff_sha256 is not None and evidence_diff != diff_sha256:
66
- continue
67
- result_hash = evidence.get("result_sha256")
68
- if isinstance(result_hash, str) and result_hash:
69
- return None
70
- return "stale_evidence"
71
-
72
-
73
- def _cheap_skip(status: RuleStageStatus) -> bool:
74
- """Latest evidence is a cheap-gate skip (command/test verifier deferred)."""
75
- if not status.evidence:
76
- return False
77
- latest = status.evidence[-1]
78
- return isinstance(latest, Mapping) and latest.get("error_code") == "skipped_in_cheap_gate"
79
-
80
-
81
- def evidence_deferral_kind(evidence: Optional[Mapping[str, object]]) -> str:
82
- """Classify a deferral without trusting the legacy catch-all error code.
83
-
84
- Only ``review_stage`` (and heavy validators, represented outside the Spec
85
- Gate) may be skipped by ``allow_deferred_skips``. Budget/env exhaustion is
86
- never a pass: those checks were not executed for this task.
87
- """
88
- if not isinstance(evidence, Mapping):
89
- return ""
90
- details = evidence.get("details")
91
- details = details if isinstance(details, Mapping) else {}
92
- code = evidence.get("error_code")
93
- if code == "verifier_budget_exhausted":
94
- return "budget"
95
- if code == "env_blocked":
96
- return "env"
97
- if code == "deferred_to_review":
98
- declared = details.get("deferral_kind")
99
- if declared in ("review_stage", "heavy", "budget", "env"):
100
- return str(declared)
101
- reason = details.get("reason")
102
- if reason in ("out_of_task_scope", "review_stage"):
103
- return "review_stage"
104
- if reason in ("budget", "over_finish_budget"):
105
- return "budget"
106
- if reason in ("env", "env_blocked"):
107
- return "env"
108
- # Unknown legacy deferrals are not provably review-stage: fail closed.
109
- return "unknown"
110
- declared = details.get("deferral_kind")
111
- if declared in ("review_stage", "heavy", "budget", "env"):
112
- return str(declared)
113
- return ""
114
-
115
-
116
- def _deferred_skip(status: RuleStageStatus, allowed: frozenset[str] = frozenset(("review_stage", "heavy"))) -> bool:
117
- """Latest evidence is a legitimate deferral (requirement-level review/heavy)."""
118
- if not status.evidence:
119
- return False
120
- latest = status.evidence[-1]
121
- return evidence_deferral_kind(latest if isinstance(latest, Mapping) else None) in allowed
122
-
123
-
124
- def _status_issue(
125
- rule: RuleBinding,
126
- status: RuleStageStatus,
127
- now: datetime,
128
- diff_sha256: Optional[str],
129
- allow_cheap_skips: bool = False,
130
- allow_deferred_skips: bool = False,
131
- ) -> Optional[str]:
132
- if status.status == "applied":
133
- return None
134
- if status.status == "verified":
135
- return _evidence_issue(rule, status, diff_sha256)
136
- if allow_cheap_skips and status.status == "unverified" and _cheap_skip(status):
137
- # 轻量门禁(stop 阶段):命令类 verifier 未执行属预期,留给 finish 全量门禁。
138
- return None
139
- if allow_deferred_skips and status.status == "unverified" and _deferred_skip(status):
140
- # finish 门禁:范围外/超预算 verifier 已标记延后,需求级 verify-e2e 全量补跑。
141
- return None
142
- if status.status == "not_applicable":
143
- return None if _decision_valid(status.decision) else "decision_invalid"
144
- if status.status == "waived":
145
- return _waiver_issue(status.decision, now)
146
- return status.status
147
-
148
-
149
- def _binding_issues(
150
- binding: SpecBinding,
151
- stage: str,
152
- now: datetime,
153
- diff_sha256: Optional[str],
154
- allow_cheap_skips: bool = False,
155
- allow_deferred_skips: bool = False,
156
- ) -> tuple[list[GateIssue], list[GateIssue]]:
157
- errors: list[GateIssue] = []
158
- warnings: list[GateIssue] = []
159
- for rule in binding.rules:
160
- status = rule.stage_status.get(stage)
161
- if status is None:
162
- continue
163
- if stage in ("code", "review") and getattr(rule, "verifier_stage", "code") != stage:
164
- continue
165
- if rule.enforcement == "advisory" and status.status == "pending":
166
- continue
167
- code = "missing_spec" if binding.status == "missing" else _status_issue(
168
- rule, status, now, diff_sha256, allow_cheap_skips, allow_deferred_skips
169
- )
170
- if stage in ("code", "review") and status.status == "applied":
171
- code = "rule_execution_evidence_missing"
172
- if code is None:
173
- continue
174
- issue = GateIssue(code, binding.spec_id, rule.ref, stage, f"{binding.spec_id}#{rule.ref}: {code}")
175
- target = errors if rule.enforcement == "required" else warnings
176
- target.append(issue)
177
- return errors, warnings
178
-
179
-
180
- def validate_stage(
181
- context: SpecContext,
182
- stage: str,
183
- artifact: str = "",
184
- task_id: str = "",
185
- now: Optional[datetime] = None,
186
- diff_sha256: Optional[str] = None,
187
- allow_cheap_skips: bool = False,
188
- allow_deferred_skips: bool = False,
189
- root: str = "",
190
- task_dir: str = "",
191
- ) -> GateResult:
192
- del artifact, task_id
193
- current = now or datetime.now(timezone.utc)
194
- errors: list[GateIssue] = []
195
- warnings: list[GateIssue] = []
196
- for binding in context.bindings:
197
- binding_errors, binding_warnings = _binding_issues(
198
- binding, stage, current, diff_sha256, allow_cheap_skips, allow_deferred_skips
199
- )
200
- errors.extend(binding_errors)
201
- warnings.extend(binding_warnings)
202
- if root:
203
- from codeflow.spec.cf_rule_readiness import applied_findings
204
- errors.extend(GateIssue(str(item["code"]), "", "", stage, str(item["message"]))
205
- for item in applied_findings(context, root, stage, task_dir=task_dir))
206
- refs = tuple(sorted({f"{item.spec_id}#{item.rule_ref}" for item in (*errors, *warnings)}))
207
- return GateResult("block" if errors else "pass", tuple(errors), tuple(warnings), refs)
208
-
209
-
210
- def _task_sections(text: str) -> Mapping[str, TaskSection]:
211
- return {task.id: task for task in parse_task_document(text).tasks}
212
-
213
-
214
- def _plan_issue(code: str, binding: SpecBinding, rule: RuleBinding, message: str) -> GateIssue:
215
- return GateIssue(code, binding.spec_id, rule.ref, "plan", message)
216
-
217
-
218
- def _task_plan_issue(code: str, task: TaskSection, ref: str, message: str) -> GateIssue:
219
- spec_id, _, rule_ref = ref.partition("#")
220
- return GateIssue(code, spec_id, rule_ref, "plan", f"{task.id}: {message}")
221
-
222
-
223
- def _plan_task_refs(task: TaskSection, known: set[str]) -> tuple[tuple[str, ...], list[GateIssue]]:
224
- try:
225
- refs = task_spec_refs(task)
226
- except ValueError as exc:
227
- return (), [_task_plan_issue("plan_spec_refs_invalid", task, "", str(exc))]
228
- issues = [_task_plan_issue("plan_spec_ref_unknown", task, ref, f"未绑定的 Spec-Ref: {ref}")
229
- for ref in refs if ref not in known]
230
- return refs, issues
231
-
232
-
233
- def _plan_task_owners(task: TaskSection, refs: tuple[str, ...], known: set[str]) -> tuple[tuple[str, ...], list[GateIssue]]:
234
- try:
235
- declared = task_rule_owner_refs(task) or ()
236
- except ValueError as exc:
237
- return (), [_task_plan_issue("plan_owner_refs_invalid", task, "", str(exc))]
238
- owners: list[str] = []
239
- issues: list[GateIssue] = []
240
- for ref in declared:
241
- if ref not in known:
242
- issues.append(_task_plan_issue("plan_owner_ref_unknown", task, ref, f"未绑定的 owner Rule: {ref}"))
243
- elif ref not in refs:
244
- issues.append(_task_plan_issue("plan_owner_not_referenced", task, ref, f"owner 未引用 Rule: {ref}"))
245
- else:
246
- owners.append(ref)
247
- return tuple(owners), issues
248
-
249
-
250
- def _plan_owners(sections: Mapping[str, TaskSection], context: SpecContext) -> tuple[dict[str, tuple[str, ...]], list[GateIssue]]:
251
- known = {f"{binding.spec_id}#{rule.ref}" for binding in context.bindings for rule in binding.rules}
252
- owners: dict[str, list[str]] = {}
253
- issues: list[GateIssue] = []
254
- for task in sections.values():
255
- refs, ref_issues = _plan_task_refs(task, known)
256
- declared, owner_issues = _plan_task_owners(task, refs, known)
257
- issues.extend((*ref_issues, *owner_issues))
258
- for ref in declared:
259
- owners.setdefault(ref, []).append(task.id)
260
- return {ref: tuple(tasks) for ref, tasks in owners.items()}, issues
261
-
262
-
263
- def _rule_plan_issues(
264
- binding: SpecBinding, rule: RuleBinding, sections: Mapping[str, TaskSection],
265
- declared_owners: Optional[Mapping[str, tuple[str, ...]]] = None,
266
- ) -> tuple[GateIssue, ...]:
267
- full_ref = f"{binding.spec_id}#{rule.ref}"
268
- owners = (declared_owners.get(full_ref, ()) if declared_owners is not None
269
- else tuple(task for task, section in sections.items() if full_ref in section.body))
270
- if not owners:
271
- return (_plan_issue("plan_owner_missing", binding, rule, f"{full_ref} 没有责任 TASK"),)
272
- if len(owners) > 1:
273
- return (_plan_issue("plan_owner_duplicate", binding, rule, f"{full_ref} 有多个责任 TASK"),)
274
- section = sections[owners[0]].body
275
- issues: list[GateIssue] = []
276
- if rule.verifier_ref == "advisory:none":
277
- issues.append(_plan_issue("plan_verifier_missing", binding, rule, f"{full_ref} 缺 verifier"))
278
- checklist = section.partition("### Checklist")[2].partition("### Acceptance Contract")[0]
279
- if rule.ref not in checklist or "verifier" not in checklist.lower():
280
- issues.append(_plan_issue("plan_checklist_missing", binding, rule, f"{full_ref} 缺 verifier Checklist"))
281
- contract = section.partition("### Acceptance Contract")[2]
282
- if not contract.strip() or not re.search(
283
- r"(?i)\b(unit|component|contract|integration|system|capacity|e2e|manual)\b", contract
284
- ):
285
- issues.append(_plan_issue("plan_acceptance_missing", binding, rule, f"{full_ref} 缺测试层级/真实边界"))
286
- return tuple(issues)
287
-
288
-
289
- def validate_plan_coverage(context: SpecContext, artifact: str,
290
- *, text: Optional[str] = None) -> GateResult:
291
- try:
292
- raw = Path(artifact).read_text(encoding="utf-8") if text is None else text
293
- except OSError as exc:
294
- issue = GateIssue("plan_read_error", "", "", "plan", str(exc))
295
- return GateResult("block", (issue,), (), ())
296
- try:
297
- sections = _task_sections(raw)
298
- except TaskDocumentError as exc:
299
- issue = GateIssue(exc.code, "", "", "plan", str(exc))
300
- return GateResult("block", (issue,), (), ())
301
- explicit = any("Rule-Owner-Refs" in task.fields for task in sections.values())
302
- declared, errors = _plan_owners(sections, context) if explicit else (None, [])
303
- for binding in context.bindings:
304
- for rule in binding.rules:
305
- if rule.enforcement == "required" and "plan" in rule.stage_status:
306
- errors.extend(_rule_plan_issues(binding, rule, sections, declared))
307
- refs = tuple(sorted({f"{item.spec_id}#{item.rule_ref}" for item in errors
308
- if item.spec_id and item.rule_ref}))
309
- return GateResult("block" if errors else "pass", tuple(errors), (), refs)
310
-
311
-
312
- def _issue_data(issue: GateIssue) -> dict[str, str]:
313
- return {
314
- "code": issue.code,
315
- "spec_id": issue.spec_id,
316
- "rule_ref": issue.rule_ref,
317
- "stage": issue.stage,
318
- "message": issue.message,
319
- }
320
-
321
-
322
- def result_to_data(result: GateResult) -> dict[str, object]:
323
- return {
324
- "decision": result.decision,
325
- "errors": [_issue_data(item) for item in result.errors],
326
- "warnings": [_issue_data(item) for item in result.warnings],
327
- "affected_refs": list(result.affected_refs),
328
- }
329
-
330
-
@@ -1,83 +0,0 @@
1
- """Project one TASK's bound Spec rules into a session document."""
2
- from __future__ import annotations
3
-
4
- from dataclasses import dataclass
5
- from pathlib import Path
6
- import re
7
- from typing import Mapping, Optional
8
-
9
- from codeflow.spec.cf_spec_context_model import RuleBinding, SpecContext, context_sha256
10
- from codeflow.task.cf_task_doc import task_section, task_spec_refs
11
-
12
-
13
- @dataclass(frozen=True)
14
- class SessionProjection:
15
- text: str
16
- included_rules: int
17
- total_rules: int
18
- truncated: bool
19
-
20
-
21
- def _task_section(text: str, task_id: str) -> str:
22
- return task_section(text, task_id).body
23
-
24
-
25
- def _refs(section: str) -> tuple[str, ...]:
26
- return task_spec_refs(section)
27
-
28
-
29
- def _rule_index(context: SpecContext) -> Mapping[str, RuleBinding]:
30
- return {
31
- f"{binding.spec_id}#{rule.ref}": rule
32
- for binding in context.bindings
33
- for rule in binding.rules
34
- }
35
-
36
-
37
- def _acceptance_contract(section: str) -> str:
38
- match = re.search(r"(?ms)^### Acceptance Contract\s*$\n(.*?)(?=^### |^## |\Z)", section)
39
- if match is None:
40
- raise ValueError("acceptance_contract_missing")
41
- return match.group(1).strip()
42
-
43
-
44
- def _rule_line(ref: str, rule: RuleBinding, compact: bool = False) -> str:
45
- hashes = f"rule_sha256={rule.text_sha256} verifier={rule.verifier_ref}"
46
- if compact:
47
- return f"- `{ref}` {hashes}\n"
48
- artifacts = sorted({item.artifact for status in rule.stage_status.values() for item in status.refs})
49
- return f"- `{ref}`: {rule.summary}\n - {hashes}; artifacts={','.join(artifacts) or 'none'}\n"
50
-
51
-
52
- def _reference_header(section: str) -> str:
53
- lines = re.findall(r"(?m)^- \*\*(?:Source|Design-Refs|Acceptance-Refs)\*\*:[ \t]*[^\n]*", section)
54
- return "".join(line + "\n" for line in lines)
55
-
56
-
57
- def project_task_session(
58
- context: SpecContext, task_file: str, task_id: str, max_chars: int = 12000,
59
- *, context_hash: Optional[str] = None,
60
- ) -> SessionProjection:
61
- section = _task_section(Path(task_file).read_text(encoding="utf-8"), task_id)
62
- refs = _refs(section)
63
- index = _rule_index(context)
64
- missing = tuple(ref for ref in refs if ref not in index)
65
- if missing:
66
- raise ValueError(f"unknown_spec_refs: {', '.join(missing)}")
67
- acceptance = _acceptance_contract(section)
68
- header = (f"# {task_id} Spec Context\n\n- Context-SHA256: `{context_hash or context_sha256(context)}`\n"
69
- + _reference_header(section) + "\n## Required Rules\n")
70
- footer = f"\n## Acceptance Contract\n{acceptance}\n"
71
- lines: list[str] = []
72
- for ref in refs:
73
- candidate = _rule_line(ref, index[ref])
74
- if len(header + "".join(lines) + candidate + footer) > max_chars:
75
- break
76
- lines.append(candidate)
77
- truncated = len(lines) < len(refs) or len(header + "".join(lines) + footer) > max_chars
78
- if truncated:
79
- footer = "\n> 投影超过预算;完整 Context 保留在需求目录,请拆分 TASK。\n" + footer
80
- text = (header + "".join(lines) + footer)[:max_chars]
81
- return SessionProjection(text, len(lines), len(refs), truncated)
82
-
83
-