@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
@@ -0,0 +1,330 @@
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
+
@@ -0,0 +1,83 @@
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
+