validate.py python
314 lines 12.1 KB
Raw
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