@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,174 @@
|
|
|
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"}
|
|
@@ -0,0 +1,300 @@
|
|
|
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}
|