feat(observatory): admit canonical recorded replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 19:21:27 +03:00
parent e6a9846167
commit 023151c186
32 changed files with 4069 additions and 342 deletions
@@ -0,0 +1,670 @@
"""Publish an immutable canonical recorded LAB result into the Session catalog."""
from __future__ import annotations
import hashlib
import json
import math
import re
import struct
from pathlib import Path, PurePosixPath
from typing import Any, Final
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
from k1link.laboratory.evidence_report import (
LaboratoryEvidenceReportError,
verify_laboratory_evidence_result,
)
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
from k1link.sessions import LabReplayCapability, LabSessionBinding, SessionStore
from k1link.sessions.models import SessionDetail, SessionStoreError
CANONICAL_RECORDED_PROJECTION_SCHEMA: Final = (
"missioncore.canonical-recorded-lab-projection/v1"
)
CANONICAL_REPLAY_CAPABILITY_SCHEMA: Final = (
"missioncore.observation-lab-replay-capability/v1"
)
CANONICAL_SOURCE_SESSION_ID: Final = "20260828T130511Z_viewer_live"
CANONICAL_SOURCE_LABEL: Final = "RAVNOVES004TREE"
CANONICAL_WORK_ID: Final = "lab-v1-vegetation-shadow"
CANONICAL_RESULT_PREFIX: Final = "lab-v1-vegetation-shadow"
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
_RESULT_ID = re.compile(r"^lab-v1-vegetation-shadow-[a-f0-9]{64}$")
_DEFINITION: Final = LaboratoryEvidenceDefinition(
work_id=CANONICAL_WORK_ID,
runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"),
result_id_prefix=CANONICAL_RESULT_PREFIX,
document_name="result.json",
result_schema_version=LAB_SCHEMA,
)
class CanonicalRecordedCatalogError(ValueError):
"""The sealed result cannot be admitted to the recorded Session catalog."""
def publish_canonical_recorded_vegetation_result(
*,
store: SessionStore,
runtime_root: Path,
result_root: Path,
) -> LabSessionBinding:
"""Project one exact, verified full-route RAV004 result without computing replay data."""
runtime = _real_directory(runtime_root, "LAB runtime root")
candidate = _real_directory(result_root, "canonical LAB result")
if _RESULT_ID.fullmatch(candidate.name) is None:
raise CanonicalRecordedCatalogError("canonical LAB result identity is invalid")
expected_parent = _real_directory(
_DEFINITION.result_root(runtime),
"canonical LAB result collection",
)
if not expected_parent.is_relative_to(runtime):
raise CanonicalRecordedCatalogError(
"canonical LAB result collection escaped its runtime root"
)
if candidate.parent != expected_parent:
raise CanonicalRecordedCatalogError("canonical LAB result escaped its registered root")
try:
proof = verify_laboratory_evidence_result(_DEFINITION, candidate)
except LaboratoryEvidenceReportError as exc:
raise CanonicalRecordedCatalogError(str(exc)) from exc
document = _read_document(
candidate / _DEFINITION.document_name,
expected_sha256=str(proof["document_sha256"]),
)
result_id = _exact_text(document.get("result_id"), candidate.name, "result id")
_exact_text(document.get("schema_version"), LAB_SCHEMA, "result schema")
_exact_text(
document.get("status"),
"visual-shadow-ready-policy-not-authorized",
"result status",
)
identity_sha256 = _exact_text(
document.get("identity_sha256"),
str(proof["identity_sha256"]),
"identity digest",
)
authority = _object(document.get("authority"), "result authority")
_require_observation_only(authority, "result authority")
identity = _object(document.get("identity"), "result identity")
identity_authority = _object(identity.get("authority"), "identity authority")
_require_observation_only(identity_authority, "identity authority")
if identity_authority != authority:
raise CanonicalRecordedCatalogError("top-level authority is not identity-bound")
if document.get("route_video") is not None or document.get("route_review") is not None:
raise CanonicalRecordedCatalogError("canonical full-route result shape is invalid")
review = _object(document.get("route_full_review"), "full-route review")
identity_review = _object(
identity.get("route_full_review"),
"identity full-route review",
)
if identity_review != review:
raise CanonicalRecordedCatalogError("full-route review is not identity-bound")
_exact_text(review.get("source_id"), CANONICAL_SOURCE_LABEL, "source label")
source_session_id = _exact_text(
review.get("session_id"),
CANONICAL_SOURCE_SESSION_ID,
"source session id",
)
if review.get("frame_count") != 6830:
raise CanonicalRecordedCatalogError("canonical frame count is invalid")
source_result_id = _text(
review.get("linked_route_review_result_id"),
"linked route review result id",
)
if _RESULT_ID.fullmatch(source_result_id) is None:
raise CanonicalRecordedCatalogError("linked route review identity is invalid")
if identity.get("base_result_id") != source_result_id:
raise CanonicalRecordedCatalogError("base result is not identity-bound")
result_source = _object(document.get("source"), "result source")
if result_source != _object(
identity.get("source"),
"identity source",
):
raise CanonicalRecordedCatalogError("result source is not identity-bound")
if result_source != {
"shadow_session": CANONICAL_SOURCE_LABEL,
"shadow_camera": "sensor.camera.right",
"shadow_frame_count": 6830,
"video_shadow_frame_count": 6830,
}:
raise CanonicalRecordedCatalogError("canonical source identity changed")
timeline_start = _finite_number(
review.get("timeline_start_seconds"),
"timeline start",
)
timeline_end = _finite_number(
review.get("timeline_end_seconds"),
"timeline end",
)
if timeline_end <= timeline_start:
raise CanonicalRecordedCatalogError("canonical timeline range is invalid")
_validate_viewer_contract(candidate, document, review)
try:
source_detail, source_catalog_sha256 = (
store.get_session_with_catalog_snapshot(source_session_id)
)
except SessionStoreError as exc:
raise CanonicalRecordedCatalogError(
"canonical source session is unavailable"
) from exc
_validate_source_catalog_binding(source_detail)
sealed_method = _object(document.get("method"), "sealed method")
pipeline_id = _exact_text(
sealed_method.get("pipeline_id"),
"ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
"sealed pipeline id",
)
execution_class = _exact_text(
sealed_method.get("execution_class"),
"ai-inference",
"sealed execution class",
)
capability = LabReplayCapability(
schema_version=CANONICAL_REPLAY_CAPABILITY_SCHEMA,
kind="canonical-recorded-rerun",
viewer_profile="recorded-session",
timeline="session_time",
activation="explicit",
commands_enabled=False,
)
provenance = {
"schema_version": CANONICAL_RECORDED_PROJECTION_SCHEMA,
"evidence_identity_sha256": identity_sha256,
"result_document_sha256": proof["document_sha256"],
"replay_capability": capability.as_dict(),
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
"actuation_accepted": False,
},
"method": {
"schema_version": "missioncore.laboratory-method/v1",
"completeness": "legacy-partial",
"execution_class": execution_class,
"pipeline_id": pipeline_id,
"components": [
{
"kind": "source",
"name": "sealed full-route LAB result",
"version": LAB_SCHEMA,
"role": "immutable Session catalog projection",
"identity_sha256": identity_sha256,
}
],
},
}
return store.publish_lab_instance(
session_id=result_id,
source_session_id=source_session_id,
display_name="RAVNOVES004TREE · полный маршрут восприятия",
lab_id="LAB V1",
result_kind="recorded-perception-qualification",
result_id=result_id,
source_result_id=source_result_id,
config_sha256=None,
run_created_at_utc=_text(document.get("created_at_utc"), "creation time"),
replay_capability=capability,
provenance=provenance,
duration_seconds=timeline_end - timeline_start,
include_recorded_media=False,
expected_source_catalog_sha256=source_catalog_sha256,
)
def _real_directory(path: Path, label: str) -> Path:
candidate = path.expanduser().absolute()
if candidate.is_symlink():
raise CanonicalRecordedCatalogError(f"{label} must not be a symlink")
try:
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise CanonicalRecordedCatalogError(f"{label} is unavailable") from exc
if not resolved.is_dir():
raise CanonicalRecordedCatalogError(f"{label} must be a directory")
return resolved
def _read_document(path: Path, *, expected_sha256: str) -> dict[str, Any]:
if path.is_symlink() or not path.is_file() or path.stat().st_size > _MAX_DOCUMENT_BYTES:
raise CanonicalRecordedCatalogError("canonical LAB document is unavailable")
try:
payload = path.read_bytes()
if hashlib.sha256(payload).hexdigest() != expected_sha256:
raise CanonicalRecordedCatalogError(
"canonical LAB document changed after verification"
)
value = json.loads(payload)
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise CanonicalRecordedCatalogError("canonical LAB document is invalid") from exc
return _object(value, "canonical LAB document")
def _object(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
raise CanonicalRecordedCatalogError(f"{label} must be an object")
return value
def _text(value: object, label: str) -> str:
if not isinstance(value, str) or not value.strip() or value != value.strip():
raise CanonicalRecordedCatalogError(f"{label} must be a non-empty string")
return value
def _exact_text(value: object, expected: str, label: str) -> str:
text = _text(value, label)
if text != expected:
raise CanonicalRecordedCatalogError(f"{label} changed")
return text
def _finite_number(value: object, label: str) -> float:
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise CanonicalRecordedCatalogError(f"{label} must be numeric")
number = float(value)
if not math.isfinite(number):
raise CanonicalRecordedCatalogError(f"{label} must be finite")
return number
def _require_observation_only(value: dict[str, Any], label: str) -> None:
expected = {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
"actuation_accepted": False,
"camera_semantics_can_clear_rigid_geometry": False,
}
if set(value) != set(expected) or any(value[key] is not False for key in expected):
raise CanonicalRecordedCatalogError(f"{label} permits control authority")
def _validate_viewer_contract(
result_root: Path,
document: dict[str, Any],
review: dict[str, Any],
) -> None:
"""Require the metadata and artifacts consumed before the shared viewer mounts."""
if document.get("ground_truth") is not False:
raise CanonicalRecordedCatalogError("canonical result ground-truth marker changed")
identity = _object(document.get("identity"), "result identity")
selected_candidate = _text(
identity.get("selected_candidate"),
"selected candidate",
)
if selected_candidate not in {"ddrnet", "ppliteseg"}:
raise CanonicalRecordedCatalogError("canonical selected candidate changed")
metrics = _object(document.get("metrics"), "result metrics")
candidates = _object(metrics.get("candidates"), "candidate metrics")
identity_candidates = _object(
identity.get("candidate_metrics"),
"identity candidate metrics",
)
if candidates != identity_candidates or set(candidates) != {"ddrnet", "ppliteseg"}:
raise CanonicalRecordedCatalogError("candidate metrics are not identity-bound")
for candidate in ("ddrnet", "ppliteseg"):
_validate_candidate_metrics(
_object(candidates.get(candidate), f"{candidate} metrics"),
candidate,
)
decision = _object(document.get("decision"), "result decision")
if (
decision.get("selected_candidate") != selected_candidate
or decision.get("visual_shadow_ready") is not True
or decision.get("mission_policy_ready_for_configuration") is not True
or decision.get("navigation_accepted") is not False
or decision.get("production_accepted") is not False
):
raise CanonicalRecordedCatalogError("canonical result decision changed")
catalogs = _object(document.get("catalogs"), "result catalogs")
if catalogs.get("goose") != [] or catalogs.get("ravnoves") != []:
raise CanonicalRecordedCatalogError("canonical result catalogs changed")
limitations = document.get("limitations")
if not isinstance(limitations, list) or not limitations or not all(
isinstance(item, str) and item.strip() for item in limitations
):
raise CanonicalRecordedCatalogError("canonical result limitations are invalid")
expected_route_values: dict[str, object] = {
"source_job_id": "recorded-camera-eb2783c5480d56bda07c8af0",
"source_job_input_sha256": (
"eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715"
),
"source_stream_sha256": (
"e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac"
),
"recorded_media_source_id": "recorded.camera.6a3945242828a038",
"recorded_media_generation_sha256": (
"b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded"
),
"width": 800,
"height": 600,
}
if any(review.get(key) != expected for key, expected in expected_route_values.items()):
raise CanonicalRecordedCatalogError("canonical full-route source binding changed")
if review.get("ground_truth") is not False:
raise CanonicalRecordedCatalogError("canonical full-route ground-truth marker changed")
artifacts = _artifact_catalog(document.get("artifacts"))
timeline = _object(review.get("timeline"), "full-route timeline")
if (
timeline.get("path") != "video/frame-source-times-ns.bin"
or timeline.get("encoding") != "uint64-le-nanoseconds"
or timeline.get("frame_count") != 6830
or timeline.get("byte_length") != 6830 * 8
):
raise CanonicalRecordedCatalogError("canonical full-route timeline changed")
timeline_sha256 = _sha256_text(timeline.get("sha256"), "timeline digest")
_require_artifact(
artifacts,
path="video/frame-source-times-ns.bin",
sha256=timeline_sha256,
byte_length=6830 * 8,
media_type="application/octet-stream",
)
_validate_timeline_payload(
result_root / "video" / "frame-source-times-ns.bin",
expected_sha256=timeline_sha256,
start_seconds=_finite_number(review.get("timeline_start_seconds"), "timeline start"),
)
decode_repair = _object(review.get("decode_repair"), "decode repair")
if (
decode_repair.get("repaired_frame_count") != 1
or decode_repair.get("sequence") != 6092
or decode_repair.get("method") != "duplicate-previous-decoded-frame"
):
raise CanonicalRecordedCatalogError("canonical decode repair changed")
repair_proofs = _object(decode_repair.get("proofs"), "decode repair proofs")
for key, path in {
"eomt": "proofs/decode_repair.json",
"ddrnet": "proofs/ddrnet_decode_repair.json",
}.items():
proof = _object(repair_proofs.get(key), f"{key} decode proof")
digest = _sha256_text(proof.get("sha256"), f"{key} decode proof digest")
if proof.get("path") != path:
raise CanonicalRecordedCatalogError("canonical decode proof changed")
_require_artifact(artifacts, path=path, sha256=digest)
route_proofs = _object(review.get("proofs"), "full-route proofs")
job_proof = _object(route_proofs.get("job"), "full-route job proof")
job_digest = _sha256_text(job_proof.get("sha256"), "full-route job digest")
if job_proof.get("path") != "proofs/job.json":
raise CanonicalRecordedCatalogError("canonical job proof changed")
_require_artifact(artifacts, path="proofs/job.json", sha256=job_digest)
layers = _object(review.get("layers"), "full-route layers")
if set(layers) != {"city", "vegetation"}:
raise CanonicalRecordedCatalogError("canonical full-route layers changed")
_validate_full_route_layer(
_object(layers.get("city"), "city layer"),
layer="city",
artifacts=artifacts,
)
_validate_full_route_layer(
_object(layers.get("vegetation"), "vegetation layer"),
layer="vegetation",
artifacts=artifacts,
)
def _validate_source_catalog_binding(detail: SessionDetail) -> None:
summary = detail.summary
if (
detail.plugin_id != "nodedc.device.xgrids-lixelkity-k1"
or detail.archive_id != "xgrids-k1.viewer-live.evidence"
or summary.session_id != CANONICAL_SOURCE_SESSION_ID
or summary.display_name != CANONICAL_SOURCE_LABEL
or summary.status != "ready"
or summary.started_at_utc != "2026-08-28T13:05:16.249Z"
or summary.completed_at_utc != "2026-08-28T13:18:45.030Z"
or summary.duration_seconds is None
or not math.isclose(summary.duration_seconds, 808.779495667, abs_tol=1e-9)
or summary.modalities != ("point-cloud", "trajectory", "video")
or summary.source_count != 3
or summary.total_bytes != 799_020_963
or summary.replayable is not True
or summary.lab is not None
):
raise CanonicalRecordedCatalogError("canonical source catalog identity changed")
sources = {
source.source_id: (
source.semantic_channel_id,
source.modality,
source.status,
source.seekable,
source.artifact_id,
)
for source in detail.sources
}
if sources != {
"sensor.camera.right": (
"camera.video.recorded",
"video",
"recorded",
True,
"recorded-video-6a3945242828a038",
),
"sensor.lidar.primary": (
"spatial.point-cloud.recorded",
"point-cloud",
"recorded",
True,
"raw-transport-primary",
),
"spatial.trajectory": (
"spatial.pose.recorded",
"trajectory",
"recorded",
True,
"raw-transport-primary",
),
}:
raise CanonicalRecordedCatalogError("canonical source channels changed")
artifacts = {artifact.artifact_id: artifact for artifact in detail.artifacts}
if set(artifacts) != {
"raw-transport-clock",
"raw-transport-clock-origin",
"raw-transport-index",
"raw-transport-primary",
"recorded-video-6a3945242828a038",
}:
raise CanonicalRecordedCatalogError("canonical source artifacts changed")
raw = artifacts["raw-transport-primary"]
video = artifacts["recorded-video-6a3945242828a038"]
if (
raw.kind != "raw-transport"
or raw.media_type != "application/x-nodedc-k1mqtt"
or raw.byte_length != 245_183_013
or raw.sha256 != "20c789eff922a6bbb53592f86614abc0729a30544df29e740e7a378d12af85c2"
or raw.integrity_status != "verified"
or video.kind != "recorded-video"
or video.media_type != "video/mp4"
or video.byte_length != 553_837_950
or video.sha256 is not None
or video.integrity_status != "validated-structure"
):
raise CanonicalRecordedCatalogError("canonical source artifact proof changed")
def _validate_candidate_metrics(value: dict[str, Any], candidate: str) -> None:
_text(value.get("loaded_model_name"), f"{candidate} model name")
_sha256_text(value.get("checkpoint_sha256"), f"{candidate} checkpoint")
validation = _object(value.get("validation_metrics"), f"{candidate} validation")
validation_timing = _object(
value.get("validation_timing"),
f"{candidate} validation timing",
)
shadow_timing = _object(value.get("shadow_timing"), f"{candidate} shadow timing")
resource = _object(value.get("resource"), f"{candidate} resource")
for key in ("mean_iou_percent", "published_mean_iou_percent", "vegetation_mean_iou"):
_finite_number(validation.get(key), f"{candidate} {key}")
for key in ("latency_ms_p95", "throughput_fps_from_mean_inference"):
_finite_number(validation_timing.get(key), f"{candidate} validation {key}")
_finite_number(shadow_timing.get(key), f"{candidate} shadow {key}")
_finite_number(shadow_timing.get("prewarm_latency_ms"), f"{candidate} prewarm")
_nonnegative_integer(resource.get("peak_reserved_vram_bytes"), f"{candidate} VRAM")
_text(resource.get("gpu_name"), f"{candidate} GPU")
def _validate_full_route_layer(
value: dict[str, Any],
*,
layer: str,
artifacts: dict[str, dict[str, Any]],
) -> None:
expected = {
"city": (
re.compile(r"^result-[a-f0-9]{64}$"),
"missioncore.recorded-eomt-taxonomy/v1",
16,
"video/eomt-semantic-masks.zip",
),
"vegetation": (
re.compile(r"^lab-v1-ravnoves-video-ddrnet-[a-f0-9]{64}$"),
"missioncore.lab-v1-vegetation-taxonomy/v1",
64,
"video/ddrnet-semantic-masks.zip",
),
}[layer]
result_id = _text(value.get("result_id"), f"{layer} result id")
if expected[0].fullmatch(result_id) is None or value.get("frame_count") != 6830:
raise CanonicalRecordedCatalogError(f"canonical {layer} layer identity changed")
_text(value.get("name"), f"{layer} layer name")
_finite_number(value.get("inference_fps"), f"{layer} inference FPS")
_finite_number(value.get("latency_p95_ms"), f"{layer} latency")
_nonnegative_integer(value.get("peak_reserved_vram_bytes"), f"{layer} VRAM")
_validate_taxonomy(
_object(value.get("taxonomy"), f"{layer} taxonomy"),
schema=expected[1],
class_count=expected[2],
label=layer,
)
archive = _object(value.get("mask_archive"), f"{layer} mask archive")
digest = _sha256_text(archive.get("sha256"), f"{layer} archive digest")
byte_length = _positive_integer(archive.get("byte_length"), f"{layer} archive bytes")
if archive.get("path") != expected[3]:
raise CanonicalRecordedCatalogError(f"canonical {layer} archive changed")
_require_artifact(
artifacts,
path=expected[3],
sha256=digest,
byte_length=byte_length,
media_type="application/zip",
)
def _validate_taxonomy(
value: dict[str, Any],
*,
schema: str,
class_count: int,
label: str,
) -> None:
classes = value.get("classes")
if value.get("schema_version") != schema or not isinstance(classes, list):
raise CanonicalRecordedCatalogError(f"canonical {label} taxonomy changed")
if len(classes) != class_count:
raise CanonicalRecordedCatalogError(f"canonical {label} taxonomy size changed")
for expected_id, raw in enumerate(classes):
item = _object(raw, f"{label} taxonomy class")
color = item.get("color_rgb")
if (
item.get("class_id") != expected_id
or isinstance(item.get("class_id"), bool)
or not isinstance(color, list)
or len(color) != 3
or any(
isinstance(channel, bool)
or not isinstance(channel, int)
or not 0 <= channel <= 255
for channel in color
)
or item.get("disposition")
not in {"labeled", "ambiguous", "prediction", "undefined"}
):
raise CanonicalRecordedCatalogError(f"canonical {label} taxonomy class changed")
_text(item.get("label"), f"{label} taxonomy label")
for optional in ("material_class", "evidence_state"):
if item.get(optional) is not None:
_text(item.get(optional), f"{label} taxonomy {optional}")
def _artifact_catalog(value: object) -> dict[str, dict[str, Any]]:
if not isinstance(value, list) or not value:
raise CanonicalRecordedCatalogError("canonical result artifacts are missing")
catalog: dict[str, dict[str, Any]] = {}
for raw in value:
descriptor = _object(raw, "canonical result artifact")
path = _text(descriptor.get("path"), "canonical artifact path")
if path in catalog:
raise CanonicalRecordedCatalogError("canonical result artifact is duplicated")
catalog[path] = descriptor
return catalog
def _require_artifact(
artifacts: dict[str, dict[str, Any]],
*,
path: str,
sha256: str,
byte_length: int | None = None,
media_type: str | None = None,
) -> None:
descriptor = artifacts.get(path)
if (
descriptor is None
or descriptor.get("sha256") != sha256
or (byte_length is not None and descriptor.get("byte_length") != byte_length)
or (media_type is not None and descriptor.get("media_type") != media_type)
):
raise CanonicalRecordedCatalogError("canonical replay artifact binding changed")
def _validate_timeline_payload(
path: Path,
*,
expected_sha256: str,
start_seconds: float,
) -> None:
try:
payload = path.read_bytes()
values = struct.unpack("<6830Q", payload)
except (OSError, struct.error) as exc:
raise CanonicalRecordedCatalogError("canonical timeline payload is invalid") from exc
if (
hashlib.sha256(payload).hexdigest() != expected_sha256
or values[0] != round(start_seconds * 1_000_000_000)
or values[-1] > 9_007_199_254_740_991
or any(
current <= previous
for previous, current in zip(values, values[1:], strict=False)
)
):
raise CanonicalRecordedCatalogError("canonical timeline payload changed")
def _sha256_text(value: object, label: str) -> str:
digest = _text(value, label)
if re.fullmatch(r"[a-f0-9]{64}", digest) is None:
raise CanonicalRecordedCatalogError(f"{label} is invalid")
return digest
def _nonnegative_integer(value: object, label: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise CanonicalRecordedCatalogError(f"{label} must be a non-negative integer")
return value
def _positive_integer(value: object, label: str) -> int:
integer = _nonnegative_integer(value, label)
if integer == 0:
raise CanonicalRecordedCatalogError(f"{label} must be positive")
return integer