check.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """Fail-closed footprint self-integrity hard gate (§KH3.4). |
| 2 | |
| 3 | Detects the precise, narrow condition that motivated this module: a kit-owned |
| 4 | file is *declared* in ``.overseer/version.lock`` (``origin`` other than |
| 5 | ``preserved``) but is completely *absent* from the working tree — the exact |
| 6 | gap that let 13 self-vendored files (``.cursor/rules/*``, |
| 7 | ``.cursor/skills/*/SKILL.md``, ``.overseer/policy/*.yaml``, |
| 8 | ``.overseer/STANDING-DECISIONS.reference.md``) go unrendered on this very |
| 9 | repo for three days without any automated check ever objecting. |
| 10 | |
| 11 | This gate checks strictly against what ``version.lock`` **already records** |
| 12 | as installed — never against a fresh ``resolve_footprint`` re-render of the |
| 13 | current kit templates. That distinction matters: a kit template that has |
| 14 | never been through a completed ``overseer sync`` yet is a *drift* condition |
| 15 | (already covered by the existing ``overseer status`` drift check), not a |
| 16 | "declared but missing" condition — only entries the lock itself already |
| 17 | promised are in scope here. It also deliberately never inspects file |
| 18 | *content* — only existence. A kit-owned file whose content differs from its |
| 19 | recorded lock hash is a different, softer condition (stale hash, upstream |
| 20 | template change, or an unmarked customization) that stays on the existing |
| 21 | opt-in ``overseer status --check-footprint`` content-digest path; see |
| 22 | §KH3.3 for why conflating the two would risk false-closing this gate for any |
| 23 | consumer repo with a legitimate, not-yet-``preserved`` drift. |
| 24 | """ |
| 25 | |
| 26 | from __future__ import annotations |
| 27 | |
| 28 | from dataclasses import dataclass |
| 29 | from pathlib import Path |
| 30 | |
| 31 | from cli.version_lock import ( |
| 32 | ORIGIN_PRESERVED, |
| 33 | LockError, |
| 34 | VersionLock, |
| 35 | entry_origin, |
| 36 | lock_path, |
| 37 | read_version_lock, |
| 38 | ) |
| 39 | |
| 40 | REMEDIATION = "ok sync" |
| 41 | |
| 42 | |
| 43 | @dataclass(frozen=True) |
| 44 | class FootprintIntegrityReport: |
| 45 | """Result of a self-footprint existence probe — no shell, no content reads.""" |
| 46 | |
| 47 | state: str # ok | missing | unreadable | not_applicable |
| 48 | message: str |
| 49 | remediation: str | None |
| 50 | missing: tuple[str, ...] = () |
| 51 | |
| 52 | @property |
| 53 | def ok(self) -> bool: |
| 54 | return self.state in {"ok", "not_applicable"} |
| 55 | |
| 56 | |
| 57 | def check_footprint_integrity( |
| 58 | repo_root: Path, |
| 59 | *, |
| 60 | lock: VersionLock | None = None, |
| 61 | ) -> FootprintIntegrityReport: |
| 62 | """Resolve a ``FootprintIntegrityReport`` for the current on-disk state. |
| 63 | |
| 64 | Pure function of ``(repo_root filesystem state, version.lock contents)`` |
| 65 | at call time (§KH3.8 data-integrity) — every non-``preserved`` entry |
| 66 | already declared in ``version.lock`` is checked with a single |
| 67 | ``Path.is_file()``; any absence fails closed as ``missing``. A lock that |
| 68 | exists but cannot be parsed also fails closed as ``unreadable`` rather |
| 69 | than optimistically reporting ``ok``. A repo with no ``version.lock`` at |
| 70 | all has nothing declared yet, so it is vacuously ``not_applicable`` |
| 71 | rather than a failure. |
| 72 | |
| 73 | ``lock`` is an optional pre-computed value — callers that already loaded |
| 74 | it for another purpose (``overseer status``) pass it through to avoid a |
| 75 | second read (§KH3.5); callers with no prior value (``overseer review |
| 76 | --freeze``, ``overseer governance-sync``) simply omit it and this |
| 77 | function reads it itself. |
| 78 | """ |
| 79 | if lock is None: |
| 80 | path = lock_path(repo_root) |
| 81 | if not path.is_file(): |
| 82 | return FootprintIntegrityReport( |
| 83 | state="not_applicable", |
| 84 | message="no version.lock yet — nothing declared to check", |
| 85 | remediation=None, |
| 86 | ) |
| 87 | try: |
| 88 | lock = read_version_lock(path) |
| 89 | except LockError as exc: |
| 90 | return FootprintIntegrityReport( |
| 91 | state="unreadable", |
| 92 | message=f"could not read version.lock — failing closed: {exc}", |
| 93 | remediation="ok init", |
| 94 | ) |
| 95 | |
| 96 | missing = tuple( |
| 97 | sorted( |
| 98 | entry.path |
| 99 | for entry in lock.footprint |
| 100 | if entry_origin(entry) != ORIGIN_PRESERVED |
| 101 | and not (repo_root / entry.path).is_file() |
| 102 | ) |
| 103 | ) |
| 104 | |
| 105 | if missing: |
| 106 | listed = ", ".join(missing) |
| 107 | return FootprintIntegrityReport( |
| 108 | state="missing", |
| 109 | message=( |
| 110 | f"{len(missing)} kit-owned footprint file(s) declared in " |
| 111 | f"version.lock are absent from disk: {listed}" |
| 112 | ), |
| 113 | remediation=REMEDIATION, |
| 114 | missing=missing, |
| 115 | ) |
| 116 | |
| 117 | return FootprintIntegrityReport( |
| 118 | state="ok", |
| 119 | message="all kit-owned footprint files declared in version.lock are present", |
| 120 | remediation=None, |
| 121 | ) |