Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies = [

[project.optional-dependencies]
dev = [
"hypothesis>=6.100,<7",
"pytest>=8.0.0,<9",
"ruff>=0.5.0",
]
Expand Down Expand Up @@ -56,5 +57,6 @@ target-version = "py311"

[dependency-groups]
dev = [
"hypothesis>=6.100,<7",
"pytest>=8.0.0,<9",
]
161 changes: 106 additions & 55 deletions scripts/build_series_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -1436,61 +1436,81 @@ def _registry_event(
}


def build_catalog(
observations_path: pathlib.Path,
docket_path: pathlib.Path | None,
existing: ExistingCatalog,
registry: UuidRegistry,
) -> tuple[dict, dict]:
"""Build the catalog and the identity plan.
def _append_gate_helpers():
"""Import the append gate's supersede-aware view and content address."""
try:
from check_thesis_facts_append import (
effective_current_rows,
expected_assertion_version_id,
)
except ImportError as exc: # pragma: no cover - environment guard
raise SystemExit(
"cannot import the append gate's content address and "
"supersede-aware current view from "
f"check_thesis_facts_append (receipt package required): {exc}"
)
return effective_current_rows, expected_assertion_version_id

The plan records every registry-affecting outcome: ``mints`` (new
identity bindings to append), ``supersedes`` (identities whose UUID
changes — these require ``--allow-remint``), and ``dropped`` (existing
rows whose UUID would vanish from the catalog — also gated).

def effective_assertion_version_id(row: dict, index: int) -> str:
"""Return the assertion version id the append gate assigns ``row``.

Mirrors receipt's ``_effective_assertion_id``: a versioned row is
addressed by its explicit ``assertionVersion.id``, and a row with no
``assertionVersion`` (every row of the immutable prefix predates
versioning) by its recomputed av2 content address. A correction of a
prefix row names that address in ``supersedes``. ``index`` only
labels refusals.
"""
raw = observations_path.read_bytes()
observation_lines = raw.decode().split("\n")
if observation_lines and observation_lines[-1] == "":
observation_lines.pop()
rows = [
json.loads(
line,
object_pairs_hook=_reject_duplicate_keys,
parse_constant=_reject_json_constants,
version = row.get("assertionVersion")
if version is None:
_, content_address = _append_gate_helpers()
try:
return content_address(row)
except (AttributeError, TypeError, ValueError) as exc:
raise SystemExit(
f"observation row {index}: cannot compute the content "
f"address of a row without assertionVersion: {exc}"
) from None
if not isinstance(version, dict) or not isinstance(
version.get("id"), str
) or not version["id"]:
raise SystemExit(
f"observation row {index}: assertionVersion must carry a "
"nonempty string id"
)
for line in observation_lines
if line.strip()
]
# The journal is append-only: a correction appends a row whose
# assertionVersion.supersedes names the version it replaces. Series
# identity must follow the supersede-aware CURRENT view (the same one
# the ledger's aggregate-fact validation uses), or superseded
# assertions would keep stale identities alive forever. The imported
# helper assumes append-gate-validated input; standalone runs get the
# same preconditions enforced here (unique ids, resolvable links, no
# cycles).
seen_ids: dict[str, int] = {}
return version["id"]


def check_assertion_versions(rows: list[dict]) -> list[str]:
"""Enforce the append gate's supersede preconditions; return every id.

The gate (receipt ``check_rows``) reserves the effective id of EVERY
row, versioned or not, and a correction names the effective id of the
version it replaces. This check reserves the same ids, so a
gate-accepted correction of a prefix row is accepted here too. It
refuses the part of the gate's rules the current view relies on: two
rows with one effective id (identical unversioned rows, or a row
restating an earlier row's exact addressed content), a link to no
row's id, a self-link and a cycle. The gate refuses each of these as
well. Its other rules (an explicit id equals the content address; a
link names the active version of the same record, on an earlier line,
so no version is superseded twice) are the gate's alone.
"""
ids: list[str] = []
owner: dict[str, int] = {}
links: dict[str, str] = {}
for index, row in enumerate(rows):
version = row.get("assertionVersion")
if version is None:
continue
if not isinstance(version, dict) or not isinstance(
version.get("id"), str
) or not version["id"]:
raise SystemExit(
f"observation row {index}: assertionVersion must carry a "
"nonempty string id"
)
vid = version["id"]
if vid in seen_ids:
vid = effective_assertion_version_id(row, index)
if vid in owner:
raise SystemExit(
f"observation row {index}: assertionVersion id {vid!r} "
f"duplicates row {seen_ids[vid]}"
f"observation row {index}: assertion version {vid!r} "
f"duplicates row {owner[vid]}"
)
seen_ids[vid] = index
supersedes = version.get("supersedes")
owner[vid] = index
ids.append(vid)
version = row.get("assertionVersion")
supersedes = version.get("supersedes") if version is not None else None
if supersedes is not None:
if not isinstance(supersedes, str) or not supersedes:
raise SystemExit(
Expand All @@ -1504,7 +1524,7 @@ def build_catalog(
)
links[vid] = supersedes
for vid, target in links.items():
if target not in seen_ids:
if target not in owner:
raise SystemExit(
f"assertionVersion {vid!r} supersedes unknown version "
f"{target!r}"
Expand All @@ -1519,13 +1539,44 @@ def build_catalog(
)
seen_chain.add(cursor)
cursor = links.get(cursor)
try:
from check_thesis_facts_append import effective_current_rows
except ImportError as exc: # pragma: no cover - environment guard
raise SystemExit(
"cannot import the supersede-aware current view from "
f"check_thesis_facts_append (receipt package required): {exc}"
return ids


def build_catalog(
observations_path: pathlib.Path,
docket_path: pathlib.Path | None,
existing: ExistingCatalog,
registry: UuidRegistry,
) -> tuple[dict, dict]:
"""Build the catalog and the identity plan.

The plan records every registry-affecting outcome: ``mints`` (new
identity bindings to append), ``supersedes`` (identities whose UUID
changes — these require ``--allow-remint``), and ``dropped`` (existing
rows whose UUID would vanish from the catalog — also gated).
"""
raw = observations_path.read_bytes()
observation_lines = raw.decode().split("\n")
if observation_lines and observation_lines[-1] == "":
observation_lines.pop()
rows = [
json.loads(
line,
object_pairs_hook=_reject_duplicate_keys,
parse_constant=_reject_json_constants,
)
for line in observation_lines
if line.strip()
]
# The journal is append-only: a correction appends a row whose
# assertionVersion.supersedes names the version it replaces. Series
# identity must follow the supersede-aware CURRENT view (the same one
# the ledger's aggregate-fact validation uses), or superseded
# assertions would keep stale identities alive forever. The imported
# helper assumes append-gate-validated input; standalone runs get the
# preconditions it relies on from check_assertion_versions.
check_assertion_versions(rows)
effective_current_rows, _ = _append_gate_helpers()
current_rows = effective_current_rows(rows)
identities = build_identities(current_rows)

Expand Down
Loading
Loading