@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
|
@@ -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
|
+
|