validate.py
python
sha256:0e9549ec7b463911bc08b7d586dc320b1ac9b1f5c943ee7e3865dcc6cb0f6f83
chore(governance): sync handover+roadmap to 84db8c8 (drift:…
Human
2 days ago
| 1 | """Fail-closed docs integrity harness for Track O product contracts (§O0.8 / O3 retarget). |
| 2 | |
| 3 | Validates the normative product contract + consumer stubs without rewriting files, |
| 4 | opening network sockets, or walking the filesystem outside declared relative paths. |
| 5 | After O3: Stage 3 points at O2 freeze + ``ok upgrade-regime``; one-click stays blocked |
| 6 | until §O2.6 (no deferred-to-O2 shipping claim). |
| 7 | """ |
| 8 | |
| 9 | from __future__ import annotations |
| 10 | |
| 11 | import re |
| 12 | from dataclasses import dataclass, field |
| 13 | from pathlib import Path |
| 14 | |
| 15 | # Declared pack paths (relative to kit root). Harness must not walk beyond these. |
| 16 | CONTRACT_REL = Path("docs/TRACK-O-NORMIE-CUSTODY-PRODUCT-CONTRACT.md") |
| 17 | SCOOLING_REL = Path("docs/consumers/scooling/OVERSEER-SETUP.md") |
| 18 | KNOWTATION_REL = Path("docs/consumers/knowtation/OVERSEER-SETUP.md") |
| 19 | RUNBOOK_REL = Path("docs/TRACK-O-STAGE3-UPGRADE-OPERATOR-RUNBOOK.md") |
| 20 | O2_FREEZE_REL = Path("docs/archive/phases/PHASE-TRACK-O-O2-STAGE3-UPGRADE-CEREMONY.md") |
| 21 | PACK_RELS: tuple[Path, ...] = (CONTRACT_REL, SCOOLING_REL, KNOWTATION_REL) |
| 22 | |
| 23 | STAGE_LABELS: tuple[str, ...] = ( |
| 24 | "Stage 1 — Start", |
| 25 | "Stage 2 — Work", |
| 26 | "Stage 3 — Optional GitHub backup", |
| 27 | "Stage 4 — Optional Knowtation bind", |
| 28 | ) |
| 29 | |
| 30 | # O3 ceremony markers (replaces O1 "deferred to Thinking O2" keywords). |
| 31 | STAGE3_CEREMONY_KEYWORDS: tuple[str, ...] = ( |
| 32 | "ok upgrade-regime", |
| 33 | "PHASE-TRACK-O-O2-STAGE3-UPGRADE-CEREMONY", |
| 34 | "one-click", |
| 35 | "Silent config edit", |
| 36 | ) |
| 37 | |
| 38 | # Backward-compatible alias for unit tests that still import the O1 name. |
| 39 | DEFERRED_CEREMONY_KEYWORDS = STAGE3_CEREMONY_KEYWORDS |
| 40 | |
| 41 | REJECTION_KEYWORDS: tuple[str, ...] = ( |
| 42 | "Require Scooling signup before `ok init`", |
| 43 | "Require MuseHub for baseline", |
| 44 | "Silent `vcs.regime` edit", |
| 45 | "Stage 3 one-click backup before O2", |
| 46 | ) |
| 47 | |
| 48 | KIT_OWNS_LANGUAGE = "`ok init` / regimes / adapters" |
| 49 | MUSEHUB_OPTIONAL = "no MuseHub-only baseline" |
| 50 | SILENT_REGIME_REJECTION = "Silent `vcs.regime` edit for Stage 3" |
| 51 | OPERATOR_GATED = "operator-gated" |
| 52 | |
| 53 | # Forbidden residual O1 shipping deferral language after O3 retarget. |
| 54 | STAGE3_FORBIDDEN_DEFERRED_MARKERS: tuple[str, ...] = ( |
| 55 | "deferred to Thinking O2", |
| 56 | "deferred to O2", |
| 57 | "coming soon / operator-assisted", |
| 58 | ) |
| 59 | |
| 60 | # Required pointers after O3. |
| 61 | STAGE3_REQUIRED_MARKERS: tuple[str, ...] = ( |
| 62 | "ok upgrade-regime", |
| 63 | "PHASE-TRACK-O-O2-STAGE3-UPGRADE-CEREMONY", |
| 64 | ) |
| 65 | |
| 66 | # Forbidden: absolute machine paths that look like /Users/... or Windows drive roots. |
| 67 | ABS_MACHINE_PATH_RE = re.compile( |
| 68 | r"(?:/Users/|/home/[a-zA-Z]|[A-Za-z]:\\Users\\)", |
| 69 | ) |
| 70 | # Heuristic secret-assignment patterns (aligned with landing validator). |
| 71 | SECRET_ASSIGNMENT_RE = re.compile( |
| 72 | r"(?i)(?:api[_-]?key|secret|password|token)\s*[:=]\s*['\"][^'\"]{8,}['\"]", |
| 73 | ) |
| 74 | SECRET_BLOB_PATTERNS: tuple[re.Pattern[str], ...] = ( |
| 75 | re.compile(r"AKIA[0-9A-Z]{16}"), |
| 76 | re.compile(r"sk-[a-zA-Z0-9]{20,}"), |
| 77 | re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"), |
| 78 | SECRET_ASSIGNMENT_RE, |
| 79 | ) |
| 80 | |
| 81 | # Claims that Stage 3 one-click shipping is already done (fail-closed). |
| 82 | ONE_CLICK_SHIPPED_RE = re.compile( |
| 83 | r"(?i)stage\s*3.*one[- ]click.*(?:shipped|available|live|enabled)", |
| 84 | ) |
| 85 | |
| 86 | MINIMAL_VALID_CONTRACT = """# Track O — Normie custody product contract |
| 87 | |
| 88 | ## Stages 1–4 (normie path) |
| 89 | |
| 90 | ### Stage 1 — Start |
| 91 | Preferred muse-only. |
| 92 | |
| 93 | ### Stage 2 — Work |
| 94 | Living docs. |
| 95 | |
| 96 | ### Stage 3 — Optional GitHub backup |
| 97 | Kit ceremony: docs/archive/phases/PHASE-TRACK-O-O2-STAGE3-UPGRADE-CEREMONY.md and ok upgrade-regime. |
| 98 | Products must not ship one-click until §O2.6. |
| 99 | Silent config edit of only vcs.regime is forbidden. |
| 100 | |
| 101 | ### Stage 4 — Optional Knowtation bind |
| 102 | Knowtation owns vault bytes. |
| 103 | |
| 104 | ## Boundary table (kit vs products) |
| 105 | |
| 106 | | Concern | Overseer Kit | Scooling | Knowtation | MuseHub | |
| 107 | | --- | --- | --- | --- | --- | |
| 108 | | `ok init` / regimes / adapters | **Owns** | Consumes | Consumes | substrate | |
| 109 | |
| 110 | **K7 MuseHub-optional guardrail:** no MuseHub-only baseline. |
| 111 | |
| 112 | ## Rejection table |
| 113 | |
| 114 | | Proposal | Verdict | |
| 115 | | --- | --- | |
| 116 | | Require Scooling signup before `ok init` | **Reject** | |
| 117 | | Require MuseHub for baseline | **Reject** (K7) | |
| 118 | | Silent `vcs.regime` edit for Stage 3 without footprint re-seed | **Reject** | |
| 119 | | Product ships Stage 3 one-click backup before O2 kit ceremony freeze | **Reject** | |
| 120 | """ |
| 121 | |
| 122 | |
| 123 | @dataclass |
| 124 | class ValidationResult: |
| 125 | """Outcome of ``validate_track_o_pack``.""" |
| 126 | |
| 127 | ok: bool |
| 128 | errors: list[str] = field(default_factory=list) |
| 129 | |
| 130 | def add(self, code: str, detail: str) -> None: |
| 131 | self.errors.append(f"{code}: {detail}") |
| 132 | self.ok = False |
| 133 | |
| 134 | |
| 135 | def _resolve_under_root(kit_root: Path, rel: Path) -> Path | None: |
| 136 | """Resolve ``rel`` under ``kit_root``; return None on path escape.""" |
| 137 | root = kit_root.resolve() |
| 138 | candidate = (root / rel).resolve() |
| 139 | try: |
| 140 | candidate.relative_to(root) |
| 141 | except ValueError: |
| 142 | return None |
| 143 | return candidate |
| 144 | |
| 145 | |
| 146 | def _read_text(path: Path) -> str: |
| 147 | return path.read_text(encoding="utf-8") |
| 148 | |
| 149 | |
| 150 | def check_stage_labels(text: str, result: ValidationResult, *, label: str = "contract") -> None: |
| 151 | """Require Stage 1–4 labels (Start / Work / GitHub backup / Knowtation bind).""" |
| 152 | for stage in STAGE_LABELS: |
| 153 | if stage not in text: |
| 154 | result.add("missing_stage", f"{label}: missing {stage!r}") |
| 155 | |
| 156 | |
| 157 | def check_stage3_ceremony(text: str, result: ValidationResult, *, label: str = "contract") -> None: |
| 158 | """Require O3 Stage 3 ceremony keywords (O2 freeze + ok upgrade-regime).""" |
| 159 | for keyword in STAGE3_CEREMONY_KEYWORDS: |
| 160 | if keyword not in text: |
| 161 | result.add("missing_stage3", f"{label}: missing {keyword!r}") |
| 162 | |
| 163 | |
| 164 | def check_deferred_ceremony(text: str, result: ValidationResult, *, label: str = "contract") -> None: |
| 165 | """Alias for ``check_stage3_ceremony`` (O1 harness name retained for unit imports).""" |
| 166 | check_stage3_ceremony(text, result, label=label) |
| 167 | |
| 168 | |
| 169 | def check_rejection_keywords(text: str, result: ValidationResult, *, label: str = "contract") -> None: |
| 170 | """Require core rejection-table keywords.""" |
| 171 | for keyword in REJECTION_KEYWORDS: |
| 172 | if keyword not in text: |
| 173 | result.add("missing_rejection", f"{label}: missing {keyword!r}") |
| 174 | |
| 175 | |
| 176 | def check_no_abs_machine_paths(text: str, result: ValidationResult, *, label: str) -> None: |
| 177 | if ABS_MACHINE_PATH_RE.search(text): |
| 178 | result.add("abs_path", f"{label}: absolute machine path detected") |
| 179 | |
| 180 | |
| 181 | def check_no_secret_patterns(text: str, result: ValidationResult, *, label: str) -> None: |
| 182 | for pattern in SECRET_BLOB_PATTERNS: |
| 183 | if pattern.search(text): |
| 184 | result.add("secret_leak", f"{label}: matches {pattern.pattern}") |
| 185 | |
| 186 | |
| 187 | def check_boundary_kit_owns(text: str, result: ValidationResult, *, label: str = "contract") -> None: |
| 188 | if KIT_OWNS_LANGUAGE not in text: |
| 189 | result.add("missing_boundary", f"{label}: missing kit-owns language {KIT_OWNS_LANGUAGE!r}") |
| 190 | if "**Owns**" not in text: |
| 191 | result.add("missing_boundary", f"{label}: missing **Owns** ownership marker") |
| 192 | |
| 193 | |
| 194 | def check_not_one_click_shipped(text: str, result: ValidationResult, *, label: str = "contract") -> None: |
| 195 | if ONE_CLICK_SHIPPED_RE.search(text): |
| 196 | result.add("one_click_shipped", f"{label}: claims Stage 3 one-click shipped without O2") |
| 197 | |
| 198 | |
| 199 | def check_stage3_not_deferred_shipping(text: str, result: ValidationResult, *, label: str) -> None: |
| 200 | """Fail if Stage 3 still uses O1 'deferred to O2' / 'coming soon' shipping language.""" |
| 201 | for marker in STAGE3_FORBIDDEN_DEFERRED_MARKERS: |
| 202 | if marker in text: |
| 203 | result.add("stage3_stale_deferral", f"{label}: stale marker {marker!r}") |
| 204 | |
| 205 | |
| 206 | def check_stage3_points_at_ceremony(text: str, result: ValidationResult, *, label: str) -> None: |
| 207 | for marker in STAGE3_REQUIRED_MARKERS: |
| 208 | if marker not in text: |
| 209 | result.add("stage3_missing_pointer", f"{label}: missing {marker!r}") |
| 210 | |
| 211 | |
| 212 | def validate_contract_text(text: str, result: ValidationResult, *, label: str = "contract") -> None: |
| 213 | """Unit-level checks against a product-contract body (fixtures or real doc).""" |
| 214 | check_stage_labels(text, result, label=label) |
| 215 | check_stage3_ceremony(text, result, label=label) |
| 216 | check_rejection_keywords(text, result, label=label) |
| 217 | check_boundary_kit_owns(text, result, label=label) |
| 218 | check_not_one_click_shipped(text, result, label=label) |
| 219 | check_no_abs_machine_paths(text, result, label=label) |
| 220 | check_no_secret_patterns(text, result, label=label) |
| 221 | check_stage3_not_deferred_shipping(text, result, label=label) |
| 222 | check_stage3_points_at_ceremony(text, result, label=label) |
| 223 | if MUSEHUB_OPTIONAL not in text: |
| 224 | result.add("missing_k7", f"{label}: missing {MUSEHUB_OPTIONAL!r}") |
| 225 | if SILENT_REGIME_REJECTION not in text: |
| 226 | result.add("missing_silent_reject", f"{label}: missing {SILENT_REGIME_REJECTION!r}") |
| 227 | |
| 228 | |
| 229 | def validate_consumer_stub( |
| 230 | text: str, |
| 231 | result: ValidationResult, |
| 232 | *, |
| 233 | label: str, |
| 234 | require_track_o_pointer: bool = True, |
| 235 | ) -> None: |
| 236 | """Checks shared by Scooling + Knowtation consumer stubs.""" |
| 237 | if OPERATOR_GATED not in text: |
| 238 | result.add("missing_operator_gate", f"{label}: live init must remain operator-gated") |
| 239 | check_no_abs_machine_paths(text, result, label=label) |
| 240 | check_no_secret_patterns(text, result, label=label) |
| 241 | if require_track_o_pointer and "TRACK-O-NORMIE-CUSTODY-PRODUCT-CONTRACT" not in text: |
| 242 | result.add("missing_track_o_link", f"{label}: missing Track O product-contract pointer") |
| 243 | |
| 244 | |
| 245 | def validate_track_o_pack(kit_root: Path) -> ValidationResult: |
| 246 | """Validate the real Track O contract pack under ``kit_root`` (fail-closed). |
| 247 | |
| 248 | Only opens the declared relative paths. Does not rewrite any file. |
| 249 | """ |
| 250 | result = ValidationResult(ok=True) |
| 251 | root = kit_root.resolve() |
| 252 | |
| 253 | texts: dict[str, str] = {} |
| 254 | for rel in PACK_RELS: |
| 255 | resolved = _resolve_under_root(root, rel) |
| 256 | if resolved is None: |
| 257 | result.add("path_escape", f"{rel} escapes kit root") |
| 258 | continue |
| 259 | if not resolved.is_file(): |
| 260 | result.add("missing_file", str(rel)) |
| 261 | continue |
| 262 | try: |
| 263 | texts[str(rel)] = _read_text(resolved) |
| 264 | except OSError as exc: |
| 265 | result.add("read_error", f"{rel}: {exc}") |
| 266 | |
| 267 | contract_key = str(CONTRACT_REL) |
| 268 | if contract_key in texts: |
| 269 | validate_contract_text(texts[contract_key], result, label=contract_key) |
| 270 | |
| 271 | scooling_key = str(SCOOLING_REL) |
| 272 | if scooling_key in texts: |
| 273 | validate_consumer_stub(texts[scooling_key], result, label=scooling_key) |
| 274 | body = texts[scooling_key] |
| 275 | if "optional" not in body.lower() or "scooling" not in body.lower(): |
| 276 | result.add( |
| 277 | "scooling_mandatory", |
| 278 | f"{scooling_key}: must keep Scooling optional for kit custody", |
| 279 | ) |
| 280 | if "never required" not in body.lower() and "optional** entry" not in body and "optional entry" not in body.lower(): |
| 281 | result.add( |
| 282 | "scooling_mandatory", |
| 283 | f"{scooling_key}: must state Scooling is optional / not required", |
| 284 | ) |
| 285 | check_stage3_not_deferred_shipping(body, result, label=scooling_key) |
| 286 | if "ok upgrade-regime" not in body and "O2" not in body and "STAGE3" not in body.upper(): |
| 287 | # Scooling must point at Stage 3 ceremony / unlock, not claim deferred shipping. |
| 288 | if "PHASE-TRACK-O-O2" not in body and "upgrade-regime" not in body: |
| 289 | result.add( |
| 290 | "stage3_missing_pointer", |
| 291 | f"{scooling_key}: missing Stage 3 ceremony / upgrade-regime pointer", |
| 292 | ) |
| 293 | |
| 294 | knowtation_key = str(KNOWTATION_REL) |
| 295 | if knowtation_key in texts: |
| 296 | validate_consumer_stub(texts[knowtation_key], result, label=knowtation_key) |
| 297 | body = texts[knowtation_key] |
| 298 | if "Stage 4" not in body: |
| 299 | result.add("missing_stage4", f"{knowtation_key}: Stage 4 pointer missing") |
| 300 | |
| 301 | runbook = _resolve_under_root(root, RUNBOOK_REL) |
| 302 | if runbook is None: |
| 303 | result.add("path_escape", f"{RUNBOOK_REL} escapes kit root") |
| 304 | elif not runbook.is_file(): |
| 305 | result.add("missing_file", str(RUNBOOK_REL)) |
| 306 | |
| 307 | return result |
| 308 | |
| 309 | |
| 310 | def validate_contract_fixture(text: str) -> ValidationResult: |
| 311 | """Validate a fixture contract body (integration tests) — same unit checks.""" |
| 312 | result = ValidationResult(ok=True) |
| 313 | validate_contract_text(text, result, label="fixture") |
| 314 | return result |
File History
1 commit
sha256:6abcf1fa82a7a621ccbc945f19acdba5bc0db54569599404a1452fb4a096a199
fix(ISR): default require_independent_second_reviewer to require
Human
minor
⚠
2 days ago