refactor(frontmatter): unified frontmatter contract + handoff spec (ORCH-52c)
src/frontmatter.py grows from a single-key reader into the full machine
contract: reader (read_frontmatter_value, unchanged), one parse primitive
(parse_frontmatter), writer (render/write_frontmatter), schema validator
(validate_schema/REQUIRED_FIELDS, warning-only by default) and a shared
strip_frontmatter helper. The five verdict gates (check_reviewer_verdict,
_parse_tests_verdict, _parse_deploy_status, _parse_staging_status,
parse_security_status) now read through the single parse_frontmatter point
instead of duplicated ad-hoc YAML logic; review_parse._strip_frontmatter and
security_gate.extract_security_findings reuse the shared helper.
Strictly backward compatible + never-raise: STAGE_TRANSITIONS, the QG_CHECKS
composition, verdict semantics (incl. ORCH-047 three-field tester + negative
token priority), reason-strings and worktree->origin/main fallback are 1:1.
The schema validator never influences a gate verdict by default; hard-fail is
reserved behind the frontmatter_validation_strict kill-switch (default False).
New formal handoff spec docs/_standards/HANDOFF_PROTOCOL.md ("stage -> required
output" + required frontmatter schema), aligned 1:1 with PIPELINE_DOCS.md.
Tests: test_frontmatter.py (TC-01..07), test_qg_verdicts.py (TC-08..15),
test_security_gate.py (TC-12), test_stages_invariants.py (TC-16). Full
tests/ green (1212).
Refs: ORCH-076
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -239,22 +239,26 @@ def _parse_tests_verdict(content: str) -> tuple[bool, str]:
|
||||
beats a positive token in another field).
|
||||
- Otherwise a positive token (PASS/PASSED/READY-TO-DEPLOY/...) in ANY field -> (True).
|
||||
- Anything else (fields set but unrecognized) -> (False, reason).
|
||||
|
||||
ORCH-52c: the YAML-frontmatter parse is now delegated to the unified
|
||||
``frontmatter.parse_frontmatter`` primitive (single source of parse logic); the
|
||||
token-logic, upper-casing, three-field set and negative-token priority are
|
||||
UNCHANGED (semantics 1:1, AC-3/AC-6). Reason-strings are reproduced from the
|
||||
structured parse states.
|
||||
"""
|
||||
import yaml
|
||||
from ..frontmatter import parse_frontmatter, maybe_warn_schema
|
||||
|
||||
if not content.startswith("---"):
|
||||
parse = parse_frontmatter(content)
|
||||
if not parse.has_block:
|
||||
return False, "No YAML frontmatter in test report (cannot read machine verdict)"
|
||||
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
if parse.malformed:
|
||||
return False, "Malformed YAML frontmatter in test report"
|
||||
|
||||
try:
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML frontmatter in test report: {e}"
|
||||
if not isinstance(fm, dict):
|
||||
return False, "Malformed YAML frontmatter in test report (not a mapping)"
|
||||
if parse.yaml_error is not None:
|
||||
return False, f"Invalid YAML frontmatter in test report: {parse.yaml_error}"
|
||||
fm = parse.data
|
||||
# Warning-only schema check (FR-2/D3): inert — never changes the verdict.
|
||||
if fm:
|
||||
maybe_warn_schema(content, "test report")
|
||||
|
||||
verdict = str(fm.get("verdict", "") or "").upper().strip()
|
||||
status = str(fm.get("status", "") or "").upper().strip()
|
||||
@@ -338,8 +342,12 @@ def check_reviewer_verdict(repo: str, work_item_id: str, branch: str | None = No
|
||||
cause false positives/negatives. Returns:
|
||||
(True, ...) -> verdict: APPROVED
|
||||
(False, ...) -> verdict: REQUEST_CHANGES, missing verdict, or no frontmatter
|
||||
|
||||
ORCH-52c: the YAML-frontmatter parse is delegated to the unified
|
||||
``frontmatter.parse_frontmatter`` primitive; the verdict semantics
|
||||
(APPROVED/REQUEST_CHANGES) are UNCHANGED (1:1, AC-3/AC-6).
|
||||
"""
|
||||
import yaml
|
||||
from ..frontmatter import parse_frontmatter, maybe_warn_schema
|
||||
repo_path = _repo_path(repo, branch)
|
||||
review_path = os.path.join(repo_path, f"docs/work-items/{work_item_id}/12-review.md")
|
||||
|
||||
@@ -350,15 +358,14 @@ def check_reviewer_verdict(repo: str, work_item_id: str, branch: str | None = No
|
||||
with open(review_path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
parse = parse_frontmatter(content)
|
||||
if parse.yaml_error is not None:
|
||||
return False, f"Invalid YAML frontmatter in review: {parse.yaml_error}"
|
||||
verdict = None
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML frontmatter in review: {e}"
|
||||
verdict = str(fm.get("verdict", "")).upper().strip()
|
||||
if parse.has_block and not parse.malformed:
|
||||
if parse.data:
|
||||
maybe_warn_schema(content, "review report")
|
||||
verdict = str(parse.data.get("verdict", "")).upper().strip()
|
||||
|
||||
if verdict == "APPROVED":
|
||||
return True, "Reviewer verdict: APPROVED"
|
||||
@@ -410,17 +417,19 @@ def _parse_deploy_status(content: str) -> tuple[bool, str]:
|
||||
deploy_status: SUCCESS -> (True, "Deploy status: SUCCESS")
|
||||
deploy_status: FAILED -> (False, "Deploy status: FAILED")
|
||||
missing field / no frontmatter / bad YAML -> (False, <reason>)
|
||||
|
||||
ORCH-52c: parse delegated to the unified ``frontmatter.parse_frontmatter``;
|
||||
the deploy_status semantics (БАГ-8) are UNCHANGED (1:1).
|
||||
"""
|
||||
import yaml
|
||||
from ..frontmatter import parse_frontmatter, maybe_warn_schema
|
||||
parse = parse_frontmatter(content)
|
||||
if parse.yaml_error is not None:
|
||||
return False, f"Invalid YAML frontmatter in deploy log: {parse.yaml_error}"
|
||||
status = None
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML frontmatter in deploy log: {e}"
|
||||
status = str(fm.get("deploy_status", "")).upper().strip()
|
||||
if parse.has_block and not parse.malformed:
|
||||
if parse.data:
|
||||
maybe_warn_schema(content, "deploy log")
|
||||
status = str(parse.data.get("deploy_status", "")).upper().strip()
|
||||
if status == "SUCCESS":
|
||||
return True, "Deploy status: SUCCESS"
|
||||
if status == "FAILED":
|
||||
@@ -525,17 +534,19 @@ def _parse_staging_status(content: str) -> tuple[bool, str]:
|
||||
staging_status: SUCCESS -> (True, "Staging status: SUCCESS")
|
||||
staging_status: FAILED -> (False, "Staging status: FAILED")
|
||||
missing field / no frontmatter / bad YAML -> (False, <reason>)
|
||||
|
||||
ORCH-52c: parse delegated to the unified ``frontmatter.parse_frontmatter``;
|
||||
the staging_status semantics (self-hosting) are UNCHANGED (1:1).
|
||||
"""
|
||||
import yaml
|
||||
from ..frontmatter import parse_frontmatter, maybe_warn_schema
|
||||
parse = parse_frontmatter(content)
|
||||
if parse.yaml_error is not None:
|
||||
return False, f"Invalid YAML frontmatter in staging log: {parse.yaml_error}"
|
||||
status = None
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML frontmatter in staging log: {e}"
|
||||
status = str(fm.get("staging_status", "")).upper().strip()
|
||||
if parse.has_block and not parse.malformed:
|
||||
if parse.data:
|
||||
maybe_warn_schema(content, "staging log")
|
||||
status = str(parse.data.get("staging_status", "")).upper().strip()
|
||||
if status == "SUCCESS":
|
||||
return True, "Staging status: SUCCESS"
|
||||
if status == "FAILED":
|
||||
|
||||
Reference in New Issue
Block a user