projectdavid-orm 1.13.0__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.0 → projectdavid_orm-1.14.0}/CHANGELOG.md +15 -0
  2. {projectdavid_orm-1.13.0/src/projectdavid_orm.egg-info → projectdavid_orm-1.14.0}/PKG-INFO +3 -1
  3. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/pyproject.toml +3 -1
  4. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/__init__.py +2 -0
  5. projectdavid_orm-1.14.0/src/projectdavid_orm/enums.py +20 -0
  6. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/ormInterface.py +2 -0
  7. projectdavid_orm-1.14.0/src/projectdavid_orm/projectdavid_orm/execution_ledger.py +340 -0
  8. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/models.py +7 -8
  9. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0/src/projectdavid_orm.egg-info}/PKG-INFO +3 -1
  10. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/SOURCES.txt +7 -1
  11. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/requires.txt +2 -0
  12. projectdavid_orm-1.14.0/tests/test_execution_ledger_registration.py +42 -0
  13. projectdavid_orm-1.14.0/tests/test_execution_ledger_schema.py +82 -0
  14. projectdavid_orm-1.14.0/tests/test_runtime_dependency_boundary.py +57 -0
  15. projectdavid_orm-1.14.0/tests/test_scratchpad_public_exports.py +6 -0
  16. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/LICENSE +0 -0
  17. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/MANIFEST.in +0 -0
  18. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/README.md +0 -0
  19. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/setup.cfg +0 -0
  20. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/PKG-INFO +0 -0
  21. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/SOURCES.txt +0 -0
  22. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/dependency_links.txt +0 -0
  23. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/entry_points.txt +0 -0
  24. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/requires.txt +0 -0
  25. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/top_level.txt +0 -0
  26. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/constants/__init__.py +0 -0
  27. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/__init__.py +0 -0
  28. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/base.py +0 -0
  29. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/utilities/__init__.py +0 -0
  30. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/dependency_links.txt +0 -0
  31. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/top_level.txt +0 -0
  32. {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/tests/test_clients.py +0 -0
@@ -1,3 +1,18 @@
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
+
8
+ ## [1.13.1](https://github.com/project-david-ai/projectdavid-orm/compare/v1.13.0...v1.13.1) (2026-09-29)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **deps:** decouple ORM from Common runtime ([4e1ca2e](https://github.com/project-david-ai/projectdavid-orm/commit/4e1ca2e9b2a1b84d014fc0447849964bf974daf5))
14
+ * **exports:** expose Scratchpad through public ORM API ([205ae03](https://github.com/project-david-ai/projectdavid-orm/commit/205ae03ebe801701d9c19ce83e1b59764a8ab93b))
15
+
1
16
  # [1.13.0](https://github.com/project-david-ai/projectdavid-orm/compare/v1.12.0...v1.13.0) (2026-09-29)
2
17
 
3
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: projectdavid-orm
3
- Version: 1.13.0
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
@@ -15,6 +15,8 @@ Requires-Dist: sqlalchemy>=2.0
15
15
  Requires-Dist: pymysql
16
16
  Requires-Dist: pydantic>=2.0
17
17
  Requires-Dist: python-dotenv
18
+ Requires-Dist: passlib==1.7.4
19
+ Requires-Dist: bcrypt==4.0.1
18
20
  Provides-Extra: dev
19
21
  Requires-Dist: pytest; extra == "dev"
20
22
  Requires-Dist: black==24.1.1; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "projectdavid-orm"
7
- version = "1.13.0"
7
+ version = "1.14.0"
8
8
  description = "Shared SQLAlchemy ORM models for ProjectDavid services"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -19,6 +19,8 @@ dependencies = [
19
19
  "pymysql",
20
20
  "pydantic>=2.0",
21
21
  "python-dotenv",
22
+ "passlib==1.7.4",
23
+ "bcrypt==4.0.1",
22
24
  ]
23
25
 
24
26
  [project.optional-dependencies]
@@ -15,6 +15,7 @@ from .ormInterface import (
15
15
  OrmInterface,
16
16
  Run,
17
17
  Sandbox,
18
+ Scratchpad,
18
19
  Thread,
19
20
  TrainingJob,
20
21
  User,
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "Assistant",
37
38
  "Action",
38
39
  "Sandbox",
40
+ "Scratchpad",
39
41
  # File system
40
42
  "File",
41
43
  "FileStorage",
@@ -0,0 +1,20 @@
1
+ from enum import Enum
2
+
3
+
4
+ class StatusEnum(str, Enum):
5
+ """Canonical persistence status values used by ORM models."""
6
+
7
+ deleted = "deleted"
8
+ active = "active"
9
+ queued = "queued"
10
+ in_progress = "in_progress"
11
+ pending_action = "action_required"
12
+ completed = "completed"
13
+ failed = "failed"
14
+ cancelling = "cancelling"
15
+ cancelled = "cancelled"
16
+ pending = "pending"
17
+ processing = "processing"
18
+ expired = "expired"
19
+ retrying = "retrying"
20
+ offline = "offline"
@@ -30,6 +30,8 @@ class OrmInterface:
30
30
  Action = Action
31
31
  # Sandbox model
32
32
  Sandbox = Sandbox
33
+ # Scratchpad model
34
+ Scratchpad = Scratchpad
33
35
  # File model
34
36
  File = File
35
37
  # FileStorage model
@@ -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
+ )
@@ -3,9 +3,6 @@ import time
3
3
  from datetime import datetime
4
4
 
5
5
  from passlib.context import CryptContext
6
- from projectdavid_common import ValidationInterface
7
- from projectdavid_common.schemas.enums import StatusEnum
8
- from projectdavid_common.utilities.logging_service import LoggingUtility
9
6
  from sqlalchemy import JSON, BigInteger, Boolean, Column, DateTime
10
7
  from sqlalchemy import Enum as SAEnum
11
8
  from sqlalchemy import (
@@ -22,11 +19,13 @@ from sqlalchemy import (
22
19
  from sqlalchemy.ext.mutable import MutableDict
23
20
  from sqlalchemy.orm import joinedload, relationship
24
21
 
25
- from .base import Base
26
-
27
- logger = LoggingUtility()
22
+ from projectdavid_orm.enums import StatusEnum
28
23
 
29
- validation = ValidationInterface
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
28
+ from .base import Base
30
29
 
31
30
  # --- Association Tables ---
32
31
 
@@ -428,7 +427,7 @@ class Run(Base):
428
427
  required_action = Column(String(256), nullable=True)
429
428
  response_format = Column(String(64), nullable=True)
430
429
 
431
- status = Column(SAEnum(validation.StatusEnum), nullable=False)
430
+ status = Column(SAEnum(StatusEnum), nullable=False)
432
431
 
433
432
  thread_id = Column(String(64), nullable=False)
434
433
  tool_choice = Column(String(64), nullable=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: projectdavid-orm
3
- Version: 1.13.0
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
@@ -15,6 +15,8 @@ Requires-Dist: sqlalchemy>=2.0
15
15
  Requires-Dist: pymysql
16
16
  Requires-Dist: pydantic>=2.0
17
17
  Requires-Dist: python-dotenv
18
+ Requires-Dist: passlib==1.7.4
19
+ Requires-Dist: bcrypt==4.0.1
18
20
  Provides-Extra: dev
19
21
  Requires-Dist: pytest; extra == "dev"
20
22
  Requires-Dist: black==24.1.1; extra == "dev"
@@ -10,6 +10,7 @@ src/projectdavid_common.egg-info/entry_points.txt
10
10
  src/projectdavid_common.egg-info/requires.txt
11
11
  src/projectdavid_common.egg-info/top_level.txt
12
12
  src/projectdavid_orm/__init__.py
13
+ src/projectdavid_orm/enums.py
13
14
  src/projectdavid_orm/ormInterface.py
14
15
  src/projectdavid_orm.egg-info/PKG-INFO
15
16
  src/projectdavid_orm.egg-info/SOURCES.txt
@@ -19,6 +20,11 @@ src/projectdavid_orm.egg-info/top_level.txt
19
20
  src/projectdavid_orm/constants/__init__.py
20
21
  src/projectdavid_orm/projectdavid_orm/__init__.py
21
22
  src/projectdavid_orm/projectdavid_orm/base.py
23
+ src/projectdavid_orm/projectdavid_orm/execution_ledger.py
22
24
  src/projectdavid_orm/projectdavid_orm/models.py
23
25
  src/projectdavid_orm/utilities/__init__.py
24
- tests/test_clients.py
26
+ tests/test_clients.py
27
+ tests/test_execution_ledger_registration.py
28
+ tests/test_execution_ledger_schema.py
29
+ tests/test_runtime_dependency_boundary.py
30
+ tests/test_scratchpad_public_exports.py
@@ -2,6 +2,8 @@ sqlalchemy>=2.0
2
2
  pymysql
3
3
  pydantic>=2.0
4
4
  python-dotenv
5
+ passlib==1.7.4
6
+ bcrypt==4.0.1
5
7
 
6
8
  [dev]
7
9
  pytest
@@ -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
@@ -0,0 +1,57 @@
1
+ from pathlib import Path
2
+
3
+ from projectdavid_orm.enums import StatusEnum
4
+ from projectdavid_orm.projectdavid_orm.models import ApiKey
5
+
6
+ EXPECTED_STATUS_VALUES = {
7
+ "deleted": "deleted",
8
+ "active": "active",
9
+ "queued": "queued",
10
+ "in_progress": "in_progress",
11
+ "pending_action": "action_required",
12
+ "completed": "completed",
13
+ "failed": "failed",
14
+ "cancelling": "cancelling",
15
+ "cancelled": "cancelled",
16
+ "pending": "pending",
17
+ "processing": "processing",
18
+ "expired": "expired",
19
+ "retrying": "retrying",
20
+ "offline": "offline",
21
+ }
22
+
23
+
24
+ def test_status_enum_persistence_contract_is_preserved():
25
+ assert {item.name: item.value for item in StatusEnum} == EXPECTED_STATUS_VALUES
26
+
27
+
28
+ def test_models_have_no_runtime_dependency_on_common():
29
+ models_path = (
30
+ Path(__file__).parents[1]
31
+ / "src"
32
+ / "projectdavid_orm"
33
+ / "projectdavid_orm"
34
+ / "models.py"
35
+ )
36
+
37
+ source = models_path.read_text(encoding="utf-8")
38
+
39
+ assert "projectdavid_common" not in source
40
+ assert "ValidationInterface" not in source
41
+ assert "LoggingUtility" not in source
42
+
43
+
44
+ def test_bcrypt_backend_hashes_and_verifies_api_keys():
45
+ plain = "project-david-runtime-dependency-probe"
46
+
47
+ hashed = ApiKey.hash_key(plain)
48
+
49
+ assert hashed != plain
50
+
51
+ api_key = ApiKey(
52
+ key_name="dependency-probe",
53
+ hashed_key=hashed,
54
+ )
55
+
56
+ assert api_key.verify_key(plain)
57
+ assert not api_key.verify_key(plain + "-invalid")
@@ -0,0 +1,6 @@
1
+ from projectdavid_orm import OrmInterface, Scratchpad
2
+
3
+
4
+ def test_scratchpad_is_exposed_through_public_orm_api():
5
+ assert Scratchpad.__tablename__ == "scratchpads"
6
+ assert OrmInterface.Scratchpad is Scratchpad