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:
@@ -553,6 +553,17 @@ class Settings(BaseSettings):
|
||||
# ORCH_TRACKER_BRD_REVIEW_CAP_S; default 7200s (2h). 0/negative -> no cap.
|
||||
tracker_brd_review_cap_s: int = 7200
|
||||
|
||||
# ORCH-076 (ORCH-52c, FR-2 / D3): kill-switch for STRICT frontmatter-schema
|
||||
# validation. The unified frontmatter contract (src/frontmatter.py) ships a
|
||||
# machine-checkable schema validator (REQUIRED_FIELDS), but by DEFAULT it is
|
||||
# warning-only and never influences any gate's boolean verdict (maybe_warn_schema
|
||||
# is inert). This flag is RESERVED for a future tightening (ORCH-52d, when agents
|
||||
# start emitting the full schema). It MUST stay False in prod / .env.staging —
|
||||
# otherwise ORCH-52c would self-block its own deploy (its docs predate the
|
||||
# schema). Env ORCH_FRONTMATTER_VALIDATION_STRICT; default False (zero behaviour
|
||||
# change). See docs/_standards/HANDOFF_PROTOCOL.md.
|
||||
frontmatter_validation_strict: bool = False
|
||||
|
||||
# ORCH-069: QG-0 upper title-length limit (entry gate _qg0_errors). The 80-char
|
||||
# cap was a hygiene limit, not structural (slug is cut to [:30] independently,
|
||||
# DB title TEXT is unbounded). Configurable via env ORCH_QG0_TITLE_MAX; default
|
||||
|
||||
@@ -1,27 +1,144 @@
|
||||
"""Safe single-key YAML frontmatter reader (ORCH-016 / ADR-001 §5).
|
||||
"""Unified YAML-frontmatter contract — reader + writer + schema validator (ORCH-52c).
|
||||
|
||||
The status-comment builder (build_status_comment) needs to surface verdict /
|
||||
deploy_status / staging_status from the per-stage artifact files (12-review.md,
|
||||
13-test-report.md, 14-deploy-log.md, 15-staging-log.md). Those files share the
|
||||
same leading-YAML-frontmatter convention used by the quality gates — but the
|
||||
comment hot-path must NEVER raise: a missing file, malformed YAML, or absent
|
||||
key should simply suppress the verdict line, not break the run.
|
||||
History
|
||||
-------
|
||||
ORCH-016 introduced this module as a *single-key reader* (``read_frontmatter_value``)
|
||||
for the status-comment hot path, intentionally duplicating ~10 lines of
|
||||
YAML-frontmatter logic already present in ``src/qg/checks.py`` and
|
||||
``src/security_gate.py`` to keep that PR's blast radius small. Its docstring noted
|
||||
*"merging into a single parser is a follow-up task"* — this module (ORCH-52c /
|
||||
ORCH-076) is that follow-up.
|
||||
|
||||
This module is a tiny defensive helper:
|
||||
- `read_frontmatter_value(path, key)` -> str | None
|
||||
- swallows every exception, logs to logger.debug, returns None.
|
||||
What this module now provides (ADR-001 / adr-0020)
|
||||
--------------------------------------------------
|
||||
A **single point of YAML-frontmatter parsing** that every verdict gate delegates to,
|
||||
plus a writer and a (warning-only by default) schema validator:
|
||||
|
||||
It intentionally duplicates ~10 lines of YAML-frontmatter logic that already
|
||||
exist in `src/qg/checks.py` (S-5 / БАГ 8 / ET-013 fixes). ADR-001 §5 accepts
|
||||
this duplication to keep the blast radius of ORCH-016 small (no QG refactor in
|
||||
this PR); merging into a single parser is a follow-up task.
|
||||
* ``read_frontmatter_value(path, key)`` — UNCHANGED single-key reader (INV-3): the
|
||||
external callers (``usage.py``, ``notifications.build_status_comment``) keep the
|
||||
exact same contract (``str | None``, never-raise, strip, case preserved).
|
||||
* ``parse_frontmatter(content)`` — the ONE YAML parse primitive; returns a
|
||||
structured :class:`FrontmatterParse` so each gate can reproduce its current
|
||||
reason-strings 1:1 (no-block / malformed / yaml-error / data).
|
||||
* ``parse_frontmatter_dict`` / ``read_frontmatter`` — convenience shortcuts to the
|
||||
parsed mapping (in-memory / from a file).
|
||||
* ``render_frontmatter`` / ``write_frontmatter`` — canonical writer; the output is
|
||||
byte-compatible with the existing ``split("---", 2)`` + ``yaml.safe_load`` parsers.
|
||||
* ``validate_schema`` + ``REQUIRED_FIELDS`` + ``maybe_warn_schema`` — the machine
|
||||
schema (``work_item / stage / author_agent / status / created_at / model_used``).
|
||||
By default it is **warning-only** and never influences any gate's boolean verdict
|
||||
(NFR-3 / INV-4); the strict mode is reserved for ORCH-52d and gated behind the
|
||||
``frontmatter_validation_strict`` kill-switch (default ``False``).
|
||||
* ``strip_frontmatter(content)`` — shared body-only helper (replaces the duplicated
|
||||
``_strip_frontmatter`` in ``review_parse``).
|
||||
|
||||
Contract — the WHOLE module is **never-raise** (NFR-2), exactly like the original
|
||||
reader: any error (I/O, YAML, serialization) is logged to ``logger.debug/warning``
|
||||
and degrades to a safe value (``{}`` / ``False`` / the input text); an exception
|
||||
never escapes into the pipeline. This is a hard self-hosting requirement: these
|
||||
functions read verdicts ON THE GATES of the instance that serves prod for every
|
||||
project from one shared DB/queue, so a regression here would stall every project.
|
||||
|
||||
This module is a **leaf**: it imports only ``logging`` (and lazily ``yaml``); it does
|
||||
not import anything project-specific, so it stays cycle-free for ``qg/checks.py``,
|
||||
``security_gate.py``, ``post_deploy.py`` and ``review_parse.py``.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Mapping
|
||||
|
||||
logger = logging.getLogger("orchestrator.frontmatter")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schema constants (the machine-checkable required frontmatter — FR-2 / D3)
|
||||
# ---------------------------------------------------------------------------
|
||||
#: The required frontmatter fields a stage handoff document is expected to carry.
|
||||
#: Source of truth for HANDOFF_PROTOCOL.md §2. The validator is warning-only by
|
||||
#: default (D3) — its presence does NOT gate the pipeline unless the
|
||||
#: ``frontmatter_validation_strict`` kill-switch is flipped on (reserved, ORCH-52d).
|
||||
REQUIRED_FIELDS = ("work_item", "stage", "author_agent", "status", "created_at", "model_used")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parse primitive — the SINGLE point of YAML-frontmatter logic (D1 / D2)
|
||||
# ---------------------------------------------------------------------------
|
||||
@dataclass(frozen=True)
|
||||
class FrontmatterParse:
|
||||
"""Structured outcome of parsing a document's leading YAML frontmatter.
|
||||
|
||||
The structure (not a bare dict) lets each gate reproduce its EXISTING
|
||||
reason-strings 1:1 (ADR-001 D2): a gate can tell "no block" from "malformed"
|
||||
from "yaml error" from "valid data" without re-implementing the parse.
|
||||
|
||||
Attributes:
|
||||
data: the parsed mapping; ``{}`` when absent / malformed / not a mapping.
|
||||
has_block: a leading ``---`` … ``---`` block was present.
|
||||
malformed: the content started with ``---`` but had < 3 ``---``-split segments
|
||||
(an unterminated frontmatter block).
|
||||
yaml_error: the ``yaml.safe_load`` error text, else ``None``.
|
||||
"""
|
||||
|
||||
data: dict = field(default_factory=dict)
|
||||
has_block: bool = False
|
||||
malformed: bool = False
|
||||
yaml_error: str | None = None
|
||||
|
||||
|
||||
def parse_frontmatter(content: str) -> FrontmatterParse:
|
||||
"""Parse the leading YAML frontmatter of ``content`` into a :class:`FrontmatterParse`.
|
||||
|
||||
The single canonical implementation of the block that used to be duplicated in
|
||||
every verdict gate (``content.startswith("---")`` -> ``split("---", 2)`` ->
|
||||
``yaml.safe_load`` -> ``isinstance(dict)``). Never raises:
|
||||
|
||||
* not a string / no leading ``---`` -> ``has_block=False``, ``data={}``.
|
||||
* ``---`` but < 3 segments (unterminated) -> ``malformed=True``, ``data={}``.
|
||||
* ``yaml.safe_load`` error -> ``yaml_error=<text>``, ``data={}``.
|
||||
* parsed value is not a mapping -> ``data={}`` (``has_block=True``).
|
||||
* valid mapping -> ``data=<dict>``.
|
||||
"""
|
||||
try:
|
||||
if not isinstance(content, str) or not content.startswith("---"):
|
||||
return FrontmatterParse()
|
||||
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
# Unterminated frontmatter block.
|
||||
return FrontmatterParse(has_block=True, malformed=True)
|
||||
|
||||
try:
|
||||
import yaml
|
||||
loaded = yaml.safe_load(parts[1])
|
||||
except Exception as e: # yaml.YAMLError + anything pyyaml may surface
|
||||
logger.debug(f"parse_frontmatter: yaml parse failed: {e}")
|
||||
return FrontmatterParse(has_block=True, yaml_error=str(e))
|
||||
|
||||
if not isinstance(loaded, dict):
|
||||
return FrontmatterParse(has_block=True)
|
||||
return FrontmatterParse(data=loaded, has_block=True)
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.debug(f"parse_frontmatter: unexpected error: {e}")
|
||||
return FrontmatterParse()
|
||||
|
||||
|
||||
def parse_frontmatter_dict(content: str) -> dict:
|
||||
"""Shortcut: the parsed mapping of ``content``'s frontmatter. Never raises -> ``{}``."""
|
||||
return parse_frontmatter(content).data
|
||||
|
||||
|
||||
def read_frontmatter(path: str) -> dict:
|
||||
"""Read ``path`` and return its parsed frontmatter mapping. Never raises -> ``{}``."""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
content = f.read()
|
||||
except OSError as e:
|
||||
logger.debug(f"read_frontmatter: cannot open {path}: {e}")
|
||||
return {}
|
||||
return parse_frontmatter(content).data
|
||||
|
||||
|
||||
def read_frontmatter_value(path: str, key: str) -> str | None:
|
||||
"""Return the value of `key` from the leading YAML frontmatter of `path`.
|
||||
|
||||
@@ -42,34 +159,158 @@ def read_frontmatter_value(path: str, key: str) -> str | None:
|
||||
|
||||
The returned value is stringified and stripped (whitespace removed); casing
|
||||
is preserved so the caller decides whether to upper/lower for matching.
|
||||
|
||||
ORCH-52c: reimplemented on top of the unified ``read_frontmatter`` primitive.
|
||||
The external contract (``str | None``, never-raise, strip, case preserved) is
|
||||
UNCHANGED — external callers (``usage.py``, ``notifications``) are unaffected
|
||||
(INV-3 / FR-3).
|
||||
"""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
content = f.read()
|
||||
except OSError as e:
|
||||
logger.debug(f"read_frontmatter_value: cannot open {path}: {e}")
|
||||
return None
|
||||
|
||||
if not content.startswith("---"):
|
||||
return None
|
||||
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
# Unterminated frontmatter.
|
||||
return None
|
||||
|
||||
try:
|
||||
import yaml
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except Exception as e: # yaml.YAMLError + anything pyyaml may surface
|
||||
logger.debug(f"read_frontmatter_value: yaml parse failed for {path}: {e}")
|
||||
return None
|
||||
|
||||
if not isinstance(fm, dict):
|
||||
return None
|
||||
|
||||
fm = read_frontmatter(path)
|
||||
raw = fm.get(key)
|
||||
if raw is None:
|
||||
return None
|
||||
value = str(raw).strip()
|
||||
return value or None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Body helper — replaces the duplicated review_parse._strip_frontmatter (D2)
|
||||
# ---------------------------------------------------------------------------
|
||||
def strip_frontmatter(content: str) -> str:
|
||||
"""Return ``content`` with a leading ``--- … ---`` YAML block removed, if present.
|
||||
|
||||
Mirrors the previous ``review_parse._strip_frontmatter`` exactly: only a
|
||||
well-formed (>= 3 ``---``-split segments) leading block is stripped; otherwise
|
||||
the input is returned unchanged. Never raises -> the input text.
|
||||
"""
|
||||
try:
|
||||
if isinstance(content, str) and content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
return parts[2]
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.debug(f"strip_frontmatter: unexpected error: {e}")
|
||||
return content
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Writer — canonical render/persist (FR-1 / D1)
|
||||
# ---------------------------------------------------------------------------
|
||||
def render_frontmatter(data: Mapping[str, object], body: str = "") -> str:
|
||||
"""Render ``data`` as a canonical leading YAML-frontmatter block + ``body``.
|
||||
|
||||
Output shape: ``"---\\n<yaml>\\n---\\n<body>"``. The YAML is emitted with
|
||||
``yaml.safe_dump`` (block style, keys unsorted) so it is byte-compatible with
|
||||
the existing readers (``split("---", 2)`` + ``yaml.safe_load``): a round-trip
|
||||
``render_frontmatter`` -> ``parse_frontmatter`` returns the same mapping.
|
||||
|
||||
never-raise (NFR-2): a serialization error is logged and the function degrades
|
||||
to returning ``body`` unchanged (a document with no frontmatter is read by the
|
||||
gates exactly as "no machine verdict", never an exception).
|
||||
"""
|
||||
try:
|
||||
import yaml
|
||||
dumped = yaml.safe_dump(
|
||||
dict(data or {}), default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
).strip("\n")
|
||||
return f"---\n{dumped}\n---\n{body}"
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.warning(f"render_frontmatter: serialization failed: {e}")
|
||||
return body
|
||||
|
||||
|
||||
def write_frontmatter(path: str, data: Mapping[str, object], body: str = "") -> bool:
|
||||
"""Persist ``render_frontmatter(data, body)`` to ``path``. Returns True on success.
|
||||
|
||||
never-raise (NFR-2): any I/O / serialization error is logged and returns
|
||||
``False`` (the caller decides how to degrade); an exception never escapes.
|
||||
"""
|
||||
try:
|
||||
content = render_frontmatter(data, body)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return True
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.warning(f"write_frontmatter: cannot write {path}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schema validator — warning-only by default; strict reserved (FR-2 / D3)
|
||||
# ---------------------------------------------------------------------------
|
||||
@dataclass(frozen=True)
|
||||
class SchemaValidation:
|
||||
"""Outcome of :func:`validate_schema`.
|
||||
|
||||
valid: all required fields are present and non-empty.
|
||||
missing: the required fields that are absent / None / blank (order = REQUIRED_FIELDS).
|
||||
"""
|
||||
|
||||
valid: bool
|
||||
missing: list
|
||||
|
||||
|
||||
def validate_schema(data: Mapping, *, required=REQUIRED_FIELDS) -> SchemaValidation:
|
||||
"""Validate that ``data`` carries every required schema field, non-empty.
|
||||
|
||||
Pure library function (INV-4). A field counts as MISSING when it is absent, or
|
||||
its value is ``None`` or — after ``str(...).strip()`` — empty. Returns a
|
||||
:class:`SchemaValidation`; never raises (a non-mapping input -> all fields
|
||||
missing -> ``valid=False``). This function NEVER influences a gate verdict by
|
||||
itself — see :func:`maybe_warn_schema` and the ``frontmatter_validation_strict``
|
||||
flag for how strict enforcement is (and is not) wired.
|
||||
"""
|
||||
missing: list = []
|
||||
try:
|
||||
mapping = data if isinstance(data, Mapping) else {}
|
||||
for fld in required:
|
||||
raw = mapping.get(fld)
|
||||
if raw is None or str(raw).strip() == "":
|
||||
missing.append(fld)
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.warning(f"validate_schema: unexpected error: {e}")
|
||||
# Conservatively report everything missing rather than raise.
|
||||
missing = list(required)
|
||||
return SchemaValidation(valid=not missing, missing=missing)
|
||||
|
||||
|
||||
def maybe_warn_schema(content: str, doc_label: str = "document") -> SchemaValidation:
|
||||
"""Best-effort schema check used at verdict-read sites — warning-only by default.
|
||||
|
||||
Parses ``content``'s frontmatter and validates it against :data:`REQUIRED_FIELDS`.
|
||||
Behaviour is governed by the ``frontmatter_validation_strict`` kill-switch
|
||||
(default ``False``):
|
||||
|
||||
* **default (False)** — when fields are missing, emit a single
|
||||
``logger.warning("frontmatter schema incomplete: missing …")`` and return.
|
||||
The result is **inert**: callers that pass it through a gate must NOT change
|
||||
their ``tuple[bool, str]`` verdict (FR-2 "warning/лог, не blocker"). This
|
||||
keeps a machine-verdict doc that lacks the (forward-looking, additive) schema
|
||||
readable exactly as before (FR-5 / AC-4) — critical so ORCH-52c does not
|
||||
self-block its own deploy (its docs predate the schema).
|
||||
* **strict (True)** — RESERVED for a future tightening (ORCH-52d+). The
|
||||
validation result is returned the same way; the flag merely documents intent
|
||||
and lets a future caller veto. It stays ``False`` in prod and ``.env.staging``.
|
||||
|
||||
Never raises (NFR-2): a config-read or parse error degrades to ``valid=True``
|
||||
(no false warning, no influence on the verdict).
|
||||
"""
|
||||
try:
|
||||
data = parse_frontmatter(content).data
|
||||
result = validate_schema(data)
|
||||
if not result.valid:
|
||||
try:
|
||||
from .config import settings
|
||||
strict = bool(getattr(settings, "frontmatter_validation_strict", False))
|
||||
except Exception: # noqa: BLE001 - config read must never raise here
|
||||
strict = False
|
||||
logger.warning(
|
||||
"frontmatter schema incomplete in %s: missing %s%s",
|
||||
doc_label,
|
||||
", ".join(result.missing),
|
||||
" [strict]" if strict else "",
|
||||
)
|
||||
return result
|
||||
except Exception as e: # noqa: BLE001 - never-raise; inert on error
|
||||
logger.debug(f"maybe_warn_schema: unexpected error for {doc_label}: {e}")
|
||||
return SchemaValidation(valid=True, missing=[])
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -44,12 +44,15 @@ def _read(path: str) -> str | None:
|
||||
|
||||
|
||||
def _strip_frontmatter(content: str) -> str:
|
||||
"""Drop a leading ``--- … ---`` YAML frontmatter block, if present."""
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
return parts[2]
|
||||
return content
|
||||
"""Drop a leading ``--- … ---`` YAML frontmatter block, if present.
|
||||
|
||||
ORCH-52c: delegates to the unified ``frontmatter.strip_frontmatter`` helper
|
||||
(single source of frontmatter logic). Behaviour is identical (only a well-formed
|
||||
>= 3-segment leading block is stripped) and the never-raise -> input contract is
|
||||
preserved.
|
||||
"""
|
||||
from .frontmatter import strip_frontmatter
|
||||
return strip_frontmatter(content)
|
||||
|
||||
|
||||
def _truncate(text: str, limit: int) -> str:
|
||||
|
||||
@@ -559,19 +559,19 @@ def parse_security_status(content: str) -> tuple[bool, str]:
|
||||
* ``security_status: FAIL`` -> ``(False, "Security status: FAIL")``
|
||||
* missing field / no frontmatter / bad YAML -> ``(False, <reason>)`` (fail-closed
|
||||
on the verdict read, AC-9).
|
||||
"""
|
||||
import yaml
|
||||
|
||||
ORCH-52c: parse delegated to the unified ``frontmatter.parse_frontmatter``
|
||||
primitive (single source of YAML-frontmatter logic); the security_status
|
||||
semantics (FAIL authoritative) are UNCHANGED (1:1).
|
||||
"""
|
||||
from .frontmatter import parse_frontmatter
|
||||
|
||||
parse = parse_frontmatter(content)
|
||||
if parse.yaml_error is not None:
|
||||
return False, f"Invalid YAML frontmatter in security report: {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 security report: {e}"
|
||||
if isinstance(fm, dict):
|
||||
status = str(fm.get("security_status", "")).upper().strip()
|
||||
if parse.has_block and not parse.malformed:
|
||||
status = str(parse.data.get("security_status", "")).upper().strip()
|
||||
if status == "FAIL":
|
||||
return False, "Security status: FAIL"
|
||||
if status == "PASS":
|
||||
@@ -593,11 +593,9 @@ def extract_security_findings(report_path: str) -> str:
|
||||
return ""
|
||||
with open(report_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
# Drop the frontmatter; keep the human body.
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
content = parts[2]
|
||||
# Drop the frontmatter; keep the human body (ORCH-52c: shared helper).
|
||||
from .frontmatter import strip_frontmatter
|
||||
content = strip_frontmatter(content)
|
||||
wanted = ("## Verdict", "## Secrets", "## Dependencies (blocking)")
|
||||
lines = content.splitlines()
|
||||
out = []
|
||||
|
||||
Reference in New Issue
Block a user