projectdavid-orm 1.13.1__tar.gz → 1.14.0__tar.gz

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 (32) hide show
  1. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/CHANGELOG.md +7 -0
  2. {projectdavid_orm-1.13.1/src/projectdavid_orm.egg-info → projectdavid_orm-1.14.0}/PKG-INFO +1 -1
  3. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/pyproject.toml +1 -1
  4. projectdavid_orm-1.14.0/src/projectdavid_orm/projectdavid_orm/execution_ledger.py +340 -0
  5. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/models.py +4 -0
  6. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0/src/projectdavid_orm.egg-info}/PKG-INFO +1 -1
  7. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/SOURCES.txt +3 -0
  8. projectdavid_orm-1.14.0/tests/test_execution_ledger_registration.py +42 -0
  9. projectdavid_orm-1.14.0/tests/test_execution_ledger_schema.py +82 -0
  10. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/LICENSE +0 -0
  11. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/MANIFEST.in +0 -0
  12. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/README.md +0 -0
  13. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/setup.cfg +0 -0
  14. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/PKG-INFO +0 -0
  15. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/SOURCES.txt +0 -0
  16. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/dependency_links.txt +0 -0
  17. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/entry_points.txt +0 -0
  18. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/requires.txt +0 -0
  19. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/top_level.txt +0 -0
  20. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm/__init__.py +0 -0
  21. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm/constants/__init__.py +0 -0
  22. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm/enums.py +0 -0
  23. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm/ormInterface.py +0 -0
  24. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/__init__.py +0 -0
  25. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/base.py +0 -0
  26. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm/utilities/__init__.py +0 -0
  27. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/dependency_links.txt +0 -0
  28. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/requires.txt +0 -0
  29. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/top_level.txt +0 -0
  30. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/tests/test_clients.py +0 -0
  31. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/tests/test_runtime_dependency_boundary.py +0 -0
  32. {projectdavid_orm-1.13.1 → projectdavid_orm-1.14.0}/tests/test_scratchpad_public_exports.py +0 -0
@@ -1,3 +1,10 @@
1
+ # [1.14.0](https://github.com/project-david-ai/projectdavid-orm/compare/v1.13.1...v1.14.0) (2026-11-13)
2
+
3
+
4
+ ### Features
5
+
6
+ * **orm:** add durable execution lineage and forensic audit models ([21f89d3](https://github.com/project-david-ai/projectdavid-orm/commit/21f89d385273d21e39fcc32911039fcde6457032))
7
+
1
8
  ## [1.13.1](https://github.com/project-david-ai/projectdavid-orm/compare/v1.13.0...v1.13.1) (2026-09-29)
2
9
 
3
10
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: projectdavid-orm
3
- Version: 1.13.1
3
+ Version: 1.14.0
4
4
  Summary: Shared SQLAlchemy ORM models for ProjectDavid services
5
5
  Author-email: "Francis N." <engineering@projectdavid.co.uk>
6
6
  License: PolyForm Noncommercial License 1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "projectdavid-orm"
7
- version = "1.13.1"
7
+ version = "1.14.0"
8
8
  description = "Shared SQLAlchemy ORM models for ProjectDavid services"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -0,0 +1,340 @@
1
+ """Durable execution lineage and forensic evidence metadata.
2
+
3
+ These tables survive normal ephemeral Assistant lifecycle operations.
4
+
5
+ Foreign keys to Runs and Actions use RESTRICT, not CASCADE. Controlled
6
+ retention and erasure services must reconcile lineage before deletion.
7
+
8
+ Import this module to register its models on the shared ORM Base.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import time
14
+
15
+ from sqlalchemy import (
16
+ JSON,
17
+ BigInteger,
18
+ Boolean,
19
+ Column,
20
+ ForeignKey,
21
+ Index,
22
+ Integer,
23
+ String,
24
+ UniqueConstraint,
25
+ )
26
+
27
+ from .base import Base
28
+
29
+
30
+ def _now() -> int:
31
+ return int(time.time())
32
+
33
+
34
+ def _now_us() -> int:
35
+ return time.time_ns() // 1_000
36
+
37
+
38
+ class RunExecutionEpoch(Base):
39
+ """One execution attempt belonging to a durable logical Run."""
40
+
41
+ __tablename__ = "run_execution_epochs"
42
+
43
+ id = Column(String(64), primary_key=True)
44
+
45
+ run_id = Column(
46
+ String(64),
47
+ ForeignKey("runs.id", ondelete="RESTRICT"),
48
+ nullable=False,
49
+ )
50
+
51
+ root_run_id = Column(
52
+ String(64),
53
+ ForeignKey("runs.id", ondelete="RESTRICT"),
54
+ nullable=False,
55
+ )
56
+
57
+ owner_user_id = Column(String(64), nullable=False)
58
+ epoch_number = Column(Integer, nullable=False)
59
+
60
+ state = Column(
61
+ String(32),
62
+ nullable=False,
63
+ default="reserved",
64
+ )
65
+
66
+ created_at = Column(BigInteger, nullable=False, default=_now)
67
+ started_at = Column(BigInteger, nullable=True)
68
+ last_seen_at = Column(BigInteger, nullable=True)
69
+ ended_at = Column(BigInteger, nullable=True)
70
+ terminal_status = Column(String(32), nullable=True)
71
+
72
+ __table_args__ = (
73
+ UniqueConstraint(
74
+ "run_id",
75
+ "epoch_number",
76
+ name="uq_exec_epoch_run_number",
77
+ ),
78
+ Index(
79
+ "ix_exec_epoch_root_created",
80
+ "root_run_id",
81
+ "created_at",
82
+ ),
83
+ Index(
84
+ "ix_exec_epoch_owner_root",
85
+ "owner_user_id",
86
+ "root_run_id",
87
+ ),
88
+ Index(
89
+ "ix_exec_epoch_state_seen",
90
+ "state",
91
+ "last_seen_at",
92
+ ),
93
+ )
94
+
95
+
96
+ class ExecutionActor(Base):
97
+ """Durable actor reservation and identity snapshot.
98
+
99
+ One actor can occupy a named slot within a Run execution epoch.
100
+ Database uniqueness prevents competing supervisor reservations.
101
+ """
102
+
103
+ __tablename__ = "execution_actors"
104
+
105
+ id = Column(String(64), primary_key=True)
106
+
107
+ epoch_id = Column(
108
+ String(64),
109
+ ForeignKey(
110
+ "run_execution_epochs.id",
111
+ ondelete="RESTRICT",
112
+ ),
113
+ nullable=False,
114
+ )
115
+
116
+ slot = Column(String(48), nullable=False)
117
+ execution_role = Column(String(48), nullable=False)
118
+
119
+ # Snapshots intentionally have no FK to assistants.
120
+ # Assistant cleanup must not invalidate execution provenance.
121
+ assistant_id_snapshot = Column(String(64), nullable=True)
122
+ model_snapshot = Column(String(128), nullable=True)
123
+
124
+ ephemeral = Column(Boolean, nullable=False, default=False)
125
+
126
+ state = Column(
127
+ String(32),
128
+ nullable=False,
129
+ default="reserved",
130
+ )
131
+
132
+ reserved_at = Column(BigInteger, nullable=False, default=_now)
133
+ activated_at = Column(BigInteger, nullable=True)
134
+ retired_at = Column(BigInteger, nullable=True)
135
+ last_seen_at = Column(BigInteger, nullable=True)
136
+
137
+ __table_args__ = (
138
+ UniqueConstraint(
139
+ "epoch_id",
140
+ "slot",
141
+ name="uq_exec_actor_epoch_slot",
142
+ ),
143
+ Index(
144
+ "ix_exec_actor_assistant",
145
+ "assistant_id_snapshot",
146
+ ),
147
+ Index(
148
+ "ix_exec_actor_state_seen",
149
+ "state",
150
+ "last_seen_at",
151
+ ),
152
+ )
153
+
154
+
155
+ class RunDelegation(Base):
156
+ """Links a parent Action to a delegated child execution."""
157
+
158
+ __tablename__ = "run_delegations"
159
+
160
+ id = Column(String(64), primary_key=True)
161
+
162
+ parent_epoch_id = Column(
163
+ String(64),
164
+ ForeignKey(
165
+ "run_execution_epochs.id",
166
+ ondelete="RESTRICT",
167
+ ),
168
+ nullable=False,
169
+ )
170
+
171
+ child_epoch_id = Column(
172
+ String(64),
173
+ ForeignKey(
174
+ "run_execution_epochs.id",
175
+ ondelete="RESTRICT",
176
+ ),
177
+ nullable=False,
178
+ )
179
+
180
+ parent_action_id = Column(
181
+ String(64),
182
+ ForeignKey("actions.id", ondelete="RESTRICT"),
183
+ nullable=False,
184
+ )
185
+
186
+ initiating_actor_id = Column(
187
+ String(64),
188
+ ForeignKey("execution_actors.id", ondelete="RESTRICT"),
189
+ nullable=True,
190
+ )
191
+
192
+ created_at = Column(BigInteger, nullable=False, default=_now)
193
+
194
+ __table_args__ = (
195
+ UniqueConstraint(
196
+ "child_epoch_id",
197
+ name="uq_run_delegation_child_epoch",
198
+ ),
199
+ Index(
200
+ "ix_run_delegation_parent",
201
+ "parent_epoch_id",
202
+ ),
203
+ Index(
204
+ "ix_run_delegation_parent_action",
205
+ "parent_action_id",
206
+ ),
207
+ )
208
+
209
+
210
+ class ExecutionEvent(Base):
211
+ """Structured, ordered execution evidence.
212
+
213
+ Raw reasoning, credentials and unfiltered tool outputs must not be
214
+ stored in details. The writer service must enforce an allowlist,
215
+ redact secrets, and protect events against unauthorised mutation.
216
+
217
+ Evidence digests require protected external checkpoints to provide
218
+ meaningful tamper-evidence.
219
+ """
220
+
221
+ __tablename__ = "execution_events"
222
+
223
+ id = Column(
224
+ BigInteger,
225
+ primary_key=True,
226
+ autoincrement=True,
227
+ )
228
+
229
+ epoch_id = Column(
230
+ String(64),
231
+ ForeignKey(
232
+ "run_execution_epochs.id",
233
+ ondelete="RESTRICT",
234
+ ),
235
+ nullable=False,
236
+ )
237
+
238
+ actor_id = Column(
239
+ String(64),
240
+ ForeignKey(
241
+ "execution_actors.id",
242
+ ondelete="RESTRICT",
243
+ ),
244
+ nullable=True,
245
+ )
246
+
247
+ origin_run_id = Column(
248
+ String(64),
249
+ ForeignKey("runs.id", ondelete="RESTRICT"),
250
+ nullable=False,
251
+ )
252
+
253
+ event_type = Column(String(64), nullable=False)
254
+ schema_version = Column(Integer, nullable=False, default=1)
255
+
256
+ evidence_class = Column(
257
+ String(24),
258
+ nullable=False,
259
+ default="operational",
260
+ )
261
+
262
+ # Microsecond timestamps permit high-resolution reconstruction
263
+ # of interleaved agent and tool activity.
264
+ occurred_at_us = Column(BigInteger, nullable=False)
265
+ recorded_at_us = Column(
266
+ BigInteger,
267
+ nullable=False,
268
+ default=_now_us,
269
+ )
270
+
271
+ evidence_expires_at = Column(BigInteger, nullable=True)
272
+
273
+ action_id_snapshot = Column(String(64), nullable=True)
274
+ tool_call_id_snapshot = Column(String(64), nullable=True)
275
+
276
+ details = Column(JSON, nullable=False, default=dict)
277
+
278
+ # Opaque reference to governed, separately protected evidence.
279
+ evidence_ref = Column(String(255), nullable=True)
280
+ evidence_sha256 = Column(String(64), nullable=True)
281
+
282
+ previous_digest = Column(String(64), nullable=True)
283
+ record_digest = Column(String(64), nullable=True)
284
+
285
+ __table_args__ = (
286
+ Index(
287
+ "ix_exec_event_epoch_id",
288
+ "epoch_id",
289
+ "id",
290
+ ),
291
+ Index(
292
+ "ix_exec_event_origin_time",
293
+ "origin_run_id",
294
+ "occurred_at_us",
295
+ ),
296
+ Index(
297
+ "ix_exec_event_type_time",
298
+ "event_type",
299
+ "occurred_at_us",
300
+ ),
301
+ )
302
+
303
+
304
+ class EvidenceRetentionHold(Base):
305
+ """Authorised preservation hold for one root Run.
306
+
307
+ An open hold blocks ordinary retention purging. Releasing a hold
308
+ does not independently authorise deletion of otherwise protected data.
309
+ """
310
+
311
+ __tablename__ = "evidence_retention_holds"
312
+
313
+ id = Column(String(64), primary_key=True)
314
+
315
+ root_run_id = Column(
316
+ String(64),
317
+ ForeignKey("runs.id", ondelete="RESTRICT"),
318
+ nullable=False,
319
+ )
320
+
321
+ case_reference = Column(String(128), nullable=False)
322
+ reason_code = Column(String(48), nullable=False)
323
+ authorised_by = Column(String(64), nullable=False)
324
+
325
+ created_at = Column(BigInteger, nullable=False, default=_now)
326
+ reviewed_at = Column(BigInteger, nullable=True)
327
+ expires_at = Column(BigInteger, nullable=True)
328
+ released_at = Column(BigInteger, nullable=True)
329
+
330
+ __table_args__ = (
331
+ Index(
332
+ "ix_evidence_hold_root_released",
333
+ "root_run_id",
334
+ "released_at",
335
+ ),
336
+ Index(
337
+ "ix_evidence_hold_case",
338
+ "case_reference",
339
+ ),
340
+ )
@@ -21,6 +21,10 @@ from sqlalchemy.orm import joinedload, relationship
21
21
 
22
22
  from projectdavid_orm.enums import StatusEnum
23
23
 
24
+ # Register execution-lineage models on the shared declarative Base.
25
+ # This import is intentionally placed after existing model declarations.
26
+ # Required for Alembic and normal ORM metadata discovery.
27
+ from . import execution_ledger as _execution_ledger # noqa: E402, F401
24
28
  from .base import Base
25
29
 
26
30
  # --- Association Tables ---
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: projectdavid-orm
3
- Version: 1.13.1
3
+ Version: 1.14.0
4
4
  Summary: Shared SQLAlchemy ORM models for ProjectDavid services
5
5
  Author-email: "Francis N." <engineering@projectdavid.co.uk>
6
6
  License: PolyForm Noncommercial License 1.0.0
@@ -20,8 +20,11 @@ src/projectdavid_orm.egg-info/top_level.txt
20
20
  src/projectdavid_orm/constants/__init__.py
21
21
  src/projectdavid_orm/projectdavid_orm/__init__.py
22
22
  src/projectdavid_orm/projectdavid_orm/base.py
23
+ src/projectdavid_orm/projectdavid_orm/execution_ledger.py
23
24
  src/projectdavid_orm/projectdavid_orm/models.py
24
25
  src/projectdavid_orm/utilities/__init__.py
25
26
  tests/test_clients.py
27
+ tests/test_execution_ledger_registration.py
28
+ tests/test_execution_ledger_schema.py
26
29
  tests/test_runtime_dependency_boundary.py
27
30
  tests/test_scratchpad_public_exports.py
@@ -0,0 +1,42 @@
1
+ """Verify normal ORM imports register the execution ledger.
2
+
3
+ The test runs in a fresh Python interpreter so that earlier test imports
4
+ cannot accidentally register the tables and mask a packaging defect.
5
+ """
6
+
7
+ import subprocess
8
+ import sys
9
+
10
+
11
+ def test_fresh_models_import_registers_execution_ledger():
12
+ code = """
13
+ from projectdavid_orm.projectdavid_orm import models
14
+
15
+ expected = {
16
+ "run_execution_epochs",
17
+ "execution_actors",
18
+ "run_delegations",
19
+ "execution_events",
20
+ "evidence_retention_holds",
21
+ }
22
+
23
+ registered = set(models.User.metadata.tables)
24
+ missing = expected - registered
25
+
26
+ assert not missing, (
27
+ f"Unregistered execution ledger tables: {sorted(missing)}"
28
+ )
29
+ """
30
+
31
+ result = subprocess.run(
32
+ [sys.executable, "-c", code],
33
+ capture_output=True,
34
+ text=True,
35
+ check=False,
36
+ )
37
+
38
+ assert result.returncode == 0, (
39
+ "Fresh ORM import failed.\n"
40
+ f"STDOUT:\n{result.stdout}\n"
41
+ f"STDERR:\n{result.stderr}"
42
+ )
@@ -0,0 +1,82 @@
1
+ """Structural tests for Project David execution-lineage models."""
2
+
3
+ from sqlalchemy import UniqueConstraint
4
+ from sqlalchemy.dialects.mysql import dialect as mysql_dialect
5
+ from sqlalchemy.schema import CreateTable
6
+
7
+ from projectdavid_orm.projectdavid_orm import execution_ledger, models
8
+
9
+ EXPECTED_TABLES = {
10
+ "run_execution_epochs",
11
+ "execution_actors",
12
+ "run_delegations",
13
+ "execution_events",
14
+ "evidence_retention_holds",
15
+ }
16
+
17
+
18
+ def test_models_share_existing_metadata():
19
+ metadata = models.User.metadata
20
+
21
+ assert EXPECTED_TABLES.issubset(metadata.tables.keys())
22
+
23
+
24
+ def test_supervisor_and_epoch_uniqueness():
25
+ metadata = models.User.metadata
26
+
27
+ expected = {
28
+ "run_execution_epochs": "uq_exec_epoch_run_number",
29
+ "execution_actors": "uq_exec_actor_epoch_slot",
30
+ "run_delegations": "uq_run_delegation_child_epoch",
31
+ }
32
+
33
+ for table_name, constraint_name in expected.items():
34
+ table = metadata.tables[table_name]
35
+ names = {
36
+ constraint.name
37
+ for constraint in table.constraints
38
+ if isinstance(constraint, UniqueConstraint)
39
+ }
40
+ assert constraint_name in names
41
+
42
+
43
+ def test_protected_foreign_keys_restrict_deletion():
44
+ metadata = models.User.metadata
45
+
46
+ for name in EXPECTED_TABLES:
47
+ table = metadata.tables[name]
48
+
49
+ assert table.foreign_keys
50
+
51
+ for foreign_key in table.foreign_keys:
52
+ assert foreign_key.ondelete == "RESTRICT"
53
+
54
+
55
+ def test_actor_identity_survives_assistant_deletion():
56
+ actor_table = models.User.metadata.tables["execution_actors"]
57
+
58
+ assistant_id = actor_table.c.assistant_id_snapshot
59
+
60
+ # This is an identity snapshot, deliberately not a foreign key.
61
+ assert not assistant_id.foreign_keys
62
+
63
+
64
+ def test_forensic_event_resolution():
65
+ table = models.User.metadata.tables["execution_events"]
66
+
67
+ assert "occurred_at_us" in table.c
68
+ assert "recorded_at_us" in table.c
69
+ assert "evidence_sha256" in table.c
70
+ assert "evidence_expires_at" in table.c
71
+
72
+
73
+ def test_mysql_ddl_compiles_for_every_new_table():
74
+ metadata = models.User.metadata
75
+ dialect = mysql_dialect()
76
+
77
+ for table_name in EXPECTED_TABLES:
78
+ statement = CreateTable(metadata.tables[table_name])
79
+ compiled = str(statement.compile(dialect=dialect))
80
+
81
+ assert "CREATE TABLE" in compiled
82
+ assert "RESTRICT" in compiled