nl2data-semantic-catalog-postgres 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. nl2data_semantic_catalog_postgres/__init__.py +18 -0
  2. nl2data_semantic_catalog_postgres/client.py +139 -0
  3. nl2data_semantic_catalog_postgres/config.py +136 -0
  4. nl2data_semantic_catalog_postgres/envelope.py +327 -0
  5. nl2data_semantic_catalog_postgres/errors.py +188 -0
  6. nl2data_semantic_catalog_postgres/fake_postgres/__init__.py +53 -0
  7. nl2data_semantic_catalog_postgres/fake_postgres/driver.py +209 -0
  8. nl2data_semantic_catalog_postgres/fake_postgres/handlers_audit.py +243 -0
  9. nl2data_semantic_catalog_postgres/fake_postgres/handlers_drafts.py +84 -0
  10. nl2data_semantic_catalog_postgres/fake_postgres/handlers_maintenance.py +263 -0
  11. nl2data_semantic_catalog_postgres/fake_postgres/handlers_publications.py +249 -0
  12. nl2data_semantic_catalog_postgres/fake_postgres/handlers_schema.py +23 -0
  13. nl2data_semantic_catalog_postgres/fake_postgres/handlers_snapshots.py +197 -0
  14. nl2data_semantic_catalog_postgres/fake_postgres/handlers_versions.py +330 -0
  15. nl2data_semantic_catalog_postgres/fake_postgres/keys.py +99 -0
  16. nl2data_semantic_catalog_postgres/fake_postgres/pool.py +152 -0
  17. nl2data_semantic_catalog_postgres/fake_postgres/registry.py +144 -0
  18. nl2data_semantic_catalog_postgres/maintenance.py +235 -0
  19. nl2data_semantic_catalog_postgres/py.typed +0 -0
  20. nl2data_semantic_catalog_postgres/repositories/__init__.py +26 -0
  21. nl2data_semantic_catalog_postgres/repositories/activation.py +849 -0
  22. nl2data_semantic_catalog_postgres/repositories/audit_evidence.py +221 -0
  23. nl2data_semantic_catalog_postgres/repositories/drafts.py +156 -0
  24. nl2data_semantic_catalog_postgres/repositories/evidence.py +415 -0
  25. nl2data_semantic_catalog_postgres/repositories/publications.py +486 -0
  26. nl2data_semantic_catalog_postgres/repositories/snapshots.py +372 -0
  27. nl2data_semantic_catalog_postgres/schema.py +337 -0
  28. nl2data_semantic_catalog_postgres/sql.py +520 -0
  29. nl2data_semantic_catalog_postgres/store.py +716 -0
  30. nl2data_semantic_catalog_postgres/unit_of_work.py +589 -0
  31. nl2data_semantic_catalog_postgres-0.1.0.dist-info/METADATA +76 -0
  32. nl2data_semantic_catalog_postgres-0.1.0.dist-info/RECORD +34 -0
  33. nl2data_semantic_catalog_postgres-0.1.0.dist-info/WHEEL +5 -0
  34. nl2data_semantic_catalog_postgres-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,152 @@
1
+ """Thread-safe in-memory pool executing the catalog's SQL statement set.
2
+
3
+ Expose ``snapshots``/``snapshot_pointers``/``proposal_sets``/``publications``/
4
+ ``bundle_pointers``/``bundle_history``/``events``/``clock`` for direct
5
+ assertions in tests. Statement dispatch goes through the domain handler
6
+ registry in :mod:`.registry`; unrecognized statements fail loudly with
7
+ ``AssertionError``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import contextlib
13
+ import threading
14
+ from collections.abc import Iterator
15
+ from datetime import datetime
16
+ from typing import Any
17
+
18
+ from ..sql import SQL_TEMPLATES
19
+ from .driver import (
20
+ FakeClock,
21
+ OperationalError,
22
+ PoolClosed,
23
+ _FakeConnection,
24
+ _FakeCursor,
25
+ _monotonic,
26
+ _normalize,
27
+ _PassFailure,
28
+ )
29
+ from .registry import HANDLERS
30
+
31
+
32
+ class FakePostgresPool:
33
+ """Thread-safe in-memory pool executing the catalog's SQL statement set."""
34
+
35
+ def __init__(self, *, start: datetime | None = None) -> None:
36
+ self.clock = FakeClock(start)
37
+ self.snapshots: dict[tuple[str, str], dict[str, Any]] = {}
38
+ self.snapshot_pointers: dict[tuple[str, str], dict[str, Any]] = {}
39
+ self.proposal_sets: dict[tuple[str, str], dict[str, Any]] = {}
40
+ self.assembly_drafts: dict[tuple[str, str], dict[str, Any]] = {}
41
+ self.publications: dict[tuple[str, str, str], dict[str, Any]] = {}
42
+ self.accepted_manifests: dict[tuple[str, str, str], dict[str, Any]] = {}
43
+ self.verification_evidence: dict[tuple[str, str, str], dict[str, Any]] = {}
44
+ self.publish_audits: dict[tuple[str, str, str], dict[str, Any]] = {}
45
+ self.published_versions: dict[tuple[str, str, str], dict[str, Any]] = {}
46
+ self.supersession_edges: dict[tuple[str, str, str], dict[str, Any]] = {}
47
+ self.bundle_pointers: dict[tuple[str, str], dict[str, Any]] = {}
48
+ self.bundle_history: dict[tuple[str, str], dict[int, dict[str, Any]]] = {}
49
+ self.events: dict[tuple[str, str], dict[str, Any]] = {}
50
+ self.publication_audit_evidence: dict[
51
+ tuple[str, str, str], dict[str, Any]
52
+ ] = {}
53
+ self.audit_entries: dict[tuple[str, str], dict[str, Any]] = {}
54
+ self.schema_metadata: dict[str, str] = {}
55
+ self._closed = False
56
+ self._struct_lock = threading.RLock()
57
+ self._lock_owner: dict[tuple[Any, ...], str] = {}
58
+ self._lock_cond = threading.Condition(self._struct_lock)
59
+ self._pending_failures: list[BaseException | _PassFailure] = []
60
+ self.statement_journal: list[str] = []
61
+ self._statement_names = {
62
+ _normalize(template.format(schema='"catalog"')): name
63
+ for name, template in SQL_TEMPLATES.items()
64
+ }
65
+
66
+ # -- lifecycle --------------------------------------------------------
67
+
68
+ @contextlib.contextmanager
69
+ def connection(self) -> Iterator[_FakeConnection]:
70
+ """One transaction-backed connection; ``commit``/``rollback`` explicit."""
71
+ with self._struct_lock:
72
+ if self._closed:
73
+ raise PoolClosed("fake pool is closed")
74
+ connection = _FakeConnection(self)
75
+ try:
76
+ yield connection
77
+ finally:
78
+ connection._finish()
79
+
80
+ def close(self) -> None:
81
+ """Close the pool; later checkouts fail with ``PoolClosed``."""
82
+ with self._struct_lock:
83
+ self._closed = True
84
+
85
+ def set_schema_version(self, version: int) -> None:
86
+ """Seed ``schema_metadata`` so catalog initialization sees a version."""
87
+ with self._struct_lock:
88
+ self.schema_metadata["schema_version"] = str(version)
89
+
90
+ def fail_next(
91
+ self, error: BaseException, *, count: int = 1, after: int = 0
92
+ ) -> None:
93
+ """Fail the next ``count`` statements, after ``after`` pass-throughs.
94
+
95
+ ``after`` lets a multi-statement operation commit earlier writes and
96
+ then fail mid-transaction, so store rollback of partial work can be
97
+ asserted deterministically.
98
+ """
99
+ with self._struct_lock:
100
+ self._pending_failures.extend([_PassFailure()] * after)
101
+ self._pending_failures.extend([error] * count)
102
+
103
+ # -- row locking ------------------------------------------------------
104
+
105
+ def _lock(
106
+ self, connection: _FakeConnection, key: tuple[Any, ...], timeout: float
107
+ ) -> None:
108
+ """Acquire a row lock, blocking while another connection holds it."""
109
+ deadline = _monotonic() + timeout
110
+ with self._lock_cond:
111
+ while self._lock_owner.get(key) not in (None, connection.id):
112
+ remaining = deadline - _monotonic()
113
+ if remaining <= 0:
114
+ raise OperationalError("lock wait timeout exceeded")
115
+ self._lock_cond.wait(remaining)
116
+ self._lock_owner[key] = connection.id
117
+ connection._owned_keys.add(key)
118
+
119
+ def _release(self, connection: _FakeConnection) -> None:
120
+ """Release every row lock owned by the connection."""
121
+ with self._lock_cond:
122
+ for key in connection._owned_keys:
123
+ if self._lock_owner.get(key) == connection.id:
124
+ del self._lock_owner[key]
125
+ connection._owned_keys.clear()
126
+ self._lock_cond.notify_all()
127
+
128
+ # -- statement dispatch -----------------------------------------------
129
+
130
+ def _execute(
131
+ self,
132
+ connection: _FakeConnection,
133
+ sql: str,
134
+ params: tuple[Any, ...],
135
+ timeout: float,
136
+ ) -> _FakeCursor:
137
+ with self._struct_lock:
138
+ while self._pending_failures:
139
+ failure = self._pending_failures.pop(0)
140
+ if not isinstance(failure, _PassFailure):
141
+ raise failure
142
+ normalized = _normalize(sql)
143
+ if normalized.startswith("CREATE "):
144
+ # DDL is emulated: tables exist by construction.
145
+ return _FakeCursor(self, connection, [], 0)
146
+ name = self._statement_names.get(normalized)
147
+ if name is None:
148
+ raise AssertionError(f"fake pool does not recognize statement: {sql[:240]}")
149
+ self.statement_journal.append(name)
150
+ handler = HANDLERS[name]
151
+ rows, rowcount = handler(self, connection, params or (), timeout)
152
+ return _FakeCursor(self, connection, rows, rowcount)
@@ -0,0 +1,144 @@
1
+ """Statement-name to handler registry for the fake pool.
2
+
3
+ Each entry maps one production SQL template name (the exact key set of
4
+ :data:`SQL_TEMPLATES`) to its domain handler, so template drift between
5
+ the store and the fake is caught at the first unrecognized statement.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+ from .handlers_audit import (
14
+ _h_count_audit_evidence,
15
+ _h_insert_audit_entry,
16
+ _h_insert_publication_audit_evidence,
17
+ _h_list_audit_evidence,
18
+ _h_read_audit_entry,
19
+ _h_read_latest_publication_entry,
20
+ _h_read_publication_audit_entry,
21
+ _h_read_publication_audit_evidence,
22
+ )
23
+ from .handlers_drafts import (
24
+ _h_insert_assembly_draft,
25
+ _h_lock_assembly_draft,
26
+ _h_read_assembly_draft,
27
+ _h_replace_assembly_draft,
28
+ )
29
+ from .handlers_maintenance import (
30
+ _h_delete_expired_audit_entries,
31
+ _h_delete_expired_events,
32
+ _h_delete_expired_publications,
33
+ _h_delete_expired_snapshots,
34
+ _h_insert_event,
35
+ )
36
+ from .handlers_publications import (
37
+ _h_insert_accepted_manifest,
38
+ _h_insert_publication,
39
+ _h_insert_publish_audit,
40
+ _h_insert_verification_evidence,
41
+ _h_list_publications,
42
+ _h_lock_publication_series,
43
+ _h_read_accepted_manifest,
44
+ _h_read_publication,
45
+ _h_read_publication_by_fingerprint,
46
+ _h_read_publication_fingerprint,
47
+ _h_read_publish_audit,
48
+ _h_read_publish_by_idempotency_key,
49
+ _h_read_verification_evidence,
50
+ )
51
+ from .handlers_schema import _h_read_schema_version, _h_write_schema_version
52
+ from .handlers_snapshots import (
53
+ _h_list_snapshot_pointers,
54
+ _h_lock_snapshot_row,
55
+ _h_read_proposal_set,
56
+ _h_read_snapshot_envelope,
57
+ _h_read_snapshot_pointer,
58
+ _h_set_snapshot_state,
59
+ _h_snapshot_exists,
60
+ _h_upsert_proposal_set,
61
+ _h_upsert_snapshot,
62
+ _h_upsert_snapshot_pointer,
63
+ )
64
+ from .handlers_versions import (
65
+ _h_delete_history_top,
66
+ _h_insert_history,
67
+ _h_insert_published_version,
68
+ _h_insert_supersession_edge,
69
+ _h_list_bundle_pointers,
70
+ _h_list_orphan_active_versions,
71
+ _h_list_published_versions,
72
+ _h_lock_bundle_pointer,
73
+ _h_next_history_position,
74
+ _h_read_bundle_pointer,
75
+ _h_read_history_top,
76
+ _h_read_latest_version,
77
+ _h_read_published_version,
78
+ _h_set_published_version_state,
79
+ _h_trim_history,
80
+ _h_update_version_successor,
81
+ _h_upsert_bundle_pointer,
82
+ )
83
+
84
+ HANDLERS: dict[str, Callable[..., tuple[list[dict[str, Any]], int]]] = {
85
+ "read_schema_version": _h_read_schema_version,
86
+ "write_schema_version": _h_write_schema_version,
87
+ "upsert_snapshot": _h_upsert_snapshot,
88
+ "read_snapshot_envelope": _h_read_snapshot_envelope,
89
+ "lock_snapshot_row": _h_lock_snapshot_row,
90
+ "set_snapshot_state": _h_set_snapshot_state,
91
+ "snapshot_exists": _h_snapshot_exists,
92
+ "upsert_snapshot_pointer": _h_upsert_snapshot_pointer,
93
+ "read_snapshot_pointer": _h_read_snapshot_pointer,
94
+ "list_snapshot_pointers": _h_list_snapshot_pointers,
95
+ "upsert_proposal_set": _h_upsert_proposal_set,
96
+ "read_proposal_set": _h_read_proposal_set,
97
+ "insert_assembly_draft": _h_insert_assembly_draft,
98
+ "read_assembly_draft": _h_read_assembly_draft,
99
+ "lock_assembly_draft": _h_lock_assembly_draft,
100
+ "replace_assembly_draft": _h_replace_assembly_draft,
101
+ "insert_publication": _h_insert_publication,
102
+ "read_publication": _h_read_publication,
103
+ "read_publication_fingerprint": _h_read_publication_fingerprint,
104
+ "read_publication_by_fingerprint": _h_read_publication_by_fingerprint,
105
+ "lock_publication_series": _h_lock_publication_series,
106
+ "list_publications": _h_list_publications,
107
+ "insert_accepted_manifest": _h_insert_accepted_manifest,
108
+ "read_accepted_manifest": _h_read_accepted_manifest,
109
+ "insert_verification_evidence": _h_insert_verification_evidence,
110
+ "read_verification_evidence": _h_read_verification_evidence,
111
+ "insert_publish_audit": _h_insert_publish_audit,
112
+ "read_publish_audit": _h_read_publish_audit,
113
+ "read_publish_by_idempotency_key": _h_read_publish_by_idempotency_key,
114
+ "read_latest_version": _h_read_latest_version,
115
+ "insert_published_version": _h_insert_published_version,
116
+ "update_version_successor": _h_update_version_successor,
117
+ "insert_supersession_edge": _h_insert_supersession_edge,
118
+ "read_published_version": _h_read_published_version,
119
+ "list_published_versions": _h_list_published_versions,
120
+ "set_published_version_state": _h_set_published_version_state,
121
+ "upsert_bundle_pointer": _h_upsert_bundle_pointer,
122
+ "read_bundle_pointer": _h_read_bundle_pointer,
123
+ "lock_bundle_pointer": _h_lock_bundle_pointer,
124
+ "next_history_position": _h_next_history_position,
125
+ "insert_history": _h_insert_history,
126
+ "read_history_top": _h_read_history_top,
127
+ "delete_history_top": _h_delete_history_top,
128
+ "trim_history": _h_trim_history,
129
+ "list_bundle_pointers": _h_list_bundle_pointers,
130
+ "list_orphan_active_versions": _h_list_orphan_active_versions,
131
+ "insert_event": _h_insert_event,
132
+ "insert_publication_audit_evidence": _h_insert_publication_audit_evidence,
133
+ "read_publication_audit_evidence": _h_read_publication_audit_evidence,
134
+ "insert_audit_entry": _h_insert_audit_entry,
135
+ "read_audit_entry": _h_read_audit_entry,
136
+ "read_publication_audit_entry": _h_read_publication_audit_entry,
137
+ "read_latest_publication_entry": _h_read_latest_publication_entry,
138
+ "count_audit_evidence": _h_count_audit_evidence,
139
+ "list_audit_evidence": _h_list_audit_evidence,
140
+ "delete_expired_snapshots": _h_delete_expired_snapshots,
141
+ "delete_expired_publications": _h_delete_expired_publications,
142
+ "delete_expired_events": _h_delete_expired_events,
143
+ "delete_expired_audit_entries": _h_delete_expired_audit_entries,
144
+ }
@@ -0,0 +1,235 @@
1
+ """Bounded maintenance and startup revalidation for the durable catalog.
2
+
3
+ The store facade delegates these cross-domain sweeps here: ``cleanup``
4
+ removes expired inactive records in bounded batches while preserving
5
+ active content and required dependencies, and ``reload_active``
6
+ revalidates every active snapshot/Bundle pointer after startup so a
7
+ newer persisted schema or envelope version fails closed.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import datetime, timedelta
13
+
14
+ from nl2data_core.bundles.publication import (
15
+ LifecycleWitnessError,
16
+ PublishedVersionState,
17
+ validate_lifecycle_witness,
18
+ witness_cause_type,
19
+ )
20
+ from nl2data_core.metadata.catalog import CatalogReloadIssue, CatalogReloadReport
21
+
22
+ from .envelope import ArtifactKind
23
+ from .errors import SemanticCatalogError, SemanticCatalogErrorCode
24
+ from .repositories.evidence import EvidenceRepository
25
+ from .unit_of_work import CatalogUnitOfWork
26
+
27
+ __all__ = ["cleanup", "reload_active"]
28
+
29
+
30
+ def cleanup(uow: CatalogUnitOfWork, *, now: datetime | None = None) -> int:
31
+ """Remove expired inactive records; preserves active content.
32
+
33
+ Expired snapshots that no pointer references, expired publications
34
+ that neither pointer nor history references (and that no publication
35
+ depends on), expired lifecycle events, and expired audit-evidence
36
+ entries (unless their bundle fingerprint belongs to a non-retired
37
+ published version, or a protected entry references them as a
38
+ predecessor) are removed in bounded batches. Active snapshots,
39
+ active Bundles, and required dependencies are never removed.
40
+ """
41
+ config = uow.config
42
+ current = uow.now() if now is None else now
43
+ event_cutoff = current - timedelta(seconds=config.event_retention_seconds)
44
+ audit_cutoff = current - timedelta(seconds=config.audit_retention_seconds)
45
+ total = 0
46
+ with uow.transaction() as conn:
47
+ cursor = uow.execute(
48
+ conn,
49
+ "delete_expired_snapshots",
50
+ (current, config.cleanup_batch_size),
51
+ )
52
+ total += int(cursor.rowcount or 0)
53
+ cursor = uow.execute(
54
+ conn,
55
+ "delete_expired_publications",
56
+ (current, config.cleanup_batch_size),
57
+ )
58
+ total += int(cursor.rowcount or 0)
59
+ cursor = uow.execute(
60
+ conn,
61
+ "delete_expired_events",
62
+ (event_cutoff, config.cleanup_batch_size),
63
+ )
64
+ total += int(cursor.rowcount or 0)
65
+ cursor = uow.execute(
66
+ conn,
67
+ "delete_expired_audit_entries",
68
+ (audit_cutoff, config.cleanup_batch_size),
69
+ )
70
+ total += int(cursor.rowcount or 0)
71
+ uow.insert_event(
72
+ conn,
73
+ "cleanup",
74
+ None,
75
+ namespace="",
76
+ occurred_at=current,
77
+ )
78
+ return total
79
+
80
+
81
+ def reload_active(uow: CatalogUnitOfWork, *, now: datetime | None = None) -> CatalogReloadReport:
82
+ """Revalidate every active snapshot/Bundle pointer after startup.
83
+
84
+ A newer persisted schema or envelope version fails closed; active
85
+ pointers whose artifact no longer revalidates are reported as
86
+ rejected and are never exposed for query-time resolution (reads
87
+ revalidate independently and fail closed). When an active Bundle
88
+ publication carries verification evidence, its immutable frozen
89
+ binding, audit, and manifest cross-links are revalidated too, so a
90
+ corrupted or legacy evidence record is reported as rejected instead
91
+ of silently passing startup revalidation.
92
+ """
93
+ checked_at = uow.now() if now is None else now
94
+ issues: list[CatalogReloadIssue] = []
95
+ snapshots_revalidated = 0
96
+ bundles_revalidated = 0
97
+ evidence = EvidenceRepository(uow)
98
+ with uow.transaction() as conn:
99
+ for pointer in uow.execute(conn, "list_snapshot_pointers").fetchall():
100
+ if len(issues) >= 16:
101
+ break
102
+ row = uow.execute(
103
+ conn,
104
+ "read_snapshot_envelope",
105
+ (
106
+ pointer["scope_namespace"],
107
+ pointer["snapshot_fingerprint"],
108
+ ),
109
+ ).fetchone()
110
+ try:
111
+ if row is None:
112
+ raise SemanticCatalogError(
113
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
114
+ "active snapshot artifact is missing",
115
+ details={"cause_type": "MissingArtifact"},
116
+ )
117
+ envelope = uow.decode(
118
+ row["envelope"],
119
+ ArtifactKind.SNAPSHOT,
120
+ row_schema_version=row["schema_version"],
121
+ )
122
+ uow.snapshot_from_envelope(
123
+ envelope, discovered_at=row["discovered_at"]
124
+ )
125
+ snapshots_revalidated += 1
126
+ except SemanticCatalogError as error:
127
+ issues.append(
128
+ CatalogReloadIssue(
129
+ code=error.code.value,
130
+ message=error.message,
131
+ member_id=pointer["source_id"],
132
+ )
133
+ )
134
+ for pointer in uow.execute(conn, "list_bundle_pointers").fetchall():
135
+ if len(issues) >= 16:
136
+ break
137
+ row = uow.execute(
138
+ conn,
139
+ "read_publication",
140
+ (
141
+ pointer["scope_namespace"],
142
+ pointer["bundle_id"],
143
+ pointer["model_version"],
144
+ ),
145
+ ).fetchone()
146
+ try:
147
+ if row is None:
148
+ raise SemanticCatalogError(
149
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
150
+ "active bundle artifact is missing",
151
+ details={"cause_type": "MissingArtifact"},
152
+ )
153
+ envelope = uow.decode(
154
+ row["envelope"],
155
+ ArtifactKind.BUNDLE,
156
+ row_schema_version=row["schema_version"],
157
+ )
158
+ bundle = uow.bundle_from_envelope(envelope)
159
+ version_row = uow.execute(
160
+ conn,
161
+ "read_published_version",
162
+ (
163
+ pointer["scope_namespace"],
164
+ pointer["bundle_id"],
165
+ bundle.fingerprint,
166
+ ),
167
+ ).fetchone()
168
+ if version_row is None:
169
+ # The pointer is a witness that this version was
170
+ # activated; a publication without its lifecycle row
171
+ # is corruption.
172
+ raise SemanticCatalogError(
173
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
174
+ "active publication has no published version record",
175
+ details={"cause_type": "PublicationVersionMissing"},
176
+ )
177
+ try:
178
+ # The pointer carries redundant fingerprint and version
179
+ # witnesses and must agree with an ACTIVE version row.
180
+ validate_lifecycle_witness(
181
+ bundle,
182
+ witness="pointer",
183
+ witness_fingerprint=pointer["bundle_fingerprint"],
184
+ witness_model_version=pointer["model_version"],
185
+ lifecycle_state=PublishedVersionState(
186
+ version_row["lifecycle_state"]
187
+ ),
188
+ require_state=PublishedVersionState.ACTIVE,
189
+ )
190
+ except LifecycleWitnessError as error:
191
+ raise SemanticCatalogError(
192
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
193
+ error.message,
194
+ details={"cause_type": witness_cause_type(error.code)},
195
+ ) from error
196
+ # Active publications must still satisfy their immutable
197
+ # frozen binding, audit, and manifest cross-links through
198
+ # the centralized integrity rule set; legacy compatibility
199
+ # records (plain Bundle, manifest-only) revalidate through
200
+ # the Bundle envelope and manifest bundle-match alone.
201
+ evidence.validated_publication_records(
202
+ conn,
203
+ pointer["scope_namespace"],
204
+ pointer["bundle_id"],
205
+ bundle.fingerprint,
206
+ audit_id=version_row["audit_id"],
207
+ )
208
+ bundles_revalidated += 1
209
+ except SemanticCatalogError as error:
210
+ issues.append(
211
+ CatalogReloadIssue(
212
+ code=error.code.value,
213
+ message=error.message,
214
+ member_id=pointer["bundle_id"],
215
+ )
216
+ )
217
+ # An ACTIVE version row without a pointer is invisible to the
218
+ # pointer sweep above; report it so orphaned lifecycle rows are
219
+ # never silently carried across a restart.
220
+ for row in uow.execute(conn, "list_orphan_active_versions", ()).fetchall():
221
+ if len(issues) >= 16:
222
+ break
223
+ issues.append(
224
+ CatalogReloadIssue(
225
+ code="orphan_active_version",
226
+ message="an active version exists without an active pointer",
227
+ member_id=row["bundle_id"],
228
+ )
229
+ )
230
+ return CatalogReloadReport(
231
+ checked_at=checked_at,
232
+ active_snapshots_revalidated=snapshots_revalidated,
233
+ active_bundles_revalidated=bundles_revalidated,
234
+ rejected=tuple(issues[:16]),
235
+ )
File without changes
@@ -0,0 +1,26 @@
1
+ """Focused repositories over the shared catalog unit of work.
2
+
3
+ Repositories persist one domain each (metadata snapshots/proposal sets,
4
+ assembly drafts, Bundle publications, verification/audit evidence, and
5
+ activation/history). They operate on connections handed to them by a
6
+ transaction owner and never commit independently during atomic
7
+ cross-domain operations such as publication, activation, or rollback.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .activation import ActivationRepository
13
+ from .audit_evidence import AuditEvidenceRepository
14
+ from .drafts import DraftRepository
15
+ from .evidence import EvidenceRepository
16
+ from .publications import PublicationRepository
17
+ from .snapshots import SnapshotRepository
18
+
19
+ __all__ = [
20
+ "ActivationRepository",
21
+ "AuditEvidenceRepository",
22
+ "DraftRepository",
23
+ "EvidenceRepository",
24
+ "PublicationRepository",
25
+ "SnapshotRepository",
26
+ ]