Files
DynamisLab/src/CCD_analysis/acquisition/artifacts.py
T
Frank14fandCursor b144d62920 feat(ccd): freeze dynamic-increment analysis pipeline
Replace the legacy CCD workspace with acquisition, direct-dq, original and lagged CCD contracts so the DRL-versus-constant-mean mechanism is reproducible and fail-closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 16:41:00 +08:00

100 lines
4.9 KiB
Python

"""Immutable, fsync-backed acquisition artifact transactions."""
from __future__ import annotations
import ctypes
import errno
from hashlib import sha256
import json, os, shutil, uuid
from pathlib import Path
from typing import Mapping, Any
import numpy as np
from .contracts import ARTIFACT_SCHEMA_ID, canonical_json
from .validation import array_sha256, validate_acquisition_semantics
def file_sha256(path: Path) -> str:
digest = sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(8 * 1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def rename_noreplace(source: Path, destination: Path) -> None:
"""Atomically publish a directory without replacing any existing inode."""
libc = ctypes.CDLL(None, use_errno=True)
renameat2 = getattr(libc, "renameat2", None)
if renameat2 is None:
raise RuntimeError("atomic no-replace publication unavailable: libc renameat2 is missing")
renameat2.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
renameat2.restype = ctypes.c_int
result = renameat2(-100, os.fsencode(source), -100, os.fsencode(destination), 1)
if result == 0:
return
code = ctypes.get_errno()
if code in (errno.EEXIST, errno.ENOTEMPTY):
raise FileExistsError(destination)
if code in (errno.ENOSYS, errno.EINVAL, errno.ENOTSUP):
raise RuntimeError("atomic no-replace publication unavailable; refusing unsafe fallback") from OSError(code, os.strerror(code))
raise OSError(code, os.strerror(code), destination)
def _hash_array(value: np.ndarray) -> str:
return sha256(np.ascontiguousarray(value).tobytes()).hexdigest()
def _fsync_file(path: Path) -> None:
with path.open("rb") as stream:
os.fsync(stream.fileno())
def _validate_sha(value: np.ndarray, key: str) -> None:
if value.ndim != 0 or value.dtype.kind not in "SU": raise ValueError(f"{key} must be a scalar string")
text = str(value.item())
if len(text) != 64: raise ValueError(f"{key} must be SHA256")
int(text, 16)
class ArtifactTransaction:
def __init__(self, destination: str | Path):
self.destination = Path(destination)
self.partial = self.destination.with_name(f".{self.destination.name}.partial.{os.getpid()}.{uuid.uuid4().hex}")
self.active = False
def __enter__(self):
if self.destination.exists(): raise FileExistsError(self.destination)
self.destination.parent.mkdir(parents=True, exist_ok=True)
self.partial.mkdir(); self.active = True
return self
def write(self, *, arrays: Mapping[str, Any], config: dict, state: Mapping[str, Any]):
if not self.active: raise RuntimeError("transaction inactive")
data, state_arrays = validate_acquisition_semantics(arrays=arrays, config=config, state=state)
np.savez_compressed(self.partial/"fields.npz", **data)
(self.partial/"config.json").write_bytes(canonical_json(config))
np.savez_compressed(self.partial/"controller_state.npz", **state_arrays)
for path in self.partial.iterdir():
if path.is_file(): _fsync_file(path)
files = {path.name:file_sha256(path) for path in sorted(self.partial.iterdir()) if path.is_file()}
manifest = {"schema_id":ARTIFACT_SCHEMA_ID,"complete":True,"files":files,"state_array_sha256":{key:array_sha256(value) for key,value in state_arrays.items()},"config_sha256":sha256(canonical_json(config)).hexdigest(),"field_count":data["ux"].shape[0]}
validate_acquisition_semantics(arrays=data, config=config, state=state_arrays, manifest=manifest)
(self.partial/"manifest.json").write_bytes(canonical_json(manifest)); _fsync_file(self.partial/"manifest.json")
return manifest
def publish(self):
manifest = json.loads((self.partial/"manifest.json").read_text())
if not manifest.get("complete") or manifest.get("schema_id") != ARTIFACT_SCHEMA_ID: raise RuntimeError("partial artifact is not complete")
for name, digest in manifest["files"].items():
if file_sha256(self.partial/name) != digest: raise RuntimeError("artifact hash validation failed")
with np.load(self.partial/"fields.npz", allow_pickle=False) as fields, np.load(self.partial/"controller_state.npz", allow_pickle=False) as state:
config=json.loads((self.partial/"config.json").read_text())
validate_acquisition_semantics(arrays={key:fields[key] for key in fields.files},config=config,state={key:state[key] for key in state.files},manifest=manifest)
rename_noreplace(self.partial, self.destination)
directory_fd = os.open(self.destination.parent, os.O_RDONLY)
try: os.fsync(directory_fd)
finally: os.close(directory_fd)
self.active = False
return self.destination
def __exit__(self, typ, value, tb):
if self.active: shutil.rmtree(self.partial, ignore_errors=True); self.active=False