@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,300 +0,0 @@
|
|
|
1
|
-
"""Parse and reconcile canonical requirement scenario sources."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import re
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Mapping, Optional, Sequence
|
|
7
|
-
|
|
8
|
-
import yaml
|
|
9
|
-
|
|
10
|
-
from codeflow.core.cf_markdown import frontmatter_block
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
SCHEMA_VERSION = 1
|
|
14
|
-
SCENARIO_ID = re.compile(r"^[SEB]-\d+$")
|
|
15
|
-
PRIORITIES = ("P0", "P1", "P2")
|
|
16
|
-
LEVELS = ("unit", "component", "contract", "integration", "system", "capacity", "e2e", "manual")
|
|
17
|
-
EXEC_MODES = ("auto", "deferred", "manual", "aspirational")
|
|
18
|
-
E2E_CRITICAL_PRIORITIES = ("P0", "P1")
|
|
19
|
-
E2E_RATIO_WARNING = 0.3
|
|
20
|
-
_PLACEHOLDERS = frozenset(("", "-", "n/a", "na", "none", "无", "todo", "tbd", "待定"))
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class ScenarioError(ValueError):
|
|
24
|
-
"""Stable parse/merge failure with a machine-readable code."""
|
|
25
|
-
|
|
26
|
-
def __init__(self, code: str, message: str, details: Optional[Mapping[str, object]] = None) -> None:
|
|
27
|
-
super().__init__(message)
|
|
28
|
-
self.code = code
|
|
29
|
-
self.message = message
|
|
30
|
-
self.details = dict(details) if details else {}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _frontmatter_text(text: str) -> str:
|
|
34
|
-
"""YAML body for `yaml.safe_load`; unclosed delimiters fail closed."""
|
|
35
|
-
block = frontmatter_block(text)
|
|
36
|
-
if block is not None:
|
|
37
|
-
return block
|
|
38
|
-
if text.startswith("---"):
|
|
39
|
-
raise ScenarioError("scenario_source_invalid_frontmatter", "frontmatter 缺少闭合 ---")
|
|
40
|
-
return text
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _mapping(value: object, field: str) -> Mapping[str, object]:
|
|
44
|
-
if not isinstance(value, Mapping):
|
|
45
|
-
raise ScenarioError("invalid_scenario_source", f"{field} 必须是 mapping")
|
|
46
|
-
return value
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _string_list(value: object, field: str) -> tuple[str, ...]:
|
|
50
|
-
if value is None:
|
|
51
|
-
return ()
|
|
52
|
-
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
53
|
-
raise ScenarioError("invalid_scenario_source", f"{field} 必须是字符串列表")
|
|
54
|
-
return tuple(item.strip() for item in value if item.strip())
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _boolean(value: object, field: str, default: bool = False) -> bool:
|
|
58
|
-
if value is None:
|
|
59
|
-
return default
|
|
60
|
-
if not isinstance(value, bool):
|
|
61
|
-
raise ScenarioError("invalid_scenario_source", f"{field} 必须是布尔值")
|
|
62
|
-
return value
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _meaningful(value: object) -> bool:
|
|
66
|
-
return str(value or "").strip().lower() not in _PLACEHOLDERS
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _validate_one(item: Mapping[str, object]) -> dict[str, object]:
|
|
70
|
-
scenario_id = item.get("id")
|
|
71
|
-
if not isinstance(scenario_id, str) or not SCENARIO_ID.fullmatch(scenario_id):
|
|
72
|
-
raise ScenarioError("invalid_scenario_id", f"非法场景 ID: {scenario_id!r}")
|
|
73
|
-
priority = str(item.get("priority", "P1")).upper()
|
|
74
|
-
if priority not in PRIORITIES:
|
|
75
|
-
raise ScenarioError("invalid_priority", f"{scenario_id}: priority 非法: {priority!r}")
|
|
76
|
-
level = str(item.get("level", "integration")).lower()
|
|
77
|
-
if level not in LEVELS:
|
|
78
|
-
raise ScenarioError("invalid_level", f"{scenario_id}: level 非法: {level!r}")
|
|
79
|
-
exec_mode = str(item.get("exec", "auto")).lower()
|
|
80
|
-
if exec_mode not in EXEC_MODES:
|
|
81
|
-
raise ScenarioError("invalid_exec", f"{scenario_id}: exec 非法: {exec_mode!r}")
|
|
82
|
-
try:
|
|
83
|
-
from codeflow.acceptance.cf_scenario_quality import validate_extensions
|
|
84
|
-
extra = validate_extensions(item)
|
|
85
|
-
except ValueError as exc:
|
|
86
|
-
raise ScenarioError("invalid_scenario_contract", f"{scenario_id}: {exc}") from exc
|
|
87
|
-
return {
|
|
88
|
-
**extra,
|
|
89
|
-
"id": scenario_id,
|
|
90
|
-
"title": str(item.get("title", "")).strip(),
|
|
91
|
-
"priority": priority,
|
|
92
|
-
"level": level,
|
|
93
|
-
"exec": exec_mode,
|
|
94
|
-
"env": extra["env"],
|
|
95
|
-
"components": list(_string_list(item.get("components"), f"{scenario_id}.components")),
|
|
96
|
-
"boundary": str(item.get("boundary", "")).strip(),
|
|
97
|
-
"preconditions": list(_string_list(item.get("preconditions"), f"{scenario_id}.preconditions")),
|
|
98
|
-
"steps": list(_string_list(item.get("steps"), f"{scenario_id}.steps")),
|
|
99
|
-
"expected": list(_string_list(item.get("expected"), f"{scenario_id}.expected")),
|
|
100
|
-
"design_refs": list(_string_list(item.get("design_refs"), f"{scenario_id}.design_refs")),
|
|
101
|
-
"rules": list(_string_list(item.get("rules"), f"{scenario_id}.rules")),
|
|
102
|
-
"checks": list(_string_list(item.get("checks"), f"{scenario_id}.checks")),
|
|
103
|
-
"sources": list(_string_list(item.get("sources"), f"{scenario_id}.sources")),
|
|
104
|
-
"why_e2e": str(item.get("why_e2e", "")).strip(),
|
|
105
|
-
"trigger_paths": list(_string_list(item.get("trigger_paths"), f"{scenario_id}.trigger_paths")),
|
|
106
|
-
"smoke": _boolean(item.get("smoke"), f"{scenario_id}.smoke"),
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def parse_scenarios(path: object, *, text: Optional[str] = None) -> dict[str, object]:
|
|
111
|
-
"""Parse and validate one scenarios source file."""
|
|
112
|
-
target = Path(path)
|
|
113
|
-
try:
|
|
114
|
-
raw = target.read_text(encoding="utf-8") if text is None else text
|
|
115
|
-
except (OSError, UnicodeError) as exc:
|
|
116
|
-
raise ScenarioError("scenario_source_unreadable", str(exc), {"path": str(target)}) from exc
|
|
117
|
-
try:
|
|
118
|
-
data = yaml.safe_load(_frontmatter_text(raw))
|
|
119
|
-
except yaml.YAMLError as exc:
|
|
120
|
-
raise ScenarioError("scenario_source_invalid_yaml", str(exc), {"path": str(target)}) from exc
|
|
121
|
-
document = _mapping(data, "scenarios")
|
|
122
|
-
schema = document.get("schema")
|
|
123
|
-
if isinstance(schema, bool) or schema not in (None, SCHEMA_VERSION):
|
|
124
|
-
raise ScenarioError("unsupported_scenario_schema", str(document.get("schema")))
|
|
125
|
-
raw = document.get("scenarios")
|
|
126
|
-
if not isinstance(raw, list) or not raw:
|
|
127
|
-
raise ScenarioError("scenarios_missing", "scenarios 必须是非空列表")
|
|
128
|
-
seen: set[str] = set()
|
|
129
|
-
scenarios: list[dict[str, object]] = []
|
|
130
|
-
for item in raw:
|
|
131
|
-
scenario = _validate_one(_mapping(item, "scenarios[]"))
|
|
132
|
-
if scenario["id"] in seen:
|
|
133
|
-
raise ScenarioError("duplicate_scenario_id", str(scenario["id"]))
|
|
134
|
-
seen.add(scenario["id"])
|
|
135
|
-
scenarios.append(scenario)
|
|
136
|
-
return {
|
|
137
|
-
"schema": SCHEMA_VERSION,
|
|
138
|
-
"requirement": str(document.get("requirement", target.parent.name)),
|
|
139
|
-
"source": str(target),
|
|
140
|
-
"scenarios": scenarios,
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _definition(scenario: Mapping[str, object]) -> dict[str, object]:
|
|
145
|
-
fields = ("title", "priority", "level", "exec", "env", "components", "boundary",
|
|
146
|
-
"preconditions", "steps", "expected", "rules", "why_e2e", "trigger_paths", "smoke")
|
|
147
|
-
from codeflow.acceptance.cf_scenario_quality import EXTRA_FIELDS
|
|
148
|
-
return {field: scenario.get(field) for field in (*fields, *EXTRA_FIELDS)}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def merge_scenarios(sets: Sequence[Mapping[str, object]]) -> list[dict[str, object]]:
|
|
152
|
-
"""Union scenario sets by global ID; reject conflicting definitions."""
|
|
153
|
-
merged: dict[str, dict[str, object]] = {}
|
|
154
|
-
for source in sets:
|
|
155
|
-
for raw in source.get("scenarios", []) or []:
|
|
156
|
-
scenario = _validate_one(_mapping(raw, "scenarios[]"))
|
|
157
|
-
current = merged.get(str(scenario["id"]))
|
|
158
|
-
if current is None:
|
|
159
|
-
merged[str(scenario["id"])] = scenario
|
|
160
|
-
continue
|
|
161
|
-
if _definition(current) != _definition(scenario):
|
|
162
|
-
fields = [key for key in _definition(current) if _definition(current)[key] != _definition(scenario)[key]]
|
|
163
|
-
raise ScenarioError(
|
|
164
|
-
"design_scenario_conflict",
|
|
165
|
-
f"{scenario['id']} 定义冲突: {', '.join(fields)}",
|
|
166
|
-
{"scenario": scenario["id"], "fields": fields},
|
|
167
|
-
)
|
|
168
|
-
current["sources"] = sorted(set(current.get("sources", [])) | set(scenario.get("sources", [])))
|
|
169
|
-
current["design_refs"] = sorted(set(current.get("design_refs", [])) | set(scenario.get("design_refs", [])))
|
|
170
|
-
current["checks"] = sorted(set(current.get("checks", [])) | set(scenario.get("checks", [])))
|
|
171
|
-
return [merged[key] for key in sorted(merged)]
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def _quality_issue(code: str, scenario: str, field: str, message: str) -> dict[str, object]:
|
|
175
|
-
return {"code": code, "scenario": scenario, "field": field, "message": message}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def validate_e2e_quality(scenarios: Sequence[Mapping[str, object]]) -> dict[str, object]:
|
|
179
|
-
"""Validate semantic acceptance contracts through one shared quality gate."""
|
|
180
|
-
from codeflow.acceptance.cf_scenario_quality import quality_report
|
|
181
|
-
return quality_report(scenarios)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def scenario_status(scenario: Mapping[str, object], checks: Mapping[str, str]) -> str:
|
|
185
|
-
"""Derive scenario state from all required checks (AND semantics)."""
|
|
186
|
-
required = [str(item) for item in scenario.get("checks", [])]
|
|
187
|
-
if not required:
|
|
188
|
-
return "aspirational" if scenario.get("exec") == "aspirational" else "designed"
|
|
189
|
-
states = [checks.get(check_id, "designed") for check_id in required]
|
|
190
|
-
for bad in ("failed", "env_blocked", "incomplete"):
|
|
191
|
-
if bad in states:
|
|
192
|
-
return bad
|
|
193
|
-
if all(state in ("passed", "verified") for state in states):
|
|
194
|
-
return "verified"
|
|
195
|
-
return "designed"
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def require_source(task_dir: object) -> Path:
|
|
199
|
-
"""Return the single scenarios source for a requirement directory."""
|
|
200
|
-
directory = Path(task_dir)
|
|
201
|
-
matches = sorted(directory.glob("*.scenarios.md")) if directory.is_dir() else []
|
|
202
|
-
if not matches:
|
|
203
|
-
raise ScenarioError("scenario_source_missing", f"缺少 <需求>.scenarios.md: {directory}")
|
|
204
|
-
if len(matches) > 1:
|
|
205
|
-
raise ScenarioError("scenario_source_ambiguous", f"多个场景源: {', '.join(path.name for path in matches)}")
|
|
206
|
-
return matches[0]
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def reconcile_coverage(scenarios: Sequence[Mapping[str, object]],
|
|
210
|
-
coverage: Sequence[Mapping[str, object]]) -> dict[str, object]:
|
|
211
|
-
"""Reconcile canonical scenarios with Acceptance Coverage rows."""
|
|
212
|
-
by_scenario = {str(item.get("id")): item for item in scenarios}
|
|
213
|
-
owners: dict[str, list[str]] = {}
|
|
214
|
-
unknown: list[str] = []
|
|
215
|
-
for row in coverage:
|
|
216
|
-
scenario_id = row.get("scenario_id") or row.get("id")
|
|
217
|
-
if not isinstance(scenario_id, str) or not scenario_id:
|
|
218
|
-
continue
|
|
219
|
-
if scenario_id not in by_scenario:
|
|
220
|
-
unknown.append(scenario_id)
|
|
221
|
-
continue
|
|
222
|
-
owner = row.get("owner")
|
|
223
|
-
if isinstance(owner, str) and owner.strip():
|
|
224
|
-
owners.setdefault(scenario_id, []).append(owner.strip())
|
|
225
|
-
covered = set(owners) | {str(row.get("scenario_id") or row.get("id")) for row in coverage
|
|
226
|
-
if isinstance(row.get("scenario_id") or row.get("id"), str)}
|
|
227
|
-
missing = sorted(set(by_scenario) - covered)
|
|
228
|
-
duplicate = sorted(key for key, values in owners.items() if len(set(values)) > 1)
|
|
229
|
-
unowned = sorted(set(by_scenario) - set(owners))
|
|
230
|
-
return {
|
|
231
|
-
"ok": not missing and not unknown and not duplicate and not unowned,
|
|
232
|
-
"missing": missing,
|
|
233
|
-
"unknown": sorted(unknown),
|
|
234
|
-
"duplicate_owner": duplicate,
|
|
235
|
-
"unowned": unowned,
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def load_requirement_scenarios(task_dir: object, allow_legacy: bool = False) -> dict[str, object]:
|
|
240
|
-
"""Load the requirement source, or an explicit deprecated legacy marker."""
|
|
241
|
-
directory = Path(task_dir)
|
|
242
|
-
try:
|
|
243
|
-
source = require_source(directory)
|
|
244
|
-
except ScenarioError as exc:
|
|
245
|
-
if exc.code != "scenario_source_missing" or not allow_legacy:
|
|
246
|
-
raise
|
|
247
|
-
return {"schema": SCHEMA_VERSION, "requirement": directory.name,
|
|
248
|
-
"source": "", "scenarios": [], "deprecated": True}
|
|
249
|
-
return parse_scenarios(source)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def audit_scenario_source(scenarios: Sequence[Mapping[str, object]],
|
|
253
|
-
coverage: Sequence[Mapping[str, object]],
|
|
254
|
-
manifest_ids: Optional[Sequence[str]] = None) -> dict[str, object]:
|
|
255
|
-
"""Three-way audit: scenarios, coverage, manifest."""
|
|
256
|
-
report = reconcile_coverage(scenarios, coverage)
|
|
257
|
-
scenario_ids = {str(item.get("id")) for item in scenarios}
|
|
258
|
-
coverage_ids = {str(item.get("scenario_id") or item.get("id")) for item in coverage}
|
|
259
|
-
manifest = {str(item) for item in manifest_ids} if manifest_ids is not None else None
|
|
260
|
-
report["manifest_missing"] = sorted(scenario_ids - manifest) if manifest is not None else []
|
|
261
|
-
report["manifest_unknown"] = sorted(manifest - scenario_ids) if manifest is not None else []
|
|
262
|
-
report["coverage_unknown"] = sorted(coverage_ids - scenario_ids)
|
|
263
|
-
report["ok"] = bool(report["ok"] and not report["coverage_unknown"]
|
|
264
|
-
and not report["manifest_missing"] and not report["manifest_unknown"])
|
|
265
|
-
return report
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def migrate_from_coverage(coverage: Sequence[Mapping[str, object]]) -> dict[str, object]:
|
|
269
|
-
"""Build an initial scenarios source from legacy Acceptance Coverage."""
|
|
270
|
-
scenarios: list[dict[str, object]] = []
|
|
271
|
-
invalid: list[str] = []
|
|
272
|
-
for row in coverage:
|
|
273
|
-
scenario_id = str(row.get("scenario_id") or row.get("id") or "")
|
|
274
|
-
if not SCENARIO_ID.fullmatch(scenario_id):
|
|
275
|
-
invalid.append(scenario_id)
|
|
276
|
-
continue
|
|
277
|
-
priority = str(row.get("priority") or "P1").upper()
|
|
278
|
-
level = str(row.get("level") or "integration").lower()
|
|
279
|
-
if priority not in PRIORITIES:
|
|
280
|
-
priority = "P1"
|
|
281
|
-
if level not in LEVELS:
|
|
282
|
-
level = "integration"
|
|
283
|
-
scenarios.append({
|
|
284
|
-
"id": scenario_id,
|
|
285
|
-
"title": str(row.get("title") or scenario_id),
|
|
286
|
-
"priority": priority,
|
|
287
|
-
"level": level,
|
|
288
|
-
"exec": "auto",
|
|
289
|
-
"env": "none",
|
|
290
|
-
"components": [],
|
|
291
|
-
"boundary": str(row.get("boundary") or ""),
|
|
292
|
-
"preconditions": [],
|
|
293
|
-
"steps": [],
|
|
294
|
-
"expected": [],
|
|
295
|
-
"design_refs": [],
|
|
296
|
-
"rules": [],
|
|
297
|
-
"checks": [],
|
|
298
|
-
"sources": ["legacy-coverage"],
|
|
299
|
-
})
|
|
300
|
-
return {"schema": SCHEMA_VERSION, "scenarios": scenarios, "migrated": True, "invalid": invalid}
|