feat(coverage): deterministic test-coverage gate on deploy-staging->deploy edge (ORCH-027)

Introduce a deterministic (no-LLM) coverage sub-gate that blocks coverage
degradation before a task branch merges into `main`. Existing gates judge only by
the FACT of passing (check_ci_green / check_tests_passed / merge-gate re-test), not
by completeness — so a batch autonomous run (ORCH-088) silently erodes coverage.

Pattern mirrors the security-gate (ORCH-022): leaf src/coverage_gate.py (never-raise)
+ thin check_coverage_gate in QG_CHECKS + _handle_coverage_gate splice in advance_stage,
run AFTER merge-gate (measured on the caught-up HEAD that lands in main) and BEFORE
image-freshness (fail before the expensive docker rebuild).

- measure_coverage: pytest --cov=src --cov-report=json in the per-branch worktree ->
  line coverage %; None on tool error -> fail-open + WARNING by default (FR-6).
- compute_coverage_verdict (pure): absolute | baseline | both + epsilon (NFR-4 anti-flap);
  baseline None -> bootstrap (absolute-only).
- coverage_baseline DB table (additive, CREATE TABLE IF NOT EXISTS) + ratchet-up in
  _handle_merge_verify (deploy->done): atomic compare-and-set under merge-lease, never
  decreases; bootstrap on first merge.
- Artefact 18-coverage-report.md (coverage_status: frontmatter, single source of truth);
  GET /queue `coverage` block; FAIL -> Telegram; optional POST /coverage/baseline override.
- Flags ORCH_COVERAGE_* (kill-switch + self-hosting-only scope) -> enduro untouched;
  STAGE_TRANSITIONS / existing check_* / verdict keys byte-for-byte unchanged (NFR-5/AC-8).
- pytest-cov==5.0.0 added to requirements.txt.

Tests: tests/test_coverage_gate.py (TC-01..TC-15). Frozen QG-registry anti-regress
tests + deploy-staging edge tests updated for the new sub-gate. Full suite green.

Docs: README / adr-0029 / PIPELINE_DOCS / 18-coverage-report.md template (architecture
stage) + CHANGELOG / CLAUDE.md / .env.example (this PR).

Refs: ORCH-027
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 01:04:21 +03:00
committed by orchestrator-deployer
parent 1f9c128a48
commit eadfd8419b
16 changed files with 1496 additions and 2 deletions

128
src/db.py
View File

@@ -199,10 +199,138 @@ def init_db():
CREATE INDEX IF NOT EXISTS idx_repo_freeze_active
ON repo_freeze (repo, cleared_at);
""")
# ORCH-027 (FR-4, ADR-001 D4): additive per-repo coverage baseline for the
# coverage-gate ratchet. One row per repo; the baseline is monotonically
# non-decreasing via ratchet_coverage_baseline (atomic compare-and-set). Purely
# ADDITIVE (CREATE TABLE IF NOT EXISTS, pattern repo_freeze/job_deps) ->
# idempotent, restart-safe on the shared prod DB; existing tables untouched
# (NFR-5). See docs/work-items/ORCH-027/08-data-requirements.md.
conn.executescript("""
CREATE TABLE IF NOT EXISTS coverage_baseline (
repo TEXT PRIMARY KEY,
coverage REAL NOT NULL,
source_sha TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
""")
conn.commit()
conn.close()
def get_coverage_baseline(repo: str) -> float | None:
"""ORCH-027: read the per-repo coverage baseline (%, line coverage).
Returns ``None`` when no baseline is stored yet (bootstrap mode — the gate then
decides on the absolute floor only, D3). Raises only on a real DB error (the
coverage_gate leaf caller wraps this in its never-raise contract).
"""
if not repo:
return None
conn = get_db()
try:
row = conn.execute(
"SELECT coverage FROM coverage_baseline WHERE repo = ?", (repo,)
).fetchone()
finally:
conn.close()
if row is None:
return None
try:
return float(row["coverage"])
except (TypeError, ValueError):
return None
def ratchet_coverage_baseline(repo: str, coverage: float, sha: str | None = None) -> bool:
"""ORCH-027 (FR-4, D5): raise the per-repo coverage baseline UP, never down.
Atomic compare-and-set: ``UPDATE ... WHERE coverage <= ?`` (the baseline never
decreases — an equal value is an idempotent no-harm re-stamp), or ``INSERT`` when
no row exists yet (bootstrap). Under the held merge-lease (ORCH-043) plus this
single-statement guard, two parallel merges can never lower or lose the value.
Returns True iff a row was inserted or raised.
"""
if not repo:
return False
try:
cov = float(coverage)
except (TypeError, ValueError):
return False
conn = get_db()
try:
cur = conn.execute(
"UPDATE coverage_baseline "
"SET coverage = ?, source_sha = ?, updated_at = datetime('now') "
"WHERE repo = ? AND coverage <= ?",
(cov, sha, repo, cov),
)
changed = cur.rowcount or 0
if changed == 0:
# No row updated: either the row is absent (bootstrap INSERT) or the
# existing baseline is already higher (skip — never lower it).
exists = conn.execute(
"SELECT 1 FROM coverage_baseline WHERE repo = ?", (repo,)
).fetchone()
if exists is None:
conn.execute(
"INSERT INTO coverage_baseline (repo, coverage, source_sha, updated_at) "
"VALUES (?, ?, ?, datetime('now'))",
(repo, cov, sha),
)
changed = 1
conn.commit()
return bool(changed)
finally:
conn.close()
def set_coverage_baseline(repo: str, coverage: float, sha: str | None = None) -> bool:
"""ORCH-027 (D8): UNCONDITIONALLY set the per-repo coverage baseline.
For a legitimate one-off coverage drop (e.g. removing a large tested module) via
the manual ``POST /coverage/baseline`` override. Unlike ``ratchet_coverage_baseline``
this CAN lower the baseline. Returns True on success.
"""
if not repo:
return False
try:
cov = float(coverage)
except (TypeError, ValueError):
return False
conn = get_db()
try:
conn.execute(
"INSERT INTO coverage_baseline (repo, coverage, source_sha, updated_at) "
"VALUES (?, ?, ?, datetime('now')) "
"ON CONFLICT(repo) DO UPDATE SET coverage = excluded.coverage, "
"source_sha = excluded.source_sha, updated_at = excluded.updated_at",
(repo, cov, sha),
)
conn.commit()
return True
finally:
conn.close()
def all_coverage_baselines() -> dict:
"""ORCH-027: all per-repo coverage baselines for the GET /queue snapshot."""
conn = get_db()
try:
rows = conn.execute(
"SELECT repo, coverage, source_sha, updated_at FROM coverage_baseline"
).fetchall()
finally:
conn.close()
return {
r["repo"]: {
"coverage": r["coverage"],
"source_sha": r["source_sha"],
"updated_at": r["updated_at"],
}
for r in rows
}
def _ensure_column(conn, table: str, column: str, decl: str):
"""Add a column to `table` if it does not already exist (idempotent migration)."""
cols = [r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()]