okstra 0.106.1 → 0.107.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/docs/for-ai/skills/okstra-brief.md +6 -3
- package/docs/for-ai/skills/okstra-container-build.md +1 -1
- package/docs/for-ai/skills/okstra-inspect.md +5 -5
- package/docs/for-ai/skills/okstra-manager.md +9 -3
- package/docs/for-ai/skills/okstra-run.md +18 -2
- package/docs/for-ai/skills/okstra-schedule.md +11 -7
- package/docs/for-ai/skills/okstra-setup.md +2 -2
- package/docs/kr/architecture.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +1 -1
- package/runtime/bin/okstra-antigravity-exec.sh +24 -3
- package/runtime/bin/okstra-claude-exec.sh +30 -3
- package/runtime/bin/okstra-codex-exec.sh +27 -4
- package/runtime/prompts/coding-preflight/clean-code.md +2 -0
- package/runtime/prompts/coding-preflight/overview.md +2 -0
- package/runtime/prompts/coding-preflight/scripts/preedit-check.sh +2 -2
- package/runtime/prompts/lead/convergence.md +9 -5
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/lead/report-writer.md +12 -8
- package/runtime/prompts/profiles/implementation-planning.md +6 -6
- package/runtime/schemas/final-report-v1.0.schema.json +27 -9
- package/runtime/skills/okstra-container-build/SKILL.md +1 -1
- package/runtime/templates/reports/final-report.template.md +14 -4
- package/runtime/templates/reports/i18n/en.json +6 -1
- package/runtime/templates/reports/i18n/ko.json +6 -1
- package/runtime/validators/validate-brief.py +37 -0
- package/runtime/validators/validate-implementation-plan-stages.py +55 -12
- package/runtime/validators/validate-run.py +354 -0
|
@@ -861,6 +861,57 @@ def _task_root_from_run_dir(run_dir: Path) -> Path:
|
|
|
861
861
|
return run_dir.parent.parent
|
|
862
862
|
|
|
863
863
|
|
|
864
|
+
def _validate_planning_conformance_declared(report_path: Path, failures: list[str]) -> None:
|
|
865
|
+
"""H4-c — at planning time, every stage that DECLARES `Conformance tests:`
|
|
866
|
+
must already carry a matching entry in the shared task-level
|
|
867
|
+
`qa/conformance-manifest.json`. The profile mandates writing the script +
|
|
868
|
+
manifest entry as part of emitting that line
|
|
869
|
+
(implementation-planning.md:83/85); without this check a declaration that
|
|
870
|
+
was never materialized only surfaces much later at the `implementation`
|
|
871
|
+
entry gate. Matches by stageKey suffix (`-stage-<N>`) like
|
|
872
|
+
`_scope_manifest_entries`; skips stages that took a `Conformance exemption:`.
|
|
873
|
+
"""
|
|
874
|
+
data_path = report_path.with_suffix(".data.json")
|
|
875
|
+
if not data_path.is_file():
|
|
876
|
+
return
|
|
877
|
+
try:
|
|
878
|
+
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
879
|
+
except (OSError, json.JSONDecodeError):
|
|
880
|
+
return
|
|
881
|
+
ip = data.get("implementationPlanning")
|
|
882
|
+
if not isinstance(ip, dict):
|
|
883
|
+
return
|
|
884
|
+
declaring_stages = [
|
|
885
|
+
s.get("stage")
|
|
886
|
+
for s in (ip.get("stages") or [])
|
|
887
|
+
if isinstance(s, dict) and str(s.get("conformanceTests") or "").strip()
|
|
888
|
+
]
|
|
889
|
+
if not declaring_stages:
|
|
890
|
+
return
|
|
891
|
+
task_root = _task_root_from_run_dir(report_path.parent.parent)
|
|
892
|
+
manifest_path = task_root / "qa" / "conformance-manifest.json"
|
|
893
|
+
entries = []
|
|
894
|
+
if manifest_path.is_file():
|
|
895
|
+
try:
|
|
896
|
+
entries = json.loads(manifest_path.read_text()).get("entries") or []
|
|
897
|
+
except (OSError, json.JSONDecodeError):
|
|
898
|
+
entries = []
|
|
899
|
+
for stage_number in declaring_stages:
|
|
900
|
+
suffix = f"-stage-{stage_number}"
|
|
901
|
+
has_entry = any(
|
|
902
|
+
isinstance(e, dict) and str(e.get("stageKey") or "").endswith(suffix)
|
|
903
|
+
for e in entries
|
|
904
|
+
)
|
|
905
|
+
if not has_entry:
|
|
906
|
+
failures.append(
|
|
907
|
+
f"final-report data.json: stage {stage_number} declares "
|
|
908
|
+
"`Conformance tests:` but no matching entry exists in "
|
|
909
|
+
f"{manifest_path} (stageKey ending `{suffix}`). Emitting the "
|
|
910
|
+
"declaration MUST also write the manifest entry + script "
|
|
911
|
+
"(implementation-planning.md §Conformance)."
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
|
|
864
915
|
def _validate_conformance(report_path: Path, failures: list[str],
|
|
865
916
|
surface_patterns: object = None) -> None:
|
|
866
917
|
"""Tier 3 conformance 게이트(implementation / final-verification).
|
|
@@ -1483,6 +1534,11 @@ def validate_final_report_data(report_path: Path, failures: list[str]) -> None:
|
|
|
1483
1534
|
elif task_type == "implementation-planning":
|
|
1484
1535
|
_validate_implementation_planning_cross_project(data, failures)
|
|
1485
1536
|
_validate_implementation_planning_decision_drafts(data, failures)
|
|
1537
|
+
_validate_plan_body_gate_recompute(data, failures)
|
|
1538
|
+
_validate_plan_item_extraction_completeness(data, failures)
|
|
1539
|
+
_validate_plan_item_subject_substance(data, failures)
|
|
1540
|
+
_validate_plan_body_clarification_matching(data, failures)
|
|
1541
|
+
_validate_requirement_coverage_covered_by(data, failures)
|
|
1486
1542
|
|
|
1487
1543
|
|
|
1488
1544
|
# path:line (foo.service.ts:268), report ID (C-001 / F-013 / G-crit-002 / RC-1),
|
|
@@ -1584,6 +1640,302 @@ def _validate_implementation_planning_decision_drafts(data: dict, failures: list
|
|
|
1584
1640
|
)
|
|
1585
1641
|
|
|
1586
1642
|
|
|
1643
|
+
# Plan-body gate outcomes ranked by how favorable each is to approval.
|
|
1644
|
+
# A higher rank claims a healthier verification result. The recompute check
|
|
1645
|
+
# below fails only when the *declared* gate outranks what the recorded
|
|
1646
|
+
# per-worker verdicts support — i.e. the lead claimed a better outcome than
|
|
1647
|
+
# the votes justify. A lead writing a conservatively *worse* gate is allowed,
|
|
1648
|
+
# so genuine edge cases in this recompute never manufacture false failures.
|
|
1649
|
+
_PLAN_GATE_RANK = {
|
|
1650
|
+
"aborted-non-result": 0,
|
|
1651
|
+
"blocked-by-disagreement": 0,
|
|
1652
|
+
"passed-with-dissent": 1,
|
|
1653
|
+
"passed": 2,
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
# Breakage kinds where a single DISAGREE blocks the gate on its own (no majority
|
|
1657
|
+
# needed), because the defect is concrete, safety-critical, and adversarially
|
|
1658
|
+
# verifiable: `a` = cited path/symbol mismatch, `d` = rollback violates
|
|
1659
|
+
# commit/dependency order. `b`/`c`/`e` still need a majority — `b` in particular
|
|
1660
|
+
# is prone to planning-vs-implementation environment false positives.
|
|
1661
|
+
_SINGLE_VOTE_BLOCKING_KINDS = {"a", "d"}
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
def _classify_plan_item_gate(item: dict) -> str:
|
|
1665
|
+
"""Recompute one plan item's gate class from its per-worker verdicts,
|
|
1666
|
+
per `prompts/lead/convergence.md` "Round protocol". Returns one of
|
|
1667
|
+
``majority-disagree`` / ``has-dissent`` / ``full-consensus`` /
|
|
1668
|
+
``all-non-result``. Collapses ``partial-consensus`` and
|
|
1669
|
+
``dissent-isolated`` into ``has-dissent`` because they resolve to the
|
|
1670
|
+
same gate value; only the majority-disagree boundary changes the gate.
|
|
1671
|
+
"""
|
|
1672
|
+
tokens = [
|
|
1673
|
+
(
|
|
1674
|
+
str(v.get("verdict") or "").strip().upper(),
|
|
1675
|
+
str(v.get("breakageKind") or "").strip().lower(),
|
|
1676
|
+
)
|
|
1677
|
+
for v in (item.get("verdicts") or [])
|
|
1678
|
+
if isinstance(v, dict)
|
|
1679
|
+
]
|
|
1680
|
+
non_error = [(vd, bk) for (vd, bk) in tokens if vd and vd != "VERIFICATION-ERROR"]
|
|
1681
|
+
if not non_error:
|
|
1682
|
+
return "all-non-result"
|
|
1683
|
+
disagree = [(vd, bk) for (vd, bk) in non_error if vd == "DISAGREE"]
|
|
1684
|
+
agree = [(vd, bk) for (vd, bk) in non_error if vd in ("AGREE", "SUPPLEMENT")]
|
|
1685
|
+
disagree_kinds = {bk for (_vd, bk) in disagree if bk}
|
|
1686
|
+
if not disagree:
|
|
1687
|
+
return "full-consensus"
|
|
1688
|
+
# Single-vote-blocking kinds: one confirmed DISAGREE on a concrete,
|
|
1689
|
+
# safety-critical, adversarially-verifiable defect is enough to block, even
|
|
1690
|
+
# in a two-worker roster — a lone correct dissent must not be outvoted here.
|
|
1691
|
+
# `a`/`d` for any item; `f` only for P-Req items (requirement coverage).
|
|
1692
|
+
is_req = str(item.get("id") or "").upper().startswith("P-REQ")
|
|
1693
|
+
if disagree_kinds & _SINGLE_VOTE_BLOCKING_KINDS or (is_req and "f" in disagree_kinds):
|
|
1694
|
+
return "majority-disagree"
|
|
1695
|
+
# Otherwise a genuine majority is required — and a majority needs at least
|
|
1696
|
+
# two participating votes, so a lone surviving DISAGREE (its peer returned a
|
|
1697
|
+
# non-result) does NOT block. That fixes the paradox where a worker failure
|
|
1698
|
+
# made the gate stricter than a healthy roster would.
|
|
1699
|
+
if len(non_error) >= 2 and len(disagree) > len(agree):
|
|
1700
|
+
return "majority-disagree"
|
|
1701
|
+
return "has-dissent"
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
def _recompute_plan_body_gate(pbv: dict) -> str | None:
|
|
1705
|
+
"""Recompute the whole §5.5.9 gate value from ``planItems[].verdicts``.
|
|
1706
|
+
Returns a value in ``PLAN_VERIFY_GATE_VALUES`` or ``None`` when there are
|
|
1707
|
+
no plan items to judge (disabled / empty round)."""
|
|
1708
|
+
classes = [
|
|
1709
|
+
_classify_plan_item_gate(it)
|
|
1710
|
+
for it in (pbv.get("planItems") or [])
|
|
1711
|
+
if isinstance(it, dict)
|
|
1712
|
+
]
|
|
1713
|
+
if not classes:
|
|
1714
|
+
return None
|
|
1715
|
+
if all(c == "all-non-result" for c in classes):
|
|
1716
|
+
return "aborted-non-result"
|
|
1717
|
+
if any(c == "majority-disagree" for c in classes):
|
|
1718
|
+
return "blocked-by-disagreement"
|
|
1719
|
+
if any(c == "has-dissent" for c in classes):
|
|
1720
|
+
return "passed-with-dissent"
|
|
1721
|
+
return "passed"
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
def _validate_plan_body_gate_recompute(data: dict, failures: list[str]) -> None:
|
|
1725
|
+
"""H1 — the declared `Gate result` must not claim a healthier outcome than
|
|
1726
|
+
the recorded per-worker verdicts support. Closes the forgery hole where a
|
|
1727
|
+
lead writes `gateResult: passed` while workers actually voted DISAGREE:
|
|
1728
|
+
the verdicts live in `planItems[].verdicts`, so the gate is recomputable
|
|
1729
|
+
and no longer depends on the lead's honesty alone.
|
|
1730
|
+
"""
|
|
1731
|
+
ip = data.get("implementationPlanning")
|
|
1732
|
+
if not isinstance(ip, dict):
|
|
1733
|
+
return
|
|
1734
|
+
pbv = ip.get("planBodyVerification")
|
|
1735
|
+
if not isinstance(pbv, dict):
|
|
1736
|
+
return
|
|
1737
|
+
declared = str(pbv.get("gateResult") or "").strip().lower()
|
|
1738
|
+
recomputed = _recompute_plan_body_gate(pbv)
|
|
1739
|
+
if recomputed is None or declared not in _PLAN_GATE_RANK:
|
|
1740
|
+
return
|
|
1741
|
+
if _PLAN_GATE_RANK[declared] > _PLAN_GATE_RANK[recomputed]:
|
|
1742
|
+
failures.append(
|
|
1743
|
+
"final-report data.json: implementationPlanning.planBodyVerification "
|
|
1744
|
+
f"`gateResult` is `{declared}` but the recorded planItems[].verdicts "
|
|
1745
|
+
f"only support `{recomputed}` (a majority DISAGREE, a DISAGREE(f) on "
|
|
1746
|
+
"a P-Req item, or all-non-result dispatches were recorded). The gate "
|
|
1747
|
+
"value must honestly aggregate the worker votes — do not upgrade it "
|
|
1748
|
+
"to unblock the run (convergence.md Round protocol)."
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
# Each plan-body deliverable array and the P-* verdict-item prefix it must be
|
|
1753
|
+
# extracted into for §5.5.9 cross-verification (convergence.md Plan-item
|
|
1754
|
+
# extraction). A weak item dropped from planItems escapes the gate entirely.
|
|
1755
|
+
_PLAN_ITEM_SOURCES = (
|
|
1756
|
+
("optionCandidates", "P-Opt", "Option Candidates (§4.5.1)"),
|
|
1757
|
+
("stepwiseExecution", "P-Step", "Stepwise Execution Order (§4.5.4)"),
|
|
1758
|
+
("dependencyMigrationRisk", "P-Dep", "Dependency / Migration Risk (§4.5.5)"),
|
|
1759
|
+
("validationChecklist", "P-Val", "Validation Checklist (§4.5.6)"),
|
|
1760
|
+
("rollbackStrategy", "P-Rb", "Rollback Strategy (§4.5.7)"),
|
|
1761
|
+
("requirementCoverage", "P-Req", "Requirement Coverage (§4.5.8)"),
|
|
1762
|
+
)
|
|
1763
|
+
|
|
1764
|
+
|
|
1765
|
+
def _validate_plan_item_extraction_completeness(data: dict, failures: list[str]) -> None:
|
|
1766
|
+
"""H2 — when a verification round ran, every plan-body deliverable item must
|
|
1767
|
+
appear as a matching `P-*` verdict item. Closes the omission hole where the
|
|
1768
|
+
lead silently drops a weak item (e.g. an out-of-order rollback) from
|
|
1769
|
+
planItems so no worker ever verifies it, yet the gate still reads passed.
|
|
1770
|
+
Requires only that no category is *under-extracted*; over-extraction is fine.
|
|
1771
|
+
"""
|
|
1772
|
+
ip = data.get("implementationPlanning")
|
|
1773
|
+
if not isinstance(ip, dict):
|
|
1774
|
+
return
|
|
1775
|
+
pbv = ip.get("planBodyVerification")
|
|
1776
|
+
if not isinstance(pbv, dict):
|
|
1777
|
+
return
|
|
1778
|
+
round_count = pbv.get("roundCount")
|
|
1779
|
+
if not isinstance(round_count, int) or round_count < 1:
|
|
1780
|
+
return
|
|
1781
|
+
extracted_ids = [
|
|
1782
|
+
str(it.get("id") or "").upper()
|
|
1783
|
+
for it in (pbv.get("planItems") or [])
|
|
1784
|
+
if isinstance(it, dict)
|
|
1785
|
+
]
|
|
1786
|
+
for field, prefix, label in _PLAN_ITEM_SOURCES:
|
|
1787
|
+
source_count = len([r for r in (ip.get(field) or []) if isinstance(r, dict)])
|
|
1788
|
+
if source_count == 0:
|
|
1789
|
+
continue
|
|
1790
|
+
extracted = len(
|
|
1791
|
+
[i for i in extracted_ids if i.startswith(prefix.upper() + "-")]
|
|
1792
|
+
)
|
|
1793
|
+
if extracted < source_count:
|
|
1794
|
+
failures.append(
|
|
1795
|
+
f"final-report data.json: {label} has {source_count} item(s) but "
|
|
1796
|
+
f"planBodyVerification.planItems carries only {extracted} "
|
|
1797
|
+
f"`{prefix}-*` verdict item(s). Every plan-body item MUST be "
|
|
1798
|
+
"extracted for cross-verification — a dropped item escapes the "
|
|
1799
|
+
"gate (convergence.md Plan-item extraction)."
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
# A `subject` this short or shaped like a bare `P-Opt-1` id is a placeholder,
|
|
1804
|
+
# not the plain-language "what this item is" label §5.5.9 renders as a heading.
|
|
1805
|
+
_MIN_SUBJECT_LEN = 3
|
|
1806
|
+
_BARE_PLAN_ITEM_ID_RE = re.compile(r"^P-(?:Opt|Step|Dep|Val|Rb|Req)-\d+$", re.IGNORECASE)
|
|
1807
|
+
|
|
1808
|
+
|
|
1809
|
+
def _validate_plan_item_subject_substance(data: dict, failures: list[str]) -> None:
|
|
1810
|
+
"""H2 follow-up — `planItems[].subject` must be a real label, not a
|
|
1811
|
+
placeholder. The schema only enforces non-empty, so `"x"` or a copied
|
|
1812
|
+
`P-Opt-1` id would otherwise slip through and defeat the whole point of the
|
|
1813
|
+
subject (letting a reader see *what* each AGREE/DISAGREE is about).
|
|
1814
|
+
"""
|
|
1815
|
+
ip = data.get("implementationPlanning")
|
|
1816
|
+
if not isinstance(ip, dict):
|
|
1817
|
+
return
|
|
1818
|
+
pbv = ip.get("planBodyVerification")
|
|
1819
|
+
if not isinstance(pbv, dict):
|
|
1820
|
+
return
|
|
1821
|
+
for item in pbv.get("planItems") or []:
|
|
1822
|
+
if not isinstance(item, dict):
|
|
1823
|
+
continue
|
|
1824
|
+
item_id = str(item.get("id") or "").strip()
|
|
1825
|
+
subject = str(item.get("subject") or "").strip()
|
|
1826
|
+
if (
|
|
1827
|
+
len(subject) < _MIN_SUBJECT_LEN
|
|
1828
|
+
or subject == item_id
|
|
1829
|
+
or _BARE_PLAN_ITEM_ID_RE.match(subject)
|
|
1830
|
+
):
|
|
1831
|
+
failures.append(
|
|
1832
|
+
f"final-report data.json: plan item `{item_id or '<unknown>'}` has a "
|
|
1833
|
+
f"placeholder subject `{subject}`. Give a plain-language label of "
|
|
1834
|
+
"what the item is (e.g. 'Option A: upload v2 를 신규 모듈로 분리') so "
|
|
1835
|
+
"the §5.5.9 reader knows what each verdict is about "
|
|
1836
|
+
"(convergence.md Plan-item extraction)."
|
|
1837
|
+
)
|
|
1838
|
+
|
|
1839
|
+
|
|
1840
|
+
def _validate_plan_body_clarification_matching(data: dict, failures: list[str]) -> None:
|
|
1841
|
+
"""H5 — every plan item that the recorded verdicts make `majority-disagree`
|
|
1842
|
+
must point (via `clarificationId`) at an existing `blocks: approval`
|
|
1843
|
+
clarification row. Closes the hole where a majority-disagree item blocks the
|
|
1844
|
+
gate but no §1 Clarification row is raised, so the user never sees why
|
|
1845
|
+
approval is withheld (convergence.md self-review step 7).
|
|
1846
|
+
"""
|
|
1847
|
+
ip = data.get("implementationPlanning")
|
|
1848
|
+
if not isinstance(ip, dict):
|
|
1849
|
+
return
|
|
1850
|
+
pbv = ip.get("planBodyVerification")
|
|
1851
|
+
if not isinstance(pbv, dict):
|
|
1852
|
+
return
|
|
1853
|
+
round_count = pbv.get("roundCount")
|
|
1854
|
+
if not isinstance(round_count, int) or round_count < 1:
|
|
1855
|
+
return
|
|
1856
|
+
clar_rows = [r for r in (data.get("clarificationItems") or []) if isinstance(r, dict)]
|
|
1857
|
+
all_ids = {r.get("id") for r in clar_rows if r.get("id")}
|
|
1858
|
+
approval_ids = {r.get("id") for r in clar_rows if r.get("blocks") == "approval" and r.get("id")}
|
|
1859
|
+
for item in pbv.get("planItems") or []:
|
|
1860
|
+
if not isinstance(item, dict):
|
|
1861
|
+
continue
|
|
1862
|
+
if _classify_plan_item_gate(item) != "majority-disagree":
|
|
1863
|
+
continue
|
|
1864
|
+
item_id = item.get("id") or "<unknown>"
|
|
1865
|
+
cid = item.get("clarificationId")
|
|
1866
|
+
if not cid:
|
|
1867
|
+
failures.append(
|
|
1868
|
+
f"final-report data.json: plan item `{item_id}` is majority-disagree "
|
|
1869
|
+
"but carries no `clarificationId`. A blocking disagreement MUST "
|
|
1870
|
+
"surface as a `## 1. Clarification Items` row (blocks=approval) so "
|
|
1871
|
+
"the user sees the blocker (convergence.md self-review step 7)."
|
|
1872
|
+
)
|
|
1873
|
+
elif cid not in approval_ids:
|
|
1874
|
+
reason = (
|
|
1875
|
+
"references a non-existent §1 row"
|
|
1876
|
+
if cid not in all_ids
|
|
1877
|
+
else "references a §1 row whose `blocks` is not `approval`"
|
|
1878
|
+
)
|
|
1879
|
+
failures.append(
|
|
1880
|
+
f"final-report data.json: plan item `{item_id}` (majority-disagree) "
|
|
1881
|
+
f"has clarificationId `{cid}` which {reason}. Every majority-disagree "
|
|
1882
|
+
"item MUST map 1:1 to a `blocks: approval` Clarification row."
|
|
1883
|
+
)
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
_COVERED_BY_ANCHOR_RE = re.compile(r"option|stage|step", re.IGNORECASE)
|
|
1887
|
+
_COVERED_BY_STAGE_REF_RE = re.compile(r"stage\s*(\d+)", re.IGNORECASE)
|
|
1888
|
+
_COVERED_BY_VAGUE = {"recommended option", "the recommended option", "recommended"}
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
def _validate_requirement_coverage_covered_by(data: dict, failures: list[str]) -> None:
|
|
1892
|
+
"""H3 (partial) — a `covered` requirement row's `coveredBy` must name a
|
|
1893
|
+
concrete plan element that actually exists, not the spec-forbidden bare
|
|
1894
|
+
"recommended option" nor a phantom stage. Whether the cited step truly
|
|
1895
|
+
*satisfies* the requirement stays a worker DISAGREE(f) judgment; this closes
|
|
1896
|
+
the coarser hole where `coveredBy` points at nothing real.
|
|
1897
|
+
"""
|
|
1898
|
+
ip = data.get("implementationPlanning")
|
|
1899
|
+
if not isinstance(ip, dict):
|
|
1900
|
+
return
|
|
1901
|
+
rows = [r for r in (ip.get("requirementCoverage") or []) if isinstance(r, dict)]
|
|
1902
|
+
if not rows:
|
|
1903
|
+
return
|
|
1904
|
+
stage_numbers = {
|
|
1905
|
+
s.get("stage") for s in (ip.get("stages") or []) if isinstance(s, dict)
|
|
1906
|
+
}
|
|
1907
|
+
for row in rows:
|
|
1908
|
+
if row.get("status") != "covered":
|
|
1909
|
+
continue
|
|
1910
|
+
rid = row.get("id") or "<row>"
|
|
1911
|
+
covered = str(row.get("coveredBy") or "").strip()
|
|
1912
|
+
if covered.lower() in _COVERED_BY_VAGUE:
|
|
1913
|
+
failures.append(
|
|
1914
|
+
f"final-report data.json: requirementCoverage `{rid}` is `covered` "
|
|
1915
|
+
f"but coveredBy is just `{covered}`. Name the specific Option "
|
|
1916
|
+
"Candidate and Stage/Step that satisfies it, not 'recommended "
|
|
1917
|
+
"option' (profile Requirement Coverage)."
|
|
1918
|
+
)
|
|
1919
|
+
continue
|
|
1920
|
+
if not _COVERED_BY_ANCHOR_RE.search(covered):
|
|
1921
|
+
failures.append(
|
|
1922
|
+
f"final-report data.json: requirementCoverage `{rid}` coveredBy "
|
|
1923
|
+
f"`{covered}` names no Option / Stage / Step. A `covered` row must "
|
|
1924
|
+
"cite the concrete plan element that satisfies the requirement."
|
|
1925
|
+
)
|
|
1926
|
+
continue
|
|
1927
|
+
phantom = [
|
|
1928
|
+
n for m in _COVERED_BY_STAGE_REF_RE.finditer(covered)
|
|
1929
|
+
if stage_numbers and (n := int(m.group(1))) not in stage_numbers
|
|
1930
|
+
]
|
|
1931
|
+
if phantom:
|
|
1932
|
+
failures.append(
|
|
1933
|
+
f"final-report data.json: requirementCoverage `{rid}` coveredBy "
|
|
1934
|
+
f"cites Stage {phantom[0]} which does not exist in the Stage Map. "
|
|
1935
|
+
"A coverage row must point at a real stage."
|
|
1936
|
+
)
|
|
1937
|
+
|
|
1938
|
+
|
|
1587
1939
|
def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
|
|
1588
1940
|
"""Enforce verdict ↔ blocker/condition/routing consistency on the
|
|
1589
1941
|
final-verification data.json (SSOT). The schema guarantees field SHAPE;
|
|
@@ -2424,6 +2776,8 @@ def main() -> int:
|
|
|
2424
2776
|
except (OSError, json.JSONDecodeError):
|
|
2425
2777
|
_sp = None
|
|
2426
2778
|
_validate_conformance(report_path, failures, surface_patterns=_sp)
|
|
2779
|
+
if task_type == "implementation-planning":
|
|
2780
|
+
_validate_planning_conformance_declared(report_path, failures)
|
|
2427
2781
|
if task_type == "improvement-discovery":
|
|
2428
2782
|
brief_relative = str(task_manifest.get("taskBriefPath") or "").strip()
|
|
2429
2783
|
brief_path = (
|