@michaelxwb/code-flow 0.6.4 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -40
- 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 +67 -35
- package/src/adapters/claude/commands/cf-task/status.md +12 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +72 -34
- 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 -40
- 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 +67 -35
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +73 -34
- 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 -40
- 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 +67 -35
- package/src/adapters/costrict/commands/cf-task/status.md +12 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +72 -34
- 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 -40
- 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 +68 -41
- package/src/adapters/opencode/commands/cf-task/status.md +13 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +72 -34
- 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 -1714
- package/src/core/code-flow/scripts/cf_spec_gate.py +5 -257
- 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 -380
- package/src/core/code-flow/scripts/cf_task_workflow.py +6 -103
- 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} +37 -3
- 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} +252 -26
- 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 -299
- 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,570 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Active-task scope expansion and hash-bound Done verification."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import time
|
|
13
|
+
from typing import Mapping, Optional, Sequence
|
|
14
|
+
|
|
15
|
+
from codeflow.cli.cf_spec_context import (
|
|
16
|
+
BindingInput,
|
|
17
|
+
RuleBinding,
|
|
18
|
+
RuleStageStatus,
|
|
19
|
+
SpecBinding,
|
|
20
|
+
SpecContext,
|
|
21
|
+
bind_specs,
|
|
22
|
+
current_owned_paths,
|
|
23
|
+
load_active_task,
|
|
24
|
+
load_context,
|
|
25
|
+
pause_active_task,
|
|
26
|
+
resync_active_hash,
|
|
27
|
+
save_context,
|
|
28
|
+
)
|
|
29
|
+
from codeflow.spec.cf_stage_gate import evidence_deferral_kind, validate_stage
|
|
30
|
+
from codeflow.spec.cf_spec_metadata import SpecMetadata, load_spec_metadata
|
|
31
|
+
from codeflow.spec.cf_spec_resolver import resolve_candidates, SpecCandidate
|
|
32
|
+
from codeflow.spec.cf_spec_context_model import context_sha256
|
|
33
|
+
from codeflow.spec.cf_spec_verify import VerificationEvidence, VerificationScope, run_all_verifiers, store_verification_evidence
|
|
34
|
+
from codeflow.core.cf_core import load_config, phase_timing, resolve_quality_loop
|
|
35
|
+
from codeflow.core import cf_git
|
|
36
|
+
from codeflow.acceptance.cf_acceptance_manifest_core import validate_execution_baseline
|
|
37
|
+
from codeflow.acceptance.cf_acceptance_schema import load_manifest
|
|
38
|
+
from codeflow.core.cf_exec_base import execution_session, record_report
|
|
39
|
+
from codeflow.core.cf_validators import (
|
|
40
|
+
PARSE_ERROR,
|
|
41
|
+
VALID,
|
|
42
|
+
ValidatorConfig,
|
|
43
|
+
is_heavy,
|
|
44
|
+
resolve_validators,
|
|
45
|
+
run_validators,
|
|
46
|
+
trigger_matches,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class ScopeResult:
|
|
52
|
+
decision: str
|
|
53
|
+
files: tuple[str, ...]
|
|
54
|
+
new_specs: tuple[str, ...]
|
|
55
|
+
message: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class DoneResult:
|
|
60
|
+
decision: str
|
|
61
|
+
files: tuple[str, ...]
|
|
62
|
+
evidence: tuple[Mapping[str, object], ...]
|
|
63
|
+
message: str = ""
|
|
64
|
+
deferred_review: int = 0
|
|
65
|
+
deferred_requirement: int = 0
|
|
66
|
+
deferred_budget: int = 0
|
|
67
|
+
deferred_heavy: int = 0
|
|
68
|
+
missing_checks: tuple[Mapping[str, object], ...] = ()
|
|
69
|
+
next_action: str = ""
|
|
70
|
+
env_blocked_details: tuple[Mapping[str, object], ...] = ()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _context_path(task_dir: str) -> str:
|
|
74
|
+
return str(Path(task_dir) / "spec-context.yml")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _acceptance_baseline_issue(task_dir: str, owner: str) -> str:
|
|
78
|
+
from codeflow.spec.cf_workflow_service import locate_task_file
|
|
79
|
+
path = Path(task_dir) / ".acceptance-manifest.json"
|
|
80
|
+
task = locate_task_file(task_dir, owner)
|
|
81
|
+
try:
|
|
82
|
+
requires = task is not None and "## Acceptance Coverage" in task.read_text(encoding="utf-8")
|
|
83
|
+
if not path.is_file():
|
|
84
|
+
return "acceptance_manifest_missing" if requires else ""
|
|
85
|
+
data = load_manifest(path)
|
|
86
|
+
if requires:
|
|
87
|
+
from codeflow.acceptance.cf_acceptance_manifest_core import validate_manifest
|
|
88
|
+
valid, reason = validate_manifest(str(task), str(path))
|
|
89
|
+
if not valid:
|
|
90
|
+
return reason
|
|
91
|
+
validate_execution_baseline(path, data)
|
|
92
|
+
return ""
|
|
93
|
+
except (OSError, ValueError) as exc:
|
|
94
|
+
return f"acceptance_manifest_invalid: {exc}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _required_candidate(candidate: SpecCandidate) -> bool:
|
|
98
|
+
metadata = candidate.metadata
|
|
99
|
+
return metadata.enforcement == "required" and any(
|
|
100
|
+
rule.enforcement == "required" for rule in metadata.rules
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def evaluate_scope(root: str, task_dir: str) -> ScopeResult:
|
|
105
|
+
active = load_active_task(root)
|
|
106
|
+
if active.baseline.head:
|
|
107
|
+
try:
|
|
108
|
+
cf_git.run_git(root, ("rev-parse", "--verify", f"{active.baseline.head}^{{commit}}"))
|
|
109
|
+
except cf_git.GitError as exc:
|
|
110
|
+
return ScopeResult("pause", (), (), f"baseline_unavailable: {exc}")
|
|
111
|
+
else:
|
|
112
|
+
return ScopeResult("pause", (), (), "baseline_unavailable: missing task baseline")
|
|
113
|
+
files = current_owned_paths(root, active)
|
|
114
|
+
context = load_context(_context_path(task_dir))
|
|
115
|
+
candidates = resolve_candidates(root, "code", files)
|
|
116
|
+
existing = {binding.spec_id for binding in context.bindings}
|
|
117
|
+
new = tuple(
|
|
118
|
+
candidate
|
|
119
|
+
for candidate in candidates
|
|
120
|
+
if candidate.scope == "path" and candidate.spec_id not in existing
|
|
121
|
+
)
|
|
122
|
+
if not new:
|
|
123
|
+
return ScopeResult("continue", files, (), "scope unchanged")
|
|
124
|
+
selections = tuple(
|
|
125
|
+
BindingInput(candidate, "scope:path", f"Git diff matched {','.join(candidate.matched_paths)}")
|
|
126
|
+
for candidate in new
|
|
127
|
+
)
|
|
128
|
+
save_context(_context_path(task_dir), bind_specs(context, selections))
|
|
129
|
+
# The toolchain expanded the context itself; re-sync the marker so this
|
|
130
|
+
# automatic change does not surface as active_context_drift with no recovery.
|
|
131
|
+
resync_active_hash(root, task_dir, context_sha256(load_context(_context_path(task_dir))))
|
|
132
|
+
required = tuple(candidate.spec_id for candidate in new if _required_candidate(candidate))
|
|
133
|
+
if required:
|
|
134
|
+
pause_active_task(root)
|
|
135
|
+
message = "新增 required Spec 已暂停 TASK;请选择局部 Plan 或回 Align 更新设计"
|
|
136
|
+
return ScopeResult("pause", files, required, message)
|
|
137
|
+
return ScopeResult("continue", files, tuple(candidate.spec_id for candidate in new), "advisory scope expanded")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _diff_hash(root: str, files: tuple[str, ...]) -> str:
|
|
141
|
+
digest = hashlib.sha256()
|
|
142
|
+
for relative in files:
|
|
143
|
+
path = Path(root) / relative
|
|
144
|
+
digest.update(relative.encode())
|
|
145
|
+
digest.update(path.read_bytes() if path.is_file() else b"<deleted>")
|
|
146
|
+
return digest.hexdigest()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _evidence_data(evidence: VerificationEvidence) -> Mapping[str, object]:
|
|
150
|
+
return {
|
|
151
|
+
"verifier_ref": evidence.verifier_ref,
|
|
152
|
+
"executed_at": evidence.executed_at,
|
|
153
|
+
"status": evidence.status,
|
|
154
|
+
"rule_text_sha256": evidence.rule_text_sha256,
|
|
155
|
+
"artifact_sha256": evidence.artifact_sha256,
|
|
156
|
+
"diff_sha256": evidence.diff_sha256,
|
|
157
|
+
"result_sha256": evidence.result_sha256,
|
|
158
|
+
"error_code": evidence.error_code,
|
|
159
|
+
"details": evidence.details,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
_NON_BLOCKING_EVIDENCE = frozenset(("skipped_in_cheap_gate", "deferred_to_review", "verifier_budget_exhausted"))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _verified_is_stale(rule: RuleBinding, current: RuleStageStatus, evidence: Mapping[str, object]) -> bool:
|
|
167
|
+
"""True when preserved verified evidence no longer matches this scope.
|
|
168
|
+
|
|
169
|
+
A deferral/cheap skip must not un-verify a rule that is still fresh (the
|
|
170
|
+
verifier did not run but the old proof applies to the same diff). Once the
|
|
171
|
+
diff or rule text changes, keeping the verified status would be a stale
|
|
172
|
+
green: fail closed and let the deferral downgrade it.
|
|
173
|
+
"""
|
|
174
|
+
latest = current.evidence[-1] if current.evidence else None
|
|
175
|
+
if not isinstance(latest, Mapping):
|
|
176
|
+
return True
|
|
177
|
+
if latest.get("rule_text_sha256") != rule.text_sha256:
|
|
178
|
+
return True
|
|
179
|
+
incoming = evidence.get("diff_sha256")
|
|
180
|
+
existing = latest.get("diff_sha256")
|
|
181
|
+
if isinstance(incoming, str) and incoming and isinstance(existing, str) and existing:
|
|
182
|
+
return incoming != existing
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _update_rule(rule: RuleBinding, evidence: Mapping[str, object]) -> RuleBinding:
|
|
187
|
+
stage = rule.verifier_stage or "code"
|
|
188
|
+
if stage not in rule.stage_status:
|
|
189
|
+
return rule
|
|
190
|
+
statuses = dict(rule.stage_status)
|
|
191
|
+
current = statuses[stage]
|
|
192
|
+
if current.status in ("not_applicable", "waived"):
|
|
193
|
+
return rule
|
|
194
|
+
# A cheap skip/deferral means "not executed in this pass", not a product
|
|
195
|
+
# failure: never un-verify an already verified rule (and never append
|
|
196
|
+
# deferral noise that hides the real evidence). Stale proof does not count.
|
|
197
|
+
if current.status == "verified" and evidence.get("status") != "verified" and \
|
|
198
|
+
evidence.get("error_code") in _NON_BLOCKING_EVIDENCE and \
|
|
199
|
+
not _verified_is_stale(rule, current, evidence):
|
|
200
|
+
statuses[stage] = current
|
|
201
|
+
return replace(rule, stage_status=statuses)
|
|
202
|
+
status = "verified" if evidence.get("status") == "verified" else "unverified"
|
|
203
|
+
# Cheap-gate skips carry no per-run diff freshness requirement: dedup on
|
|
204
|
+
# (verifier_ref, result_sha256) — the third slot stays None on BOTH sides.
|
|
205
|
+
# Storing a real diff_sha256 in the signature made every Stop append a
|
|
206
|
+
# duplicate skip entry (signature never matched).
|
|
207
|
+
signature = (evidence.get("verifier_ref"), evidence.get("result_sha256"),
|
|
208
|
+
None if evidence.get("error_code") == "skipped_in_cheap_gate" else evidence.get("diff_sha256"))
|
|
209
|
+
if any(
|
|
210
|
+
(item.get("verifier_ref"), item.get("result_sha256"),
|
|
211
|
+
None if item.get("error_code") == "skipped_in_cheap_gate" else item.get("diff_sha256")) == signature
|
|
212
|
+
for item in current.evidence
|
|
213
|
+
):
|
|
214
|
+
statuses[stage] = replace(current, status=status)
|
|
215
|
+
else:
|
|
216
|
+
statuses[stage] = replace(current, status=status, evidence=(*current.evidence, evidence))
|
|
217
|
+
return replace(rule, stage_status=statuses)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _apply_evidence(context: SpecContext, evidence: tuple[Mapping[str, object], ...]) -> SpecContext:
|
|
221
|
+
by_ref = {str(item["verifier_ref"]): item for item in evidence}
|
|
222
|
+
bindings: list[SpecBinding] = []
|
|
223
|
+
for binding in context.bindings:
|
|
224
|
+
rules = tuple(
|
|
225
|
+
_update_rule(rule, by_ref[f"{binding.spec_id}#{rule.ref}"])
|
|
226
|
+
if f"{binding.spec_id}#{rule.ref}" in by_ref else rule
|
|
227
|
+
for rule in binding.rules
|
|
228
|
+
)
|
|
229
|
+
bindings.append(replace(binding, rules=rules))
|
|
230
|
+
return replace(context, bindings=tuple(bindings))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def integrate_verification_evidence(
|
|
234
|
+
context: SpecContext, evidence: Sequence[VerificationEvidence],
|
|
235
|
+
) -> tuple[SpecContext, tuple[Mapping[str, object], ...]]:
|
|
236
|
+
"""Apply verifier results with their existing freshness and deduplication rules."""
|
|
237
|
+
records = tuple(_evidence_data(item) for item in evidence)
|
|
238
|
+
return _apply_evidence(context, records), records
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _rule_manual_confirmation(rule: RuleBinding) -> Optional[Mapping[str, object]]:
|
|
242
|
+
status = rule.stage_status.get("code")
|
|
243
|
+
if status is None or status.decision is None or status.decision.kind != "manual_verification":
|
|
244
|
+
return None
|
|
245
|
+
decision = status.decision
|
|
246
|
+
return {
|
|
247
|
+
"reason": decision.reason,
|
|
248
|
+
"confirmed_by": decision.confirmed_by,
|
|
249
|
+
"confirmed_at": decision.confirmed_at,
|
|
250
|
+
"source": decision.source,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def task_spec_scope(root: str, task_dir: str, owner: str, files: tuple[str, ...]) -> set[str]:
|
|
255
|
+
"""本任务的验证范围:Spec-Refs ∪ 改动文件经 path_mapping 命中的 spec。
|
|
256
|
+
|
|
257
|
+
范围外的 code 层 command/test verifier 延后到需求级 verify-e2e 全量补跑。
|
|
258
|
+
"""
|
|
259
|
+
scope = {
|
|
260
|
+
candidate.spec_id
|
|
261
|
+
for candidate in resolve_candidates(root, "code", files)
|
|
262
|
+
if candidate.scope == "path"
|
|
263
|
+
}
|
|
264
|
+
from codeflow.spec.cf_workflow_service import locate_task_file
|
|
265
|
+
|
|
266
|
+
task = locate_task_file(task_dir, owner)
|
|
267
|
+
if task is None:
|
|
268
|
+
return scope
|
|
269
|
+
from codeflow.task.cf_task_doc import task_section, task_spec_refs
|
|
270
|
+
|
|
271
|
+
section = task_section(task.read_text(encoding="utf-8"), owner)
|
|
272
|
+
# Older TASK documents may omit Spec-Refs; invalid declared refs must not
|
|
273
|
+
# silently narrow the verification scope to path mappings alone.
|
|
274
|
+
refs = task_spec_refs(section) if "Spec-Refs" in section.fields else ()
|
|
275
|
+
for ref in refs:
|
|
276
|
+
spec_id = ref.split("#", 1)[0].strip()
|
|
277
|
+
if spec_id:
|
|
278
|
+
scope.add(spec_id)
|
|
279
|
+
return scope
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _binding_deferrals(
|
|
283
|
+
binding: SpecBinding,
|
|
284
|
+
metadata: SpecMetadata,
|
|
285
|
+
scope_specs: set[str],
|
|
286
|
+
budget: float,
|
|
287
|
+
run_started: float,
|
|
288
|
+
) -> dict[str, str]:
|
|
289
|
+
"""计算本 binding 内 code 层 command/test verifier 的延后原因。
|
|
290
|
+
|
|
291
|
+
- `review_stage`:不属于本任务(需求级累积绑定),留到 verify-e2e 补跑;
|
|
292
|
+
- `budget`:本次 finish 的真实预算已经耗尽(声明 timeout 本身不是延后理由)。
|
|
293
|
+
document/regex/ast 静态检查始终执行,不参与延后。
|
|
294
|
+
"""
|
|
295
|
+
deferred: dict[str, str] = {}
|
|
296
|
+
verifier_by_rule = {item.rule: item for item in metadata.verifiers}
|
|
297
|
+
for rule in binding.rules:
|
|
298
|
+
verifier = verifier_by_rule.get(rule.ref)
|
|
299
|
+
if verifier is None or verifier.type not in ("command", "test"):
|
|
300
|
+
continue
|
|
301
|
+
status = rule.stage_status.get("code")
|
|
302
|
+
if status is None or status.status in ("not_applicable", "waived"):
|
|
303
|
+
continue
|
|
304
|
+
if binding.spec_id not in scope_specs:
|
|
305
|
+
deferred[rule.ref] = "review_stage"
|
|
306
|
+
continue
|
|
307
|
+
# A declared timeout larger than the remaining budget does not prove
|
|
308
|
+
# exhaustion: run it and let the runner cap the subprocess deadline.
|
|
309
|
+
# Only an already exhausted wall-clock budget is a budget deferral.
|
|
310
|
+
if budget > 0 and (time.monotonic() - run_started) >= budget:
|
|
311
|
+
deferred[rule.ref] = "budget"
|
|
312
|
+
return deferred
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
_NON_EXECUTION_ERRORS = frozenset((
|
|
316
|
+
"skipped_in_cheap_gate",
|
|
317
|
+
"no_matching_files",
|
|
318
|
+
"verifier_missing",
|
|
319
|
+
"command_unavailable",
|
|
320
|
+
"file_read_error",
|
|
321
|
+
"artifact_read_error",
|
|
322
|
+
"regex_unavailable",
|
|
323
|
+
))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def is_non_execution_error(code: object) -> bool:
|
|
327
|
+
"""Distinguish verifier results that did not execute from actual failures."""
|
|
328
|
+
return isinstance(code, str) and code in _NON_EXECUTION_ERRORS
|
|
329
|
+
|
|
330
|
+
_PROVISIONAL_NEXT = (
|
|
331
|
+
"补齐/修复阻塞项后重新运行 task finish;budget/env/未执行不算通过。"
|
|
332
|
+
"review_stage 延后项由需求级 task verify-e2e 补跑。"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _required_code_gaps(context: SpecContext) -> tuple[tuple[Mapping[str, object], ...], tuple[str, ...]]:
|
|
337
|
+
"""Collect required code checks that did not run vs. checks that failed.
|
|
338
|
+
|
|
339
|
+
Provisional is only allowed for non-execution (budget/env/missing). A real
|
|
340
|
+
failing assertion must stay a block so "not executed" never hides a broken
|
|
341
|
+
implementation.
|
|
342
|
+
"""
|
|
343
|
+
missing: dict[str, Mapping[str, object]] = {}
|
|
344
|
+
failed: list[str] = []
|
|
345
|
+
for binding in context.bindings:
|
|
346
|
+
for rule in binding.rules:
|
|
347
|
+
if rule.enforcement != "required" or (rule.verifier_stage or "code") != "code":
|
|
348
|
+
continue
|
|
349
|
+
status = rule.stage_status.get("code")
|
|
350
|
+
if status is None or status.status in ("verified", "not_applicable", "waived", "applied"):
|
|
351
|
+
continue
|
|
352
|
+
latest = status.evidence[-1] if status.evidence else None
|
|
353
|
+
latest = latest if isinstance(latest, Mapping) else None
|
|
354
|
+
kind = evidence_deferral_kind(latest)
|
|
355
|
+
if kind in ("review_stage", "heavy"):
|
|
356
|
+
continue
|
|
357
|
+
ref = f"{binding.spec_id}#{rule.ref}"
|
|
358
|
+
error = latest.get("error_code") if latest is not None else None
|
|
359
|
+
if kind in ("budget", "env"):
|
|
360
|
+
missing[ref] = {
|
|
361
|
+
"ref": ref,
|
|
362
|
+
"kind": kind,
|
|
363
|
+
"error_code": error or kind,
|
|
364
|
+
"verifier_ref": rule.verifier_ref,
|
|
365
|
+
}
|
|
366
|
+
elif error is None or is_non_execution_error(error):
|
|
367
|
+
missing[ref] = {
|
|
368
|
+
"ref": ref,
|
|
369
|
+
"kind": "not_executed",
|
|
370
|
+
"error_code": error or status.status,
|
|
371
|
+
"verifier_ref": rule.verifier_ref,
|
|
372
|
+
}
|
|
373
|
+
else:
|
|
374
|
+
failed.append(ref)
|
|
375
|
+
return tuple(missing[key] for key in sorted(missing)), tuple(sorted(set(failed)))
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _provisional_message(missing: tuple[Mapping[str, object], ...]) -> str:
|
|
379
|
+
detail = ", ".join(f"{item['ref']} ({item['kind']})" for item in missing)
|
|
380
|
+
return f"required code checks not executed: {detail}"
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _run_acceptance(root: str, task_dir: str, owner: str, include_e2e: bool,
|
|
384
|
+
deadline: Optional[float], cheap: bool = False
|
|
385
|
+
) -> tuple[str, str, tuple[Mapping[str, object], ...]]:
|
|
386
|
+
baseline_issue = _acceptance_baseline_issue(task_dir, owner)
|
|
387
|
+
if baseline_issue:
|
|
388
|
+
return baseline_issue, "block", ()
|
|
389
|
+
# manual 场景的人工确认统一在需求级终验(verify-e2e + confirm-manual)完成,
|
|
390
|
+
# 不在每个任务的 Done Gate 中逐任务阻断。
|
|
391
|
+
if cheap:
|
|
392
|
+
# 轻量门禁:只做 manifest 基线检查,不执行 functional 场景;
|
|
393
|
+
# 场景命令由 finish 的全量 Done Gate 执行。
|
|
394
|
+
return "", "pass", ()
|
|
395
|
+
manifest_path = Path(task_dir) / ".acceptance-manifest.json"
|
|
396
|
+
if manifest_path.is_file():
|
|
397
|
+
from codeflow.cli.cf_acceptance_runner import run_manifest
|
|
398
|
+
scenario_result = run_manifest(str(manifest_path), root, write_evidence=True, include_e2e=include_e2e, owner=owner, deadline=deadline)
|
|
399
|
+
if scenario_result["decision"] == "block":
|
|
400
|
+
statuses = [
|
|
401
|
+
str(item.get("status", ""))
|
|
402
|
+
for item in scenario_result.get("results", [])
|
|
403
|
+
if isinstance(item, dict)
|
|
404
|
+
]
|
|
405
|
+
failed = [value for value in statuses if value in ("failed", "not_configured")]
|
|
406
|
+
if failed:
|
|
407
|
+
reason = scenario_result.get("error") or "acceptance scenario failed"
|
|
408
|
+
return f"acceptance scenario failed: {reason}", "block", ()
|
|
409
|
+
if "env_blocked" in statuses:
|
|
410
|
+
details = tuple(
|
|
411
|
+
item for item in scenario_result.get("env_blocked_details", [])
|
|
412
|
+
if isinstance(item, Mapping)
|
|
413
|
+
)
|
|
414
|
+
return "acceptance env_blocked: " + (scenario_result.get("error") or "environment unavailable"), "env_blocked", details
|
|
415
|
+
if "incomplete" in statuses:
|
|
416
|
+
return "acceptance incomplete: " + (scenario_result.get("error") or "deadline exceeded"), "incomplete", ()
|
|
417
|
+
reason = scenario_result.get("error") or "acceptance scenario failed"
|
|
418
|
+
return f"acceptance scenario failed: {reason}", "block", ()
|
|
419
|
+
return "", "pass", ()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _run_finish_validation(root: str, files: tuple[str, ...]) -> tuple[str, int]:
|
|
423
|
+
"""Light validation.yml once per TASK finish; heavy validators defer by default.
|
|
424
|
+
|
|
425
|
+
Stop-time checks stay cheap; `heavy: true` suites (full tests/builds/e2e) are
|
|
426
|
+
skipped unless `quality_loop.heavy_at_finish: true`, and run again at archive
|
|
427
|
+
`cf_validation` / `/cf-validate`. Returns (issue, deferred_heavy_count).
|
|
428
|
+
"""
|
|
429
|
+
quality = resolve_quality_loop(load_config(root))
|
|
430
|
+
if not quality.get("finish_check"):
|
|
431
|
+
return "", 0
|
|
432
|
+
if not files:
|
|
433
|
+
return "", 0
|
|
434
|
+
include_heavy = bool(quality.get("heavy_at_finish"))
|
|
435
|
+
try:
|
|
436
|
+
config, effective = resolve_validators(root)
|
|
437
|
+
if config.state == PARSE_ERROR:
|
|
438
|
+
return f"finish validation blocked: invalid_validation_config: {config.error}", 0
|
|
439
|
+
deferred_heavy = 0 if include_heavy else sum(
|
|
440
|
+
1
|
|
441
|
+
for validator in effective
|
|
442
|
+
if is_heavy(validator)
|
|
443
|
+
and any(trigger_matches(str(validator.get("trigger", "")), name) for name in files)
|
|
444
|
+
)
|
|
445
|
+
validators = [item for item in effective if include_heavy or not is_heavy(item)]
|
|
446
|
+
if not validators:
|
|
447
|
+
return "", deferred_heavy
|
|
448
|
+
maximum = sum(float(item.get("timeout", 30000)) / 1000 for item in validators)
|
|
449
|
+
if not math.isfinite(maximum) or maximum <= 0:
|
|
450
|
+
raise ValueError("validation budget must be finite and positive")
|
|
451
|
+
with execution_session():
|
|
452
|
+
outcome = run_validators(root, ValidatorConfig(VALID, tuple(validators)), list(files),
|
|
453
|
+
"cf-validate", time.monotonic() + maximum, strict=True, cache=True)
|
|
454
|
+
except (OSError, ValueError) as exc:
|
|
455
|
+
return f"finish validation error: {exc}", 0
|
|
456
|
+
if not outcome.failures and not outcome.truncated:
|
|
457
|
+
return "", deferred_heavy
|
|
458
|
+
if outcome.truncated and not outcome.failures:
|
|
459
|
+
return "finish validation 预算耗尽:部分 validator 未执行", deferred_heavy
|
|
460
|
+
names = (", ".join(str(item.get("name", "validator")) for item in outcome.failures[:3])
|
|
461
|
+
if outcome.failures else "validator")
|
|
462
|
+
suffix = "(预算耗尽,部分未执行)" if outcome.truncated else ""
|
|
463
|
+
return f"finish validation failed{suffix}: {names}", deferred_heavy
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _record_spec_phase(name: str, started: float) -> None:
|
|
467
|
+
record_report({"kind": "spec_phases", "timings": {name: (time.monotonic() - started) * 1000}})
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _run_done_binding(binding: SpecBinding, scope: VerificationScope, task_dir: str, scope_specs: set[str],
|
|
471
|
+
verifier_budget: float, run_started: float, deadline: Optional[float],
|
|
472
|
+
cheap: bool) -> tuple[tuple[VerificationEvidence, ...], int, int, int]:
|
|
473
|
+
phase_started = time.monotonic()
|
|
474
|
+
metadata = load_spec_metadata(str(Path(scope.root) / ".code-flow/specs" / binding.path))
|
|
475
|
+
_record_spec_phase("planning_ms", phase_started)
|
|
476
|
+
deferred_review = sum(1 for rule in binding.rules if rule.verifier_stage == "review")
|
|
477
|
+
deferrals = {} if cheap else _binding_deferrals(binding, metadata, scope_specs, verifier_budget, run_started)
|
|
478
|
+
deferred_requirement = sum(1 for rule in binding.rules
|
|
479
|
+
if deferrals.get(rule.ref) == "review_stage" and rule.enforcement == "required")
|
|
480
|
+
deferred_budget = sum(1 for rule in binding.rules
|
|
481
|
+
if deferrals.get(rule.ref) == "budget" and rule.enforcement == "required")
|
|
482
|
+
confirmations = {rule.ref: value for rule in binding.rules
|
|
483
|
+
if (value := _rule_manual_confirmation(rule)) is not None}
|
|
484
|
+
remaining = None if deadline is None else deadline - time.monotonic()
|
|
485
|
+
result = run_all_verifiers(metadata, scope, confirmations, cheap, remaining, stage="code", deferred=deferrals or None)
|
|
486
|
+
phase_timing(f"done.verify.{binding.spec_id}", phase_started)
|
|
487
|
+
evidence_started = time.monotonic()
|
|
488
|
+
try:
|
|
489
|
+
from codeflow.evidence.cf_evidence_store import requirement_key
|
|
490
|
+
store_verification_evidence(metadata, scope, result, requirement=requirement_key(Path(task_dir)))
|
|
491
|
+
except (OSError, ValueError) as exc:
|
|
492
|
+
sys.stderr.write(f"cf_task_runtime: evidence sidecar write failed: {exc}\n")
|
|
493
|
+
_record_spec_phase("evidence_ms", evidence_started)
|
|
494
|
+
return result.evidence, deferred_review, deferred_requirement, deferred_budget
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _run_done_verifiers(context: SpecContext, scope: VerificationScope, task_dir: str, owner: str,
|
|
498
|
+
deadline: Optional[float], cheap: bool) -> tuple[list[VerificationEvidence], int, int, int]:
|
|
499
|
+
quality = resolve_quality_loop(load_config(scope.root))
|
|
500
|
+
verifier_budget = float(quality.get("finish_verifier_budget", 300.0))
|
|
501
|
+
scope_specs = set() if cheap else task_spec_scope(scope.root, task_dir, owner, scope.files)
|
|
502
|
+
if not cheap and not scope_specs:
|
|
503
|
+
scope_specs = {binding.spec_id for binding in context.bindings}
|
|
504
|
+
run_started = time.monotonic()
|
|
505
|
+
evidence: list[VerificationEvidence] = []
|
|
506
|
+
review = requirement = budget = 0
|
|
507
|
+
for binding in context.bindings:
|
|
508
|
+
records, one_review, one_requirement, one_budget = _run_done_binding(
|
|
509
|
+
binding, scope, task_dir, scope_specs, verifier_budget, run_started, deadline, cheap)
|
|
510
|
+
evidence.extend(records)
|
|
511
|
+
review += one_review
|
|
512
|
+
requirement += one_requirement
|
|
513
|
+
budget += one_budget
|
|
514
|
+
return evidence, review, requirement, budget
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _run_done_gate(root: str, task_dir: str, cheap: bool = False, budget: Optional[float] = None, include_e2e: bool = False, task_id: str = "") -> DoneResult:
|
|
518
|
+
active = load_active_task(root)
|
|
519
|
+
owner = task_id or active.task_id
|
|
520
|
+
if owner != active.task_id or (Path(root) / active.task_dir).resolve() != Path(task_dir).resolve():
|
|
521
|
+
return DoneResult("block", (), (), "active_mismatch: task identity does not match marker")
|
|
522
|
+
started = time.monotonic()
|
|
523
|
+
scope_result = evaluate_scope(root, task_dir)
|
|
524
|
+
phase_timing("done.evaluate_scope", started)
|
|
525
|
+
_record_spec_phase("preflight_ms", started)
|
|
526
|
+
if scope_result.decision == "pause":
|
|
527
|
+
return DoneResult("block", scope_result.files, (), scope_result.message)
|
|
528
|
+
deadline = started + budget if budget is not None else None
|
|
529
|
+
issue, acceptance_kind, env_details = _run_acceptance(root, task_dir, owner, include_e2e, deadline, cheap)
|
|
530
|
+
if issue:
|
|
531
|
+
decision = "provisional" if acceptance_kind in ("env_blocked", "incomplete") else "block"
|
|
532
|
+
return DoneResult(decision, scope_result.files, (), issue,
|
|
533
|
+
env_blocked_details=env_details)
|
|
534
|
+
phase_started = time.monotonic()
|
|
535
|
+
context = load_context(_context_path(task_dir))
|
|
536
|
+
diff_hash = _diff_hash(root, scope_result.files)
|
|
537
|
+
phase_timing("done.load_context_and_diff", phase_started)
|
|
538
|
+
_record_spec_phase("planning_ms", phase_started)
|
|
539
|
+
deferred_heavy = 0
|
|
540
|
+
verifier_evidence, deferred_review, deferred_requirement, deferred_budget = _run_done_verifiers(
|
|
541
|
+
context, VerificationScope(root, scope_result.files, diff_hash), task_dir, owner, deadline, cheap)
|
|
542
|
+
evidence_started = time.monotonic()
|
|
543
|
+
updated, all_evidence = integrate_verification_evidence(context, verifier_evidence)
|
|
544
|
+
if updated != context:
|
|
545
|
+
save_context(_context_path(task_dir), updated)
|
|
546
|
+
_record_spec_phase("evidence_ms", evidence_started)
|
|
547
|
+
gate = validate_stage(updated, "code", diff_sha256=diff_hash, allow_cheap_skips=cheap,
|
|
548
|
+
allow_deferred_skips=True)
|
|
549
|
+
if gate.decision == "block" and not cheap:
|
|
550
|
+
missing, failed = _required_code_gaps(updated)
|
|
551
|
+
if missing and not failed:
|
|
552
|
+
budget_gaps = sum(1 for item in missing if item.get("kind") == "budget")
|
|
553
|
+
return DoneResult("provisional", scope_result.files, tuple(all_evidence),
|
|
554
|
+
_provisional_message(missing), deferred_review,
|
|
555
|
+
deferred_requirement, budget_gaps, deferred_heavy,
|
|
556
|
+
missing, _PROVISIONAL_NEXT)
|
|
557
|
+
if gate.decision == "pass" and not cheap:
|
|
558
|
+
validation_issue, deferred_heavy = _run_finish_validation(root, scope_result.files)
|
|
559
|
+
if validation_issue:
|
|
560
|
+
return DoneResult("block", scope_result.files, tuple(all_evidence), validation_issue,
|
|
561
|
+
deferred_review, deferred_requirement, deferred_budget, deferred_heavy)
|
|
562
|
+
phase_timing("done.total", started)
|
|
563
|
+
return DoneResult(gate.decision, scope_result.files, tuple(all_evidence),
|
|
564
|
+
"; ".join(issue.message for issue in gate.errors), deferred_review,
|
|
565
|
+
deferred_requirement, deferred_budget, deferred_heavy)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def run_done_gate(root: str, task_dir: str, cheap: bool = False, budget: Optional[float] = None, include_e2e: bool = False, task_id: str = "") -> DoneResult:
|
|
569
|
+
with execution_session():
|
|
570
|
+
return _run_done_gate(root, task_dir, cheap, budget, include_e2e, task_id)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Validation scope and execution shared by task verification and CLI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import time
|
|
7
|
+
from typing import Optional, Sequence
|
|
8
|
+
|
|
9
|
+
from codeflow.core import cf_git
|
|
10
|
+
from codeflow.core.cf_exec_base import execution_session
|
|
11
|
+
from codeflow.core.cf_validators import PARSE_ERROR, VALID, ValidatorConfig, is_heavy, resolve_validators, run_validators
|
|
12
|
+
from codeflow.spec.cf_active_task import current_owned_paths, load_active_task
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_MANAGED_PREFIXES = (".code-flow/", ".claude/", ".codex/", ".costrict/", ".opencode/", ".agents/")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_managed(path: str) -> bool:
|
|
19
|
+
return path.startswith(_MANAGED_PREFIXES)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validation_scope(root: str, files: Optional[Sequence[str]] = None) -> tuple[str, ...]:
|
|
23
|
+
base = Path(root).resolve()
|
|
24
|
+
if files is not None:
|
|
25
|
+
selected = []
|
|
26
|
+
for name in files:
|
|
27
|
+
path = (base / name).resolve()
|
|
28
|
+
if base not in path.parents:
|
|
29
|
+
raise ValueError(f"validation path outside project: {name}")
|
|
30
|
+
relative = path.relative_to(base).as_posix()
|
|
31
|
+
if _is_managed(relative):
|
|
32
|
+
continue
|
|
33
|
+
selected.append(relative)
|
|
34
|
+
return tuple(sorted(set(selected)))
|
|
35
|
+
# Tool-managed trees (deployed hooks/commands/plugins) are not project
|
|
36
|
+
# source: validating them makes template validators (eslint/vitest/mypy)
|
|
37
|
+
# fire on every install and blocks verify-e2e for unrelated stacks.
|
|
38
|
+
if (base / ".code-flow/.active-task.json").exists():
|
|
39
|
+
scoped = current_owned_paths(str(base), load_active_task(str(base)))
|
|
40
|
+
else:
|
|
41
|
+
scoped = tuple(sorted(cf_git.changed_files(str(base))))
|
|
42
|
+
return tuple(sorted(item for item in scoped if not _is_managed(item)))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_files(root: str, files: Optional[Sequence[str]] = None, budget: Optional[float] = None,
|
|
46
|
+
include_heavy: bool = True, use_cache: bool = True) -> dict[str, object]:
|
|
47
|
+
started = time.monotonic()
|
|
48
|
+
selected = validation_scope(root, files)
|
|
49
|
+
config, effective = resolve_validators(root)
|
|
50
|
+
if config.state == PARSE_ERROR:
|
|
51
|
+
return {"decision": "block", "files": selected, "reason": "invalid_validation_config",
|
|
52
|
+
"error": config.error}
|
|
53
|
+
validators = list(effective)
|
|
54
|
+
if not include_heavy:
|
|
55
|
+
validators = [item for item in validators if not is_heavy(item)]
|
|
56
|
+
if not selected:
|
|
57
|
+
return {"decision": "pass", "files": [], "reason": "no_changes"}
|
|
58
|
+
if not validators:
|
|
59
|
+
return {"decision": "block", "files": selected, "reason": "no_validators_configured"}
|
|
60
|
+
maximum = budget if budget is not None else sum(float(v.get("timeout", 30000)) / 1000 for v in validators)
|
|
61
|
+
if not math.isfinite(maximum) or maximum <= 0:
|
|
62
|
+
raise ValueError("validation budget must be finite and positive")
|
|
63
|
+
timings = {"planning_ms": (time.monotonic() - started) * 1000,
|
|
64
|
+
"preflight_ms": 0.0, "execution_ms": 0.0, "evidence_ms": 0.0}
|
|
65
|
+
observations: list[dict[str, str]] = []
|
|
66
|
+
with execution_session():
|
|
67
|
+
outcome = run_validators(root, ValidatorConfig(VALID, tuple(validators)), list(selected),
|
|
68
|
+
"cf-validate", time.monotonic() + maximum, strict=True, cache=use_cache,
|
|
69
|
+
timings=timings, observations=observations)
|
|
70
|
+
timings["elapsed_ms"] = (time.monotonic() - started) * 1000
|
|
71
|
+
return {"decision": "block" if outcome.failures or outcome.truncated else "pass", "files": selected,
|
|
72
|
+
"failures": outcome.failures, "incomplete": outcome.truncated, "reused": outcome.reused,
|
|
73
|
+
"timings": timings, "observations": observations}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"gitignore": {
|
|
4
|
+
"start": "# >>> code-flow:runtime schema=1",
|
|
5
|
+
"end": "# <<< code-flow:runtime schema=1",
|
|
6
|
+
"entries": [
|
|
7
|
+
".active-task.json",
|
|
8
|
+
".active-task.lock",
|
|
9
|
+
".stop.lock",
|
|
10
|
+
".catalog-state.json",
|
|
11
|
+
".session-state.json",
|
|
12
|
+
".context-state.json",
|
|
13
|
+
".task-projection-state.json",
|
|
14
|
+
"migrations/",
|
|
15
|
+
"backups/",
|
|
16
|
+
".debug.log",
|
|
17
|
+
".session-log.jsonl",
|
|
18
|
+
".check-state.json",
|
|
19
|
+
"sessions/",
|
|
20
|
+
"specs/_session/",
|
|
21
|
+
"worktrees/",
|
|
22
|
+
".validation-cache.json",
|
|
23
|
+
".artifact-hash-cache.json",
|
|
24
|
+
".runtime-install.json",
|
|
25
|
+
".runtime-migration.lock/",
|
|
26
|
+
".verifier-cache.json"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|