Skip to content

Commit ff8d451

Browse files
committed
[feat.dirac.Ω.D9+] make Program the versioned discovery root
WHY: downstream design, motif, execution, and evidence workflows need one durable aggregate that preserves objectives, hypotheses, decisions, milestones, lineage, and reproducible snapshots. WHAT: add migrations 020-025, governed Program atoms and commands, Motif execution foundations, connected FASCIA workspaces, voice composition surface, contract/DB gates, and an automatically regenerated function-level architecture twin. VERIFY: Program domain 6/6; PostgreSQL artifacts 10/10; app-shell 11/11; contract generators; TypeScript; production build; digital twin 4552 nodes / 7817 edges.
1 parent 7fdf3d9 commit ff8d451

123 files changed

Lines changed: 87998 additions & 46778 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/dirac.yml‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ jobs:
8787
run: |
8888
pip install --quiet jsonschema
8989
python3 scripts/gen_contracts.py --check
90+
python3 scripts/check_public_registry.py
91+
PYTHONPATH=backend python3 -m unittest \
92+
backend.tests.test_motif_contracts \
93+
backend.tests.test_execution_identity \
94+
backend.tests.test_local_cas \
95+
backend.tests.test_execution_control \
96+
backend.tests.test_motif_core
9097
9198
- name: 'Gate 7 — contract drift (errors.json / iface.* / FIELD_META_SCHEMA)'
9299
if: ${{ !cancelled() }}
@@ -143,3 +150,40 @@ jobs:
143150
node scripts/watch_digital_twin.mjs --selftest
144151
python3 scripts/check_digital_twin.py --selftest
145152
python3 scripts/check_digital_twin.py
153+
154+
contract-database:
155+
name: Motif contract and clean-database alignment
156+
runs-on: ubuntu-latest
157+
services:
158+
postgres:
159+
image: pgvector/pgvector:pg18
160+
env:
161+
POSTGRES_USER: postgres
162+
POSTGRES_PASSWORD: dirac
163+
POSTGRES_DB: dirac
164+
ports:
165+
- 5432:5432
166+
options: >-
167+
--health-cmd "pg_isready -U postgres -d dirac"
168+
--health-interval 5s
169+
--health-timeout 5s
170+
--health-retries 12
171+
env:
172+
DIRAC_DSN: postgresql://postgres:dirac@localhost:5432/dirac
173+
PGPASSWORD: dirac
174+
steps:
175+
- name: Checkout
176+
uses: actions/checkout@v4
177+
- name: PostgreSQL client and Python
178+
run: |
179+
sudo apt-get update
180+
sudo apt-get install -y postgresql-client
181+
- name: Clean migration 000 through current
182+
run: |
183+
for file in backend/db/migrations/*.sql; do
184+
psql "$DIRAC_DSN" -X -v ON_ERROR_STOP=1 -f "$file"
185+
done
186+
- name: Canonical vocabulary matches the live schema
187+
run: |
188+
python3 scripts/verify_contract_db_alignment.py
189+
bash backend/db/check_migration_hashes.sh
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Replaceable byte stores behind Dirac-owned Artifact capabilities."""
2+
3+
from .local_cas import LocalCAS
4+
from .protocol import ArtifactReader, ArtifactWriter, CheckpointWriter, StoredBlob
5+
6+
__all__ = ["ArtifactReader", "ArtifactWriter", "CheckpointWriter", "LocalCAS", "StoredBlob"]
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"""Atomic streaming SHA-256 content-addressed storage for a local appliance."""
2+
from __future__ import annotations
3+
4+
import hashlib
5+
import io
6+
import os
7+
import re
8+
import tempfile
9+
from pathlib import Path
10+
from typing import BinaryIO, Iterable
11+
12+
from .protocol import StoredBlob
13+
14+
_DIGEST = re.compile(r"^(?:sha256:)?([0-9a-f]{64})$")
15+
16+
17+
def _hex(digest: str) -> str:
18+
match = _DIGEST.fullmatch(digest)
19+
if not match:
20+
raise ValueError("digest must be sha256:<64 lowercase hex>")
21+
return match.group(1)
22+
23+
24+
class _BoundedReader(io.RawIOBase):
25+
def __init__(self, stream: BinaryIO, remaining: int | None) -> None:
26+
self._stream = stream
27+
self._remaining = remaining
28+
29+
def readable(self) -> bool:
30+
return True
31+
32+
def read(self, size: int = -1) -> bytes:
33+
if self._remaining == 0:
34+
return b""
35+
if self._remaining is not None:
36+
size = self._remaining if size < 0 else min(size, self._remaining)
37+
data = self._stream.read(size)
38+
if self._remaining is not None:
39+
self._remaining -= len(data)
40+
return data
41+
42+
def close(self) -> None:
43+
self._stream.close()
44+
super().close()
45+
46+
47+
class _PendingLocalWrite:
48+
def __init__(self, store: "LocalCAS") -> None:
49+
self._store = store
50+
fd, raw_path = tempfile.mkstemp(prefix="upload-", suffix=".partial", dir=store.staging)
51+
self._path = Path(raw_path)
52+
self._stream = os.fdopen(fd, "wb")
53+
self._hash = hashlib.sha256()
54+
self._size = 0
55+
self._closed = False
56+
57+
def write(self, chunk: bytes) -> int:
58+
if self._closed:
59+
raise ValueError("write session is closed")
60+
if not isinstance(chunk, bytes):
61+
raise TypeError("artifact chunks must be bytes")
62+
written = self._stream.write(chunk)
63+
self._hash.update(chunk[:written])
64+
self._size += written
65+
return written
66+
67+
def commit(self) -> StoredBlob:
68+
if self._closed:
69+
raise ValueError("write session is closed")
70+
self._stream.flush()
71+
os.fsync(self._stream.fileno())
72+
self._stream.close()
73+
self._closed = True
74+
digest_hex = self._hash.hexdigest()
75+
target = self._store._path(digest_hex)
76+
target.parent.mkdir(parents=True, exist_ok=True)
77+
if target.exists():
78+
self._path.unlink()
79+
if target.stat().st_size != self._size:
80+
raise IOError("content-address collision has a different stored size")
81+
else:
82+
os.replace(self._path, target)
83+
directory_fd = os.open(target.parent, os.O_RDONLY)
84+
try:
85+
os.fsync(directory_fd)
86+
finally:
87+
os.close(directory_fd)
88+
return StoredBlob(
89+
digest=f"sha256:{digest_hex}",
90+
size_bytes=self._size,
91+
locator=str(target),
92+
backend=self._store.kind,
93+
)
94+
95+
def abort(self) -> None:
96+
if not self._closed:
97+
self._stream.close()
98+
self._closed = True
99+
self._path.unlink(missing_ok=True)
100+
101+
def __enter__(self) -> "_PendingLocalWrite":
102+
return self
103+
104+
def __exit__(self, exc_type, exc, traceback) -> bool:
105+
if exc_type is not None:
106+
self.abort()
107+
elif not self._closed:
108+
self.abort()
109+
return False
110+
111+
112+
class LocalCAS:
113+
kind = "local_cas"
114+
115+
def __init__(self, root: str | Path) -> None:
116+
self.root = Path(root).resolve()
117+
self.blobs = self.root / "blobs" / "sha256"
118+
self.staging = self.root / ".staging"
119+
self.blobs.mkdir(parents=True, exist_ok=True)
120+
self.staging.mkdir(parents=True, exist_ok=True)
121+
122+
def _path(self, digest: str) -> Path:
123+
value = _hex(digest)
124+
return self.blobs / value[:2] / value[2:4] / value
125+
126+
def begin(self) -> _PendingLocalWrite:
127+
return _PendingLocalWrite(self)
128+
129+
def put_chunks(self, chunks: Iterable[bytes]) -> StoredBlob:
130+
pending = self.begin()
131+
try:
132+
for chunk in chunks:
133+
pending.write(chunk)
134+
return pending.commit()
135+
except BaseException:
136+
pending.abort()
137+
raise
138+
139+
def open(self, digest: str, *, offset: int = 0, length: int | None = None) -> BinaryIO:
140+
if offset < 0 or (length is not None and length < 0):
141+
raise ValueError("offset and length must be non-negative")
142+
path = self._path(digest)
143+
stream = path.open("rb")
144+
size = path.stat().st_size
145+
if offset > size:
146+
stream.close()
147+
raise ValueError("offset exceeds artifact size")
148+
stream.seek(offset)
149+
return _BoundedReader(stream, length)
150+
151+
def iter_bytes(
152+
self, digest: str, *, offset: int = 0, length: int | None = None,
153+
chunk_size: int = 1024 * 1024,
154+
) -> Iterable[bytes]:
155+
if chunk_size <= 0:
156+
raise ValueError("chunk_size must be positive")
157+
with self.open(digest, offset=offset, length=length) as stream:
158+
while True:
159+
chunk = stream.read(chunk_size)
160+
if not chunk:
161+
break
162+
yield chunk
163+
164+
def verify(self, digest: str) -> StoredBlob:
165+
expected = _hex(digest)
166+
hasher = hashlib.sha256()
167+
size = 0
168+
for chunk in self.iter_bytes(expected):
169+
hasher.update(chunk)
170+
size += len(chunk)
171+
if hasher.hexdigest() != expected:
172+
raise IOError(f"artifact digest verification failed for sha256:{expected}")
173+
path = self._path(expected)
174+
return StoredBlob(
175+
digest=f"sha256:{expected}",
176+
size_bytes=size,
177+
locator=str(path),
178+
backend=self.kind,
179+
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Narrow streaming capabilities injected into scientific workers."""
2+
from __future__ import annotations
3+
4+
from dataclasses import dataclass
5+
from typing import BinaryIO, Iterable, Protocol
6+
7+
8+
@dataclass(frozen=True)
9+
class StoredBlob:
10+
digest: str
11+
size_bytes: int
12+
locator: str
13+
backend: str
14+
15+
16+
class PendingWrite(Protocol):
17+
def write(self, chunk: bytes) -> int: ...
18+
def commit(self) -> StoredBlob: ...
19+
def abort(self) -> None: ...
20+
21+
22+
class ArtifactReader(Protocol):
23+
def open(self, digest: str, *, offset: int = 0, length: int | None = None) -> BinaryIO: ...
24+
def iter_bytes(
25+
self, digest: str, *, offset: int = 0, length: int | None = None,
26+
chunk_size: int = 1024 * 1024,
27+
) -> Iterable[bytes]: ...
28+
def verify(self, digest: str) -> StoredBlob: ...
29+
30+
31+
class ArtifactWriter(Protocol):
32+
def begin(self) -> PendingWrite: ...
33+
34+
35+
class CheckpointWriter(ArtifactWriter, Protocol):
36+
"""A separate capability so workers cannot mistake partial state for output."""
37+
38+
39+
__all__ = [
40+
"ArtifactReader", "ArtifactWriter", "CheckpointWriter", "PendingWrite", "StoredBlob"
41+
]

‎backend/cache_fields.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def __init__(self, get_cube, *, put_cube=None, prepare_mol=None,
7171
'skipped_uncacheable': 0, 'error': 0,
7272
'write_queued': 0, 'write_ok': 0, 'write_error': 0}
7373

74-
def lookup(self, method_id: str, payload: dict) -> HandlerResult | None:
74+
def lookup(self, method_id: str, payload: dict, *,
75+
execution_digest: str | None = None) -> HandlerResult | None:
7576
kind = CACHEABLE_KIND.get(method_id)
7677
if kind is None:
7778
self.counters['skipped_uncacheable'] += 1
@@ -109,7 +110,8 @@ def lookup(self, method_id: str, payload: dict) -> HandlerResult | None:
109110

110111
def store(self, method_id: str, payload: dict, out: HandlerResult, *,
111112
seconds: float, job_id: str | None = None,
112-
envelope: dict | None = None) -> None:
113+
envelope: dict | None = None,
114+
execution_digest: str | None = None) -> None:
113115
"""Queue a validated computed field for durable persistence.
114116
115117
The invocation never waits for PostgreSQL. This preserves the existing interaction

‎backend/cache_results.py‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@
1313
from invocation import HandlerResult
1414

1515

16-
def request_digest(payload: dict) -> bytes:
16+
def request_digest(payload: dict, *, execution_digest: str | None = None) -> bytes:
17+
canonical = json.dumps(
18+
{"schema_version": "2.0", "execution_digest": execution_digest,
19+
"payload": payload}, sort_keys=True, separators=(',', ':'))
20+
return hashlib.sha256(canonical.encode()).digest()
21+
22+
23+
def legacy_request_digest(payload: dict) -> bytes:
1724
canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
1825
return hashlib.sha256(canonical.encode()).digest()
1926

@@ -24,9 +31,11 @@ class CompositeCache:
2431
def __init__(self, *repositories: Any) -> None:
2532
self.repositories = tuple(r for r in repositories if r is not None)
2633

27-
def lookup(self, method_id: str, payload: dict) -> HandlerResult | None:
34+
def lookup(self, method_id: str, payload: dict, *,
35+
execution_digest: str | None = None) -> HandlerResult | None:
2836
for repository in self.repositories:
29-
hit = repository.lookup(method_id, payload)
37+
hit = repository.lookup(method_id, payload,
38+
execution_digest=execution_digest)
3039
if hit is not None:
3140
return hit
3241
return None
@@ -46,7 +55,8 @@ def __init__(self, connect, artifact_store, *, excluded_methods=()) -> None:
4655
self.counters = {'looked_up': 0, 'hit': 0, 'miss': 0,
4756
'stored': 0, 'deduplicated': 0}
4857

49-
def lookup(self, method_id: str, payload: dict) -> HandlerResult | None:
58+
def lookup(self, method_id: str, payload: dict, *,
59+
execution_digest: str | None = None) -> HandlerResult | None:
5060
if method_id in self._excluded:
5161
return None
5262
self.counters['looked_up'] += 1
@@ -56,8 +66,19 @@ def lookup(self, method_id: str, payload: dict) -> HandlerResult | None:
5666
'FROM app.v_result_cache_servable c '
5767
'JOIN meta.method m ON m.id = c.method_row_id '
5868
'WHERE m.method_id = %s AND c.request_digest = %s',
59-
(method_id, request_digest(payload)))
69+
(method_id, request_digest(payload, execution_digest=execution_digest)))
6070
row = cur.fetchone()
71+
if row is None and execution_digest is not None and '.motif.' not in method_id:
72+
# Compatibility window for results written before cache identity v2.
73+
# Motif never falls back because checkpoint/calibration/runtime identity
74+
# is part of correctness, not only freshness.
75+
cur.execute(
76+
'SELECT c.id, c.result, c.provenance, c.warnings, c.parameters_used '
77+
'FROM app.v_result_cache_servable c '
78+
'JOIN meta.method m ON m.id = c.method_row_id '
79+
'WHERE m.method_id = %s AND c.request_digest = %s',
80+
(method_id, legacy_request_digest(payload)))
81+
row = cur.fetchone()
6182
if row is None:
6283
self.counters['miss'] += 1
6384
return None
@@ -78,7 +99,8 @@ def lookup(self, method_id: str, payload: dict) -> HandlerResult | None:
7899

79100
def store(self, method_id: str, payload: dict, out: HandlerResult, *,
80101
seconds: float, job_id: str | None = None,
81-
envelope: dict | None = None) -> None:
102+
envelope: dict | None = None,
103+
execution_digest: str | None = None) -> None:
82104
if method_id in self._excluded or out.cache != 'computed' or envelope is None:
83105
return
84106
references = list(envelope.get('artifacts') or [])
@@ -94,7 +116,7 @@ def store(self, method_id: str, payload: dict, out: HandlerResult, *,
94116
' parameters_used, source_job_id, compute_seconds) '
95117
'VALUES (%s,%s,%s,%s,%s,%s,%s,%s) '
96118
'ON CONFLICT (method_row_id, request_digest) DO NOTHING RETURNING id',
97-
(method[0], request_digest(payload), json.dumps(out.result),
119+
(method[0], request_digest(payload, execution_digest=execution_digest), json.dumps(out.result),
98120
json.dumps(out.provenance), json.dumps(out.warnings),
99121
json.dumps(out.parameters_used), job_id, seconds))
100122
inserted = cur.fetchone()

0 commit comments

Comments
 (0)