oc_chat
Loading...
Searching...
No Matches
invariant_matrix.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/invariant_matrix.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_invariant_matrix.py"
6# OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
7"""
8The V0_3(.1) invariant-preservation matrix generator (Plan 142 P1.5, §5.3.1).
9
10Purpose
11-------
12Close the ``regenerate``-bypass class by construction for the DDL half and
13machine-gate it against drift for the application-layer half (Registry Source &
14Storage Spec V0.22 §5.3.1 / AC §8.49). ``regenerate`` builds the temp DB by
15**direct insert**, not through ``execute_domain_command``, so every
16application-layer-only invariant is silently bypassed on rebuild and must be
17independently handled. This module builds the **two machine-extracted
18inventories** that anchor the matrix:
19
201. A **DDL-constraint inventory** parsed from the consolidated
21 ``REGISTRY_SCHEMA_V0_3_1.sql`` (the post-amendment baseline) plus the
22 ``registry_meta.store_scope`` constraint emitted by
23 ``v0_3_2_roadmap_source_fields.migration_sql()`` — one stable-ID record for
24 every enforceable constraint (CHECK / unique index / ``UNIQUE`` / PK / FK /
25 ``NOT NULL`` / trigger); plain non-unique performance indexes are excluded.
262. An **application-layer ``@invariant`` inventory** extracted from
27 ``@invariant: <id>`` markers in the ``REGISTRY_ROADMAP_SCHEMA_V0_3_1``
28 amendment doc (HTML-comment + SQL-fence forms) and the rebuild enforcement
29 code (code-comment form).
30
31Each inventory ID is classified exactly once (``D``/``R``/``X``/``NA``) by the
32authored :data:`CLASSIFICATION` map; :func:`generate_matrix` joins the parsed
33inventories to that map and applies the D-safety predicate, so the test suite
34(``test_invariant_matrix.py``) can assert correctness, the two mutation gates,
35the ``{code} ⊆ {matrix}`` drift gate, and the reverse-inclusion R gate.
36
37Why ``.1.sql`` (not ``V0_3.sql`` + ``.1.sql``)
38----------------------------------------------
39AC §8.49 names "``REGISTRY_SCHEMA_V0_3.sql`` + the consolidated
40``REGISTRY_SCHEMA_V0_3_1.sql``". The consolidated ``.1.sql`` is, by its own
41header, the fresh-create state **after** applying ``V0_3.sql`` then the
42``v0_3_2`` migration — i.e. a superset of every ``V0_3`` roadmap constraint with
43the amendments folded in. Parsing both would double-count (duplicate stable
44IDs); parsing the consolidated baseline alone covers every ``V0_3`` constraint
45with no loss. The one constraint the consolidated ``.1.sql`` omits is the base
46``registry_meta.store_scope`` (a base-capability ALTER, not part of the roadmap
47snapshot — umbrella §12 wave-1 follow-up), which is recovered from
48``migration_sql()``.
49
50Responsibilities
51----------------
52- :func:`parse_ddl_inventory` — parse a ``.sql`` file into ``DdlConstraint`` records.
53- :func:`extract_markers` — extract ``@invariant: <id>`` markers from md/sql/py text.
54- :func:`generate_matrix` — join inventories to :data:`CLASSIFICATION`; return a
55 :class:`Matrix` with the first-class derive-verify rows + the validity hashes.
56- :func:`build_audit_payload` — the §9.1 audit ``.json`` payload (counts + hashes).
57
58Diagnostics
59-----------
60Domain: OC_CHAT.MAIN
61Levels:
62 L2 - generation lifecycle
63 L3 - inventory + classification joins
64 L4 - per-constraint parse tracing
65
66Contracts
67---------
68- Pure / read-only: parses files + the authored classification; never mutates them.
69- Deterministic: inventories + matrix rows are order-stable (sorted by stable ID).
70
71Dependencies
72------------
73- oc_diagnostics (structured debug logging via _dbg)
74- oc_chat.registry.migrations.v0_3_2_roadmap_source_fields (store_scope DDL)
75"""
76from __future__ import annotations
77
78import hashlib
79import re
80from dataclasses import dataclass, field
81from pathlib import Path
82from typing import Optional
83
84from oc_diagnostics import _dbg
85
86from oc_chat.registry.migrations import v0_3_2_roadmap_source_fields
87
88_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
89
90# --- Repository anchors ------------------------------------------------------
91_ORCH = Path(__file__).resolve().parents[1] / "docs" / "architecture" / "orchestration"
92#: The consolidated post-amendment DDL baseline (the matrix's ``.1.sql`` input).
93SCHEMA_V0_3_1_SQL = _ORCH / "REGISTRY_SCHEMA_V0_3_1.sql"
94#: The live aggregating amendment doc carrying the prose ``@invariant`` markers.
95V0_3_1_DOC = _ORCH / "REGISTRY_ROADMAP_SCHEMA_V0_3_1.md"
96
97_REGEN = Path(__file__).resolve().parent / "regenerate"
98#: The rebuild enforcement code the matrix tags + drift-gates ({code} ⊆ {matrix}).
99#: These are the §5.3 source-validation / derive-verify sites; the live
100#: ``operations.py`` path is deliberately OUT of the matrix (§5.3.1 Codex V18 F3:
101#: the matrix is scoped to ``store_scope='authored_projection'`` — live commands
102#: are governed by the ``store_scope`` guard, not this matrix).
103ENFORCEMENT_CODE_FILES: tuple[Path, ...] = (
104 _REGEN / "cross_record.py",
105 _REGEN / "derive_verify.py",
106 _REGEN / "build.py",
107 _REGEN / "pipeline.py",
108)
109
110# --- Table populations (§5.3.1 D-safety predicate) --------------------------
111#: Tables the authored rebuild writes (a constraint on one of these is exercised).
112SOURCE_POPULATED_TABLES: frozenset[str] = frozenset({
113 "roadmap_eras", "roadmap_versions", "roadmap_phases", "roadmap_tracks",
114 "roadmap_items_v3", "roadmap_item_tracks", "roadmap_item_dependencies",
115 "roadmap_render_sections",
116})
117#: Tables the authored rebuild never writes → every constraint on them is NA.
118NEVER_WRITTEN_TABLES: frozenset[str] = frozenset({
119 "roadmap_item_events", "roadmap_plan_links",
120})
121#: Runtime-only FK targets — a FK into one fails D-safety predicate (i) → X/NA.
122RUNTIME_TABLES: frozenset[str] = frozenset({
123 "agents", "rounds", "plans", "operation_log",
124})
125
126#: Classification tokens (§5.3.1).
127VALID_CLASSES: frozenset[str] = frozenset({"D", "R", "X", "NA"})
128#: The three admissible D-safety witnesses (§5.3.1 / AC §8.49).
129WITNESS_TYPES: frozenset[str] = frozenset({
130 "source_validation_rule", "projection_fill_rule", "derive_verify_rule",
131})
132
133
134# ===========================================================================
135# Data model
136# ===========================================================================
137@dataclass(frozen=True)
139 """One enforceable DDL constraint with a stable ID (the CI anchor)."""
140
141 stable_id: str
142 table: str
143 kind: str # pk | unique_col | unique_index | fk | notnull | check | trigger
144 detail: str # human-readable expression / column / target
145 target_table: Optional[str] = None # for fk
146 partial: bool = False # for a partial unique index
147
148
149@dataclass(frozen=True)
150class Marker:
151 """One extracted ``@invariant: <id>`` marker and where it was found."""
152
153 invariant_id: str
154 source: str # the file (rel) the marker was extracted from
155 form: str # html | sql | code
156
157
158@dataclass(frozen=True)
160 """An authored classification of one inventory ID (the human-readable view,
161 machine-precise). The ``test_v0_3_invariant_matrix_complete`` predicate runs
162 over these."""
163
164 classification: str # D | R | X | NA
165 handling: str # one-line rationale
166 # D rows name the witness that makes the violation unreachable:
167 witness_type: Optional[str] = None # source_validation_rule | projection_fill_rule | derive_verify_rule
168 witness_id: Optional[str] = None
169 # R rows bind the source-validation assertion that rejects the violation:
170 source_validation_rule: Optional[str] = None # the @invariant id on the assertion
171 acceptance_criterion: Optional[str] = None # the V0.22 §8 AC #
172 failure_row: Optional[str] = None # the §10 SRC_*/REGEN_* code + test
173 negative_fixture: Optional[str] = None # the violating-source test
174 # X rows name the V0_3_1 ratification rationale:
175 ratification: Optional[str] = None
176 # derive_verify D rows are first-class in the generated inventory (§5.3.1):
177 derive_rule_id: Optional[str] = None
178 source_fields: tuple[str, ...] = ()
179 stored_columns: tuple[str, ...] = ()
180 verify_phase: Optional[str] = None
181 read_barrier: Optional[str] = None
182 positive_fixtures: tuple[str, ...] = ()
183 error_code: Optional[str] = None
184
185
186@dataclass(frozen=True)
187class Matrix:
188 """The generated matrix: the joined inventories + their classifications."""
189
190 ddl: tuple[DdlConstraint, ...]
191 doc_markers: tuple[Marker, ...]
192 code_markers: tuple[Marker, ...]
193 rows: dict[str, MatrixRow] # stable_id -> MatrixRow (every inventory id)
194 constraint_inventory_hash: str
195 app_invariant_inventory_hash: str
196
197
198# ===========================================================================
199# DDL inventory parser
200# ===========================================================================
201_CREATE_TABLE_RE = re.compile(
202 r"CREATE TABLE(?: IF NOT EXISTS)?\s+(\w+)\s*\‍((.*?)\n\‍)\s*;",
203 re.DOTALL | re.IGNORECASE,
204)
205_CREATE_UNIQUE_INDEX_RE = re.compile(
206 r"CREATE\s+UNIQUE\s+INDEX(?: IF NOT EXISTS)?\s+(\w+)\s+ON\s+(\w+)\s*\‍(([^)]*)\‍)"
207 r"(?:\s+WHERE\s+(.+?))?\s*;",
208 re.DOTALL | re.IGNORECASE,
209)
210_CREATE_TRIGGER_RE = re.compile(
211 r"CREATE\s+TRIGGER(?: IF NOT EXISTS)?\s+(\w+)\s+(BEFORE|AFTER)\s+(\w+)\s+ON\s+(\w+)",
212 re.IGNORECASE,
213)
214_ALTER_ADD_COLUMN_RE = re.compile(
215 r"ALTER TABLE\s+(\w+)\s+ADD COLUMN\s+(.+?);",
216 re.DOTALL | re.IGNORECASE,
217)
218
219
220def _slug(text: str) -> str:
221 """Deterministic readable slug from a SQL expression (stable across runs)."""
222 lowered = text.lower()
223 lowered = lowered.replace("!=", "_ne_").replace("=", "_eq_")
224 cleaned = re.sub(r"[^a-z0-9]+", "_", lowered).strip("_")
225 return re.sub(r"_+", "_", cleaned)
226
227
228def _split_table_body(body: str) -> list[str]:
229 """Split a CREATE TABLE body into top-level comma-separated clauses (paren-aware)."""
230 clauses: list[str] = []
231 depth = 0
232 current: list[str] = []
233 for char in body:
234 if char == "(":
235 depth += 1
236 elif char == ")":
237 depth -= 1
238 if char == "," and depth == 0:
239 clauses.append("".join(current).strip())
240 current = []
241 else:
242 current.append(char)
243 tail = "".join(current).strip()
244 if tail:
245 clauses.append(tail)
246 return clauses
247
248
249_SQL_RESERVED_CLAUSE_HEADS = ("CHECK", "PRIMARY", "UNIQUE", "FOREIGN", "CONSTRAINT")
250
251
252def _parse_column_clause(table: str, clause: str) -> list[DdlConstraint]:
253 """Parse a single column-definition clause into its constraints."""
254 out: list[DdlConstraint] = []
255 name = clause.split()[0]
256 upper = clause.upper()
257
258 if "PRIMARY KEY" in upper:
259 out.append(DdlConstraint(f"ddl:{table}:pk:{name}", table, "pk", name))
260 # A column-level UNIQUE (not the surrogate PK).
261 if re.search(r"\bUNIQUE\b", upper) and "PRIMARY KEY" not in upper:
262 out.append(DdlConstraint(f"ddl:{table}:unique_col:{name}", table, "unique_col", name))
263 # NOT NULL (PRIMARY KEY columns are implicitly NOT NULL; do not double-count).
264 if "NOT NULL" in upper and "PRIMARY KEY" not in upper:
265 out.append(DdlConstraint(f"ddl:{table}:notnull:{name}", table, "notnull", name))
266 # Column-level REFERENCES (foreign key).
267 ref = re.search(r"REFERENCES\s+(\w+)", clause, re.IGNORECASE)
268 if ref:
269 target = ref.group(1)
270 out.append(DdlConstraint(
271 f"ddl:{table}:fk:{name}->{target}", table, "fk", f"{name} -> {target}",
272 target_table=target,
273 ))
274 # Column-level CHECK (e.g. enum / range).
275 col_check = re.search(r"CHECK\s*\‍((.+)\‍)", clause, re.IGNORECASE | re.DOTALL)
276 if col_check:
277 expr = " ".join(col_check.group(1).split())
278 out.append(DdlConstraint(
279 f"ddl:{table}:check:{name}", table, "check", expr,
280 ))
281 return out
282
283
284def _parse_table_constraint_clause(table: str, clause: str) -> list[DdlConstraint]:
285 """Parse a table-level clause (composite PK, table CHECK, table FK)."""
286 out: list[DdlConstraint] = []
287 upper = clause.upper()
288 if upper.startswith("PRIMARY KEY"):
289 cols = re.search(r"PRIMARY KEY\s*\‍((.+)\‍)", clause, re.IGNORECASE)
290 col_text = _slug(cols.group(1)) if cols else "composite"
291 out.append(DdlConstraint(f"ddl:{table}:pk:{col_text}", table, "pk", clause.strip()))
292 elif upper.startswith("CHECK"):
293 expr_m = re.search(r"CHECK\s*\‍((.+)\‍)\s*$", clause, re.IGNORECASE | re.DOTALL)
294 expr = " ".join((expr_m.group(1) if expr_m else clause).split())
295 out.append(DdlConstraint(
296 f"ddl:{table}:check:{_slug(expr)}", table, "check", expr,
297 ))
298 elif upper.startswith("UNIQUE"):
299 cols = re.search(r"UNIQUE\s*\‍((.+)\‍)", clause, re.IGNORECASE)
300 col_text = _slug(cols.group(1)) if cols else "cols"
301 out.append(DdlConstraint(
302 f"ddl:{table}:unique_cols:{col_text}", table, "unique_index", clause.strip(),
303 ))
304 elif upper.startswith("FOREIGN KEY"):
305 fk = re.search(r"FOREIGN KEY\s*\‍((.+?)\‍)\s*REFERENCES\s+(\w+)", clause, re.IGNORECASE)
306 if fk:
307 col_text = _slug(fk.group(1))
308 target = fk.group(2)
309 out.append(DdlConstraint(
310 f"ddl:{table}:fk:{col_text}->{target}", table, "fk",
311 clause.strip(), target_table=target,
312 ))
313 return out
314
315
316def _parse_create_tables(sql: str) -> list[DdlConstraint]:
317 out: list[DdlConstraint] = []
318 for match in _CREATE_TABLE_RE.finditer(sql):
319 table = match.group(1)
320 if table.endswith("_new"):
321 continue # the table-rebuild scratch table — not a published constraint
322 body = match.group(2)
323 for clause in _split_table_body(body):
324 if not clause:
325 continue
326 head = clause.split("(", 1)[0].strip().upper().split()
327 head_word = head[0] if head else ""
328 if head_word in _SQL_RESERVED_CLAUSE_HEADS:
329 out.extend(_parse_table_constraint_clause(table, clause))
330 else:
331 out.extend(_parse_column_clause(table, clause))
332 return out
333
334
335def _parse_indexes_and_triggers(sql: str) -> list[DdlConstraint]:
336 out: list[DdlConstraint] = []
337 for match in _CREATE_UNIQUE_INDEX_RE.finditer(sql):
338 index_name, table, cols, where = match.groups()
339 if table.endswith("_new"):
340 continue
341 partial = where is not None
342 out.append(DdlConstraint(
343 f"ddl:{table}:unique_index:{index_name}", table, "unique_index",
344 f"{index_name}({' '.join(cols.split())})"
345 + (f" WHERE {' '.join(where.split())}" if partial else ""),
346 partial=partial,
347 ))
348 for match in _CREATE_TRIGGER_RE.finditer(sql):
349 trigger_name, _timing, _event, table = match.groups()
350 if table.endswith("_new"):
351 continue
352 out.append(DdlConstraint(
353 f"ddl:{table}:trigger:{trigger_name}", table, "trigger", trigger_name,
354 ))
355 return out
356
357
358def _parse_store_scope() -> list[DdlConstraint]:
359 """Recover ``registry_meta.store_scope`` from the v0_3_2 migration ALTER (the
360 consolidated ``.1.sql`` omits the base ``registry_meta`` table — umbrella §12)."""
361 out: list[DdlConstraint] = []
362 sql = v0_3_2_roadmap_source_fields.migration_sql()
363 for match in _ALTER_ADD_COLUMN_RE.finditer(sql):
364 table, coldef = match.groups()
365 if table != "registry_meta":
366 continue
367 # The store_scope ALTER is a single column def; reuse the column parser.
368 out.extend(_parse_column_clause(table, " ".join(coldef.split())))
369 return out
370
371
373 sql_path: Path = SCHEMA_V0_3_1_SQL,
374 *,
375 include_store_scope: bool = True,
376) -> list[DdlConstraint]:
377 """Parse ``sql_path`` (+ the ``registry_meta.store_scope`` migration ALTER)
378 into a deterministic, de-duplicated DDL-constraint inventory."""
379 sql = sql_path.read_text(encoding="utf-8")
380 constraints = _parse_create_tables(sql) + _parse_indexes_and_triggers(sql)
381 if include_store_scope:
382 constraints += _parse_store_scope()
383 # De-dup by stable id (a repeated constraint is the same invariant) + sort.
384 seen: dict[str, DdlConstraint] = {}
385 for constraint in constraints:
386 seen.setdefault(constraint.stable_id, constraint)
387 return sorted(seen.values(), key=lambda c: c.stable_id)
388
389
390# ===========================================================================
391# @invariant marker extraction
392# ===========================================================================
393# The id charset includes ``>`` so a marker may name a raw DDL stable id
394# (e.g. ``ddl:roadmap_versions:fk:source_round_no->rounds``).
395_HTML_MARKER_RE = re.compile(r"<!--\s*@invariant:\s*([A-Za-z0-9._:>-]+)\s*-->")
396_SQL_MARKER_RE = re.compile(r"--\s*@invariant:\s*([A-Za-z0-9._:>-]+)")
397# OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
398_CODE_MARKER_RE = re.compile(r"#\s*@invariant:\s*([A-Za-z0-9._:>-]+)")
399
400
401def extract_markers(text: str, *, form: str, source: str) -> list[Marker]:
402 """Extract ``@invariant: <id>`` markers from ``text`` in the given ``form``
403 (``html`` = Markdown prose, ``sql`` = SQL fence, ``code`` = Python comment)."""
404 if form == "html":
405 ids = _HTML_MARKER_RE.findall(text)
406 # SQL-fence form inside a Markdown doc is also accepted (§5.3.1 / AC §8.49).
407 ids += _SQL_MARKER_RE.findall(text)
408 elif form == "sql":
409 ids = _SQL_MARKER_RE.findall(text)
410 elif form == "code":
411 ids = _CODE_MARKER_RE.findall(text)
412 else: # pragma: no cover - guarded by callers
413 raise ValueError(f"unknown marker form {form!r}")
414 return [Marker(invariant_id=mid, source=source, form=form) for mid in ids]
415
416
417def extract_doc_markers(doc_path: Path = V0_3_1_DOC) -> list[Marker]:
418 """Extract the prose ``@invariant`` markers from the V0_3_1 amendment doc."""
419 return extract_markers(
420 doc_path.read_text(encoding="utf-8"), form="html", source=doc_path.name
421 )
422
423
424def extract_code_markers(files: tuple[Path, ...] = ENFORCEMENT_CODE_FILES) -> list[Marker]:
425 """Extract ``@invariant`` markers from the rebuild enforcement code."""
426 markers: list[Marker] = []
427 for path in files:
428 if not path.exists():
429 continue
430 markers.extend(extract_markers(
431 path.read_text(encoding="utf-8"), form="code", source=path.name
432 ))
433 return markers
434
435
436# ===========================================================================
437# Hashing (the audit's validity key — §9.1 item 1)
438# ===========================================================================
439def _hash_lines(lines: list[str]) -> str:
440 digest = hashlib.sha256()
441 for line in sorted(lines):
442 digest.update(line.encode("utf-8"))
443 digest.update(b"\n")
444 return digest.hexdigest()
445
446
447def constraint_inventory_hash(ddl: list[DdlConstraint]) -> str:
448 """A stable hash over the DDL inventory (stable IDs) — the stamp + audit key."""
449 return _hash_lines([c.stable_id for c in ddl])
450
451
452def app_invariant_inventory_hash(markers: list[Marker]) -> str:
453 """A stable hash over the extracted app-layer ``@invariant`` ids."""
454 return _hash_lines(sorted({m.invariant_id for m in markers}))
455
456
457# ===========================================================================
458# The witness registry (the D-safety predicate's mechanical half — §5.3.1)
459# ===========================================================================
460# Each D row names a witness that makes its violation unreachable from valid
461# authored source. The registry records the witness type + where it is enforced;
462# ``test_v0_3_invariant_matrix_complete`` asserts every D's witness is registered
463# (exists) and that derive-verify witnesses cover their stored columns.
464#
465# source_validation_rule witnesses live in the (P1.1) pydantic contracts /
466# (P1.3) loader and are NAMED here (P1.5 does not tag P1.1/P1.3 surfaces);
467# projection_fill_rule witnesses live in the (P1.4) builder; the single
468# derive_verify_rule witness is tagged in ``derive_verify.py``.
469WITNESS_REGISTRY: dict[str, tuple[str, str]] = {
470 # --- source_validation_rule (pydantic / loader — unreachable from valid source) ---
471 "sv.status_enum": ("source_validation_rule",
472 "option_c_registry_contracts.base.Status Literal (§7 enum)"),
473 "sv.priority_enum": ("source_validation_rule",
474 "option_c_registry_contracts.base.Priority Literal (§7 enum)"),
475 "sv.track_kind_enum": ("source_validation_rule",
476 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
477 "base.NonPhaseTrackKind + builder 'm_phase' (§7 enum)"),
478 "sv.dep_kind_enum": ("source_validation_rule",
479 "base.DepKind 3-token Literal ⊂ the 4-token DDL CHECK"),
480 "sv.lifecycle": ("source_validation_rule",
481 "base.check_item_lifecycle (self-ref, superseded⇒superseded_by, "
482 "successor⇒deferred)"),
483 "sv.stem_eq_id": ("source_validation_rule",
484 "canonical.load_record / pipeline._load_promoted stem==id "
485 "(filename-keyed natural id — a duplicate needs two same-named files)"),
486 "sv.required_fields": ("source_validation_rule",
487 "pydantic required strict types (RequiredLine / Line300 / "
488 "EntityId / IsoDate / int) — a NULL/missing authored value is rejected on load"),
489 # --- projection_fill_rule (P1.4 builder — byte-stable fill, never authored) ---
490 "pf.timestamps": ("projection_fill_rule",
491 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
492 "build.SOURCE_EPOCH fill for created_at/updated_at/last_updated_at (EXCLUDED)"),
493 "pf.created_by_slot": ("projection_fill_rule",
494 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
495 "build.SOURCE_SLOT 'source' placeholder NOT-NULL fill (FK half is X)"),
496 "pf.store_scope": ("projection_fill_rule",
497 "store_scope.stamp_authored_projection writes 'authored_projection'"),
498 "pf.parallel_track": ("projection_fill_rule", "build sets parallel_track=0 (no source field)"),
499 "pf.parent_item_id_derived": ("projection_fill_rule",
500 "parent_item_id set only from sub_phase nesting / promoted dir "
501 "(never authored) — a self-parent is unreachable"),
502 "pf.phase_parent_null": ("projection_fill_rule",
503 "build always sets parent_phase_id=NULL (v1 has no nested phases)"),
504 "pf.surrogate_pk": ("projection_fill_rule",
505 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
506 "SQLite AUTOINCREMENT assigns the surrogate phase_id/track_id/id"),
507 "pf.item_phase_id": ("projection_fill_rule",
508 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
509 "phase fan-out sets phase_id for every m_phase row; non-phase items get NULL "
510 "(no source phase_id field) — C17/C18 unreachable"),
511 "pf.insert_or_ignore": ("projection_fill_rule",
512 "build INSERT OR IGNORE dedups the composite membership/dependency PK"),
513 "pf.track_id_resolved": ("projection_fill_rule",
514 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
515 "build resolves track_code→track_id (SRC_BAD_TRACK_CODES if unknown); "
516 "the surrogate is never NULL"),
517 "pf.render_sections": ("projection_fill_rule",
518 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
519 "render_sections.seed_render_sections deterministic builder-fill from "
520 "the constant manifest (no §3.1 source entity, never authored); every "
521 "file_kind/query_kind/active value satisfies its CHECK by construction "
522 "and required_min_version is NULL → the FK never fires (P1.6 seam)"),
523 "sv.track_active": ("source_validation_rule",
524 "base.Track.active bool (default True) → always a valid 0/1"),
525 # --- derive_verify_rule (P1.4 derive_verify — tagged @invariant) ---
526 "inv.version_lineage_derived": ("derive_verify_rule",
527 "derive_verify.derive_versions + verify_versions (step-7 publish-gate)"),
528}
529
530
531# ===========================================================================
532# The authored classification (the human-readable §5.3.1 view, machine-precise)
533# ===========================================================================
534def _D(witness_id: str, handling: str, **kw: object) -> MatrixRow:
535 wtype = WITNESS_REGISTRY[witness_id][0]
536 return MatrixRow(
537 classification="D", handling=handling,
538 witness_type=wtype, witness_id=witness_id, **kw, # type: ignore[arg-type]
539 )
540
541
542def _R(rule: str, ac: str, code: str, neg_fixture: str, handling: str) -> MatrixRow:
543 return MatrixRow(
544 classification="R", handling=handling, source_validation_rule=rule,
545 acceptance_criterion=ac, failure_row=code, negative_fixture=neg_fixture,
546 )
547
548
549def _X(ratification: str, handling: str) -> MatrixRow:
550 return MatrixRow(classification="X", handling=handling, ratification=ratification)
551
552
553def _NA(handling: str) -> MatrixRow:
554 return MatrixRow(classification="NA", handling=handling)
555
556
557# The single first-class derive-verify row (§5.3.1 — recorded with all fields so
558# "not R" never means "not in the app-layer audit surface").
559_VERSION_LINEAGE_ROW = MatrixRow(
560 classification="D",
561 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
562 handling="version status / lineage (C3–C6) / one-current-per-era / bump_kind are "
563 "DERIVED from the ordered changelog (numeric-per-field) then recomputed-and-"
564 "verified at step-7 before any use; a violating value is unreachable in the built store",
565 witness_type="derive_verify_rule",
566 witness_id="inv.version_lineage_derived",
567 derive_rule_id="derive_verify.derive_versions",
568 source_fields=("version", "era", "date", "round", "bump_override"),
569 stored_columns=("status", "parent_version_id", "supersedes_version_id",
570 "superseded_by_version_id", "bump_kind"),
571 verify_phase="step-7",
572 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
573 read_barrier="verified_projection.assert_verified_projection (REGEN_UNVERIFIED_PROJECTION)",
574 positive_fixtures=("test_version_ordering_numeric_across_major_boundary",
575 "test_version_selection_skip_superseded",
576 "test_version_selection_per_era_cardinality"),
577 error_code="REGEN_BUILD_FAILED",
578)
579
580_DV = "inv.version_lineage_derived"
581
582#: stable_id -> MatrixRow for the SOURCE-POPULATED tables + registry_meta (the
583#: rows the predicate does not assign mechanically; never-written + render_v1
584#: tables are filled programmatically by :func:`_build_classification`).
585_EXPLICIT: dict[str, MatrixRow] = {
586 # --- registry_meta (store_scope base capability, §9.3 / AC §8.57) -------
587 "ddl:registry_meta:check:store_scope": _D("pf.store_scope",
588 "builder stamps a valid scope token; author cannot write registry_meta"),
589 "ddl:registry_meta:notnull:store_scope": _D("pf.store_scope",
590 "builder always stamps store_scope before any runtime open"),
591 # --- roadmap_eras -------------------------------------------------------
592 "ddl:roadmap_eras:notnull:era_label": _D("sv.required_fields", "Era.era_label RequiredLine"),
593 "ddl:roadmap_eras:notnull:sort_order": _D("sv.required_fields", "Era.order required int"),
594 "ddl:roadmap_eras:notnull:started_at": _D("sv.required_fields", "Era.started_at IsoDate"),
595 "ddl:roadmap_eras:pk:era_id": _D("sv.stem_eq_id",
596 "era_id is the filename stem (stem==id enforced); a duplicate needs two same-named files"),
597 "ddl:roadmap_eras:unique_index:idx_roadmap_eras_sort": _R(
598 "inv.era_track_order_unique", "8.56", "SRC_DUPLICATE_ORDER",
599 "test_duplicate_era_order_rejected", "two eras with the same order — author-reachable"),
600 # --- roadmap_versions ---------------------------------------------------
601 "ddl:roadmap_versions:check:bump_kind": _D(_DV, "bump_kind derived (verified step-7)"),
602 "ddl:roadmap_versions:check:not_status_eq_superseded_and_superseded_by_version_id_is_null": _D(
603 _DV, "C6: lineage derived — superseded⇒superseded_by unreachable in built store"),
604 "ddl:roadmap_versions:check:status": _D(_DV, "status derived (draft/current/superseded)"),
605 "ddl:roadmap_versions:check:version_id_ne_parent_version_id": _D(_DV, "C3: lineage derived (no authored field)"),
606 "ddl:roadmap_versions:check:version_id_ne_superseded_by_version_id": _D(_DV, "C5: lineage derived"),
607 "ddl:roadmap_versions:check:version_id_ne_supersedes_version_id": _D(_DV, "C4: lineage derived"),
608 "ddl:roadmap_versions:fk:era_id->roadmap_eras": _R(
609 "inv.intra_source_fk_resolves", "8.32", "REGEN_BUILD_FAILED",
610 "test_dangling_version_era_fails", "version.era may reference a missing era — author-reachable"),
611 "ddl:roadmap_versions:fk:parent_version_id->roadmap_versions": _D(_DV, "lineage derived within the version set"),
612 "ddl:roadmap_versions:fk:source_round_no->rounds": _X("V0_3_1 §9.2",
613 "runtime-reference relaxation (Option A): authored provenance, FK not enforced on the authored rebuild"),
614 "ddl:roadmap_versions:fk:superseded_by_version_id->roadmap_versions": _D(_DV, "lineage derived"),
615 "ddl:roadmap_versions:fk:supersedes_version_id->roadmap_versions": _D(_DV, "lineage derived"),
616 "ddl:roadmap_versions:notnull:bump_kind": _D(_DV, "bump_kind derived (always set)"),
617 "ddl:roadmap_versions:notnull:era_id": _D("sv.required_fields", "Version.era required EntityId"),
618 "ddl:roadmap_versions:notnull:last_updated_at": _D("pf.timestamps", "SOURCE_EPOCH fill (EXCLUDED)"),
619 "ddl:roadmap_versions:notnull:released_at": _D("sv.required_fields", "Version.date IsoDate (released_at)"),
620 "ddl:roadmap_versions:notnull:status": _D(_DV, "status derived (always set)"),
621 "ddl:roadmap_versions:pk:version_id": _R(
622 "inv.version_metadata_agreement", "8.38", "SRC_VERSION_METADATA_CONFLICT",
623 "test_version_metadata_conflict", "a roadmap+product pair shares version_id — merged to one row, agreement-checked"),
624 "ddl:roadmap_versions:unique_index:idx_roadmap_versions_one_current_per_era": _D(
625 _DV, "one-current-per-era derived + verified step-7 (AC §8.26)"),
626 # --- roadmap_phases -----------------------------------------------------
627 "ddl:roadmap_phases:check:parallel_track": _D("pf.parallel_track", "builder sets 0 (no source field)"),
628 "ddl:roadmap_phases:check:phase_id_ne_parent_phase_id": _D("pf.phase_parent_null",
629 "C9: parent_phase_id always NULL on the rebuild"),
630 "ddl:roadmap_phases:check:priority_label": _D("sv.priority_enum", "Phase.priority Priority enum"),
631 "ddl:roadmap_phases:fk:introduced_in_version_id->roadmap_versions": _R(
632 "inv.intra_source_fk_resolves", "8.32", "REGEN_BUILD_FAILED",
633 "test_dangling_phase_version_pointer_fails", "introduced_in_version may dangle — author-reachable"),
634 "ddl:roadmap_phases:fk:parent_phase_id->roadmap_phases": _D("pf.phase_parent_null",
635 "parent_phase_id always NULL (v1 has no nested phases) — resolves trivially"),
636 "ddl:roadmap_phases:fk:retired_in_version_id->roadmap_versions": _R(
637 "inv.intra_source_fk_resolves", "8.32", "REGEN_BUILD_FAILED",
638 "test_dangling_phase_version_pointer_fails", "retired_in_version may dangle — author-reachable"),
639 "ddl:roadmap_phases:notnull:created_at": _D("pf.timestamps", "SOURCE_EPOCH fill"),
640 "ddl:roadmap_phases:notnull:label": _D("sv.required_fields", "Phase.title RequiredLine (label)"),
641 "ddl:roadmap_phases:notnull:parallel_track": _D("pf.parallel_track", "builder sets 0"),
642 "ddl:roadmap_phases:notnull:phase_code": _D("sv.required_fields", "Phase.id EntityId (phase_code)"),
643 "ddl:roadmap_phases:notnull:purpose": _D("sv.required_fields", "Phase.purpose Line300 (non-empty)"),
644 "ddl:roadmap_phases:notnull:sort_order": _D("sv.required_fields", "Phase.order required int"),
645 "ddl:roadmap_phases:notnull:updated_at": _D("pf.timestamps", "SOURCE_EPOCH fill"),
646 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
647 "ddl:roadmap_phases:pk:phase_id": _D("pf.surrogate_pk", "AUTOINCREMENT surrogate (source-populated)"),
648 "ddl:roadmap_phases:unique_col:phase_code": _R(
649 "inv.phase_track_code_unique", "8.56", "SRC_DUPLICATE_CODE",
650 "test_duplicate_phase_code_rejected", "two phases with the same code — author-reachable"),
651 # --- roadmap_tracks -----------------------------------------------------
652 "ddl:roadmap_tracks:check:active": _D("sv.track_active", "Track.active bool → 0/1"),
653 "ddl:roadmap_tracks:notnull:active": _D("sv.track_active", "Track.active bool (default True)"),
654 "ddl:roadmap_tracks:notnull:label": _D("sv.required_fields", "Track.label RequiredLine"),
655 "ddl:roadmap_tracks:notnull:sort_order": _D("sv.required_fields", "Track.order required int"),
656 "ddl:roadmap_tracks:notnull:track_code": _D("sv.required_fields", "Track.track_code EntityId"),
657 "ddl:roadmap_tracks:pk:track_id": _D("pf.surrogate_pk", "AUTOINCREMENT surrogate"),
658 "ddl:roadmap_tracks:unique_col:track_code": _R(
659 "inv.phase_track_code_unique", "8.56", "SRC_DUPLICATE_CODE",
660 "test_duplicate_track_code_rejected", "two tracks with the same code — author-reachable"),
661 "ddl:roadmap_tracks:unique_index:idx_roadmap_tracks_sort": _R(
662 "inv.era_track_order_unique", "8.56", "SRC_DUPLICATE_ORDER",
663 "test_duplicate_track_order_rejected", "two tracks with the same order — author-reachable"),
664 # --- roadmap_items_v3 ---------------------------------------------------
665 "ddl:roadmap_items_v3:check:item_id_ne_parent_item_id": _D("pf.parent_item_id_derived",
666 "C14: parent_item_id is builder-derived; a self-parent is unreachable"),
667 "ddl:roadmap_items_v3:check:item_id_ne_superseded_by_item_id": _D("sv.lifecycle",
668 "C15: check_item_lifecycle rejects superseded_by==id"),
669 "ddl:roadmap_items_v3:check:priority": _D("sv.priority_enum", "RoadmapItem.priority Priority enum (nullable)"),
670 "ddl:roadmap_items_v3:check:status": _D("sv.status_enum", "Status enum"),
671 "ddl:roadmap_items_v3:check:status_ne_superseded_or_superseded_by_item_id_is_not_null": _D(
672 "sv.lifecycle", "C16: check_item_lifecycle requires superseded_by when status=superseded"),
673 "ddl:roadmap_items_v3:check:track_kind": _D("sv.track_kind_enum", "track_kind enum"),
674 "ddl:roadmap_items_v3:check:track_kind_ne_m_phase_or_phase_id_is_not_null": _D("pf.item_phase_id",
675 "C17: phase fan-out always sets phase_id for m_phase rows"),
676 "ddl:roadmap_items_v3:check:track_kind_not_in_backlog_vision_or_phase_id_is_null": _D("pf.item_phase_id",
677 "C18: non-phase items have no source phase_id field → builder sets NULL"),
678 "ddl:roadmap_items_v3:fk:closed_in_version_id->roadmap_versions": _R(
679 "inv.intra_source_fk_resolves", "8.32", "REGEN_BUILD_FAILED",
680 "test_dangling_item_version_pointer_fails", "closed_in_version may dangle — author-reachable"),
681 "ddl:roadmap_items_v3:fk:closed_round_no->rounds": _X("V0_3_1 §9.2",
682 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
683 "runtime-reference relaxation (Option A): always NULL on a pure source rebuild, EXCLUDED from the hash"),
684 "ddl:roadmap_items_v3:fk:introduced_in_version_id->roadmap_versions": _R(
685 "inv.intra_source_fk_resolves", "8.32", "REGEN_BUILD_FAILED",
686 "test_dangling_item_version_pointer_fails", "introduced_in_version may dangle — author-reachable"),
687 "ddl:roadmap_items_v3:fk:parent_item_id->roadmap_items_v3": _D("pf.parent_item_id_derived",
688 "parent_item_id derived from nesting/promoted dir → always resolves to a built item"),
689 "ddl:roadmap_items_v3:fk:phase_id->roadmap_phases": _D("pf.item_phase_id",
690 "phase_id derived from the fan-out → always resolves"),
691 "ddl:roadmap_items_v3:fk:successor_item_id->roadmap_items_v3": _R(
692 "inv.intra_source_fk_resolves", "8.32", "REGEN_BUILD_FAILED",
693 "test_dangling_successor_pointer_fails", "successor is authored and may dangle — author-reachable"),
694 "ddl:roadmap_items_v3:fk:superseded_by_item_id->roadmap_items_v3": _R(
695 "inv.intra_source_fk_resolves", "8.32", "REGEN_BUILD_FAILED",
696 "test_dangling_superseded_by_pointer_fails", "superseded_by is authored and may dangle — author-reachable"),
697 "ddl:roadmap_items_v3:notnull:created_at": _D("pf.timestamps", "SOURCE_EPOCH fill"),
698 "ddl:roadmap_items_v3:notnull:purpose": _D("sv.required_fields", "purpose Line300 (non-empty)"),
699 "ddl:roadmap_items_v3:notnull:sort_order": _D("sv.required_fields", "order required int"),
700 "ddl:roadmap_items_v3:notnull:status": _D("sv.status_enum", "status enum (always set)"),
701 "ddl:roadmap_items_v3:notnull:title": _D("sv.required_fields", "title RequiredLine"),
702 "ddl:roadmap_items_v3:notnull:track_kind": _D("sv.track_kind_enum", "track_kind enum (always set)"),
703 "ddl:roadmap_items_v3:notnull:updated_at": _D("pf.timestamps", "SOURCE_EPOCH fill"),
704 "ddl:roadmap_items_v3:pk:item_id": _R(
705 "inv.item_id_unique", "8.39", "SRC_ITEM_ID_COLLISION",
706 "test_item_id_union_collision_case_insensitive", "an id may collide across file ids ∪ nested sub_phase ids ∪ promoted"),
707 # --- roadmap_item_tracks ------------------------------------------------
708 "ddl:roadmap_item_tracks:fk:item_id->roadmap_items_v3": _D("pf.parent_item_id_derived",
709 "the membership owner is the item being built — always present"),
710 "ddl:roadmap_item_tracks:fk:track_id->roadmap_tracks": _R(
711 "inv.track_codes_resolve", "8.31", "SRC_BAD_TRACK_CODES",
712 "test_unknown_track_code_rejected", "track_codes may reference an unknown track — author-reachable"),
713 "ddl:roadmap_item_tracks:notnull:created_at": _D("pf.timestamps", "SOURCE_EPOCH fill"),
714 "ddl:roadmap_item_tracks:notnull:item_id": _D("sv.required_fields", "membership owner id"),
715 "ddl:roadmap_item_tracks:notnull:track_id": _D("pf.track_id_resolved", "resolved track_code→track_id"),
716 "ddl:roadmap_item_tracks:pk:item_id_track_id": _D("pf.insert_or_ignore", "INSERT OR IGNORE dedups duplicates"),
717 # --- roadmap_item_dependencies ------------------------------------------
718 "ddl:roadmap_item_dependencies:check:from_item_id_ne_to_item_id": _R(
719 "inv.dependency_acyclic", "8.47", "SRC_DEPENDENCY_CYCLE",
720 "test_dependency_self_edge_rejected", "C20: a self-edge is a degenerate cycle — author-reachable"),
721 "ddl:roadmap_item_dependencies:check:kind": _D("sv.dep_kind_enum",
722 "the 3 authorable kinds are a subset of the 4-token DDL CHECK; the builder never writes part_of"),
723 "ddl:roadmap_item_dependencies:fk:created_by_slot->agents": _X("V0_3_1 §9.2",
724 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
725 "runtime-reference relaxation (Option A): FK-to-agents half (the NOT-NULL half is D via 'source' placeholder)"),
726 "ddl:roadmap_item_dependencies:fk:from_item_id->roadmap_items_v3": _R(
727 "inv.depends_on_resolves", "8.3", "SRC_DANGLING_DEPENDS_ON",
728 "test_dangling_depends_on_rejected", "depends_on predecessor may be unknown — author-reachable"),
729 "ddl:roadmap_item_dependencies:fk:to_item_id->roadmap_items_v3": _R(
730 "inv.depends_on_resolves", "8.3", "SRC_DANGLING_DEPENDS_ON",
731 "test_dangling_depends_on_rejected", "the dependent is the owning item; resolution shares the depends_on check"),
732 "ddl:roadmap_item_dependencies:notnull:created_at": _D("pf.timestamps", "SOURCE_EPOCH fill"),
733 "ddl:roadmap_item_dependencies:notnull:created_by_slot": _D("pf.created_by_slot", "'source' placeholder fill"),
734 "ddl:roadmap_item_dependencies:notnull:from_item_id": _D("sv.required_fields", "depends_on edge id (EntityId)"),
735 "ddl:roadmap_item_dependencies:notnull:kind": _D("sv.dep_kind_enum", "DepKind defaults to 'blocks' (always set)"),
736 "ddl:roadmap_item_dependencies:notnull:to_item_id": _D("sv.required_fields", "the owning item id"),
737 "ddl:roadmap_item_dependencies:pk:from_item_id_to_item_id_kind": _D("pf.insert_or_ignore",
738 "INSERT OR IGNORE dedups a repeated (from,to,kind) edge"),
739}
740
741# App-layer invariants that have no DDL constraint OR carry the X/derive
742# semantics — the §9.1 audit checklist + the doc/code markers reference these.
743_APP_LAYER: dict[str, MatrixRow] = {
744 # The first-class derive-verify row.
745 _DV: _VERSION_LINEAGE_ROW,
746 # The bounded-suspension inv-(b) — app-layer only, no DDL form (dormant in v1).
747 "inv.b_closed_version_required": _X("V0_3_1 §9.1",
748 "inv-(b) done/deferred⇒closed_in_version: a deliberate post-cutover tightening, "
749 "grandfathered by the terminal-at-cutover seed ledger; v1 ships lenient (dormant)"),
750 # The cross-record R invariants (the step-6 / build / pipeline assertions).
751 "inv.item_id_unique": _R("inv.item_id_unique", "8.39", "SRC_ITEM_ID_COLLISION",
752 "test_item_id_union_collision_case_insensitive", "id unique across file ids ∪ nested ∪ promoted (step-6)"),
753 "inv.depends_on_resolves": _R("inv.depends_on_resolves", "8.3", "SRC_DANGLING_DEPENDS_ON",
754 "test_dangling_depends_on_rejected", "every depends_on predecessor resolves (step-6)"),
755 "inv.dependency_acyclic": _R("inv.dependency_acyclic", "8.47", "SRC_DEPENDENCY_CYCLE",
756 "test_dependency_cycle_rejected_from_source", "kind-unscoped dependency acyclicity (step-6, OQ-R-07)"),
757 "inv.successor_acyclic": _R("inv.successor_acyclic", "8.42", "SRC_SUCCESSOR_CYCLE",
758 "test_successor_cycle_rejected", "successor-chain acyclicity (step-6)"),
759 "inv.done_parent_terminal_children": _R("inv.done_parent_terminal_children", "8.50",
760 "SRC_DONE_PARENT_LIVE_CHILD", "test_done_parent_requires_terminal_children",
761 "a done parent has only terminal children (step-6, OQ-R-07)"),
762 "inv.era_track_order_unique": _R("inv.era_track_order_unique", "8.56", "SRC_DUPLICATE_ORDER",
763 "test_duplicate_era_order_rejected", "era/track order uniqueness pre-check (step-6)"),
764 "inv.phase_track_code_unique": _R("inv.phase_track_code_unique", "8.56", "SRC_DUPLICATE_CODE",
765 "test_duplicate_phase_code_rejected", "phase_code/track_code uniqueness pre-check (step-6)"),
766 "inv.version_metadata_agreement": _R("inv.version_metadata_agreement", "8.38",
767 "SRC_VERSION_METADATA_CONFLICT", "test_version_metadata_conflict",
768 "same-version roadmap+product metadata agreement → one row (step-6)"),
769 "inv.track_codes_resolve": _R("inv.track_codes_resolve", "8.31", "SRC_BAD_TRACK_CODES",
770 "test_unknown_track_code_rejected", "every track_code resolves to a track (build)"),
771 "inv.intra_source_fk_resolves": _R("inv.intra_source_fk_resolves", "8.32", "REGEN_BUILD_FAILED",
772 "test_dangling_version_era_fails",
773 "intra-source FK resolution backstop (post-build foreign_key_check; Option A tolerates runtime FKs)"),
774}
775
776
777def _build_classification() -> dict[str, MatrixRow]:
778 """Assemble the authoritative classification: explicit source-populated +
779 app-layer rows, plus programmatic NA for the never-written + v1-empty tables.
780
781 Built once from the canonical ``.1.sql`` inventory so the mutation tests
782 (which parse a *fixture* ``.sql``) see injected ids absent from this map.
783 """
784 classification: dict[str, MatrixRow] = dict(_EXPLICIT)
785 classification.update(_APP_LAYER)
786 for constraint in parse_ddl_inventory():
787 if constraint.stable_id in classification:
788 continue
789 if constraint.table in NEVER_WRITTEN_TABLES:
790 classification[constraint.stable_id] = _NA(
791 f"{constraint.table} is never written on the authored rebuild "
792 "(events/plan-links are runtime-only; AC §8.23) — never exercised")
793 elif constraint.table == "roadmap_render_sections":
794 classification[constraint.stable_id] = _D("pf.render_sections",
795 # OCT-LINT: disable=no_hardcoded_secrets reason="SQL keywords, table/column names and SRC_* codes are schema constants"
796 "roadmap_render_sections has no §3.1 source entity but is SEEDED deterministically "
797 "on every rebuild by P1.6 render_sections.seed_render_sections (the P1.4↔P1.6 seam, "
798 "wired in build.build_roadmap); the constant manifest satisfies every CHECK/NOT-NULL "
799 "by construction and seeds required_min_version NULL → the FK never fires")
800 return classification
801
802
803#: stable_id -> MatrixRow. Every DDL inventory id AND every app-layer @invariant
804#: id appears here exactly once (test_v0_3_invariant_matrix_complete).
805CLASSIFICATION: dict[str, MatrixRow] = _build_classification()
806
807
809 sql_path: Path = SCHEMA_V0_3_1_SQL,
810 doc_path: Path = V0_3_1_DOC,
811 code_files: tuple[Path, ...] = ENFORCEMENT_CODE_FILES,
812) -> Matrix:
813 """Join the two machine-extracted inventories to :data:`CLASSIFICATION`."""
814 func = "generate_matrix"
815 ddl = parse_ddl_inventory(sql_path)
816 doc_markers = extract_doc_markers(doc_path)
817 code_markers = extract_code_markers(code_files)
818 all_markers = doc_markers + code_markers
819 rows = {
820 cid: CLASSIFICATION[cid]
821 for cid in ({c.stable_id for c in ddl} | {m.invariant_id for m in all_markers})
822 if cid in CLASSIFICATION
823 }
824 _dbg("OC_CHAT.MAIN", func,
825 f"ddl={len(ddl)} doc_markers={len(doc_markers)} code_markers={len(code_markers)}", 3)
826 return Matrix(
827 ddl=tuple(ddl),
828 doc_markers=tuple(doc_markers),
829 code_markers=tuple(code_markers),
830 rows=rows,
831 constraint_inventory_hash=constraint_inventory_hash(ddl),
832 app_invariant_inventory_hash=app_invariant_inventory_hash(all_markers),
833 )
834
835
836# ===========================================================================
837# The §9.1 application-layer checklist + audit payload
838# ===========================================================================
839def verify_matrix(matrix: Matrix) -> list[str]:
840 """Run the §5.3.1 correctness predicate over ``matrix``; return a list of
841 violation strings (empty == the matrix is complete + correct).
842
843 Asserts: (a) every DDL-inventory id + every extracted ``@invariant`` id is
844 classified **exactly once**; (b) every ``D`` satisfies the D-safety predicate
845 (a registered witness; a ``derive_verify_rule`` names stored columns + a
846 positive fixture per branch); (c) no catch-all (every row carries a specific
847 handling); (d) every ``R`` names a source-validation rule + AC + §10 code +
848 negative fixture; (e) every ``X`` names a ``V0_3_1`` ratification; (f) every
849 ``NA`` names why the rebuild never exercises it.
850 """
851 violations: list[str] = []
852 classified = matrix.rows
853 inventory_ids = {c.stable_id for c in matrix.ddl}
854 inventory_ids |= {m.invariant_id for m in matrix.doc_markers}
855 inventory_ids |= {m.invariant_id for m in matrix.code_markers}
856 for cid in sorted(inventory_ids):
857 if cid not in classified:
858 violations.append(f"unclassified inventory id (no matrix row): {cid}")
859 for cid, row in sorted(classified.items()):
860 if row.classification not in VALID_CLASSES:
861 violations.append(f"{cid}: invalid class {row.classification!r}")
862 if not row.handling.strip():
863 violations.append(f"{cid}: catch-all — no handling rationale")
864 if row.classification == "D":
865 if row.witness_id not in WITNESS_REGISTRY:
866 violations.append(f"{cid}: D names no registered witness ({row.witness_id!r})")
867 elif row.witness_type != WITNESS_REGISTRY[row.witness_id][0]:
868 violations.append(f"{cid}: D witness_type mismatch vs registry")
869 if row.witness_type == "derive_verify_rule":
870 # The first-class derive-verify witness row (§5.3.1) carries the
871 # branch-coverage evidence; every constraint D-via-derive-verify
872 # references it. Validate that witness row exists + is complete.
873 witness_row = classified.get(row.witness_id or "")
874 if witness_row is None:
875 violations.append(f"{cid}: derive_verify witness {row.witness_id!r} has no first-class row")
876 else:
877 if not witness_row.stored_columns:
878 violations.append(f"{row.witness_id}: derive_verify_rule names no stored_columns")
879 if not witness_row.positive_fixtures:
880 violations.append(
881 f"{row.witness_id}: derive_verify_rule names no positive_fixture (branch coverage)")
882 elif row.classification == "R":
883 if not row.source_validation_rule:
884 violations.append(f"{cid}: R names no source_validation_rule")
885 if not row.acceptance_criterion:
886 violations.append(f"{cid}: R names no acceptance criterion")
887 if not row.failure_row:
888 violations.append(f"{cid}: R names no §10 failure code")
889 if not row.negative_fixture:
890 violations.append(f"{cid}: R names no negative fixture")
891 elif row.classification == "X":
892 if not row.ratification:
893 violations.append(f"{cid}: X names no V0_3_1 ratification")
894 return violations
895
896
897def code_markers_not_in_matrix(matrix: Matrix) -> list[str]:
898 """{code-enforced @invariant ids} ⊄ {matrix ids} — the drift gate (AC §8.49)."""
899 return sorted(
900 m.invariant_id for m in matrix.code_markers if m.invariant_id not in matrix.rows
901 )
902
903
904def unimplemented_r_rows(matrix: Matrix) -> list[str]:
905 """Matrix ``R`` ids whose ``source_validation_rule`` has no enforcement-code
906 ``@invariant`` marker — the reverse-inclusion R-completeness gate (AC §8.49)."""
907 code_ids = {m.invariant_id for m in matrix.code_markers}
908 return sorted(
909 cid for cid, row in matrix.rows.items()
910 if row.classification == "R" and row.source_validation_rule not in code_ids
911 )
912
913
914def app_layer_invariant_ids(classification: Optional[dict[str, MatrixRow]] = None) -> set[str]:
915 """The §9.1 app-layer-invariant checklist denominator: every invariant the
916 rebuild bypasses unless independently handled — the ``inv.*`` rows (R +
917 derive-verify) plus the ``X`` conscious relaxations. The pure-DDL ``D``/``NA``
918 constraints are by-construction and are NOT app-layer invariants (§5.3.1)."""
919 rows = classification if classification is not None else CLASSIFICATION
920 return {
921 cid for cid, row in rows.items()
922 if cid.startswith("inv.") or row.classification == "X"
923 }
924
925
926def build_audit_payload(matrix: Optional[Matrix] = None) -> dict[str, object]:
927 """The machine-readable §9.1 audit ``.json`` payload: the frozen checklist,
928 the generated-inventory counts, and the audit's own validity-key hashes
929 (the Accepted-doc + enforcement-code marker hashes + the generated-inventory
930 hash — NOT the roadmap source_hash, V0.19 Codex V16 F4)."""
931 matrix = matrix or generate_matrix()
932 counts: dict[str, int] = {"D": 0, "R": 0, "X": 0, "NA": 0}
933 for row in matrix.rows.values():
934 counts[row.classification] += 1
935 checklist = sorted(app_layer_invariant_ids(matrix.rows))
936 return {
937 "spec_version": "REGISTRY_SOURCE_AND_STORAGE_SPEC_V0_22",
938 "generated_by": "oc_chat/registry/invariant_matrix.py",
939 "validity_key": {
940 "v0_3_1_doc": V0_3_1_DOC.name,
941 "doc_marker_inventory_hash": app_invariant_inventory_hash(list(matrix.doc_markers)),
942 "code_marker_inventory_hash": app_invariant_inventory_hash(list(matrix.code_markers)),
943 "constraint_inventory_hash": matrix.constraint_inventory_hash,
944 },
945 "inventory_counts": {
946 "ddl_constraints": len(matrix.ddl),
947 "classified": len(matrix.rows),
948 **{f"class_{k}": v for k, v in counts.items()},
949 "app_layer_checklist": len(checklist),
950 },
951 "app_layer_checklist": checklist,
952 "signer": {
953 "name": None,
954 "role": None,
955 "date": None,
956 "statement": None,
957 "independent_of_author": None,
958 },
959 "freeze_point": "142/P1 implementation-acceptance commit",
960 }
list[Marker] extract_markers(str text, *, str form, str source)
dict[str, MatrixRow] _build_classification()
list[Marker] extract_code_markers(tuple[Path,...] files=ENFORCEMENT_CODE_FILES)
str constraint_inventory_hash(list[DdlConstraint] ddl)
set[str] app_layer_invariant_ids(Optional[dict[str, MatrixRow]] classification=None)
list[str] unimplemented_r_rows(Matrix matrix)
list[DdlConstraint] parse_ddl_inventory(Path sql_path=SCHEMA_V0_3_1_SQL, *, bool include_store_scope=True)
list[Marker] extract_doc_markers(Path doc_path=V0_3_1_DOC)
MatrixRow _R(str rule, str ac, str code, str neg_fixture, str handling)
list[str] verify_matrix(Matrix matrix)
dict[str, object] build_audit_payload(Optional[Matrix] matrix=None)
list[DdlConstraint] _parse_create_tables(str sql)
list[DdlConstraint] _parse_store_scope()
Matrix generate_matrix(Path sql_path=SCHEMA_V0_3_1_SQL, Path doc_path=V0_3_1_DOC, tuple[Path,...] code_files=ENFORCEMENT_CODE_FILES)
list[DdlConstraint] _parse_table_constraint_clause(str table, str clause)
list[DdlConstraint] _parse_indexes_and_triggers(str sql)
MatrixRow _D(str witness_id, str handling, **object kw)
str app_invariant_inventory_hash(list[Marker] markers)
list[DdlConstraint] _parse_column_clause(str table, str clause)
MatrixRow _X(str ratification, str handling)
list[str] code_markers_not_in_matrix(Matrix matrix)