@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.
- package/README.md +47 -3
- package/docs/USAGE.md +1529 -0
- package/docs/acceptance-contract.md +217 -0
- package/docs/ci/acceptance-evidence.md +77 -0
- package/docs/migrations/runtime-0.7.0.md +41 -0
- package/docs/migrations/runtime-0.8.0.md +154 -0
- package/docs/migrations/runtime-0.9.0.md +40 -0
- package/docs/spec-injection-hardening.md +54 -0
- package/docs/spec-injection-performance.json +73 -0
- package/docs/spec-optimization.md +12 -0
- package/package.json +18 -3
- package/src/adapters/claude/CLAUDE.md +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -3
- package/src/adapters/claude/commands/cf-spec.md +23 -3
- package/src/adapters/claude/commands/cf-stats.md +11 -1
- package/src/adapters/claude/commands/cf-sync.md +20 -6
- package/src/adapters/claude/commands/cf-task/align.md +25 -3
- package/src/adapters/claude/commands/cf-task/archive.md +36 -42
- package/src/adapters/claude/commands/cf-task/block.md +15 -2
- package/src/adapters/claude/commands/cf-task/graph.md +13 -1
- package/src/adapters/claude/commands/cf-task/note.md +13 -1
- package/src/adapters/claude/commands/cf-task/plan.md +137 -23
- package/src/adapters/claude/commands/cf-task/prd.md +13 -2
- package/src/adapters/claude/commands/cf-task/start.md +52 -39
- package/src/adapters/claude/commands/cf-task/status.md +12 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/claude/commands/cf-validate.md +30 -5
- package/src/adapters/codex/AGENTS.md +1 -1
- package/src/adapters/codex/hooks.json +14 -10
- package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
- package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
- package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
- package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -42
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +52 -39
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +69 -35
- package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
- package/src/adapters/commands.json +115 -0
- package/src/adapters/costrict/CLAUDE.md +1 -1
- package/src/adapters/costrict/commands/cf-init.md +1 -3
- package/src/adapters/costrict/commands/cf-spec.md +23 -3
- package/src/adapters/costrict/commands/cf-stats.md +11 -1
- package/src/adapters/costrict/commands/cf-sync.md +20 -6
- package/src/adapters/costrict/commands/cf-task/align.md +25 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +36 -42
- package/src/adapters/costrict/commands/cf-task/block.md +15 -2
- package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
- package/src/adapters/costrict/commands/cf-task/note.md +13 -1
- package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
- package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
- package/src/adapters/costrict/commands/cf-task/start.md +52 -39
- package/src/adapters/costrict/commands/cf-task/status.md +12 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/costrict/commands/cf-validate.md +30 -5
- package/src/adapters/install-plan.json +100 -0
- package/src/adapters/opencode/AGENTS.md +1 -1
- package/src/adapters/opencode/commands/cf-init.md +7 -7
- package/src/adapters/opencode/commands/cf-learn.md +6 -6
- package/src/adapters/opencode/commands/cf-spec.md +34 -2
- package/src/adapters/opencode/commands/cf-stats.md +17 -7
- package/src/adapters/opencode/commands/cf-sync.md +21 -6
- package/src/adapters/opencode/commands/cf-task/align.md +25 -8
- package/src/adapters/opencode/commands/cf-task/archive.md +36 -42
- package/src/adapters/opencode/commands/cf-task/block.md +16 -2
- package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
- package/src/adapters/opencode/commands/cf-task/note.md +14 -1
- package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
- package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
- package/src/adapters/opencode/commands/cf-task/start.md +53 -45
- package/src/adapters/opencode/commands/cf-task/status.md +13 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/opencode/commands/cf-validate.md +35 -9
- package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
- package/src/adapters/platform-bindings.json +370 -0
- package/src/cli.js +369 -316
- package/src/core/code-flow/config.yml +4 -1
- package/src/core/code-flow/runtime-commands.json +259 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
- package/src/core/code-flow/scripts/cf_audit.py +10 -0
- package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
- package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
- package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
- package/src/core/code-flow/scripts/cf_feedback.py +5 -51
- package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
- package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +5 -1741
- package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
- package/src/core/code-flow/scripts/cf_stats.py +5 -501
- package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
- package/src/core/code-flow/scripts/cf_sync.py +5 -135
- package/src/core/code-flow/scripts/cf_task_index.py +5 -143
- package/src/core/code-flow/scripts/cf_task_parallel.py +5 -389
- package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
- package/src/core/code-flow/scripts/cf_validation.py +6 -77
- package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
- package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
- package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
- package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
- package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
- package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +5 -2
- package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
- package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
- package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
- package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +163 -35
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
- package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
- package/src/core/code-flow/scripts/runtime-resources.json +29 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
- package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
- package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
- package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
- package/src/core/code-flow/validation.yml +9 -3
- package/src/migrate/runtime.js +213 -0
- package/src/migrate/spec-workflow.js +16 -1
- package/src/runtime.js +219 -0
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- /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
|
-
"""
|
|
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
|
-
"""
|
|
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
|