@michaelxwb/code-flow 0.8.1 → 1.0.0
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 +73 -152
- package/bin/code-flow.js +12 -0
- package/docs/README.md +19 -0
- package/docs/USAGE.md +156 -1417
- package/docs/v1-design.md +1858 -0
- package/docs/v1-implementation.md +186 -0
- package/lib/adapters.js +126 -0
- package/lib/cli.js +258 -0
- package/lib/config.js +85 -0
- package/lib/doctor.js +283 -0
- package/lib/frontmatter.js +42 -0
- package/lib/fs.js +141 -0
- package/lib/glob.js +23 -0
- package/lib/hook.js +99 -0
- package/lib/init.js +132 -0
- package/lib/learn.js +170 -0
- package/lib/locks.js +52 -0
- package/lib/markdown.js +13 -0
- package/lib/process.js +116 -0
- package/lib/render.js +115 -0
- package/lib/repo.js +48 -0
- package/lib/runs.js +155 -0
- package/lib/session.js +40 -0
- package/lib/spec.js +80 -0
- package/lib/task.js +258 -0
- package/lib/taskdoc.js +308 -0
- package/lib/toml.js +140 -0
- package/lib/verify.js +168 -0
- package/lib/worktree.js +930 -0
- package/package.json +18 -21
- package/templates/adapters/opencode.mjs +65 -0
- package/templates/config.json +39 -0
- package/templates/instructions.md +13 -0
- package/templates/launcher/bridge.cjs +295 -0
- package/templates/launcher/hook.cmd +15 -0
- package/templates/launcher/hook.ps1 +10 -0
- package/templates/launcher/hook.sh +20 -0
- package/templates/skills/cf-check.md +19 -0
- package/templates/skills/cf-init.md +16 -0
- package/templates/skills/cf-spec.md +20 -0
- package/templates/skills/cf-task-align.md +19 -0
- package/templates/skills/cf-task-archive.md +20 -0
- package/templates/skills/cf-task-plan.md +31 -0
- package/templates/skills/cf-task-prd.md +14 -0
- package/templates/skills/cf-task-start.md +41 -0
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +12 -12
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +17 -19
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +12 -12
- package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
- package/templates/workflow/project-guidelines.md +48 -0
- package/templates/workflow/scenarios.md +67 -0
- package/templates/workflow/task.md +56 -0
- package/docs/acceptance-contract.md +0 -217
- package/docs/ci/acceptance-evidence.md +0 -77
- package/docs/migrations/runtime-0.7.0.md +0 -41
- package/docs/migrations/runtime-0.8.0.md +0 -154
- package/docs/migrations/runtime-0.9.0.md +0 -40
- package/docs/migrations/spec-workflow-0.6.0.md +0 -42
- package/docs/spec-injection-hardening.md +0 -54
- package/docs/spec-injection-performance.json +0 -73
- package/docs/spec-optimization.md +0 -12
- package/src/adapters/claude/CLAUDE.md +0 -45
- package/src/adapters/claude/commands/cf-init.md +0 -262
- package/src/adapters/claude/commands/cf-learn.md +0 -291
- package/src/adapters/claude/commands/cf-spec.md +0 -87
- package/src/adapters/claude/commands/cf-stats.md +0 -96
- package/src/adapters/claude/commands/cf-sync.md +0 -39
- package/src/adapters/claude/commands/cf-task/align.md +0 -331
- package/src/adapters/claude/commands/cf-task/archive.md +0 -155
- package/src/adapters/claude/commands/cf-task/block.md +0 -49
- package/src/adapters/claude/commands/cf-task/graph.md +0 -92
- package/src/adapters/claude/commands/cf-task/note.md +0 -248
- package/src/adapters/claude/commands/cf-task/plan.md +0 -477
- package/src/adapters/claude/commands/cf-task/prd.md +0 -173
- package/src/adapters/claude/commands/cf-task/start.md +0 -317
- package/src/adapters/claude/commands/cf-task/status.md +0 -71
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/claude/commands/cf-validate.md +0 -100
- package/src/adapters/claude/settings.local.json +0 -50
- package/src/adapters/codex/AGENTS.md +0 -45
- package/src/adapters/codex/config.toml +0 -3
- package/src/adapters/codex/hooks.json +0 -54
- package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
- package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
- package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -88
- package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -97
- package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -45
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -332
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -156
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -50
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -93
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -245
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -478
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -174
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -318
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -72
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -145
- package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -102
- package/src/adapters/commands.json +0 -115
- package/src/adapters/costrict/CLAUDE.md +0 -45
- package/src/adapters/costrict/commands/cf-init.md +0 -262
- package/src/adapters/costrict/commands/cf-learn.md +0 -291
- package/src/adapters/costrict/commands/cf-spec.md +0 -87
- package/src/adapters/costrict/commands/cf-stats.md +0 -96
- package/src/adapters/costrict/commands/cf-sync.md +0 -39
- package/src/adapters/costrict/commands/cf-task/align.md +0 -331
- package/src/adapters/costrict/commands/cf-task/archive.md +0 -155
- package/src/adapters/costrict/commands/cf-task/block.md +0 -49
- package/src/adapters/costrict/commands/cf-task/graph.md +0 -92
- package/src/adapters/costrict/commands/cf-task/note.md +0 -248
- package/src/adapters/costrict/commands/cf-task/plan.md +0 -477
- package/src/adapters/costrict/commands/cf-task/prd.md +0 -173
- package/src/adapters/costrict/commands/cf-task/start.md +0 -317
- package/src/adapters/costrict/commands/cf-task/status.md +0 -71
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/costrict/commands/cf-validate.md +0 -100
- package/src/adapters/costrict/settings.local.json +0 -50
- package/src/adapters/install-plan.json +0 -100
- package/src/adapters/opencode/AGENTS.md +0 -45
- package/src/adapters/opencode/commands/cf-init.md +0 -281
- package/src/adapters/opencode/commands/cf-learn.md +0 -295
- package/src/adapters/opencode/commands/cf-spec.md +0 -91
- package/src/adapters/opencode/commands/cf-stats.md +0 -100
- package/src/adapters/opencode/commands/cf-sync.md +0 -44
- package/src/adapters/opencode/commands/cf-task/align.md +0 -331
- package/src/adapters/opencode/commands/cf-task/archive.md +0 -160
- package/src/adapters/opencode/commands/cf-task/block.md +0 -54
- package/src/adapters/opencode/commands/cf-task/graph.md +0 -97
- package/src/adapters/opencode/commands/cf-task/note.md +0 -253
- package/src/adapters/opencode/commands/cf-task/plan.md +0 -477
- package/src/adapters/opencode/commands/cf-task/prd.md +0 -178
- package/src/adapters/opencode/commands/cf-task/start.md +0 -317
- package/src/adapters/opencode/commands/cf-task/status.md +0 -76
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/opencode/commands/cf-validate.md +0 -105
- package/src/adapters/opencode/opencode.json +0 -3
- package/src/adapters/opencode/plugins/code-flow/index.js +0 -162
- package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
- package/src/adapters/platform-bindings.json +0 -370
- package/src/cli.js +0 -1076
- package/src/core/code-flow/config.yml +0 -161
- package/src/core/code-flow/runtime-commands.json +0 -259
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -10
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -10
- package/src/core/code-flow/scripts/cf_audit.py +0 -10
- package/src/core/code-flow/scripts/cf_codex_hook.py +0 -10
- package/src/core/code-flow/scripts/cf_design_lint.py +0 -10
- package/src/core/code-flow/scripts/cf_evidence_import.py +0 -10
- package/src/core/code-flow/scripts/cf_feedback.py +0 -10
- package/src/core/code-flow/scripts/cf_opencode_event.py +0 -10
- package/src/core/code-flow/scripts/cf_post_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_scenarios.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_context.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_gate.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_session.py +0 -10
- package/src/core/code-flow/scripts/cf_stats.py +0 -10
- package/src/core/code-flow/scripts/cf_stop_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_sync.py +0 -10
- package/src/core/code-flow/scripts/cf_task_index.py +0 -10
- package/src/core/code-flow/scripts/cf_task_parallel.py +0 -10
- package/src/core/code-flow/scripts/cf_task_workflow.py +0 -10
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_validation.py +0 -10
- 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 +0 -347
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +0 -660
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +0 -202
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +0 -60
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +0 -73
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +0 -282
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +0 -266
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +0 -172
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +0 -326
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_checks.py +0 -333
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +0 -197
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +0 -174
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +0 -300
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +0 -502
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +0 -347
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +0 -1490
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +0 -533
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +0 -172
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +0 -607
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +0 -53
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +0 -58
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +0 -1175
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +0 -60
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +0 -46
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +0 -540
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +0 -144
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +0 -177
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +0 -1142
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +0 -742
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +0 -31
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +0 -67
- package/src/core/code-flow/scripts/codeflow/core/cf_core.py +0 -609
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +0 -325
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +0 -57
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +0 -92
- package/src/core/code-flow/scripts/codeflow/core/cf_log.py +0 -160
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +0 -77
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +0 -26
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +0 -33
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +0 -32
- package/src/core/code-flow/scripts/codeflow/core/cf_session_state.py +0 -37
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +0 -53
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +0 -9
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +0 -408
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +0 -253
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +0 -527
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +0 -95
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +0 -98
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +0 -30
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +0 -41
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +0 -292
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +0 -161
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +0 -412
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +0 -301
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +0 -149
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +0 -111
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +0 -114
- package/src/core/code-flow/scripts/codeflow/spec/cf_scan.py +0 -367
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +0 -55
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +0 -477
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +0 -119
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_metadata.py +0 -387
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py +0 -529
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +0 -193
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +0 -187
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_resolver.py +0 -369
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_router.py +0 -304
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_verify.py +0 -687
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +0 -330
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +0 -83
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +0 -755
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_transaction.py +0 -110
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +0 -867
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +0 -178
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +0 -635
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +0 -251
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +0 -570
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +0 -73
- package/src/core/code-flow/scripts/runtime-resources.json +0 -29
- package/src/core/code-flow/specs/backend/_map.md +0 -56
- package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
- package/src/core/code-flow/specs/backend/database.md +0 -51
- package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
- package/src/core/code-flow/specs/backend/logging.md +0 -50
- package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
- package/src/core/code-flow/specs/frontend/_map.md +0 -52
- package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
- package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
- package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
- package/src/core/code-flow/specs/shared/_map.md +0 -57
- package/src/core/code-flow/specs/shared/scenarios-template.md +0 -55
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +0 -59
- package/src/migrate/runtime.js +0 -213
- package/src/migrate/spec-workflow.js +0 -253
- package/src/runtime.js +0 -219
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
"""One executable plan gate for manifest identity, semantics and design closure."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import re
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from shlex import quote
|
|
9
|
-
from typing import Mapping, Optional
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass(frozen=True)
|
|
13
|
-
class FileSnapshot:
|
|
14
|
-
"""One file read once, with a stat fingerprint for drift detection."""
|
|
15
|
-
|
|
16
|
-
path: str
|
|
17
|
-
text: str
|
|
18
|
-
size: int
|
|
19
|
-
mtime_ns: int
|
|
20
|
-
|
|
21
|
-
def drift(self) -> bool:
|
|
22
|
-
try:
|
|
23
|
-
stat = Path(self.path).stat()
|
|
24
|
-
except OSError:
|
|
25
|
-
return True
|
|
26
|
-
return stat.st_size != self.size or stat.st_mtime_ns != self.mtime_ns
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@dataclass(frozen=True)
|
|
30
|
-
class PlanSnapshot:
|
|
31
|
-
"""Verified inputs of one verify_plan operation; no persistent cache."""
|
|
32
|
-
|
|
33
|
-
task: FileSnapshot
|
|
34
|
-
manifest: FileSnapshot
|
|
35
|
-
scenario_file: str
|
|
36
|
-
scenario: Optional[FileSnapshot]
|
|
37
|
-
|
|
38
|
-
def drift(self) -> bool:
|
|
39
|
-
if self.task.drift() or self.manifest.drift():
|
|
40
|
-
return True
|
|
41
|
-
return self.scenario is not None and self.scenario.drift()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _read_snapshot(path: Path) -> FileSnapshot:
|
|
45
|
-
stat = path.stat()
|
|
46
|
-
return FileSnapshot(str(path), path.read_text(encoding="utf-8"), stat.st_size, stat.st_mtime_ns)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def load_plan_snapshot(directory: str, task_file: str,
|
|
50
|
-
require_scenarios: bool = False) -> PlanSnapshot:
|
|
51
|
-
"""Read TASK, manifest and scenario source at most once for one operation."""
|
|
52
|
-
from codeflow.acceptance.cf_scenario_source import ScenarioError
|
|
53
|
-
base = Path(directory)
|
|
54
|
-
manifest = _read_snapshot(base / ".acceptance-manifest.json")
|
|
55
|
-
task = _read_snapshot(Path(task_file))
|
|
56
|
-
try:
|
|
57
|
-
data = json.loads(manifest.text)
|
|
58
|
-
except ValueError:
|
|
59
|
-
data = {}
|
|
60
|
-
try:
|
|
61
|
-
source_file = _source(base, data if isinstance(data, Mapping) else {}, require_scenarios)
|
|
62
|
-
except ScenarioError:
|
|
63
|
-
source_file = ""
|
|
64
|
-
source = _read_snapshot(Path(source_file)) if source_file and Path(source_file).is_file() else None
|
|
65
|
-
return PlanSnapshot(task, manifest, source_file, source)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _root(directory: Path) -> Path:
|
|
69
|
-
return next((parent.parent for parent in (directory, *directory.parents)
|
|
70
|
-
if parent.name == ".code-flow"), directory)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _source(directory: Path, manifest: Mapping[str, object], required: bool) -> str:
|
|
74
|
-
from codeflow.acceptance.cf_scenario_source import require_source
|
|
75
|
-
named = manifest.get("scenarios_file")
|
|
76
|
-
if isinstance(named, str) and named:
|
|
77
|
-
return str(directory / named)
|
|
78
|
-
candidates = sorted(directory.glob("*.scenarios.md"))
|
|
79
|
-
if required or candidates:
|
|
80
|
-
return str(require_source(directory))
|
|
81
|
-
return ""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _projection(source: Mapping[str, object], manifest: Mapping[str, object]) -> list[dict[str, object]]:
|
|
85
|
-
from codeflow.acceptance.cf_acceptance_schema import scenario_check_ids
|
|
86
|
-
rows = {str(row["id"]): row for row in manifest.get("scenarios", [])}
|
|
87
|
-
checks = {str(check["id"]) for check in manifest.get("checks", [])}
|
|
88
|
-
findings: list[dict[str, object]] = []
|
|
89
|
-
for row in source["scenarios"]:
|
|
90
|
-
required = scenario_check_ids(row)
|
|
91
|
-
current = rows.get(str(row["id"]), {})
|
|
92
|
-
unknown = set(required) - checks
|
|
93
|
-
if unknown:
|
|
94
|
-
findings.append({"code": "unknown_check", "scenario": row["id"], "checks": sorted(unknown)})
|
|
95
|
-
if required and set(required) != set(scenario_check_ids(current)):
|
|
96
|
-
findings.append({"code": "scenario_checks_not_projected", "scenario": row["id"]})
|
|
97
|
-
return findings
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _scenario_snapshot(snapshot: PlanSnapshot, source_file: str) -> Optional[FileSnapshot]:
|
|
101
|
-
scenario = snapshot.scenario
|
|
102
|
-
if not source_file:
|
|
103
|
-
return None
|
|
104
|
-
if scenario is not None and Path(scenario.path).resolve() == Path(source_file).resolve():
|
|
105
|
-
return scenario
|
|
106
|
-
return _read_snapshot(Path(source_file)) if Path(source_file).is_file() else None
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _scenario_ids(text: str) -> set[str]:
|
|
110
|
-
"""Expand explicit IDs and `S-01~04` / `S-01~S-04` ranges."""
|
|
111
|
-
ids = {item.upper() for item in re.findall(r"\b(?:S|E|B)-\d+\b", text, re.IGNORECASE)}
|
|
112
|
-
for match in re.finditer(r"\b([SEB])-(\d+)\s*~\s*(?:\1-)?(\d+)\b", text, re.IGNORECASE):
|
|
113
|
-
prefix, start, end = match.group(1).upper(), int(match.group(2)), int(match.group(3))
|
|
114
|
-
width = max(len(match.group(2)), len(match.group(3)))
|
|
115
|
-
for value in range(start, end + 1):
|
|
116
|
-
ids.add(f"{prefix}-{value:0{width}d}")
|
|
117
|
-
return ids
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _checklist_issues(text: str) -> list[dict[str, str]]:
|
|
121
|
-
"""Flag scenario IDs used in a Checklist but not declared in Acceptance-Refs.
|
|
122
|
-
|
|
123
|
-
Cross-task boundaries start as this mismatch: a task writes another task's
|
|
124
|
-
scenario into its checklist, then the plan assigns the scenario to only one
|
|
125
|
-
owner. Reported as a non-blocking warning for existing plans.
|
|
126
|
-
"""
|
|
127
|
-
from codeflow.task.cf_task_doc import TaskDocumentError, parse_task_document
|
|
128
|
-
try:
|
|
129
|
-
document = parse_task_document(text)
|
|
130
|
-
except TaskDocumentError:
|
|
131
|
-
return []
|
|
132
|
-
issues: list[dict[str, str]] = []
|
|
133
|
-
for task in document.tasks:
|
|
134
|
-
declared = _scenario_ids(str(task.fields.get("Acceptance-Refs", "")))
|
|
135
|
-
checklist = task.body.partition("### Checklist")[2].partition("### Acceptance Contract")[0]
|
|
136
|
-
used = _scenario_ids(checklist)
|
|
137
|
-
for scenario in sorted(used - declared):
|
|
138
|
-
issues.append({"code": "checklist_scenario_not_in_refs", "task": task.id, "scenario": scenario})
|
|
139
|
-
for scenario in sorted(declared - used):
|
|
140
|
-
issues.append({"code": "acceptance_ref_not_in_checklist", "task": task.id, "scenario": scenario})
|
|
141
|
-
return issues
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def verify_plan(directory: str, task_file: str, require_scenarios: bool = False,
|
|
145
|
-
*, snapshot: Optional[PlanSnapshot] = None) -> dict[str, object]:
|
|
146
|
-
from codeflow.acceptance.cf_acceptance_manifest_core import validate_manifest
|
|
147
|
-
from codeflow.acceptance.cf_acceptance_schema import load_manifest
|
|
148
|
-
from codeflow.acceptance.cf_check_registration import validate_registration
|
|
149
|
-
from codeflow.cli.cf_design_lint import lint_requirement
|
|
150
|
-
from codeflow.acceptance.cf_scenario_source import parse_scenarios, validate_e2e_quality
|
|
151
|
-
from codeflow.spec.cf_spec_context_model import load_context
|
|
152
|
-
from codeflow.spec.cf_stage_gate import result_to_data, validate_plan_coverage, validate_stage
|
|
153
|
-
base = Path(directory)
|
|
154
|
-
path = base / ".acceptance-manifest.json"
|
|
155
|
-
if not path.is_file():
|
|
156
|
-
return {"decision": "block", "reason": "acceptance_manifest_missing"}
|
|
157
|
-
plan = snapshot if snapshot is not None else load_plan_snapshot(directory, task_file, require_scenarios)
|
|
158
|
-
if plan.drift():
|
|
159
|
-
return {"decision": "block", "reason": "plan_snapshot_drift"}
|
|
160
|
-
try:
|
|
161
|
-
manifest = load_manifest(path, text=plan.manifest.text)
|
|
162
|
-
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
163
|
-
return {"decision": "block", "reason": f"acceptance_manifest_invalid: {exc}"}
|
|
164
|
-
source_file = _source(base, manifest, require_scenarios)
|
|
165
|
-
scenario = _scenario_snapshot(plan, source_file)
|
|
166
|
-
valid, reason = validate_manifest(task_file, str(path), task_text=plan.task.text,
|
|
167
|
-
manifest=manifest, scenario_file=source_file,
|
|
168
|
-
scenario_text=scenario.text if scenario is not None else None)
|
|
169
|
-
if not valid:
|
|
170
|
-
return {"decision": "block", "reason": reason}
|
|
171
|
-
if scenario is not None:
|
|
172
|
-
source = parse_scenarios(scenario.path, text=scenario.text)
|
|
173
|
-
elif source_file:
|
|
174
|
-
source = parse_scenarios(source_file)
|
|
175
|
-
else:
|
|
176
|
-
source = {"scenarios": manifest["scenarios"]}
|
|
177
|
-
quality = validate_e2e_quality(source["scenarios"])
|
|
178
|
-
projection = _projection(source, manifest)
|
|
179
|
-
registration = validate_registration(str(_root(base)), manifest["scenarios"], manifest.get("checks", []))
|
|
180
|
-
quality.update({"runnable_smoke_count": registration["runnable_smoke_count"],
|
|
181
|
-
"runnable_browser_count": registration["runnable_browser_count"],
|
|
182
|
-
"registration_status": "passed" if registration["ok"] else "failed"})
|
|
183
|
-
design = lint_requirement(task_file, source_file, task_text=plan.task.text,
|
|
184
|
-
scenarios=source["scenarios"] if source_file else None)
|
|
185
|
-
context = load_context(str(base / "spec-context.yml"))
|
|
186
|
-
gate = validate_stage(context, "plan", root=str(_root(base)), task_dir=directory)
|
|
187
|
-
coverage = validate_plan_coverage(context, task_file, text=plan.task.text)
|
|
188
|
-
checklist = _checklist_issues(plan.task.text)
|
|
189
|
-
blocked = (not quality["ok"] or projection or not registration["ok"] or
|
|
190
|
-
design["decision"] != "pass" or gate.decision != "pass" or coverage.decision != "pass")
|
|
191
|
-
recovery = (["code-flow spec refresh --task-dir " + quote(str(base)) +
|
|
192
|
-
" --root " + quote(str(_root(base))) + " --json"]
|
|
193
|
-
if any(issue.code == "rule_artifact_stale" for issue in gate.errors) else [])
|
|
194
|
-
return {"decision": "block" if blocked else "pass", "gate": result_to_data(gate), "next": recovery,
|
|
195
|
-
"coverage": result_to_data(coverage), "e2e_quality": quality,
|
|
196
|
-
"registration": registration, "design_lint": design, "projection": projection,
|
|
197
|
-
"checklist": checklist}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
"""Shared semantic checks for canonical scenarios and plan projections."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import re
|
|
5
|
-
from typing import Mapping, Sequence
|
|
6
|
-
|
|
7
|
-
ENVIRONMENTS = ("none", "local", "local-api", "browser", "external", "staging", "capacity", "ci", "production")
|
|
8
|
-
TARGET_PHASES = ("plan", "code", "review", "ci", "release")
|
|
9
|
-
EXTRA_FIELDS = ("expected_refs", "expected_checks", "assertions", "defer_reason", "owner", "target_phase", "features", "tags")
|
|
10
|
-
_PLACEHOLDERS = frozenset(("", "-", "n/a", "na", "none", "无", "todo", "tbd", "待定"))
|
|
11
|
-
_VAGUE = re.compile(r"可观|不回归|表现良好|符合预期|适当提升|as expected|significant improvement", re.I)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def meaningful(value: object) -> bool:
|
|
15
|
-
return str(value or "").strip().lower() not in _PLACEHOLDERS
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def issue(code: str, row: Mapping[str, object], field: str, message: str) -> dict[str, object]:
|
|
19
|
-
return {"code": code, "scenario": str(row.get("id", "")), "field": field, "message": message}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def validate_expected_checks(row: Mapping[str, object]) -> list[dict[str, object]]:
|
|
23
|
-
"""Validate explicit composition without weakening the scenario's checks."""
|
|
24
|
-
links = row.get("expected_checks")
|
|
25
|
-
expected, checks = row.get("expected", []), row.get("checks", [])
|
|
26
|
-
if not isinstance(links, list) or not links or not isinstance(expected, list) or not isinstance(checks, list):
|
|
27
|
-
raise ValueError("expected_checks 需要非空映射、expected 与 checks 列表")
|
|
28
|
-
seen: set[int] = set()
|
|
29
|
-
for link in links:
|
|
30
|
-
if not isinstance(link, Mapping):
|
|
31
|
-
raise ValueError("expected_checks 成员必须是映射")
|
|
32
|
-
index, members = link.get("expected"), link.get("checks")
|
|
33
|
-
if type(index) is not int or not 1 <= index <= len(expected) or index in seen:
|
|
34
|
-
raise ValueError("expected_checks 的 expected 索引无效或重复")
|
|
35
|
-
if (not isinstance(members, list) or not members
|
|
36
|
-
or not all(isinstance(member, str) and member in checks for member in members)
|
|
37
|
-
or len(set(members)) != len(members)):
|
|
38
|
-
raise ValueError("expected_checks 引用了无效 check")
|
|
39
|
-
seen.add(index)
|
|
40
|
-
if seen != set(range(1, len(expected) + 1)):
|
|
41
|
-
raise ValueError("expected_checks 必须覆盖所有 expected")
|
|
42
|
-
return [dict(link) for link in links]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def validate_extensions(row: Mapping[str, object]) -> dict[str, object]:
|
|
46
|
-
env = str(row.get("env", "none")).lower()
|
|
47
|
-
if env not in ENVIRONMENTS:
|
|
48
|
-
raise ValueError(f"env 非法: {env!r}; allowed: {', '.join(ENVIRONMENTS)}")
|
|
49
|
-
result: dict[str, object] = {"env": env}
|
|
50
|
-
if "expected_checks" in row:
|
|
51
|
-
result["expected_checks"] = validate_expected_checks(row)
|
|
52
|
-
for field in ("features", "tags", "assertions"):
|
|
53
|
-
values = row.get(field, [])
|
|
54
|
-
if not isinstance(values, list) or not all(isinstance(value, str) and value.strip() for value in values):
|
|
55
|
-
raise ValueError(f"{field} 必须是非空字符串列表")
|
|
56
|
-
result[field] = list(values)
|
|
57
|
-
for component in row.get("components", []) or []:
|
|
58
|
-
if not isinstance(component, str) or not re.fullmatch(r"[a-zA-Z][a-zA-Z0-9_.:-]*", component):
|
|
59
|
-
raise ValueError("components 必须使用稳定标识符,不能使用自由描述文本")
|
|
60
|
-
refs = row.get("expected_refs", [])
|
|
61
|
-
if not isinstance(refs, list) or not all(isinstance(value, Mapping) for value in refs):
|
|
62
|
-
raise ValueError("expected_refs 必须是映射列表")
|
|
63
|
-
result["expected_refs"] = [dict(value) for value in refs]
|
|
64
|
-
for field in ("defer_reason", "owner", "target_phase"):
|
|
65
|
-
value = row.get(field, "")
|
|
66
|
-
if not isinstance(value, str):
|
|
67
|
-
raise ValueError(f"{field} 必须是字符串")
|
|
68
|
-
result[field] = value.strip()
|
|
69
|
-
if result["target_phase"] and result["target_phase"] not in TARGET_PHASES:
|
|
70
|
-
raise ValueError("target_phase 非法")
|
|
71
|
-
return result
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def browser_boundary(row: Mapping[str, object]) -> bool:
|
|
75
|
-
components = {str(value).lower() for value in row.get("components", []) or []}
|
|
76
|
-
return str(row.get("env", "")).lower() == "browser" or bool(components & {"frontend", "browser"})
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _expected_links(row: Mapping[str, object], index: int) -> bool:
|
|
80
|
-
steps = row.get("steps", []) or []
|
|
81
|
-
for link in row.get("expected_refs", []) or []:
|
|
82
|
-
if not isinstance(link, Mapping) or type(link.get("expected")) is not int or link["expected"] != index:
|
|
83
|
-
continue
|
|
84
|
-
linked_steps = link.get("steps", [])
|
|
85
|
-
assertions = link.get("assertions", [])
|
|
86
|
-
if not isinstance(linked_steps, list) or not isinstance(assertions, list):
|
|
87
|
-
continue
|
|
88
|
-
valid_steps = linked_steps and all(type(value) is int and 1 <= value <= len(steps) for value in linked_steps)
|
|
89
|
-
declared = row.get("assertions", [])
|
|
90
|
-
valid_assertions = assertions and all(isinstance(value, str) and meaningful(value) and value in declared
|
|
91
|
-
for value in assertions)
|
|
92
|
-
if (valid_steps or valid_assertions) and (not linked_steps or valid_steps):
|
|
93
|
-
return True
|
|
94
|
-
return False
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def expectation_issues(row: Mapping[str, object]) -> list[dict[str, object]]:
|
|
98
|
-
errors: list[dict[str, object]] = []
|
|
99
|
-
for index, expected in enumerate(row.get("expected", []) or [], 1):
|
|
100
|
-
text = str(expected).strip()
|
|
101
|
-
if not meaningful(text) or _VAGUE.search(text):
|
|
102
|
-
errors.append(issue("scenario_expected_nondeterministic", row, "expected",
|
|
103
|
-
f"expected[{index}] 需要具体可观察结果或量化阈值"))
|
|
104
|
-
if re.search(r"^(若|如果|if\b)", text, re.I) and not row.get("preconditions"):
|
|
105
|
-
errors.append(issue("scenario_expected_nondeterministic", row, "preconditions",
|
|
106
|
-
"条件式预期必须声明可构造的前置条件"))
|
|
107
|
-
if not _expected_links(row, index):
|
|
108
|
-
errors.append(issue("scenario_expected_reference_missing", row, "expected_refs",
|
|
109
|
-
f"expected[{index}] 必须引用存在的步骤或稳定断言 ID"))
|
|
110
|
-
return errors
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def deferred_issues(row: Mapping[str, object]) -> list[dict[str, object]]:
|
|
114
|
-
if row.get("exec") != "deferred":
|
|
115
|
-
return []
|
|
116
|
-
missing = [field for field in ("defer_reason", "owner", "target_phase") if not meaningful(row.get(field))]
|
|
117
|
-
return [issue("scenario_defer_metadata_missing", row, "exec", "延后信息缺少: " + ", ".join(missing))] if missing else []
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def e2e_issues(row: Mapping[str, object]) -> list[dict[str, object]]:
|
|
121
|
-
errors: list[dict[str, object]] = []
|
|
122
|
-
requirements = (("why_e2e", "e2e_justification_missing"), ("boundary", "e2e_boundary_missing"))
|
|
123
|
-
for field, code in requirements:
|
|
124
|
-
if not meaningful(row.get(field)):
|
|
125
|
-
errors.append(issue(code, row, field, "E2E 必须声明真实边界和低层测试不足的理由"))
|
|
126
|
-
for field in ("steps", "expected"):
|
|
127
|
-
if not any(meaningful(value) for value in row.get(field, []) or []):
|
|
128
|
-
errors.append(issue(f"e2e_{field}_missing", row, field, "E2E 必须声明可复现步骤及可观察结果"))
|
|
129
|
-
if str(row.get("priority", "")).upper() not in ("P0", "P1"):
|
|
130
|
-
errors.append(issue("e2e_priority_not_critical", row, "priority", "E2E 只允许 P0/P1"))
|
|
131
|
-
if not browser_boundary(row):
|
|
132
|
-
errors.append(issue("e2e_browser_boundary_missing", row, "components",
|
|
133
|
-
"浏览器 E2E 必须声明 browser/frontend;API/CLI 全链路使用 system/integration"))
|
|
134
|
-
if browser_boundary(row) and not re.search(r"browser|frontend|浏览器|前端|\bUI\b", str(row.get("boundary", "")), re.I):
|
|
135
|
-
errors.append(issue("scenario_boundary_components_mismatch", row, "boundary",
|
|
136
|
-
"声明的浏览器组件必须出现在真实边界中"))
|
|
137
|
-
if row.get("exec") == "aspirational":
|
|
138
|
-
errors.append(issue("e2e_aspirational", row, "exec", "aspirational 不能作为 E2E 验收"))
|
|
139
|
-
paths = row.get("trigger_paths", []) or []
|
|
140
|
-
if not row.get("smoke") and not paths:
|
|
141
|
-
errors.append(issue("e2e_trigger_paths_missing", row, "trigger_paths", "非 smoke E2E 必须声明触发路径"))
|
|
142
|
-
if any(not meaningful(value) for value in paths):
|
|
143
|
-
errors.append(issue("e2e_trigger_path_placeholder", row, "trigger_paths", "触发路径不能使用占位值"))
|
|
144
|
-
return errors
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def quality_report(scenarios: Sequence[Mapping[str, object]]) -> dict[str, object]:
|
|
148
|
-
errors: list[dict[str, object]] = []
|
|
149
|
-
warnings: list[dict[str, object]] = []
|
|
150
|
-
e2e = [row for row in scenarios if str(row.get("level", "")).lower() == "e2e"]
|
|
151
|
-
signatures: dict[str, str] = {}
|
|
152
|
-
for row in scenarios:
|
|
153
|
-
errors.extend(expectation_issues(row))
|
|
154
|
-
errors.extend(deferred_issues(row))
|
|
155
|
-
if row in e2e:
|
|
156
|
-
errors.extend(e2e_issues(row))
|
|
157
|
-
signature = repr((row.get("steps"), row.get("expected"), row.get("boundary")))
|
|
158
|
-
if signature in signatures:
|
|
159
|
-
warnings.append(issue("duplicate_e2e_journey", row, "steps", f"与 {signatures[signature]} 重复"))
|
|
160
|
-
signatures[signature] = str(row.get("id"))
|
|
161
|
-
elif meaningful(row.get("why_e2e")):
|
|
162
|
-
errors.append(issue("why_e2e_on_non_e2e", row, "why_e2e", "why_e2e 只属于浏览器 E2E"))
|
|
163
|
-
ratio = len(e2e) / len(scenarios) if scenarios else 0.0
|
|
164
|
-
smoke = sum(bool(row.get("smoke")) for row in e2e)
|
|
165
|
-
if ratio > 0.3:
|
|
166
|
-
warnings.append(issue("e2e_ratio_high", {}, "level", f"E2E 占比 {ratio:.0%};优先下沉低层"))
|
|
167
|
-
if e2e and not smoke:
|
|
168
|
-
warnings.append(issue("e2e_smoke_missing", {}, "smoke", "没有声明 smoke E2E"))
|
|
169
|
-
return {"ok": not errors, "errors": errors, "warnings": warnings, "total": len(scenarios),
|
|
170
|
-
"e2e_count": len(e2e), "e2e_browser_count": sum(browser_boundary(row) for row in e2e),
|
|
171
|
-
"system_count": sum(row.get("level") == "system" for row in scenarios),
|
|
172
|
-
"capacity_count": sum(row.get("level") == "capacity" or "capacity" in row.get("tags", []) for row in scenarios),
|
|
173
|
-
"smoke_count": smoke, "e2e_ratio": round(ratio, 4),
|
|
174
|
-
"runnable_smoke_count": None, "verified_browser_count": None, "registration_status": "not_checked"}
|