7The three AuthoredSource hash projections (+ envelope nullability).
11Implement Registry Source & Storage Spec V0.22 §3.2: (1) the canonical
12source-tree hash, (2) the source semantic projection, (3) the canonical
13logical-state hash over the RuntimeStore durable roadmap rows (surrogate →
14natural-key resolution + canonical ``query_params_json``), plus the §10 raw
15source-tree hash and the failure-envelope hash nullability rule.
19- ``raw_source_tree_hash`` — sha256 over raw observed bytes (incl. files that
20 later fail validation; computable before parse; null only on scan failure).
21- ``source_hash`` — sha256 over canonical record bytes (projection 1).
22- ``semantic_hash`` — sha256 over the source semantic projection (projection 2).
23- ``logical_state_hash`` — sha256 over the §3.2 store projection (projection 3).
24- ``hash_preimage`` — the printed preimage (with ``hash_preimage_schema_version``).
25- ``stage_hashes`` — the §10 nullability of the three hashes per failure stage.
32 L3 - projection details
33 L4 - per-row projection tracing
37- All hashes are sha256 hex over ``canonical_json`` of a preimage that begins
38 with ``hash_preimage_schema_version`` (starts at integer 1).
39- The logical projection includes/excludes exactly the §3.2 columns; surrogate
40 keys are resolved to natural keys; ``query_params_json`` is re-emitted canonical.
41- ``logical_state_hash`` reads an already-built store; it derives nothing
42 (version status/lineage derive-verify is the P1.4 step-7 publish-gate).
46- oc_diagnostics (structured debug logging via _dbg)
47- oc_chat.registry.canonical (canonical_json)
48- oc_chat.registry.source.scanner (ScannedRecord and observed_files - the records and files the hashes cover)
49- oc_chat.registry.source.canonical (canonical_record_bytes - the canonical bytes behind the source hash)
50- oc_chat.registry.source.errors (SRC_BAD_QUERY_PARAMS and SourceValidationError - an invalid query_params_json)
51- option_c_registry_contracts (RegistryModel - the loaded record models behind the semantic hash)
53from __future__
import annotations
58from pathlib
import Path
59from typing
import Any, Optional
61from oc_diagnostics
import _dbg
63from option_c_registry_contracts
import RegistryModel
69 SourceValidationError,
73_dbg(
"OC_CHAT.MAIN",
"module",
"loaded", 4)
76HASH_PREIMAGE_SCHEMA_VERSION = 1
81STAGE_CROSS_RECORD =
"cross_record"
87 return hashlib.sha256(data).hexdigest()
91 return _sha256_hex(canonical_json(preimage).encode(
"utf-8"))
96 """``[(relative_posix_path, sha256(raw_file_bytes))]`` over observed files."""
98 [path.relative_to(root).as_posix(),
_sha256_hex(path.read_bytes())]
99 for path
in observed_files(Path(root))
103 "hash_preimage_schema_version": HASH_PREIMAGE_SCHEMA_VERSION,
113 """``[(relative_posix_path, sha256(canonical_record_bytes))]`` (projection 1).
115 [OP-5] promoted parity: promoted items are now included (the earlier
116 model-gap skip is removed), so ``source_hash`` is a complete fingerprint of
117 the authored tree — an edit to a promoted file moves ``source_hash``.
119 entries: list[list[str]] = []
120 for scanned
in records:
122 [scanned.rel_path,
_sha256_hex(canonical_record_bytes(scanned))]
126 "hash_preimage_schema_version": HASH_PREIMAGE_SCHEMA_VERSION,
137 """Authored records normalized to canonical JSON using source field names."""
140 "kind": scanned.kind,
141 "rel_path": scanned.rel_path,
142 "fields": model.model_dump(mode=
"json"),
144 for scanned, model
in loaded
146 records.sort(key=
lambda entry: entry[
"rel_path"])
148 "hash_preimage_schema_version": HASH_PREIMAGE_SCHEMA_VERSION,
161 "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (name,)
163 return row
is not None
166def _rows(conn: sqlite3.Connection, table: str, columns: str) -> list[sqlite3.Row]:
169 return conn.execute(f
"SELECT {columns} FROM {table}").fetchall()
173 if value
is None or value ==
"":
176 parsed = json.loads(value)
177 except (ValueError, TypeError)
as error:
179 f
"render section {section_path!r}: invalid query_params_json: {error}",
180 code=SRC_BAD_QUERY_PARAMS,
181 records=[section_path],
183 return canonical_json(parsed)
187 """Build the §3.2 projection-3 preimage over the durable roadmap rows."""
188 conn.row_factory = sqlite3.Row
190 row[
"phase_id"]: row[
"phase_code"]
191 for row
in _rows(conn,
"roadmap_phases",
"phase_id, phase_code")
194 row[
"track_id"]: row[
"track_code"]
195 for row
in _rows(conn,
"roadmap_tracks",
"track_id, track_code")
203 "era_id, era_label, major_bump_reason, archived_under, "
204 "sort_order, started_at, ended_at",
207 eras.sort(key=
lambda row: row[
"era_id"])
214 "version_id, era_id, bump_kind, notes, source_round_no, released_at",
217 versions.sort(key=
lambda row: row[
"version_id"])
223 "phase_id, phase_code, label, parent_phase_id, branch_suggestion, "
224 "commit_prefix, target_version, priority_label, parallel_track, "
225 "full_spec_url, sort_order, purpose, introduced_in_version_id, "
226 "retired_in_version_id",
229 record[
"parent_phase_code"] = phase_code_by_id.get(record.pop(
"parent_phase_id"))
230 record.pop(
"phase_id")
231 phases.append(record)
232 phases.sort(key=
lambda row: row[
"phase_code"])
239 "track_code, label, description, sort_order, active",
242 tracks.sort(key=
lambda row: row[
"track_code"])
248 "item_id, track_kind, phase_id, parent_item_id, title, prose, status, "
249 "priority, sort_order, target_version, superseded_by_item_id, "
250 "successor_item_id, purpose, introduced_in_version_id, closed_in_version_id",
253 record[
"phase_code"] = phase_code_by_id.get(record.pop(
"phase_id"))
255 items.sort(key=
lambda row: row[
"item_id"])
258 for row
in _rows(conn,
"roadmap_item_tracks",
"item_id, track_id"):
260 {
"item_id": row[
"item_id"],
"track_code": track_code_by_id.get(row[
"track_id"])}
262 item_tracks.sort(key=
lambda row: (row[
"item_id"], row[
"track_code"]
or ""))
265 {
"from_item_id": row[
"from_item_id"],
"to_item_id": row[
"to_item_id"],
"kind": row[
"kind"]}
267 conn,
"roadmap_item_dependencies",
"from_item_id, to_item_id, kind"
270 dependencies.sort(key=
lambda row: (row[
"from_item_id"], row[
"to_item_id"], row[
"kind"]))
275 "roadmap_render_sections",
276 "file_kind, section_path, sort_order, query_kind, query_params_json, "
277 "required_min_version, active",
281 record[
"query_params_json"], record[
"section_path"]
283 render_sections.append(record)
284 render_sections.sort(key=
lambda row: (row[
"file_kind"], row[
"section_path"]))
287 "hash_preimage_schema_version": HASH_PREIMAGE_SCHEMA_VERSION,
288 "roadmap_eras": eras,
289 "roadmap_versions": versions,
290 "roadmap_phases": phases,
291 "roadmap_tracks": tracks,
292 "roadmap_items_v3": items,
293 "roadmap_item_tracks": item_tracks,
294 "roadmap_item_dependencies": dependencies,
295 "roadmap_render_sections": render_sections,
307 raw: Optional[str] =
None,
308 source: Optional[str] =
None,
309 logical: Optional[str] =
None,
310) -> dict[str, Optional[str]]:
311 """Return the three envelope hash fields with the §10 nullability for ``stage``.
313 scan failure → raw null; parse/local/canonicalisation → source null, raw
314 present; cross-record / pre-build → logical null, source present; clean →
317 if stage == STAGE_SCAN:
318 return {
"raw_source_tree_hash":
None,
"source_hash":
None,
"logical_state_hash":
None}
319 if stage == STAGE_PARSE:
320 return {
"raw_source_tree_hash": raw,
"source_hash":
None,
"logical_state_hash":
None}
321 if stage
in (STAGE_CROSS_RECORD, STAGE_BUILD):
322 return {
"raw_source_tree_hash": raw,
"source_hash": source,
"logical_state_hash":
None}
323 return {
"raw_source_tree_hash": raw,
"source_hash": source,
"logical_state_hash": logical}
330 root: Optional[Path] =
None,
331 records: Optional[list[ScannedRecord]] =
None,
332 loaded: Optional[list[tuple[ScannedRecord, RegistryModel]]] =
None,
333 conn: Optional[sqlite3.Connection] =
None,
335 """Return the canonical preimage for ``which`` projection (debug surface)."""
336 if which ==
"source-tree":
338 raise ValueError(
"source-tree preimage requires records")
340 if which ==
"raw-source-tree":
342 raise ValueError(
"raw-source-tree preimage requires root")
344 if which ==
"semantic":
346 raise ValueError(
"semantic preimage requires loaded records")
348 if which ==
"logical-state":
350 raise ValueError(
"logical-state preimage requires a store connection")
352 raise ValueError(f
"unknown preimage kind: {which!r}")
str _hash_preimage(dict[str, Any] preimage)
str _sha256_hex(bytes data)
dict[str, Any] semantic_preimage(list[tuple[ScannedRecord, RegistryModel]] loaded)
Optional[str] _canonical_query_params(Optional[str] value, str section_path)
bool _table_exists(sqlite3.Connection conn, str name)
str raw_source_tree_hash(Path root)
dict[str, Any] logical_state_projection(sqlite3.Connection conn)
dict[str, Optional[str]] stage_hashes(str stage, *, Optional[str] raw=None, Optional[str] source=None, Optional[str] logical=None)
dict[str, Any] hash_preimage(str which, *, Optional[Path] root=None, Optional[list[ScannedRecord]] records=None, Optional[list[tuple[ScannedRecord, RegistryModel]]] loaded=None, Optional[sqlite3.Connection] conn=None)
str logical_state_hash(sqlite3.Connection conn)
str semantic_hash(list[tuple[ScannedRecord, RegistryModel]] loaded)
list[sqlite3.Row] _rows(sqlite3.Connection conn, str table, str columns)
str source_hash(list[ScannedRecord] records)
dict[str, Any] source_hash_preimage(list[ScannedRecord] records)
dict[str, Any] raw_source_tree_preimage(Path root)