okstra 0.172.0 → 0.174.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 +8 -6
- package/docs/architecture/storage-model.md +24 -3
- package/docs/architecture.md +21 -35
- package/docs/cli.md +39 -7
- package/docs/container.md +1 -1
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/performance-improvement-plan-v2.md +6 -5
- package/docs/project-structure-overview.md +33 -25
- package/docs/task-process/README.md +6 -4
- package/docs/task-process/error-analysis.md +2 -2
- package/docs/task-process/final-verification.md +2 -2
- package/docs/task-process/implementation-option-selection.md +70 -0
- package/docs/task-process/implementation-planning.md +24 -16
- package/docs/task-process/requirements-discovery.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/report-writer-worker.md +30 -6
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +2 -1
- package/runtime/bin/lib/okstra/usage.sh +3 -0
- package/runtime/bin/okstra-provider-exec.py +29 -12
- package/runtime/bin/okstra-trace-cleanup.sh +58 -129
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/duties/direction-selection-worker.md +44 -0
- package/runtime/prompts/duties/planning-worker.md +12 -4
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +5 -5
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -6
- package/runtime/prompts/lead/plan-body-verification.md +23 -6
- package/runtime/prompts/lead/report-writer.md +33 -11
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/error-analysis.md +4 -4
- package/runtime/prompts/profiles/final-verification.md +3 -3
- package/runtime/prompts/profiles/forbidden-actions.json +7 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +35 -0
- package/runtime/prompts/profiles/implementation-planning.md +61 -46
- package/runtime/prompts/profiles/implementation.md +4 -2
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +3 -3
- package/runtime/prompts/wizard/prompts.ko.json +9 -1
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
- package/runtime/python/okstra_ctl/agent_invocation.py +1 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +6 -0
- package/runtime/python/okstra_ctl/conformance.py +68 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
- package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
- package/runtime/python/okstra_ctl/doctor.py +2 -2
- package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
- package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
- package/runtime/python/okstra_ctl/fix_cycles.py +3 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +836 -0
- package/runtime/python/okstra_ctl/implementation_options.py +479 -0
- package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
- package/runtime/python/okstra_ctl/plan_items.py +51 -3
- package/runtime/python/okstra_ctl/render.py +1 -0
- package/runtime/python/okstra_ctl/render_final_report.py +16 -19
- package/runtime/python/okstra_ctl/report_contract.py +45 -14
- package/runtime/python/okstra_ctl/report_finalize.py +68 -9
- package/runtime/python/okstra_ctl/report_html/render.py +4 -2
- package/runtime/python/okstra_ctl/report_html/router.py +4 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +32 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +25 -10
- package/runtime/python/okstra_ctl/report_views.py +148 -12
- package/runtime/python/okstra_ctl/run.py +393 -4
- package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
- package/runtime/python/okstra_ctl/scope_provenance.py +16 -10
- package/runtime/python/okstra_ctl/session.py +69 -12
- package/runtime/python/okstra_ctl/team.py +51 -25
- package/runtime/python/okstra_ctl/tmux.py +19 -149
- package/runtime/python/okstra_ctl/user_response.py +75 -0
- package/runtime/python/okstra_ctl/wizard.py +144 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
- package/runtime/python/okstra_ctl/worker_request.py +2 -0
- package/runtime/python/okstra_ctl/workflow.py +29 -7
- package/runtime/python/okstra_ctl/worktree.py +69 -3
- package/runtime/python/okstra_token_usage/cli.py +1 -1
- package/runtime/python/okstra_token_usage/collect.py +66 -6
- package/runtime/schemas/final-report-v2.0.schema.json +1428 -137
- package/runtime/skills/okstra-setup/references/project-config.md +11 -0
- package/runtime/templates/reports/final-report-v2.template.md +4 -0
- package/runtime/templates/reports/final-verification-input.template.md +1 -1
- package/runtime/templates/reports/html/base.template.html +3 -2
- package/runtime/templates/reports/html/i18n/en.json +21 -1
- package/runtime/templates/reports/html/i18n/ko.json +21 -1
- package/runtime/templates/reports/html/macros/forms.html +21 -2
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +49 -0
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +36 -2
- package/runtime/templates/reports/i18n/en.json +13 -0
- package/runtime/templates/reports/implementation-input.template.md +4 -2
- package/runtime/templates/reports/implementation-planning-input.template.md +18 -4
- package/runtime/templates/reports/improvement-discovery-input.template.md +1 -1
- package/runtime/templates/reports/md/tasks/implementation-option-selection.template.md +13 -0
- package/runtime/templates/reports/md/tasks/implementation-planning.template.md +17 -0
- package/runtime/templates/reports/report.js +111 -4
- package/runtime/templates/reports/settings.template.json +0 -24
- package/runtime/templates/reports/task-brief.template.md +9 -3
- package/runtime/templates/reports/user-response.template.md +25 -4
- package/runtime/templates/worker-prompt-preamble.md +8 -0
- package/runtime/validators/lib/fixtures.sh +49 -17
- package/runtime/validators/validate-implementation-plan-stages.py +169 -4
- package/runtime/validators/validate-report-views.py +2 -2
- package/runtime/validators/validate-run.py +149 -498
- package/runtime/validators/validate_improvement_report.py +5 -1
- package/runtime/validators/validate_session_conformance.py +1 -1
- package/src/cli-registry.mjs +8 -1
- package/src/commands/execute/codex-run.mjs +1 -0
- package/src/commands/execute/render-bundle.mjs +1 -0
- package/src/commands/execute/team.mjs +3 -3
- package/src/commands/execute/worktree-status.mjs +109 -0
- package/src/commands/lifecycle/install.mjs +0 -2
- package/src/commands/report/finalize.mjs +13 -6
- package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
- package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
- package/runtime/templates/reports/final-report.template.md +0 -1258
|
@@ -43,14 +43,16 @@ from okstra_project.dirs import tasks_root as _okstra_tasks_root # noqa: E402
|
|
|
43
43
|
from okstra_project.resolver import resolve_architecture # noqa: E402
|
|
44
44
|
|
|
45
45
|
from okstra_ctl.conformance import ( # noqa: E402
|
|
46
|
-
CAPABILITY_WHITELIST,
|
|
47
46
|
detect_surfaces,
|
|
48
47
|
evaluate_conformance,
|
|
49
48
|
manifest_required_surfaces,
|
|
49
|
+
normalize_conformance_script as _normalize_conformance_script,
|
|
50
|
+
parse_conformance_tests as _parse_conformance_tests,
|
|
50
51
|
qa_result_from_dict,
|
|
51
52
|
validate_conformance_manifest,
|
|
52
53
|
)
|
|
53
54
|
from okstra_ctl.paths import RunRef # noqa: E402
|
|
55
|
+
from okstra_ctl.report_contract import CURRENT_REPORT_SCHEMA_VERSION # noqa: E402
|
|
54
56
|
from okstra_ctl.domain.host import HostNotRegistered # noqa: E402
|
|
55
57
|
from okstra_ctl.registry.host_registry import default_host_registry # noqa: E402
|
|
56
58
|
from okstra_ctl.build_tools import ( # noqa: E402
|
|
@@ -71,7 +73,12 @@ from okstra_ctl.incremental_scope import ( # noqa: E402
|
|
|
71
73
|
coverage_row_blocked_on,
|
|
72
74
|
stages_for_clarification,
|
|
73
75
|
)
|
|
74
|
-
from okstra_ctl.workflow import
|
|
76
|
+
from okstra_ctl.workflow import ( # noqa: E402
|
|
77
|
+
DEFAULT_NEXT_PHASE,
|
|
78
|
+
ERROR_ANALYSIS_ROUTING_DIRECTIONS,
|
|
79
|
+
PHASE_SEQUENCE,
|
|
80
|
+
REQUIREMENTS_DISCOVERY_ROUTING_TARGETS,
|
|
81
|
+
)
|
|
75
82
|
from okstra_ctl.md_table import ( # noqa: E402
|
|
76
83
|
is_separator_row as _is_markdown_separator,
|
|
77
84
|
split_pipe_row as _split_pipe_row,
|
|
@@ -80,9 +87,16 @@ from okstra_ctl.final_report_paths import final_report_data_path as _data_path_f
|
|
|
80
87
|
from okstra_ctl.improvement_assignment import ( # noqa: E402
|
|
81
88
|
validate_primary_lens_assignments,
|
|
82
89
|
)
|
|
90
|
+
from okstra_ctl.implementation_options import ( # noqa: E402
|
|
91
|
+
validate_implementation_option_selection,
|
|
92
|
+
)
|
|
93
|
+
from okstra_ctl.implementation_direction import ( # noqa: E402
|
|
94
|
+
validate_selected_direction_plan,
|
|
95
|
+
)
|
|
83
96
|
from okstra_ctl.worker_prompt_policy import GRILLING_LOG_HEADER # noqa: E402
|
|
84
97
|
from okstra_ctl.scope_provenance import ( # noqa: E402
|
|
85
98
|
brief_citation_problem,
|
|
99
|
+
brief_end_state_id_sequence,
|
|
86
100
|
brief_end_state_ids,
|
|
87
101
|
brief_headings,
|
|
88
102
|
parse_source,
|
|
@@ -129,7 +143,7 @@ from okstra_ctl.convergence_provenance import ( # noqa: E402
|
|
|
129
143
|
|
|
130
144
|
TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
131
145
|
ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
|
|
132
|
-
WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed"
|
|
146
|
+
WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed"}
|
|
133
147
|
_AGENT_DISPATCH_DIGEST_KEYS = (
|
|
134
148
|
"catalogDigest",
|
|
135
149
|
"assignmentDigest",
|
|
@@ -406,7 +420,28 @@ def _error_analysis_next_phase(data: Mapping[str, Any]) -> str | None:
|
|
|
406
420
|
if not isinstance(routing, Mapping):
|
|
407
421
|
return None
|
|
408
422
|
target = routing.get("nextTaskType")
|
|
409
|
-
if target in
|
|
423
|
+
if target in ERROR_ANALYSIS_ROUTING_DIRECTIONS:
|
|
424
|
+
return str(target)
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _requirements_discovery_next_phase(data: Mapping[str, Any]) -> str | None:
|
|
429
|
+
if not isinstance(data, Mapping):
|
|
430
|
+
return None
|
|
431
|
+
header = data.get("header")
|
|
432
|
+
if (
|
|
433
|
+
not isinstance(header, Mapping)
|
|
434
|
+
or header.get("taskType") != "requirements-discovery"
|
|
435
|
+
):
|
|
436
|
+
return None
|
|
437
|
+
requirements = data.get("requirementsDiscovery")
|
|
438
|
+
if not isinstance(requirements, Mapping):
|
|
439
|
+
return None
|
|
440
|
+
routing = requirements.get("routing")
|
|
441
|
+
if not isinstance(routing, Mapping):
|
|
442
|
+
return None
|
|
443
|
+
target = routing.get("nextTaskType")
|
|
444
|
+
if target in REQUIREMENTS_DISCOVERY_ROUTING_TARGETS:
|
|
410
445
|
return str(target)
|
|
411
446
|
return None
|
|
412
447
|
|
|
@@ -445,11 +480,12 @@ def update_workflow_metadata(
|
|
|
445
480
|
# Validation just passed → actively advance to the next phase in
|
|
446
481
|
# the sequence rather than preserving a stale value that may equal
|
|
447
482
|
# current_phase (which would cause the lifecycle pointer to stall).
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
483
|
+
if current_phase == "requirements-discovery":
|
|
484
|
+
report_next_phase = _requirements_discovery_next_phase(report_data or {})
|
|
485
|
+
elif current_phase == "error-analysis":
|
|
486
|
+
report_next_phase = _error_analysis_next_phase(report_data or {})
|
|
487
|
+
else:
|
|
488
|
+
report_next_phase = None
|
|
453
489
|
next_recommended_phase = report_next_phase or advance_next_phase(
|
|
454
490
|
current_phase, phase_sequence
|
|
455
491
|
)
|
|
@@ -1648,35 +1684,6 @@ def _approved_plan_stage_entry(
|
|
|
1648
1684
|
}
|
|
1649
1685
|
|
|
1650
1686
|
|
|
1651
|
-
_CONFORMANCE_TESTS_RE = re.compile(
|
|
1652
|
-
r"^(?P<script>\S+)\s+\(requires=\[(?P<requires>[^\]]+)\]\)$"
|
|
1653
|
-
)
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
def _normalize_conformance_script(script: str) -> str:
|
|
1657
|
-
prefix = "<task_root>/"
|
|
1658
|
-
return script[len(prefix):] if script.startswith(prefix) else script
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
def _parse_conformance_tests(value: object) -> tuple[str, frozenset[str]] | None:
|
|
1662
|
-
"""Parse `<script> (requires=[cap,...])` from a stage declaration."""
|
|
1663
|
-
if not isinstance(value, str):
|
|
1664
|
-
return None
|
|
1665
|
-
match = _CONFORMANCE_TESTS_RE.fullmatch(value.strip())
|
|
1666
|
-
if match is None:
|
|
1667
|
-
return None
|
|
1668
|
-
script = _normalize_conformance_script(match.group("script"))
|
|
1669
|
-
capabilities = [part.strip() for part in match.group("requires").split(",")]
|
|
1670
|
-
if (
|
|
1671
|
-
not script
|
|
1672
|
-
or any(not capability for capability in capabilities)
|
|
1673
|
-
or len(set(capabilities)) != len(capabilities)
|
|
1674
|
-
or any(capability not in CAPABILITY_WHITELIST for capability in capabilities)
|
|
1675
|
-
):
|
|
1676
|
-
return None
|
|
1677
|
-
return script, frozenset(capabilities)
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
1687
|
def _approved_plan_conformance_manifest(
|
|
1681
1688
|
approved_plan_path: Path,
|
|
1682
1689
|
task_root: Path,
|
|
@@ -2321,7 +2328,7 @@ def _validate_selfmock(report_path: Path, failures: list[str]) -> None:
|
|
|
2321
2328
|
read the flat `self-mock.json`.
|
|
2322
2329
|
|
|
2323
2330
|
Only the implementation template renders the §5.7.3 diff summary
|
|
2324
|
-
(`templates/reports/final-report.template.md:589`); a final-verification report
|
|
2331
|
+
(`templates/reports/final-report-v2.template.md:589`); a final-verification report
|
|
2325
2332
|
quotes the diff as a blockquote instead, so this gate is vacuous there by
|
|
2326
2333
|
design — self-mock is enforced at the implementation stage, and
|
|
2327
2334
|
final-verification is a read-only re-verify that adds no test files.
|
|
@@ -2683,65 +2690,6 @@ def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
|
|
|
2683
2690
|
)
|
|
2684
2691
|
|
|
2685
2692
|
|
|
2686
|
-
PLANNING_REQUIRED_SECTIONS = (
|
|
2687
|
-
"Option Candidates",
|
|
2688
|
-
"Trade-off",
|
|
2689
|
-
"Recommended Option",
|
|
2690
|
-
"Stage Map",
|
|
2691
|
-
"Stepwise Execution Order",
|
|
2692
|
-
"Dependency",
|
|
2693
|
-
"Validation Checklist",
|
|
2694
|
-
"Rollback",
|
|
2695
|
-
"Requirement Coverage",
|
|
2696
|
-
"Plan Body Verification",
|
|
2697
|
-
"Cross-Project Dependencies",
|
|
2698
|
-
"Decision Drafts",
|
|
2699
|
-
)
|
|
2700
|
-
|
|
2701
|
-
# §5.7 implementation deliverables — substring scan against report body.
|
|
2702
|
-
IMPLEMENTATION_REQUIRED_SECTIONS = (
|
|
2703
|
-
"Approved Plan Reference",
|
|
2704
|
-
"Commit List",
|
|
2705
|
-
"Diff Summary",
|
|
2706
|
-
"Out-of-plan Edits",
|
|
2707
|
-
"Stage Sidecar Evidence",
|
|
2708
|
-
"Validation Evidence",
|
|
2709
|
-
"Verifier Results",
|
|
2710
|
-
"Rollback Verification",
|
|
2711
|
-
"Manual User Test (Draft)",
|
|
2712
|
-
"Routing Recommendation",
|
|
2713
|
-
)
|
|
2714
|
-
|
|
2715
|
-
# §5.8 final-verification deliverables — substring scan against report body.
|
|
2716
|
-
FINAL_VERIFICATION_REQUIRED_SECTIONS = (
|
|
2717
|
-
"Source Implementation Report",
|
|
2718
|
-
"Acceptance Blockers",
|
|
2719
|
-
"Residual Risk",
|
|
2720
|
-
"Validation Evidence",
|
|
2721
|
-
"Read-only Command Log",
|
|
2722
|
-
"Conditional Acceptance Conditions",
|
|
2723
|
-
"Manual User Test Results",
|
|
2724
|
-
"Routing Recommendation",
|
|
2725
|
-
"Could-Not-Verify Roll-up",
|
|
2726
|
-
)
|
|
2727
|
-
|
|
2728
|
-
# Allowed Verdict Token vocabulary, by task-type. `release-handoff` is
|
|
2729
|
-
# author-tagged but reads its entry gate from final-verification's report
|
|
2730
|
-
# and renders `not-applicable` itself.
|
|
2731
|
-
FINAL_VERIFICATION_VERDICT_TOKENS = (
|
|
2732
|
-
"accepted",
|
|
2733
|
-
"conditional-accept",
|
|
2734
|
-
"blocked",
|
|
2735
|
-
)
|
|
2736
|
-
|
|
2737
|
-
# `## 7. Final Verdict` Verdict Token cell — captures the value between
|
|
2738
|
-
# backticks on the `Verdict Token` row. Tolerant to extra column whitespace
|
|
2739
|
-
# and to leading bold/italic markers in the label cell.
|
|
2740
|
-
_FINAL_VERDICT_TOKEN_RE = re.compile(
|
|
2741
|
-
r"^\|[ \t]*\*{0,2}Verdict Token\*{0,2}[ \t]*\|[ \t]*`(?P<value>[^`\n]*)`",
|
|
2742
|
-
re.MULTILINE,
|
|
2743
|
-
)
|
|
2744
|
-
|
|
2745
2693
|
# Verdict Card Verdict Token row (top-of-report at-a-glance). Same shape
|
|
2746
2694
|
# as `_FINAL_VERDICT_TOKEN_RE` but matched against the first occurrence in
|
|
2747
2695
|
# the Verdict Card block, scoped to the body between `## Verdict Card`
|
|
@@ -2752,23 +2700,6 @@ _VERDICT_CARD_BLOCK_RE = re.compile(
|
|
|
2752
2700
|
re.DOTALL | re.MULTILINE,
|
|
2753
2701
|
)
|
|
2754
2702
|
|
|
2755
|
-
# `## 7. Final Verdict` block scope — used to scope the Verdict Token
|
|
2756
|
-
# regex so that we don't accidentally match a Verdict Token row that
|
|
2757
|
-
# lives in the Verdict Card or anywhere else.
|
|
2758
|
-
_FINAL_VERDICT_BLOCK_RE = re.compile(
|
|
2759
|
-
r"^##[ \t]+7\.[ \t]+Final Verdict" + _HEADING_TAIL
|
|
2760
|
-
+ r"\n(?P<body>.*?)(?=^##[ \t]|\Z)",
|
|
2761
|
-
re.DOTALL | re.MULTILINE,
|
|
2762
|
-
)
|
|
2763
|
-
|
|
2764
|
-
# `## 5.6 Release Handoff Deliverables` and `## 5.6.6 Merge Conflict
|
|
2765
|
-
# Probe` are required when task-type == release-handoff. The probe sub-
|
|
2766
|
-
# section was retro-added to the template; old runs that predate it ship
|
|
2767
|
-
# without it, but new runs must include it.
|
|
2768
|
-
_MERGE_CONFLICT_PROBE_HEADING_RE = re.compile(
|
|
2769
|
-
r"^###[ \t]+5\.6\.6[ \t]+Merge Conflict Probe\b", re.MULTILINE
|
|
2770
|
-
)
|
|
2771
|
-
|
|
2772
2703
|
PLAN_VERIFY_GATE_VALUES = (
|
|
2773
2704
|
"passed",
|
|
2774
2705
|
"passed-with-dissent",
|
|
@@ -2776,207 +2707,10 @@ PLAN_VERIFY_GATE_VALUES = (
|
|
|
2776
2707
|
"aborted-non-result",
|
|
2777
2708
|
)
|
|
2778
2709
|
|
|
2779
|
-
# `Gate result:` line in §5.5.9 of the final report — captures the value
|
|
2780
|
-
# token that follows. Tolerates arbitrary markdown formatting between the
|
|
2781
|
-
# label and the value (backticks for inline code, double-asterisks for
|
|
2782
|
-
# bold, colons, hyphens, whitespace). The captured value is then
|
|
2783
|
-
# validated against `PLAN_VERIFY_GATE_VALUES` below so typo'd or unknown
|
|
2784
|
-
# values surface as their own failure rather than silently no-matching.
|
|
2785
|
-
_GATE_RESULT_RE = re.compile(
|
|
2786
|
-
r"Gate result[^A-Za-z\n]+(?P<value>[a-z][a-z\-]+)",
|
|
2787
|
-
re.IGNORECASE,
|
|
2788
|
-
)
|
|
2789
|
-
|
|
2790
|
-
# §5.5.9 Plan Body Verification section body — scopes the gate-result search
|
|
2791
|
-
# so an earlier prose mention of "Gate result" elsewhere in the report cannot
|
|
2792
|
-
# hijack the authoritative value. Matched from the `Plan Body Verification`
|
|
2793
|
-
# heading to the next heading (or end-of-file).
|
|
2794
|
-
_PLAN_BODY_VERIFICATION_BLOCK_RE = re.compile(
|
|
2795
|
-
r"^#{2,}[ \t][^\n]*\bPlan Body Verification\b[^\n]*\n"
|
|
2796
|
-
r"(?P<body>.*?)(?=^#{2,}[ \t]|\Z)",
|
|
2797
|
-
re.DOTALL | re.MULTILINE,
|
|
2798
|
-
)
|
|
2799
|
-
|
|
2800
|
-
# Frontmatter approval flag — `approved: true|false` line inside the
|
|
2801
|
-
# leading `---` YAML block. Mirrors `APPROVED_FRONTMATTER_PATTERN` in
|
|
2802
|
-
# scripts/okstra_ctl/run.py.
|
|
2803
|
-
_APPROVED_FRONTMATTER_RE = re.compile(
|
|
2804
|
-
r"^approved:[ \t]+(true|false)[ \t]*$",
|
|
2805
|
-
re.IGNORECASE | re.MULTILINE,
|
|
2806
|
-
)
|
|
2807
2710
|
# Tolerate a leading UTF-8 BOM and/or blank lines before the opening `---`
|
|
2808
2711
|
# so a hand-edited or differently-rendered report does not silently bypass
|
|
2809
2712
|
# the approved-frontmatter gate (the `.match` would otherwise return None).
|
|
2810
2713
|
_FRONTMATTER_BLOCK_RE = re.compile(r"\A\ufeff?\s*---\n(.*?)\n---\n", re.DOTALL)
|
|
2811
|
-
_REQUIREMENT_COVERAGE_HEADING_RE = re.compile(
|
|
2812
|
-
r"^###[ \t]+(?:5\.5\.8[ \t]+)?Requirement Coverage\b",
|
|
2813
|
-
re.IGNORECASE | re.MULTILINE,
|
|
2814
|
-
)
|
|
2815
|
-
_NEXT_THIRD_LEVEL_HEADING_RE = re.compile(r"^###[ \t]+", re.MULTILINE)
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
def _extract_final_verdict_token(content: str) -> str | None:
|
|
2819
|
-
"""Return the `Verdict Token` cell value from the `## 7. Final Verdict`
|
|
2820
|
-
block, or None when the row is absent. Scoped to §7 so the Verdict
|
|
2821
|
-
Card row (which has the same shape) does not shadow the authoritative
|
|
2822
|
-
value.
|
|
2823
|
-
"""
|
|
2824
|
-
block = _FINAL_VERDICT_BLOCK_RE.search(content)
|
|
2825
|
-
if block is None:
|
|
2826
|
-
return None
|
|
2827
|
-
match = _FINAL_VERDICT_TOKEN_RE.search(block.group("body"))
|
|
2828
|
-
if match is None:
|
|
2829
|
-
return None
|
|
2830
|
-
return match.group("value")
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
# 렌더러는 ID 정의 셀(`<a id="r-001"></a>R-001`)에도 스크롤 앵커를 넣는다.
|
|
2834
|
-
# 셀 정규화 때 그 빈 앵커를 벗겨야 ID 컬럼이 bare 토큰으로 읽힌다
|
|
2835
|
-
# (clarification_items._CELL_ANCHOR_RE 와 동형).
|
|
2836
|
-
_CELL_ANCHOR_RE = re.compile(r'<a id="[^"]*"></a>')
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
def _split_markdown_row(line: str) -> list[str]:
|
|
2840
|
-
return [
|
|
2841
|
-
_CELL_ANCHOR_RE.sub("", cell).strip().strip("`").strip()
|
|
2842
|
-
for cell in _split_pipe_row(line)
|
|
2843
|
-
]
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
def _append_requirement_coverage_failures(
|
|
2847
|
-
content: str,
|
|
2848
|
-
gate_value: str,
|
|
2849
|
-
failures: list[str],
|
|
2850
|
-
) -> None:
|
|
2851
|
-
"""Validate implementation-planning §5.5.8 requirement coverage.
|
|
2852
|
-
|
|
2853
|
-
The table is intentionally lightweight: it cannot prove semantic truth, but
|
|
2854
|
-
it makes requirement-to-plan mapping explicit and blocks publishable plans
|
|
2855
|
-
that admit uncovered requirements.
|
|
2856
|
-
"""
|
|
2857
|
-
heading = _REQUIREMENT_COVERAGE_HEADING_RE.search(content)
|
|
2858
|
-
if heading is None:
|
|
2859
|
-
failures.append(
|
|
2860
|
-
"implementation-planning report is missing `Requirement Coverage` "
|
|
2861
|
-
"section — every task-brief requirement must map to option/stage/step."
|
|
2862
|
-
)
|
|
2863
|
-
return
|
|
2864
|
-
|
|
2865
|
-
rest = content[heading.end():]
|
|
2866
|
-
next_heading = _NEXT_THIRD_LEVEL_HEADING_RE.search(rest)
|
|
2867
|
-
section = rest[: next_heading.start()] if next_heading else rest
|
|
2868
|
-
lines = section.splitlines()
|
|
2869
|
-
|
|
2870
|
-
header_idx = -1
|
|
2871
|
-
headers: list[str] = []
|
|
2872
|
-
for idx, line in enumerate(lines):
|
|
2873
|
-
if not line.lstrip().startswith("|"):
|
|
2874
|
-
continue
|
|
2875
|
-
cells = [c.lower() for c in _split_markdown_row(line)]
|
|
2876
|
-
if "id" in cells and "requirement" in cells and "status" in cells:
|
|
2877
|
-
header_idx = idx
|
|
2878
|
-
headers = cells
|
|
2879
|
-
break
|
|
2880
|
-
if header_idx < 0:
|
|
2881
|
-
failures.append(
|
|
2882
|
-
"implementation-planning Requirement Coverage section has no table "
|
|
2883
|
-
"with `ID`, `Requirement`, and `Status` columns."
|
|
2884
|
-
)
|
|
2885
|
-
return
|
|
2886
|
-
|
|
2887
|
-
id_col = headers.index("id")
|
|
2888
|
-
status_col = headers.index("status")
|
|
2889
|
-
rows: list[tuple[str, str]] = []
|
|
2890
|
-
body_started = False
|
|
2891
|
-
for line in lines[header_idx + 1:]:
|
|
2892
|
-
if not line.lstrip().startswith("|"):
|
|
2893
|
-
if body_started:
|
|
2894
|
-
break
|
|
2895
|
-
continue
|
|
2896
|
-
if _is_markdown_separator(line):
|
|
2897
|
-
body_started = True
|
|
2898
|
-
continue
|
|
2899
|
-
if not body_started:
|
|
2900
|
-
continue
|
|
2901
|
-
cells = _split_markdown_row(line)
|
|
2902
|
-
if max(id_col, status_col) >= len(cells):
|
|
2903
|
-
failures.append(
|
|
2904
|
-
"implementation-planning Requirement Coverage table has a "
|
|
2905
|
-
f"malformed row: `{line.strip()}`"
|
|
2906
|
-
)
|
|
2907
|
-
continue
|
|
2908
|
-
rows.append((cells[id_col], cells[status_col].lower()))
|
|
2909
|
-
|
|
2910
|
-
if not rows:
|
|
2911
|
-
failures.append(
|
|
2912
|
-
"implementation-planning Requirement Coverage table has no data rows."
|
|
2913
|
-
)
|
|
2914
|
-
return
|
|
2915
|
-
|
|
2916
|
-
for row_id, status in rows:
|
|
2917
|
-
# `status` is already lower-cased at parse time, so the blocker form
|
|
2918
|
-
# arrives as `blocked c-001`; match lower-case here even though the
|
|
2919
|
-
# canonical authored form (and the remedy message) is `blocked C-NNN`.
|
|
2920
|
-
if not re.fullmatch(
|
|
2921
|
-
r"covered|gap|blocked c-\d{3,}|"
|
|
2922
|
-
r"documented-deviation — refs: (?:c-\d{3,}|d-\d{4,})"
|
|
2923
|
-
r"(?:, (?:c-\d{3,}|d-\d{4,}))*; approval: "
|
|
2924
|
-
r"(?:accepted|blocked c-\d{3,})",
|
|
2925
|
-
status,
|
|
2926
|
-
):
|
|
2927
|
-
failures.append(
|
|
2928
|
-
"implementation-planning Requirement Coverage row "
|
|
2929
|
-
f"`{row_id}` has invalid Status `{status}`; expected "
|
|
2930
|
-
"`covered`, `gap`, `blocked C-NNN`, or a rendered "
|
|
2931
|
-
"`documented-deviation` disposition."
|
|
2932
|
-
)
|
|
2933
|
-
|
|
2934
|
-
if gate_value in ("passed", "passed-with-dissent"):
|
|
2935
|
-
uncovered = [
|
|
2936
|
-
f"{row_id} ({status})"
|
|
2937
|
-
for row_id, status in rows
|
|
2938
|
-
if status != "covered"
|
|
2939
|
-
and not status.endswith("; approval: accepted")
|
|
2940
|
-
]
|
|
2941
|
-
if uncovered:
|
|
2942
|
-
failures.append(
|
|
2943
|
-
"implementation-planning Gate result is publishable but "
|
|
2944
|
-
"Requirement Coverage has uncovered row(s): "
|
|
2945
|
-
+ ", ".join(uncovered)
|
|
2946
|
-
)
|
|
2947
|
-
def _extract_verdict_card_token(content: str) -> str | None:
|
|
2948
|
-
"""Return the `Verdict Token` cell from the Verdict Card block."""
|
|
2949
|
-
block = _VERDICT_CARD_BLOCK_RE.search(content)
|
|
2950
|
-
if block is None:
|
|
2951
|
-
return None
|
|
2952
|
-
match = _FINAL_VERDICT_TOKEN_RE.search(block.group("body"))
|
|
2953
|
-
if match is None:
|
|
2954
|
-
return None
|
|
2955
|
-
return match.group("value")
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
def _validate_verdict_card_consistency(content: str, failures: list[str]) -> None:
|
|
2959
|
-
"""Verdict Card is a non-authoritative index of §7. If both blocks
|
|
2960
|
-
carry a Verdict Token row, the values MUST byte-match (modulo case
|
|
2961
|
-
and surrounding whitespace) — divergence is a contract violation per
|
|
2962
|
-
`report-writer` SKILL.md "Authoring Contract".
|
|
2963
|
-
"""
|
|
2964
|
-
card_value = _extract_verdict_card_token(content)
|
|
2965
|
-
final_value = _extract_final_verdict_token(content)
|
|
2966
|
-
if card_value is None or final_value is None:
|
|
2967
|
-
# Missing-Card and missing-§7 are surfaced by other checks; this
|
|
2968
|
-
# function only enforces the consistency contract between the two.
|
|
2969
|
-
return
|
|
2970
|
-
if card_value.strip().lower() != final_value.strip().lower():
|
|
2971
|
-
failures.append(
|
|
2972
|
-
"Verdict Card `Verdict Token` value "
|
|
2973
|
-
f"`{card_value}` does not match `## 7. Final Verdict` value "
|
|
2974
|
-
f"`{final_value}` — the Card is a non-authoritative index and "
|
|
2975
|
-
"MUST byte-match §7. Either fix the Card or update §7; do not "
|
|
2976
|
-
"ship divergent values."
|
|
2977
|
-
)
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
2714
|
def _validate_verdict_card_fields(data: dict, failures: list[str]) -> None:
|
|
2981
2715
|
"""`verdictCard.direction` must byte-match its authoritative home in §7.
|
|
2982
2716
|
|
|
@@ -3142,10 +2876,15 @@ def _validate_error_analysis_consistency(
|
|
|
3142
2876
|
target = routing.get("nextTaskType")
|
|
3143
2877
|
leading_cause_id = routing.get("leadingCauseId")
|
|
3144
2878
|
candidate_id_set = set(candidate_ids)
|
|
3145
|
-
if target
|
|
2879
|
+
if isinstance(target, str) and target not in ERROR_ANALYSIS_ROUTING_DIRECTIONS:
|
|
2880
|
+
failures.append(
|
|
2881
|
+
"final-report data.json: errorAnalysis.routing has unsupported "
|
|
2882
|
+
f"routing target `{target}`."
|
|
2883
|
+
)
|
|
2884
|
+
if target == "implementation-option-selection":
|
|
3146
2885
|
if not candidates:
|
|
3147
2886
|
failures.append(
|
|
3148
|
-
"final-report data.json: implementation-
|
|
2887
|
+
"final-report data.json: implementation-option-selection routing requires "
|
|
3149
2888
|
"at least one cause candidate."
|
|
3150
2889
|
)
|
|
3151
2890
|
if (
|
|
@@ -3153,7 +2892,7 @@ def _validate_error_analysis_consistency(
|
|
|
3153
2892
|
or leading_cause_id not in candidate_id_set
|
|
3154
2893
|
):
|
|
3155
2894
|
failures.append(
|
|
3156
|
-
"final-report data.json: implementation-
|
|
2895
|
+
"final-report data.json: implementation-option-selection routing "
|
|
3157
2896
|
"leadingCauseId must reference a cause candidate."
|
|
3158
2897
|
)
|
|
3159
2898
|
elif target == "error-analysis" and (
|
|
@@ -3165,10 +2904,7 @@ def _validate_error_analysis_consistency(
|
|
|
3165
2904
|
"empty or reference a cause candidate."
|
|
3166
2905
|
)
|
|
3167
2906
|
|
|
3168
|
-
expected_direction =
|
|
3169
|
-
"implementation-planning": "begin-planning",
|
|
3170
|
-
"error-analysis": "continue-investigation",
|
|
3171
|
-
}.get(target)
|
|
2907
|
+
expected_direction = ERROR_ANALYSIS_ROUTING_DIRECTIONS.get(target)
|
|
3172
2908
|
verdict_card_value = data.get("verdictCard")
|
|
3173
2909
|
verdict_card = (
|
|
3174
2910
|
verdict_card_value if isinstance(verdict_card_value, Mapping) else {}
|
|
@@ -3230,7 +2966,7 @@ def _validate_error_analysis_consistency(
|
|
|
3230
2966
|
|
|
3231
2967
|
if isinstance(target, str) and target in {
|
|
3232
2968
|
"error-analysis",
|
|
3233
|
-
"implementation-
|
|
2969
|
+
"implementation-option-selection",
|
|
3234
2970
|
}:
|
|
3235
2971
|
for field_name, value in (
|
|
3236
2972
|
("verdictCard.nextStep", verdict_card.get("nextStep")),
|
|
@@ -3301,9 +3037,8 @@ def validate_final_report_data(
|
|
|
3301
3037
|
The data.json is the source-of-truth that the renderer reads to
|
|
3302
3038
|
produce the markdown. If schema validation passes here, the rendered
|
|
3303
3039
|
markdown is guaranteed to contain every section / row the contract
|
|
3304
|
-
requires (the template loops over the data)
|
|
3305
|
-
|
|
3306
|
-
net but are expected to be redundant.
|
|
3040
|
+
requires (the template loops over the data), so the schema is the
|
|
3041
|
+
only place the deliverable contract is enforced.
|
|
3307
3042
|
|
|
3308
3043
|
Missing data.json is reported as a single failure rather than a
|
|
3309
3044
|
cascade of substring failures — that points the writer at the right
|
|
@@ -3373,7 +3108,25 @@ def validate_final_report_data(
|
|
|
3373
3108
|
|
|
3374
3109
|
task_type = (data.get("header") or {}).get("taskType")
|
|
3375
3110
|
_validate_verifier_fail_blocks_verdict(data, failures)
|
|
3376
|
-
if task_type == "implementation":
|
|
3111
|
+
if task_type == "implementation-option-selection":
|
|
3112
|
+
selection = data.get("implementationOptionSelection") or {}
|
|
3113
|
+
validation_root = project_root or report_path.parent
|
|
3114
|
+
original_ids = brief_end_state_id_sequence(
|
|
3115
|
+
_brief_path_from_manifest(manifest, validation_root)
|
|
3116
|
+
)
|
|
3117
|
+
roster = manifest.get("recommendedWorkers") or ()
|
|
3118
|
+
participating_analysers = tuple(
|
|
3119
|
+
worker for worker in roster if worker != "report-writer"
|
|
3120
|
+
)
|
|
3121
|
+
failures.extend(
|
|
3122
|
+
f"implementation-option-selection: {error}"
|
|
3123
|
+
for error in validate_implementation_option_selection(
|
|
3124
|
+
selection,
|
|
3125
|
+
original_ids,
|
|
3126
|
+
participating_analysers,
|
|
3127
|
+
)
|
|
3128
|
+
)
|
|
3129
|
+
elif task_type == "implementation":
|
|
3377
3130
|
_validate_stage_carry_sidecar_exists(data, report_path, failures)
|
|
3378
3131
|
if task_type == "error-analysis":
|
|
3379
3132
|
_validate_error_analysis_consistency(data, failures)
|
|
@@ -3382,6 +3135,31 @@ def validate_final_report_data(
|
|
|
3382
3135
|
_validate_verified_row_recorded(data, report_path, failures)
|
|
3383
3136
|
elif task_type == "implementation-planning":
|
|
3384
3137
|
active_report_contracts = report_contracts or set()
|
|
3138
|
+
planning = data.get("implementationPlanning") or {}
|
|
3139
|
+
selected_direction_contract = (
|
|
3140
|
+
isinstance(planning, Mapping)
|
|
3141
|
+
and planning.get("planningContract") == "selected-direction"
|
|
3142
|
+
)
|
|
3143
|
+
if selected_direction_contract:
|
|
3144
|
+
validation_root = project_root or report_path.parent
|
|
3145
|
+
try:
|
|
3146
|
+
brief_path = _brief_path_from_manifest(manifest, validation_root)
|
|
3147
|
+
except (OSError, ValueError) as exc:
|
|
3148
|
+
failures.append(
|
|
3149
|
+
"implementation-planning selected-direction: run manifest "
|
|
3150
|
+
f"taskBriefPath is malformed: {exc}"
|
|
3151
|
+
)
|
|
3152
|
+
brief_path = validation_root / "__invalid-brief__"
|
|
3153
|
+
task_root = _task_root_from_run_dir(report_path.parent.parent)
|
|
3154
|
+
snapshot_path = task_root / "instruction-set" / "selected-direction.json"
|
|
3155
|
+
failures.extend(
|
|
3156
|
+
f"implementation-planning selected-direction: {error}"
|
|
3157
|
+
for error in validate_selected_direction_plan(
|
|
3158
|
+
data, brief_path, snapshot_path
|
|
3159
|
+
)
|
|
3160
|
+
)
|
|
3161
|
+
if planning.get("outcome") == "direction-invalidated":
|
|
3162
|
+
return data
|
|
3385
3163
|
_validate_implementation_planning_cross_project(data, failures)
|
|
3386
3164
|
_validate_implementation_planning_decision_drafts(data, failures)
|
|
3387
3165
|
for warning in validate_plan_body_section(data, report_path, failures):
|
|
@@ -3401,8 +3179,9 @@ def validate_final_report_data(
|
|
|
3401
3179
|
resolve_architecture(_project_root_from_report(report_path)),
|
|
3402
3180
|
failures,
|
|
3403
3181
|
)
|
|
3404
|
-
|
|
3405
|
-
|
|
3182
|
+
if not selected_direction_contract:
|
|
3183
|
+
_validate_requirement_deviations(data, failures)
|
|
3184
|
+
_validate_requirement_coverage_covered_by(data, failures)
|
|
3406
3185
|
warnings = _validate_design_prep_contract(
|
|
3407
3186
|
data,
|
|
3408
3187
|
report_path,
|
|
@@ -7314,7 +7093,7 @@ def _validate_unverified_critic_gaps_recorded(data: dict, failures: list[str]) -
|
|
|
7314
7093
|
|
|
7315
7094
|
|
|
7316
7095
|
# Allowed `fixability` values, mirroring the schema enum
|
|
7317
|
-
# (schemas/final-report-
|
|
7096
|
+
# (schemas/final-report-v2.0.schema.json planItems[].verdicts[].fixability).
|
|
7318
7097
|
_FIXABILITY_VALUES = frozenset({"planner-fixable", "needs-user-input"})
|
|
7319
7098
|
|
|
7320
7099
|
|
|
@@ -7803,6 +7582,13 @@ def _validate_stage_has_requirement(data: dict, failures: list[str]) -> None:
|
|
|
7803
7582
|
)
|
|
7804
7583
|
|
|
7805
7584
|
|
|
7585
|
+
_FINAL_VERIFICATION_ROUTING_TOKEN_RE = re.compile(
|
|
7586
|
+
r"(?<![A-Za-z-])(?:release-handoff\(stage-group\)|release-handoff|done|"
|
|
7587
|
+
r"implementation|error-analysis|implementation-option-selection|"
|
|
7588
|
+
r"implementation-planning)(?![A-Za-z-])"
|
|
7589
|
+
)
|
|
7590
|
+
|
|
7591
|
+
|
|
7806
7592
|
def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
|
|
7807
7593
|
"""Enforce verdict ↔ blocker/condition/routing consistency on the
|
|
7808
7594
|
final-verification data.json (SSOT). The schema guarantees field SHAPE;
|
|
@@ -7817,7 +7603,16 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
|
|
|
7817
7603
|
fv = data.get("finalVerification") or {}
|
|
7818
7604
|
blockers = fv.get("acceptanceBlockers") or []
|
|
7819
7605
|
conditions = verdict.get("conditionalAcceptanceConditions") or []
|
|
7820
|
-
|
|
7606
|
+
routing_value = fv.get("routingRecommendation")
|
|
7607
|
+
routing = routing_value if isinstance(routing_value, str) else ""
|
|
7608
|
+
routing_tokens = _FINAL_VERIFICATION_ROUTING_TOKEN_RE.findall(routing)
|
|
7609
|
+
routing_token = routing_tokens[0] if len(routing_tokens) == 1 else None
|
|
7610
|
+
|
|
7611
|
+
if routing_token is None:
|
|
7612
|
+
failures.append(
|
|
7613
|
+
"final-verification: routingRecommendation must contain exactly one "
|
|
7614
|
+
"supported routing token."
|
|
7615
|
+
)
|
|
7821
7616
|
|
|
7822
7617
|
if token == "accepted" and blockers:
|
|
7823
7618
|
failures.append(
|
|
@@ -7834,7 +7629,10 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
|
|
|
7834
7629
|
"final-verification: verdict `conditional-accept` but "
|
|
7835
7630
|
"conditionalAcceptanceConditions is empty — list every condition."
|
|
7836
7631
|
)
|
|
7837
|
-
if
|
|
7632
|
+
if (
|
|
7633
|
+
routing_token in {"release-handoff", "release-handoff(stage-group)"}
|
|
7634
|
+
and token != "accepted"
|
|
7635
|
+
):
|
|
7838
7636
|
failures.append(
|
|
7839
7637
|
f"final-verification: routingRecommendation cites `release-handoff` "
|
|
7840
7638
|
f"but verdict is `{token}` — release-handoff routing is allowed only "
|
|
@@ -7847,8 +7645,7 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
|
|
|
7847
7645
|
f"final-verification: verificationScope must be `whole-task` or "
|
|
7848
7646
|
f"`single-stage`, got {scope!r}."
|
|
7849
7647
|
)
|
|
7850
|
-
if
|
|
7851
|
-
and "release-handoff(stage-group)" not in routing):
|
|
7648
|
+
if scope == "single-stage" and routing_token == "release-handoff":
|
|
7852
7649
|
failures.append(
|
|
7853
7650
|
"final-verification: verificationScope `single-stage` cannot recommend "
|
|
7854
7651
|
"plain release-handoff routing — a single-stage accepted verdict may "
|
|
@@ -7917,14 +7714,13 @@ def _load_stage_validator():
|
|
|
7917
7714
|
def _append_stage_data_failures(data: Mapping[str, Any], failures: list[str]) -> None:
|
|
7918
7715
|
"""Run the stage relationship checks that schema v2 cannot express.
|
|
7919
7716
|
|
|
7920
|
-
|
|
7921
|
-
the
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
owns both modes so the rule vocabulary stays defined once.
|
|
7717
|
+
The depends-on DAG, parallel-stage file safety, RED→GREEN ordering, and
|
|
7718
|
+
the TDD-exemption vocabulary are relationships between stages, which a
|
|
7719
|
+
JSON Schema cannot state. They are enforced here, against the data.json,
|
|
7720
|
+
by the same validator that owns the rule vocabulary.
|
|
7925
7721
|
"""
|
|
7926
|
-
if (data or {}).get("schemaVersion") !=
|
|
7927
|
-
return #
|
|
7722
|
+
if (data or {}).get("schemaVersion") != CURRENT_REPORT_SCHEMA_VERSION:
|
|
7723
|
+
return # Schema validation already rejected an unknown version.
|
|
7928
7724
|
planning = (data or {}).get("implementationPlanning")
|
|
7929
7725
|
if not isinstance(planning, Mapping):
|
|
7930
7726
|
return # Schema validation already reported the missing block.
|
|
@@ -7939,157 +7735,6 @@ def _append_stage_data_failures(data: Mapping[str, Any], failures: list[str]) ->
|
|
|
7939
7735
|
)
|
|
7940
7736
|
|
|
7941
7737
|
|
|
7942
|
-
def _append_stage_structure_failures(content: str, failures: list[str]) -> None:
|
|
7943
|
-
"""Enforce the Stage Map structural contract at the implementation-planning
|
|
7944
|
-
boundary. Without this, a plan missing `## 5.5 Stage Map` passes the
|
|
7945
|
-
planning gate, gets approved, and only fails later at the `implementation`
|
|
7946
|
-
entry (validators/validate-implementation-plan-stages.py via
|
|
7947
|
-
prepare_task_bundle). Running the same validator here moves the failure to
|
|
7948
|
-
produce-time."""
|
|
7949
|
-
mod = _load_stage_validator()
|
|
7950
|
-
if mod is None: # pragma: no cover — repo/runtime always ship the file
|
|
7951
|
-
failures.append(f"cannot load Stage Map validator at {_STAGE_VALIDATOR_PATH}")
|
|
7952
|
-
return
|
|
7953
|
-
for e in mod.collect_validation_errors(content):
|
|
7954
|
-
failures.append(
|
|
7955
|
-
f"implementation-planning Stage Map structure invalid "
|
|
7956
|
-
f"[{e.code} stage={e.stage}]: {e.message}"
|
|
7957
|
-
)
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
def validate_phase_boundary(
|
|
7961
|
-
task_type: str,
|
|
7962
|
-
report_path: Path,
|
|
7963
|
-
failures: list[str],
|
|
7964
|
-
*,
|
|
7965
|
-
report_data: Mapping[str, Any] | None = None,
|
|
7966
|
-
) -> None:
|
|
7967
|
-
"""Phase-specific contract checks.
|
|
7968
|
-
|
|
7969
|
-
For `implementation-planning` runs, the final report must contain the
|
|
7970
|
-
required deliverable sections; absence indicates a planning run that
|
|
7971
|
-
skipped its core outputs (or an implementation run that ran under the
|
|
7972
|
-
wrong task type).
|
|
7973
|
-
|
|
7974
|
-
Additionally enforces the Plan Body Verification gate (§5.5.9):
|
|
7975
|
-
- gate ∈ {passed, passed-with-dissent} → top-level Approval checkbox
|
|
7976
|
-
MUST be present.
|
|
7977
|
-
- gate ∈ {blocked-by-disagreement, aborted-non-result} → checkbox
|
|
7978
|
-
MUST be absent (lead converted findings into Clarification rows
|
|
7979
|
-
instead of opening the gate).
|
|
7980
|
-
"""
|
|
7981
|
-
if (report_data or {}).get("schemaVersion") == "2.0":
|
|
7982
|
-
return
|
|
7983
|
-
if not report_path.exists():
|
|
7984
|
-
return
|
|
7985
|
-
content = report_path.read_text()
|
|
7986
|
-
|
|
7987
|
-
# Verdict Card vs §7. Final Verdict Verdict Token consistency. The Card
|
|
7988
|
-
# is a non-authoritative index; divergence is a contract violation.
|
|
7989
|
-
_validate_verdict_card_consistency(content, failures)
|
|
7990
|
-
|
|
7991
|
-
if task_type == "implementation":
|
|
7992
|
-
for needle in IMPLEMENTATION_REQUIRED_SECTIONS:
|
|
7993
|
-
if needle not in content:
|
|
7994
|
-
failures.append(
|
|
7995
|
-
"implementation report is missing required §5.7 "
|
|
7996
|
-
f"deliverable section: `{needle}`"
|
|
7997
|
-
)
|
|
7998
|
-
|
|
7999
|
-
if task_type == "final-verification":
|
|
8000
|
-
for needle in FINAL_VERIFICATION_REQUIRED_SECTIONS:
|
|
8001
|
-
if needle not in content:
|
|
8002
|
-
failures.append(
|
|
8003
|
-
"final-verification report is missing required §5.8 "
|
|
8004
|
-
f"deliverable section: `{needle}`"
|
|
8005
|
-
)
|
|
8006
|
-
token_value = _extract_final_verdict_token(content)
|
|
8007
|
-
if token_value is None:
|
|
8008
|
-
failures.append(
|
|
8009
|
-
"final-verification report `## 7. Final Verdict` table is "
|
|
8010
|
-
"missing the `Verdict Token` row — required by the release-"
|
|
8011
|
-
"handoff entry gate."
|
|
8012
|
-
)
|
|
8013
|
-
elif token_value.strip().lower() not in FINAL_VERIFICATION_VERDICT_TOKENS:
|
|
8014
|
-
failures.append(
|
|
8015
|
-
"final-verification report `Verdict Token` value "
|
|
8016
|
-
f"`{token_value}` is not one of "
|
|
8017
|
-
f"{', '.join(FINAL_VERIFICATION_VERDICT_TOKENS)}."
|
|
8018
|
-
)
|
|
8019
|
-
|
|
8020
|
-
if task_type == "release-handoff":
|
|
8021
|
-
if _MERGE_CONFLICT_PROBE_HEADING_RE.search(content) is None:
|
|
8022
|
-
failures.append(
|
|
8023
|
-
"release-handoff report is missing `### 5.6.6 Merge Conflict "
|
|
8024
|
-
"Probe` sub-section — required by the release-handoff profile "
|
|
8025
|
-
"(self-review 6, merge-conflict probe audit). When the run is "
|
|
8026
|
-
"`local checkout` / `skip`, record the single line `- Not run "
|
|
8027
|
-
"(user picked local checkout or skip).` under the heading."
|
|
8028
|
-
)
|
|
8029
|
-
|
|
8030
|
-
if task_type != "implementation-planning":
|
|
8031
|
-
return
|
|
8032
|
-
for needle in PLANNING_REQUIRED_SECTIONS:
|
|
8033
|
-
if needle not in content:
|
|
8034
|
-
failures.append(
|
|
8035
|
-
"implementation-planning report is missing required section: "
|
|
8036
|
-
f"`{needle}`"
|
|
8037
|
-
)
|
|
8038
|
-
|
|
8039
|
-
# Scope the gate-result search to the §5.5.9 Plan Body Verification block
|
|
8040
|
-
# so an earlier prose mention of "Gate result" cannot hijack the value.
|
|
8041
|
-
pbv_block = _PLAN_BODY_VERIFICATION_BLOCK_RE.search(content)
|
|
8042
|
-
gate_match = (
|
|
8043
|
-
_GATE_RESULT_RE.search(pbv_block.group("body")) if pbv_block else None
|
|
8044
|
-
)
|
|
8045
|
-
if gate_match is None:
|
|
8046
|
-
# The `Plan Body Verification` heading check above already covers
|
|
8047
|
-
# the wholly-missing case; a heading present without a `Gate result`
|
|
8048
|
-
# line is its own contract violation.
|
|
8049
|
-
if "Plan Body Verification" in content:
|
|
8050
|
-
failures.append(
|
|
8051
|
-
"implementation-planning report has `Plan Body Verification` "
|
|
8052
|
-
"section but no `Gate result:` line — required by §5.5.9."
|
|
8053
|
-
)
|
|
8054
|
-
return
|
|
8055
|
-
gate_value = gate_match.group("value").strip().lower()
|
|
8056
|
-
if gate_value not in PLAN_VERIFY_GATE_VALUES:
|
|
8057
|
-
failures.append(
|
|
8058
|
-
"implementation-planning report `Gate result` value "
|
|
8059
|
-
f"`{gate_value}` is not one of "
|
|
8060
|
-
f"{', '.join(PLAN_VERIFY_GATE_VALUES)}."
|
|
8061
|
-
)
|
|
8062
|
-
return
|
|
8063
|
-
fm_block = _FRONTMATTER_BLOCK_RE.match(content)
|
|
8064
|
-
fm_match = (
|
|
8065
|
-
_APPROVED_FRONTMATTER_RE.search(fm_block.group(1)) if fm_block else None
|
|
8066
|
-
)
|
|
8067
|
-
if gate_value in ("passed", "passed-with-dissent") and fm_match is None:
|
|
8068
|
-
failures.append(
|
|
8069
|
-
"implementation-planning report Gate result is "
|
|
8070
|
-
f"`{gate_value}` but the frontmatter `approved:` field is missing — "
|
|
8071
|
-
"render `approved: false` so the user (or `--approve`) can flip it."
|
|
8072
|
-
)
|
|
8073
|
-
if (
|
|
8074
|
-
gate_value in ("blocked-by-disagreement", "aborted-non-result")
|
|
8075
|
-
and fm_match is not None
|
|
8076
|
-
and fm_match.group(1).lower() == "true"
|
|
8077
|
-
):
|
|
8078
|
-
failures.append(
|
|
8079
|
-
"implementation-planning report Gate result is "
|
|
8080
|
-
f"`{gate_value}` but the frontmatter has `approved: true` — gate "
|
|
8081
|
-
"must NOT publish a pre-approved plan when verification did not pass."
|
|
8082
|
-
)
|
|
8083
|
-
|
|
8084
|
-
_append_requirement_coverage_failures(content, gate_value, failures)
|
|
8085
|
-
|
|
8086
|
-
# Only a publishable plan (gate passed) can be flipped to `approved: true`
|
|
8087
|
-
# and reach the `implementation` entry, so the Stage Map structure is
|
|
8088
|
-
# enforced only here — a blocked/aborted plan may legitimately be incomplete.
|
|
8089
|
-
if gate_value in ("passed", "passed-with-dissent"):
|
|
8090
|
-
_append_stage_structure_failures(content, failures)
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
7738
|
def _brief_path_from_manifest(task_manifest: dict, project_root: Path) -> Path:
|
|
8094
7739
|
"""This run's brief, or a non-existent path when the manifest names none.
|
|
8095
7740
|
|
|
@@ -9092,12 +8737,6 @@ def main() -> int:
|
|
|
9092
8737
|
)
|
|
9093
8738
|
validate_team_state_usage(team_state, failures)
|
|
9094
8739
|
|
|
9095
|
-
validate_phase_boundary(
|
|
9096
|
-
task_type,
|
|
9097
|
-
report_path,
|
|
9098
|
-
failures,
|
|
9099
|
-
report_data=validation_data,
|
|
9100
|
-
)
|
|
9101
8740
|
_validate_phase_boundary_error_log(
|
|
9102
8741
|
report_path.parent.parent,
|
|
9103
8742
|
task_type,
|
|
@@ -9144,16 +8783,28 @@ def main() -> int:
|
|
|
9144
8783
|
for warning in conformance_warnings:
|
|
9145
8784
|
print(f"validate-run: warning: {warning}", file=sys.stderr)
|
|
9146
8785
|
if task_type in _BRIEF_DERIVED_PHASES:
|
|
9147
|
-
|
|
8786
|
+
planning = validation_data.get("implementationPlanning")
|
|
8787
|
+
selected_direction_plan = (
|
|
8788
|
+
task_type == "implementation-planning"
|
|
8789
|
+
and isinstance(planning, Mapping)
|
|
8790
|
+
and planning.get("planningContract") == "selected-direction"
|
|
8791
|
+
)
|
|
8792
|
+
brief_path = (
|
|
8793
|
+
project_root / "__selected-direction-brief-validated-from-run-manifest__"
|
|
8794
|
+
if selected_direction_plan
|
|
8795
|
+
else _brief_path_from_manifest(task_manifest, project_root)
|
|
8796
|
+
)
|
|
9148
8797
|
if task_type in _END_STATE_PHASES:
|
|
9149
8798
|
if task_type == "implementation-planning":
|
|
9150
8799
|
_validate_planning_conformance_declared(report_path, failures)
|
|
9151
|
-
|
|
9152
|
-
|
|
8800
|
+
if not selected_direction_plan:
|
|
8801
|
+
_validate_end_state_coverage(validation_data, brief_path, failures)
|
|
8802
|
+
if task_type == "implementation-planning" and not selected_direction_plan:
|
|
9153
8803
|
_validate_requirement_provenance(
|
|
9154
8804
|
validation_data, brief_path, failures
|
|
9155
8805
|
)
|
|
9156
8806
|
_validate_stage_has_requirement(validation_data, failures)
|
|
8807
|
+
if task_type == "implementation-planning":
|
|
9157
8808
|
_append_stage_data_failures(validation_data, failures)
|
|
9158
8809
|
if task_type == "improvement-discovery":
|
|
9159
8810
|
run_dir = report_path.parent.parent
|