@michaelxwb/code-flow 0.6.5 → 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 -42
  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 +52 -39
  25. package/src/adapters/claude/commands/cf-task/status.md +12 -0
  26. package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
  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 -42
  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 +52 -39
  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 +69 -35
  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 -42
  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 +52 -39
  59. package/src/adapters/costrict/commands/cf-task/status.md +12 -0
  60. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
  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 -42
  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 +53 -45
  77. package/src/adapters/opencode/commands/cf-task/status.md +13 -0
  78. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
  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 -1741
  97. package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
  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 -389
  104. package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
  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} +5 -2
  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} +163 -35
  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 -304
  205. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  206. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  207. /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
@@ -1,264 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """Fail-closed Stage Gate for required Spec Context rules."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- from dataclasses import dataclass
8
- from datetime import datetime, timezone
9
- import json
10
- from pathlib import Path
11
- import re
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_spec_gate."""
12
3
  import sys
13
- import time
14
- from typing import IO, Mapping, Optional, Sequence
15
-
16
- from cf_spec_context import Decision, RuleBinding, RuleStageStatus, SpecBinding, SpecContext, load_context
17
- from cf_core import phase_timing
18
-
19
-
20
- @dataclass(frozen=True)
21
- class GateIssue:
22
- code: str
23
- spec_id: str
24
- rule_ref: str
25
- stage: str
26
- message: str
27
-
28
-
29
- @dataclass(frozen=True)
30
- class GateResult:
31
- decision: str
32
- errors: tuple[GateIssue, ...]
33
- warnings: tuple[GateIssue, ...]
34
- affected_refs: tuple[str, ...]
35
-
36
-
37
- def _parse_time(value: str) -> Optional[datetime]:
38
- try:
39
- parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
40
- except ValueError:
41
- return None
42
- return parsed if parsed.tzinfo is not None else None
43
-
44
-
45
- def _decision_valid(decision: Optional[Decision]) -> bool:
46
- if decision is None:
47
- return False
48
- return all((decision.reason, decision.confirmed_by, decision.confirmed_at, decision.source))
49
-
50
-
51
- def _waiver_issue(decision: Optional[Decision], now: datetime) -> Optional[str]:
52
- if not _decision_valid(decision) or decision is None or not decision.expires_at:
53
- return "waiver_invalid"
54
- expires = _parse_time(decision.expires_at)
55
- if expires is None:
56
- return "waiver_invalid"
57
- return "waiver_expired" if expires <= now else None
58
-
59
-
60
- def _evidence_issue(rule: RuleBinding, status: RuleStageStatus, diff_sha256: Optional[str]) -> Optional[str]:
61
- if not status.evidence:
62
- return "evidence_missing"
63
- for evidence in status.evidence:
64
- if evidence.get("status") != "verified":
65
- continue
66
- if evidence.get("rule_text_sha256") != rule.text_sha256:
67
- continue
68
- evidence_diff = evidence.get("diff_sha256")
69
- if evidence_diff is not None and diff_sha256 is not None and evidence_diff != diff_sha256:
70
- continue
71
- result_hash = evidence.get("result_sha256")
72
- if isinstance(result_hash, str) and result_hash:
73
- return None
74
- return "stale_evidence"
75
-
76
-
77
- def _cheap_skip(status: RuleStageStatus) -> bool:
78
- """Latest evidence is a cheap-gate skip (command/test verifier deferred)."""
79
- if not status.evidence:
80
- return False
81
- latest = status.evidence[-1]
82
- return isinstance(latest, Mapping) and latest.get("error_code") == "skipped_in_cheap_gate"
83
-
84
-
85
- def _status_issue(
86
- rule: RuleBinding,
87
- status: RuleStageStatus,
88
- now: datetime,
89
- diff_sha256: Optional[str],
90
- allow_cheap_skips: bool = False,
91
- ) -> Optional[str]:
92
- if status.status == "applied":
93
- return None
94
- if status.status == "verified":
95
- return _evidence_issue(rule, status, diff_sha256)
96
- if allow_cheap_skips and status.status == "unverified" and _cheap_skip(status):
97
- # 轻量门禁(stop 阶段):命令类 verifier 未执行属预期,留给 finish 全量门禁。
98
- return None
99
- if status.status == "not_applicable":
100
- return None if _decision_valid(status.decision) else "decision_invalid"
101
- if status.status == "waived":
102
- return _waiver_issue(status.decision, now)
103
- return status.status
104
-
105
-
106
- def _binding_issues(
107
- binding: SpecBinding,
108
- stage: str,
109
- now: datetime,
110
- diff_sha256: Optional[str],
111
- allow_cheap_skips: bool = False,
112
- ) -> tuple[list[GateIssue], list[GateIssue]]:
113
- errors: list[GateIssue] = []
114
- warnings: list[GateIssue] = []
115
- for rule in binding.rules:
116
- status = rule.stage_status.get(stage)
117
- if status is None:
118
- continue
119
- if stage in ("code", "review") and getattr(rule, "verifier_stage", "code") != stage:
120
- continue
121
- if rule.enforcement == "advisory" and status.status == "pending":
122
- continue
123
- code = "missing_spec" if binding.status == "missing" else _status_issue(rule, status, now, diff_sha256, allow_cheap_skips)
124
- if code is None:
125
- continue
126
- issue = GateIssue(code, binding.spec_id, rule.ref, stage, f"{binding.spec_id}#{rule.ref}: {code}")
127
- target = errors if rule.enforcement == "required" else warnings
128
- target.append(issue)
129
- return errors, warnings
130
-
131
-
132
- def validate_stage(
133
- context: SpecContext,
134
- stage: str,
135
- artifact: str = "",
136
- task_id: str = "",
137
- now: Optional[datetime] = None,
138
- diff_sha256: Optional[str] = None,
139
- allow_cheap_skips: bool = False,
140
- ) -> GateResult:
141
- del artifact, task_id
142
- current = now or datetime.now(timezone.utc)
143
- errors: list[GateIssue] = []
144
- warnings: list[GateIssue] = []
145
- for binding in context.bindings:
146
- binding_errors, binding_warnings = _binding_issues(binding, stage, current, diff_sha256, allow_cheap_skips)
147
- errors.extend(binding_errors)
148
- warnings.extend(binding_warnings)
149
- refs = tuple(sorted({f"{item.spec_id}#{item.rule_ref}" for item in (*errors, *warnings)}))
150
- return GateResult("block" if errors else "pass", tuple(errors), tuple(warnings), refs)
151
-
152
-
153
- def _task_sections(text: str) -> Mapping[str, str]:
154
- matches = list(re.finditer(r"(?m)^## (TASK-\d+):[^\n]*$", text))
155
- sections: dict[str, str] = {}
156
- for index, match in enumerate(matches):
157
- end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
158
- sections[match.group(1)] = text[match.start():end]
159
- return sections
160
-
161
-
162
- def _plan_issue(code: str, binding: SpecBinding, rule: RuleBinding, message: str) -> GateIssue:
163
- return GateIssue(code, binding.spec_id, rule.ref, "plan", message)
164
-
165
-
166
- def _rule_plan_issues(
167
- binding: SpecBinding, rule: RuleBinding, sections: Mapping[str, str]
168
- ) -> tuple[GateIssue, ...]:
169
- full_ref = f"{binding.spec_id}#{rule.ref}"
170
- owners = tuple(task for task, section in sections.items() if full_ref in section)
171
- if not owners:
172
- return (_plan_issue("plan_owner_missing", binding, rule, f"{full_ref} 没有责任 TASK"),)
173
- if len(owners) > 1:
174
- return (_plan_issue("plan_owner_duplicate", binding, rule, f"{full_ref} 有多个责任 TASK"),)
175
- section = sections[owners[0]]
176
- issues: list[GateIssue] = []
177
- if rule.verifier_ref == "advisory:none":
178
- issues.append(_plan_issue("plan_verifier_missing", binding, rule, f"{full_ref} 缺 verifier"))
179
- checklist = section.partition("### Checklist")[2].partition("### Acceptance Contract")[0]
180
- if rule.ref not in checklist or "verifier" not in checklist.lower():
181
- issues.append(_plan_issue("plan_checklist_missing", binding, rule, f"{full_ref} 缺 verifier Checklist"))
182
- contract = section.partition("### Acceptance Contract")[2]
183
- if not contract.strip() or not re.search(r"\b(unit|integration|E2E|manual)\b", contract):
184
- issues.append(_plan_issue("plan_acceptance_missing", binding, rule, f"{full_ref} 缺测试层级/真实边界"))
185
- return tuple(issues)
186
-
187
-
188
- def validate_plan_coverage(context: SpecContext, artifact: str) -> GateResult:
189
- try:
190
- text = Path(artifact).read_text(encoding="utf-8")
191
- except OSError as exc:
192
- issue = GateIssue("plan_read_error", "", "", "plan", str(exc))
193
- return GateResult("block", (issue,), (), ())
194
- sections = _task_sections(text)
195
- errors: list[GateIssue] = []
196
- for binding in context.bindings:
197
- for rule in binding.rules:
198
- if rule.enforcement == "required" and "plan" in rule.stage_status:
199
- errors.extend(_rule_plan_issues(binding, rule, sections))
200
- refs = tuple(sorted(f"{item.spec_id}#{item.rule_ref}" for item in errors))
201
- return GateResult("block" if errors else "pass", tuple(errors), (), refs)
202
-
203
-
204
- def _issue_data(issue: GateIssue) -> dict[str, str]:
205
- return {
206
- "code": issue.code,
207
- "spec_id": issue.spec_id,
208
- "rule_ref": issue.rule_ref,
209
- "stage": issue.stage,
210
- "message": issue.message,
211
- }
212
-
213
-
214
- def result_to_data(result: GateResult) -> dict[str, object]:
215
- return {
216
- "decision": result.decision,
217
- "errors": [_issue_data(item) for item in result.errors],
218
- "warnings": [_issue_data(item) for item in result.warnings],
219
- "affected_refs": list(result.affected_refs),
220
- }
221
-
222
-
223
- def _merge_results(first: GateResult, second: GateResult) -> GateResult:
224
- errors = (*first.errors, *second.errors)
225
- warnings = (*first.warnings, *second.warnings)
226
- refs = tuple(sorted(set((*first.affected_refs, *second.affected_refs))))
227
- return GateResult("block" if errors else "pass", errors, warnings, refs)
228
-
229
-
230
- def _parser() -> argparse.ArgumentParser:
231
- parser = argparse.ArgumentParser(prog="cf_spec_gate.py")
232
- parser.add_argument("--task-dir", required=True)
233
- parser.add_argument("--stage", required=True)
234
- parser.add_argument("--task", default="")
235
- parser.add_argument("--artifact", default="")
236
- parser.add_argument("--json", action="store_true")
237
- return parser
238
-
239
-
240
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
241
- started = time.monotonic()
242
- try:
243
- args = _parser().parse_args(argv)
244
- phase_started = time.monotonic()
245
- context = load_context(str(Path(args.task_dir) / "spec-context.yml"))
246
- phase_timing("gate.load_context", phase_started)
247
- phase_started = time.monotonic()
248
- result = validate_stage(context, args.stage, task_id=args.task)
249
- phase_timing("gate.validate_stage", phase_started)
250
- if args.stage == "plan" and args.artifact:
251
- phase_started = time.monotonic()
252
- result = _merge_results(result, validate_plan_coverage(context, args.artifact))
253
- phase_timing("gate.validate_plan", phase_started)
254
- stdout.write(json.dumps(result_to_data(result), ensure_ascii=False))
255
- phase_timing("gate.total", started)
256
- return 0 if result.decision == "pass" else 3
257
- except Exception as exc:
258
- sys.stderr.write(f"cf_spec_gate error: {exc}\n")
259
- stdout.write(json.dumps({"decision": "block", "errors": [{"code": "gate_error"}]}, ensure_ascii=False))
260
- return 2
261
4
 
5
+ from codeflow.cli import cf_spec_gate as _impl
262
6
 
263
7
  if __name__ == "__main__":
264
- raise SystemExit(main())
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl
@@ -1,119 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """Project one TASK's bound rules into a bounded session document."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- from dataclasses import dataclass
8
- import json
9
- from pathlib import Path
10
- import re
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_spec_session."""
11
3
  import sys
12
- from typing import IO, Mapping, Optional, Sequence
13
-
14
- from cf_spec_context import RuleBinding, SpecContext, context_sha256, load_context
15
-
16
-
17
- @dataclass(frozen=True)
18
- class SessionProjection:
19
- text: str
20
- included_rules: int
21
- total_rules: int
22
- truncated: bool
23
-
24
-
25
- # context_sha256 is defined next to the identity projection in cf_spec_context
26
- # and re-exported here for callers that import the session module.
27
-
28
-
29
- def _task_section(text: str, task_id: str) -> str:
30
- match = re.search(rf"(?m)^## {re.escape(task_id)}:[^\n]*$", text)
31
- if match is None:
32
- raise ValueError(f"task_not_found: {task_id}")
33
- following = re.search(r"(?m)^## TASK-\d+:", text[match.end():])
34
- end = match.end() + following.start() if following else len(text)
35
- return text[match.start():end]
36
-
37
-
38
- def _refs(section: str) -> tuple[str, ...]:
39
- # [ \t]* 而非 \s*:空 Spec-Refs 不得跨行吞掉下一字段(Acceptance-Refs 等)
40
- match = re.search(r"(?m)^- \*\*Spec-Refs\*\*:[ \t]*(.*)$", section)
41
- if match is None:
42
- raise ValueError("spec_refs_missing")
43
- return tuple(item.strip() for item in match.group(1).split(",") if item.strip())
44
-
45
-
46
- def _rule_index(context: SpecContext) -> Mapping[str, RuleBinding]:
47
- return {
48
- f"{binding.spec_id}#{rule.ref}": rule
49
- for binding in context.bindings
50
- for rule in binding.rules
51
- }
52
-
53
-
54
- def _acceptance_contract(section: str) -> str:
55
- match = re.search(r"(?ms)^### Acceptance Contract\s*$\n(.*?)(?=^### |^## |\Z)", section)
56
- if match is None:
57
- raise ValueError("acceptance_contract_missing")
58
- return match.group(1).strip()
59
-
60
-
61
- def _rule_line(ref: str, rule: RuleBinding, compact: bool = False) -> str:
62
- hashes = f"rule_sha256={rule.text_sha256} verifier={rule.verifier_ref}"
63
- if compact:
64
- return f"- `{ref}` {hashes}\n"
65
- artifacts = sorted({item.artifact for status in rule.stage_status.values() for item in status.refs})
66
- return f"- `{ref}`: {rule.summary}\n - {hashes}; artifacts={','.join(artifacts) or 'none'}\n"
67
-
68
-
69
- def project_task_session(
70
- context: SpecContext, task_file: str, task_id: str, max_chars: int = 12000
71
- ) -> SessionProjection:
72
- section = _task_section(Path(task_file).read_text(encoding="utf-8"), task_id)
73
- refs = _refs(section)
74
- index = _rule_index(context)
75
- missing = tuple(ref for ref in refs if ref not in index)
76
- if missing:
77
- raise ValueError(f"unknown_spec_refs: {', '.join(missing)}")
78
- acceptance = _acceptance_contract(section)
79
- header = f"# {task_id} Spec Context\n\n- Context-SHA256: `{context_sha256(context)}`\n\n## Required Rules\n"
80
- footer = f"\n## Acceptance Contract\n{acceptance}\n"
81
- lines: list[str] = []
82
- for ref in refs:
83
- candidate = _rule_line(ref, index[ref])
84
- if len(header + "".join(lines) + candidate + footer) > max_chars:
85
- break
86
- lines.append(candidate)
87
- truncated = len(lines) < len(refs) or len(header + "".join(lines) + footer) > max_chars
88
- if truncated:
89
- footer = "\n> 投影超过预算;完整 Context 保留在需求目录,请拆分 TASK。\n" + footer
90
- text = (header + "".join(lines) + footer)[:max_chars]
91
- return SessionProjection(text, len(lines), len(refs), truncated)
92
-
93
-
94
- def _parser() -> argparse.ArgumentParser:
95
- parser = argparse.ArgumentParser(prog="cf_spec_session.py")
96
- parser.add_argument("--task-dir", required=True)
97
- parser.add_argument("--task-file", required=True)
98
- parser.add_argument("--task", required=True)
99
- parser.add_argument("--output", required=True)
100
- parser.add_argument("--budget", type=int, default=12000)
101
- parser.add_argument("--json", action="store_true")
102
- return parser
103
-
104
-
105
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
106
- try:
107
- args = _parser().parse_args(argv)
108
- context = load_context(str(Path(args.task_dir) / "spec-context.yml"))
109
- result = project_task_session(context, args.task_file, args.task, args.budget)
110
- Path(args.output).write_text(result.text, encoding="utf-8")
111
- stdout.write(json.dumps(result.__dict__, ensure_ascii=False))
112
- return 0
113
- except Exception as exc:
114
- stdout.write(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
115
- return 3
116
4
 
5
+ from codeflow.cli import cf_spec_session as _impl
117
6
 
118
7
  if __name__ == "__main__":
119
- raise SystemExit(main())
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl