651 lines
35 KiB
Python
651 lines
35 KiB
Python
"""Persistent correlation relationships for the AIOS baseline.
|
|
|
|
This is deliberately a relationship authority. It does not create or advance
|
|
Tasks, Jobs, Releases, or Deployments. In particular, a Task is admitted only
|
|
after the configured Task authority returns the real Task and its authenticated
|
|
tenant/subject binding has been checked.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass, is_dataclass
|
|
from datetime import datetime, timezone
|
|
import json
|
|
from pathlib import Path
|
|
import sqlite3
|
|
from typing import Any, Callable, Mapping
|
|
|
|
|
|
class LineageError(RuntimeError):
|
|
"""Base class for persistent lineage failures."""
|
|
|
|
|
|
class LineageConflict(LineageError):
|
|
"""An opaque identity was already bound to different lineage."""
|
|
|
|
|
|
class LineageVerificationError(LineageError):
|
|
"""An external authority did not verify the requested relationship."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class VerifiedAcceptanceEvidence:
|
|
"""Typed output of a configured acceptance evidence verifier."""
|
|
|
|
acceptance_id: str
|
|
tenant_id: str
|
|
subject_id: str
|
|
component_id: str
|
|
deployment_id: str
|
|
release_id: str
|
|
artifact: Any
|
|
accepted: bool
|
|
evidence_id: str
|
|
observed: Mapping[str, Any] | None = None
|
|
observed_at: str | None = None
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def _required(value: Any, name: str) -> str:
|
|
text = str(value or "")
|
|
if not text or len(text) > 512 or any(ord(char) < 32 for char in text):
|
|
raise ValueError(f"invalid {name}")
|
|
return text
|
|
|
|
|
|
def _json(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
|
|
|
|
_SELF_HASH_FIELDS = {
|
|
"manifest_sha256",
|
|
"manifest_hash",
|
|
"outer_manifest_hash",
|
|
"outer_manifest_sha256",
|
|
"self_hash",
|
|
"self_sha256",
|
|
}
|
|
|
|
|
|
def canonical_artifact(artifact: Any) -> str:
|
|
"""Return an exact artifact identity without permitting outer self-hashes."""
|
|
|
|
if not isinstance(artifact, (str, Mapping)):
|
|
raise ValueError("artifact must be a string or object")
|
|
if isinstance(artifact, str):
|
|
_required(artifact, "artifact")
|
|
else:
|
|
if not artifact:
|
|
raise ValueError("artifact must not be empty")
|
|
if any(not isinstance(key,str) for key in artifact):
|
|
raise ValueError("artifact object keys must be strings")
|
|
if _SELF_HASH_FIELDS.intersection(key.lower() for key in artifact):
|
|
raise ValueError("outer artifact manifest must not contain a self hash")
|
|
try:
|
|
return _json(artifact)
|
|
except (TypeError, ValueError) as exc:
|
|
raise ValueError("artifact must be canonical JSON") from exc
|
|
|
|
|
|
def decode_artifact(value: str) -> Any:
|
|
return json.loads(value)
|
|
|
|
|
|
def _mapping(value: Any) -> Mapping[str, Any]:
|
|
if isinstance(value, Mapping):
|
|
return value
|
|
if is_dataclass(value):
|
|
return asdict(value)
|
|
converter = getattr(value, "to_dict", None)
|
|
if not callable(converter):
|
|
converter = getattr(value, "as_dict", None)
|
|
if callable(converter):
|
|
try:
|
|
converted = converter(redact_payload=False)
|
|
except TypeError:
|
|
converted = converter()
|
|
if isinstance(converted, Mapping):
|
|
return converted
|
|
raise LineageVerificationError("task_authority returned an invalid Task")
|
|
|
|
|
|
def _task_plan(task: Any) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
|
|
raw = _mapping(task)
|
|
plan: Any = raw.get("plan") or raw.get("task_plan")
|
|
if plan is None and hasattr(task, "task_plan"):
|
|
plan = getattr(task, "task_plan")
|
|
if plan is None:
|
|
plan = raw
|
|
plan_map = _mapping(plan)
|
|
return raw, plan_map
|
|
|
|
|
|
class LineageLedger:
|
|
"""SQLite-backed authority for cross-domain identity relationships."""
|
|
|
|
def __init__(self, path: str | Path, task_authority: Any, job_authority: Any):
|
|
if task_authority is None:
|
|
raise ValueError("task_authority is required")
|
|
if job_authority is None:
|
|
raise ValueError("job_authority is required")
|
|
self.path = str(path)
|
|
self.task_authority = task_authority
|
|
self.job_authority = job_authority
|
|
Path(self.path).parent.mkdir(parents=True, exist_ok=True)
|
|
self._initialize()
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
connection = sqlite3.connect(self.path, timeout=30)
|
|
connection.row_factory = sqlite3.Row
|
|
connection.execute("PRAGMA foreign_keys=ON")
|
|
connection.execute("PRAGMA busy_timeout=30000")
|
|
return connection
|
|
|
|
def _initialize(self) -> None:
|
|
with self._connect() as connection:
|
|
connection.execute("PRAGMA journal_mode=WAL")
|
|
tables={row[0] for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
if "lineage_tasks" in tables and "lineage_schema_meta" not in tables:
|
|
raise LineageConflict("unversioned candidate lineage schema requires explicit migration")
|
|
if "lineage_schema_meta" in tables:
|
|
version=connection.execute("SELECT version FROM lineage_schema_meta WHERE domain='lineage'").fetchone()
|
|
if version is None or version[0] != 2:
|
|
raise LineageConflict("unsupported lineage schema version")
|
|
connection.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS lineage_schema_meta (
|
|
domain TEXT PRIMARY KEY,
|
|
version INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lineage_tasks (
|
|
tenant_id TEXT NOT NULL,
|
|
task_id TEXT NOT NULL,
|
|
subject_id TEXT NOT NULL,
|
|
mission_id TEXT NOT NULL,
|
|
component_id TEXT NOT NULL,
|
|
grant_id TEXT NOT NULL,
|
|
source_task_id TEXT,
|
|
metadata_json TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY (tenant_id, task_id),
|
|
UNIQUE (tenant_id, task_id, subject_id, mission_id, component_id, grant_id),
|
|
FOREIGN KEY (tenant_id, source_task_id)
|
|
REFERENCES lineage_tasks(tenant_id, task_id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lineage_effects (
|
|
tenant_id TEXT NOT NULL,
|
|
effect_id TEXT NOT NULL,
|
|
subject_id TEXT NOT NULL,
|
|
mission_id TEXT NOT NULL,
|
|
component_id TEXT NOT NULL,
|
|
grant_id TEXT NOT NULL,
|
|
task_id TEXT NOT NULL,
|
|
job_id TEXT NOT NULL,
|
|
source_effect_id TEXT,
|
|
metadata_json TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY (tenant_id, effect_id),
|
|
UNIQUE (tenant_id, job_id),
|
|
UNIQUE (tenant_id, effect_id, subject_id, mission_id, component_id),
|
|
UNIQUE (tenant_id, effect_id, subject_id, mission_id, component_id, grant_id),
|
|
FOREIGN KEY (tenant_id, task_id, subject_id, mission_id, component_id, grant_id)
|
|
REFERENCES lineage_tasks(tenant_id, task_id, subject_id, mission_id, component_id, grant_id),
|
|
FOREIGN KEY (tenant_id, source_effect_id, subject_id, mission_id, component_id)
|
|
REFERENCES lineage_effects(tenant_id, effect_id, subject_id, mission_id, component_id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lineage_releases (
|
|
tenant_id TEXT NOT NULL,
|
|
release_id TEXT NOT NULL,
|
|
subject_id TEXT NOT NULL,
|
|
mission_id TEXT NOT NULL,
|
|
component_id TEXT NOT NULL,
|
|
grant_id TEXT NOT NULL,
|
|
effect_id TEXT NOT NULL,
|
|
source_task_id TEXT NOT NULL,
|
|
source_effect_id TEXT NOT NULL,
|
|
artifact_json TEXT NOT NULL,
|
|
metadata_json TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY (tenant_id, release_id),
|
|
UNIQUE (tenant_id, effect_id),
|
|
UNIQUE (tenant_id, release_id, subject_id, mission_id, component_id, artifact_json),
|
|
FOREIGN KEY (tenant_id, effect_id, subject_id, mission_id, component_id, grant_id)
|
|
REFERENCES lineage_effects(tenant_id, effect_id, subject_id, mission_id, component_id, grant_id),
|
|
FOREIGN KEY (tenant_id, source_task_id, subject_id, mission_id, component_id, grant_id)
|
|
REFERENCES lineage_tasks(tenant_id, task_id, subject_id, mission_id, component_id, grant_id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lineage_deployments (
|
|
tenant_id TEXT NOT NULL,
|
|
deployment_id TEXT NOT NULL,
|
|
subject_id TEXT NOT NULL,
|
|
mission_id TEXT NOT NULL,
|
|
component_id TEXT NOT NULL,
|
|
release_id TEXT NOT NULL,
|
|
environment TEXT NOT NULL,
|
|
target_revision INTEGER NOT NULL,
|
|
artifact_json TEXT NOT NULL,
|
|
metadata_json TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY (tenant_id, deployment_id),
|
|
UNIQUE (tenant_id, deployment_id, subject_id, mission_id, component_id, release_id, artifact_json),
|
|
FOREIGN KEY (tenant_id, release_id, subject_id, mission_id, component_id, artifact_json)
|
|
REFERENCES lineage_releases(tenant_id, release_id, subject_id, mission_id, component_id, artifact_json)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lineage_acceptances (
|
|
tenant_id TEXT NOT NULL,
|
|
acceptance_id TEXT NOT NULL,
|
|
subject_id TEXT NOT NULL,
|
|
mission_id TEXT NOT NULL,
|
|
component_id TEXT NOT NULL,
|
|
deployment_id TEXT NOT NULL,
|
|
release_id TEXT NOT NULL,
|
|
artifact_json TEXT NOT NULL,
|
|
accepted INTEGER NOT NULL CHECK (accepted IN (0,1)),
|
|
evidence_id TEXT NOT NULL,
|
|
evidence_json TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY (tenant_id, acceptance_id),
|
|
FOREIGN KEY (tenant_id, deployment_id, subject_id, mission_id, component_id, release_id, artifact_json)
|
|
REFERENCES lineage_deployments(
|
|
tenant_id, deployment_id, subject_id, mission_id, component_id, release_id, artifact_json
|
|
)
|
|
);
|
|
"""
|
|
)
|
|
connection.execute("INSERT OR IGNORE INTO lineage_schema_meta(domain,version) VALUES('lineage',2)")
|
|
|
|
def close(self) -> None:
|
|
"""Compatibility hook; connections are transaction-scoped."""
|
|
|
|
def _authorized_task(self, tenant_id: str, subject_id: str, task_id: str) -> Any:
|
|
authority = self.task_authority
|
|
try:
|
|
if hasattr(authority, "get_task"):
|
|
task = authority.get_task(task_id)
|
|
elif callable(authority):
|
|
try:
|
|
task = authority(tenant_id, subject_id, task_id)
|
|
except TypeError:
|
|
task = authority(task_id)
|
|
else:
|
|
raise TypeError("authority has no task lookup")
|
|
except Exception as exc:
|
|
raise LineageVerificationError("task_authority did not confirm task") from exc
|
|
raw, plan = _task_plan(task)
|
|
returned_id = raw.get("task_id") or plan.get("task_id") or getattr(task, "task_id", None)
|
|
if str(returned_id or "") != task_id:
|
|
raise LineageVerificationError("task_authority returned wrong task_id")
|
|
issuer = plan.get("issuer") if isinstance(plan.get("issuer"), Mapping) else {}
|
|
metadata = plan.get("metadata") if isinstance(plan.get("metadata"), Mapping) else {}
|
|
baseline = metadata.get("baseline") if isinstance(metadata.get("baseline"), Mapping) else {}
|
|
tenant_values = [value for value in (issuer.get("tenant_id"), metadata.get("tenant_id"), baseline.get("tenant_id")) if value is not None]
|
|
subject_values = [
|
|
value for value in (issuer.get("subject_id"), metadata.get("subject_id"), baseline.get("subject_id")) if value is not None
|
|
]
|
|
# issuer.id is accepted as subject identity only when no explicit subject_id exists.
|
|
if not subject_values and issuer.get("id") is not None:
|
|
subject_values.append(issuer["id"])
|
|
if not tenant_values or any(str(value) != tenant_id for value in tenant_values):
|
|
raise LineageVerificationError("task_authority tenant mismatch")
|
|
if not subject_values or any(str(value) != subject_id for value in subject_values):
|
|
raise LineageVerificationError("task_authority subject mismatch")
|
|
return task
|
|
|
|
def _authorized_job(
|
|
self, tenant_id: str, subject_id: str, mission_id: str, grant_id: str,
|
|
task_id: str, effect_id: str, job_id: str,
|
|
) -> None:
|
|
authority=self.job_authority
|
|
try:
|
|
if hasattr(authority,"get_job"):
|
|
job=authority.get_job(job_id)
|
|
elif hasattr(authority,"read"):
|
|
job=authority.read(job_id)
|
|
elif callable(authority):
|
|
job=authority(job_id)
|
|
else:
|
|
raise TypeError("authority has no Job lookup")
|
|
except Exception as exc:
|
|
raise LineageVerificationError("job_authority did not confirm Job") from exc
|
|
raw=_mapping(job)
|
|
request=raw.get("request") or raw.get("binding")
|
|
if request is None and isinstance(raw.get("request_json"),str):
|
|
try:
|
|
request=json.loads(raw["request_json"])
|
|
except json.JSONDecodeError as exc:
|
|
raise LineageVerificationError("job_authority returned invalid request") from exc
|
|
if not isinstance(request,Mapping):
|
|
request=raw
|
|
if isinstance(request.get("binding"),Mapping):
|
|
request=request["binding"]
|
|
bindings={
|
|
"job_id":raw.get("job_id") or request.get("job_id"),
|
|
"tenant_id":request.get("tenant_id"),"subject_id":request.get("subject_id"),
|
|
"mission_id":request.get("mission_id"),"grant_id":request.get("grant_id"),
|
|
"task_id":request.get("task_id"),"effect_id":request.get("effect_id"),
|
|
}
|
|
expected={"job_id":job_id,"tenant_id":tenant_id,"subject_id":subject_id,
|
|
"mission_id":mission_id,"grant_id":grant_id,"task_id":task_id,"effect_id":effect_id}
|
|
if any(str(bindings[key] or "") != value for key,value in expected.items()):
|
|
raise LineageVerificationError("job_authority binding mismatch")
|
|
|
|
@staticmethod
|
|
def _check_task_context(task: Any, mission_id: str, grant_id: str) -> None:
|
|
_, plan = _task_plan(task)
|
|
metadata = plan.get("metadata") if isinstance(plan.get("metadata"), Mapping) else {}
|
|
baseline = metadata.get("baseline") if isinstance(metadata.get("baseline"), Mapping) else {}
|
|
mission_values = [value for value in (metadata.get("mission_id"), baseline.get("mission_id")) if value is not None]
|
|
grant_values = [value for value in (metadata.get("grant_id"), baseline.get("grant_id")) if value is not None]
|
|
if any(str(value) != mission_id for value in mission_values):
|
|
raise LineageVerificationError("task_authority mission mismatch")
|
|
if any(str(value) != grant_id for value in grant_values):
|
|
raise LineageVerificationError("task_authority grant mismatch")
|
|
|
|
@staticmethod
|
|
def _task_component(task: Any, requested: str | None) -> str:
|
|
_, plan = _task_plan(task)
|
|
metadata = plan.get("metadata") if isinstance(plan.get("metadata"), Mapping) else {}
|
|
baseline = metadata.get("baseline") if isinstance(metadata.get("baseline"), Mapping) else {}
|
|
values=[value for value in (metadata.get("component_id"),baseline.get("component_id")) if value is not None]
|
|
if requested is not None:
|
|
requested=_required(requested,"component_id")
|
|
if any(str(value) != requested for value in values):
|
|
raise LineageVerificationError("task_authority component mismatch")
|
|
return requested
|
|
if not values or any(str(value) != str(values[0]) for value in values):
|
|
raise LineageVerificationError("task_authority component missing or inconsistent")
|
|
return _required(values[0],"component_id")
|
|
|
|
def link_task(
|
|
self,
|
|
tenant_id: str,
|
|
subject_id: str,
|
|
mission_id: str,
|
|
grant_id: str,
|
|
task_id: str,
|
|
*,
|
|
component_id: str | None = None,
|
|
source_task_id: str | None = None,
|
|
metadata: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
tenant_id, subject_id, mission_id, grant_id, task_id = (
|
|
_required(tenant_id, "tenant_id"), _required(subject_id, "subject_id"),
|
|
_required(mission_id, "mission_id"), _required(grant_id, "grant_id"),
|
|
_required(task_id, "task_id"),
|
|
)
|
|
task = self._authorized_task(tenant_id, subject_id, task_id)
|
|
self._check_task_context(task, mission_id, grant_id)
|
|
component_id=self._task_component(task,component_id)
|
|
source_task_id = _required(source_task_id, "source_task_id") if source_task_id else None
|
|
raw_task, task_plan = _task_plan(task)
|
|
authoritative_source = task_plan.get("parent_task_id") or raw_task.get("parent_task_id")
|
|
if authoritative_source is not None:
|
|
authoritative_source = _required(authoritative_source, "authoritative source_task_id")
|
|
if source_task_id is not None and source_task_id != authoritative_source:
|
|
raise LineageVerificationError("source_task_id does not match Task authority")
|
|
source_task_id = authoritative_source
|
|
metadata_json = _json(dict(metadata or {}))
|
|
values = (subject_id, mission_id, component_id, grant_id, source_task_id)
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
existing = connection.execute(
|
|
"SELECT * FROM lineage_tasks WHERE tenant_id=? AND task_id=?", (tenant_id, task_id)
|
|
).fetchone()
|
|
if existing:
|
|
if tuple(existing[key] for key in ("subject_id", "mission_id", "component_id", "grant_id", "source_task_id")) != values:
|
|
raise LineageConflict("task_id already has different lineage")
|
|
return self._task_row(existing)
|
|
if source_task_id:
|
|
self._require_row(connection, "lineage_tasks", "task_id", tenant_id, source_task_id, subject_id, mission_id, component_id)
|
|
connection.execute(
|
|
"INSERT INTO lineage_tasks VALUES(?,?,?,?,?,?,?,?,?)",
|
|
(tenant_id, task_id, subject_id, mission_id, component_id, grant_id, source_task_id, metadata_json, _now()),
|
|
)
|
|
row = connection.execute(
|
|
"SELECT * FROM lineage_tasks WHERE tenant_id=? AND task_id=?", (tenant_id, task_id)
|
|
).fetchone()
|
|
return self._task_row(row)
|
|
|
|
@staticmethod
|
|
def _require_row(connection, table, id_column, tenant_id, object_id, subject_id, mission_id, component_id=None):
|
|
row = connection.execute(
|
|
f"SELECT * FROM {table} WHERE tenant_id=? AND {id_column}=?", (tenant_id, object_id)
|
|
).fetchone()
|
|
if row is None:
|
|
raise LineageConflict(f"{id_column} has no linked authority record")
|
|
if row["subject_id"] != subject_id or row["mission_id"] != mission_id:
|
|
raise LineageConflict(f"{id_column} belongs to different subject or mission")
|
|
if component_id is not None and row["component_id"] != component_id:
|
|
raise LineageConflict(f"{id_column} belongs to different component")
|
|
return row
|
|
|
|
def link_effect(
|
|
self, tenant_id: str, subject_id: str, mission_id: str, task_id: str,
|
|
effect_id: str, job_id: str, *, source_effect_id: str | None = None,
|
|
metadata: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
tenant_id, subject_id, mission_id, task_id, effect_id, job_id = map(
|
|
lambda pair: _required(pair[0], pair[1]),
|
|
((tenant_id,"tenant_id"),(subject_id,"subject_id"),(mission_id,"mission_id"),
|
|
(task_id,"task_id"),(effect_id,"effect_id"),(job_id,"job_id")),
|
|
)
|
|
source_effect_id = _required(source_effect_id, "source_effect_id") if source_effect_id else None
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
task=self._require_row(connection, "lineage_tasks", "task_id", tenant_id, task_id, subject_id, mission_id)
|
|
component_id=task["component_id"]
|
|
grant_id=task["grant_id"]
|
|
self._authorized_job(tenant_id,subject_id,mission_id,grant_id,task_id,effect_id,job_id)
|
|
existing = connection.execute(
|
|
"SELECT * FROM lineage_effects WHERE tenant_id=? AND effect_id=?", (tenant_id, effect_id)
|
|
).fetchone()
|
|
expected = (subject_id, mission_id, component_id, grant_id, task_id, job_id, source_effect_id)
|
|
if existing:
|
|
actual = tuple(existing[key] for key in ("subject_id","mission_id","component_id","grant_id","task_id","job_id","source_effect_id"))
|
|
if actual != expected:
|
|
raise LineageConflict("effect_id already has different task/job identity")
|
|
return self._effect_row(existing)
|
|
job = connection.execute(
|
|
"SELECT effect_id,task_id,subject_id FROM lineage_effects WHERE tenant_id=? AND job_id=?",
|
|
(tenant_id, job_id),
|
|
).fetchone()
|
|
if job:
|
|
raise LineageConflict("job_id already belongs to exactly one Task/effect")
|
|
if source_effect_id:
|
|
self._require_row(connection, "lineage_effects", "effect_id", tenant_id, source_effect_id, subject_id, mission_id, component_id)
|
|
connection.execute(
|
|
"INSERT INTO lineage_effects VALUES(?,?,?,?,?,?,?,?,?,?,?)",
|
|
(tenant_id,effect_id,subject_id,mission_id,component_id,grant_id,task_id,job_id,source_effect_id,_json(dict(metadata or {})),_now()),
|
|
)
|
|
row = connection.execute("SELECT * FROM lineage_effects WHERE tenant_id=? AND effect_id=?",(tenant_id,effect_id)).fetchone()
|
|
return self._effect_row(row)
|
|
|
|
def link_release(
|
|
self, tenant_id: str, subject_id: str, mission_id: str, effect_id: str,
|
|
release_id: str, artifact: Any, *, metadata: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
tenant_id, subject_id, mission_id, effect_id, release_id = map(
|
|
lambda pair: _required(pair[0],pair[1]),
|
|
((tenant_id,"tenant_id"),(subject_id,"subject_id"),(mission_id,"mission_id"),
|
|
(effect_id,"effect_id"),(release_id,"release_id")),
|
|
)
|
|
artifact_json = canonical_artifact(artifact)
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
effect = self._require_row(connection,"lineage_effects","effect_id",tenant_id,effect_id,subject_id,mission_id)
|
|
existing = connection.execute("SELECT * FROM lineage_releases WHERE tenant_id=? AND release_id=?",(tenant_id,release_id)).fetchone()
|
|
expected = (subject_id,mission_id,effect["component_id"],effect["grant_id"],effect_id,effect["task_id"],effect_id,artifact_json)
|
|
if existing:
|
|
actual = tuple(existing[key] for key in ("subject_id","mission_id","component_id","grant_id","effect_id","source_task_id","source_effect_id","artifact_json"))
|
|
if actual != expected:
|
|
raise LineageConflict("release_id already has different source lineage")
|
|
return self._release_row(existing)
|
|
producer = connection.execute("SELECT release_id FROM lineage_releases WHERE tenant_id=? AND effect_id=?",(tenant_id,effect_id)).fetchone()
|
|
if producer:
|
|
raise LineageConflict("release-producing effect already points to a unique release")
|
|
connection.execute(
|
|
"INSERT INTO lineage_releases VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
(tenant_id,release_id,subject_id,mission_id,effect["component_id"],effect["grant_id"],effect_id,effect["task_id"],effect_id,artifact_json,_json(dict(metadata or {})),_now()),
|
|
)
|
|
row=connection.execute("SELECT * FROM lineage_releases WHERE tenant_id=? AND release_id=?",(tenant_id,release_id)).fetchone()
|
|
return self._release_row(row)
|
|
|
|
def link_deployment(
|
|
self, tenant_id: str, subject_id: str, mission_id: str, release_id: str,
|
|
deployment_id: str, component_id: str, environment: str, target_revision: int, artifact: Any,
|
|
*, metadata: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
tenant_id,subject_id,mission_id,release_id,deployment_id,component_id,environment=map(
|
|
lambda pair:_required(pair[0],pair[1]),
|
|
((tenant_id,"tenant_id"),(subject_id,"subject_id"),(mission_id,"mission_id"),
|
|
(release_id,"release_id"),(deployment_id,"deployment_id"),(component_id,"component_id"),(environment,"environment")),
|
|
)
|
|
if not isinstance(target_revision,int) or isinstance(target_revision,bool) or target_revision < 1:
|
|
raise ValueError("invalid target_revision")
|
|
artifact_json=canonical_artifact(artifact)
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
release=self._require_row(connection,"lineage_releases","release_id",tenant_id,release_id,subject_id,mission_id,component_id)
|
|
if release["artifact_json"] != artifact_json:
|
|
raise LineageConflict("deployment artifact does not match release")
|
|
existing=connection.execute("SELECT * FROM lineage_deployments WHERE tenant_id=? AND deployment_id=?",(tenant_id,deployment_id)).fetchone()
|
|
expected=(subject_id,mission_id,component_id,release_id,environment,target_revision,artifact_json)
|
|
if existing:
|
|
actual=tuple(existing[key] for key in ("subject_id","mission_id","component_id","release_id","environment","target_revision","artifact_json"))
|
|
if actual != expected:
|
|
raise LineageConflict("deployment_id already has different release identity")
|
|
return self._deployment_row(existing)
|
|
connection.execute(
|
|
"INSERT INTO lineage_deployments VALUES(?,?,?,?,?,?,?,?,?,?,?)",
|
|
(tenant_id,deployment_id,subject_id,mission_id,component_id,release_id,environment,target_revision,artifact_json,_json(dict(metadata or {})),_now()),
|
|
)
|
|
row=connection.execute("SELECT * FROM lineage_deployments WHERE tenant_id=? AND deployment_id=?",(tenant_id,deployment_id)).fetchone()
|
|
return self._deployment_row(row)
|
|
|
|
def record_acceptance(
|
|
self, tenant_id: str, subject_id: str, mission_id: str, component_id: str, acceptance_id: str,
|
|
deployment_id: str, release_id: str, artifact: Any, accepted: bool, *,
|
|
evidence: Any, verifier: Callable[[Any, Mapping[str, Any]], VerifiedAcceptanceEvidence],
|
|
) -> dict[str, Any]:
|
|
if not callable(verifier):
|
|
raise LineageVerificationError("acceptance verifier is required")
|
|
tenant_id,subject_id,mission_id,component_id,acceptance_id,deployment_id,release_id=map(
|
|
lambda pair:_required(pair[0],pair[1]),
|
|
((tenant_id,"tenant_id"),(subject_id,"subject_id"),(mission_id,"mission_id"),(component_id,"component_id"),
|
|
(acceptance_id,"acceptance_id"),(deployment_id,"deployment_id"),(release_id,"release_id")),
|
|
)
|
|
if type(accepted) is not bool:
|
|
raise ValueError("accepted must be boolean")
|
|
artifact_json=canonical_artifact(artifact)
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
deployment=self._require_row(connection,"lineage_deployments","deployment_id",tenant_id,deployment_id,subject_id,mission_id,component_id)
|
|
if deployment["release_id"] != release_id:
|
|
raise LineageConflict("acceptance release_id does not match deployment")
|
|
if deployment["artifact_json"] != artifact_json:
|
|
raise LineageConflict("acceptance artifact does not match deployment")
|
|
expected={"acceptance_id":acceptance_id,"tenant_id":tenant_id,"subject_id":subject_id,"component_id":component_id,
|
|
"deployment_id":deployment_id,"release_id":release_id,"artifact":decode_artifact(artifact_json),
|
|
"accepted":accepted}
|
|
try:
|
|
verified=verifier(evidence,expected)
|
|
except Exception as exc:
|
|
raise LineageVerificationError("acceptance evidence was not verified") from exc
|
|
if not isinstance(verified,VerifiedAcceptanceEvidence):
|
|
raise LineageVerificationError("verifier must return VerifiedAcceptanceEvidence")
|
|
bindings=(verified.acceptance_id,verified.tenant_id,verified.subject_id,verified.component_id,verified.deployment_id,verified.release_id,canonical_artifact(verified.artifact),verified.accepted)
|
|
if bindings != (acceptance_id,tenant_id,subject_id,component_id,deployment_id,release_id,artifact_json,accepted):
|
|
raise LineageVerificationError("verified acceptance evidence binding mismatch")
|
|
evidence_id=_required(verified.evidence_id,"evidence_id")
|
|
evidence_json=_json({
|
|
"evidence_id":evidence_id,
|
|
"observed":dict(verified.observed or {}),
|
|
"observed_at":verified.observed_at,
|
|
"signed_envelope":dict(evidence) if isinstance(evidence,Mapping) else None,
|
|
})
|
|
existing=connection.execute("SELECT * FROM lineage_acceptances WHERE tenant_id=? AND acceptance_id=?",(tenant_id,acceptance_id)).fetchone()
|
|
expected_row=(subject_id,mission_id,component_id,deployment_id,release_id,artifact_json,int(accepted),evidence_id,evidence_json)
|
|
if existing:
|
|
actual=tuple(existing[key] for key in ("subject_id","mission_id","component_id","deployment_id","release_id","artifact_json","accepted","evidence_id","evidence_json"))
|
|
if actual != expected_row:
|
|
raise LineageConflict("acceptance_id already has different binding")
|
|
return self._acceptance_row(existing)
|
|
connection.execute(
|
|
"INSERT INTO lineage_acceptances VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
(tenant_id,acceptance_id,subject_id,mission_id,component_id,deployment_id,release_id,artifact_json,int(accepted),evidence_id,evidence_json,_now()),
|
|
)
|
|
row=connection.execute("SELECT * FROM lineage_acceptances WHERE tenant_id=? AND acceptance_id=?",(tenant_id,acceptance_id)).fetchone()
|
|
return self._acceptance_row(row)
|
|
|
|
def explain(self, tenant_id: str, subject_id: str, mission_id: str) -> dict[str, Any]:
|
|
tenant_id,subject_id,mission_id=(_required(tenant_id,"tenant_id"),_required(subject_id,"subject_id"),_required(mission_id,"mission_id"))
|
|
specs=(("tasks","lineage_tasks",self._task_row),("effects","lineage_effects",self._effect_row),
|
|
("releases","lineage_releases",self._release_row),("deployments","lineage_deployments",self._deployment_row),
|
|
("acceptances","lineage_acceptances",self._acceptance_row))
|
|
result={"tenant_id":tenant_id,"subject_id":subject_id,"mission_id":mission_id}
|
|
with self._connect() as connection:
|
|
for key,table,converter in specs:
|
|
rows=connection.execute(f"SELECT * FROM {table} WHERE tenant_id=? AND subject_id=? AND mission_id=? ORDER BY created_at",(tenant_id,subject_id,mission_id)).fetchall()
|
|
result[key]=[converter(row) for row in rows]
|
|
return result
|
|
|
|
def get_release(self, tenant_id: str, subject_id: str, release_id: str) -> dict[str, Any]:
|
|
"""Resolve an exact tenant/subject release without disclosing other scopes."""
|
|
|
|
tenant_id, subject_id, release_id = (
|
|
_required(tenant_id, "tenant_id"), _required(subject_id, "subject_id"),
|
|
_required(release_id, "release_id"),
|
|
)
|
|
with self._connect() as connection:
|
|
row = connection.execute(
|
|
"SELECT * FROM lineage_releases WHERE tenant_id=? AND release_id=? AND subject_id=?",
|
|
(tenant_id, release_id, subject_id),
|
|
).fetchone()
|
|
if row is None:
|
|
raise KeyError("release lineage not found")
|
|
return self._release_row(row)
|
|
|
|
def get_release_for_component(self, tenant_id: str, component_id: str, release_id: str) -> dict[str, Any]:
|
|
tenant_id,component_id,release_id=(_required(tenant_id,"tenant_id"),_required(component_id,"component_id"),_required(release_id,"release_id"))
|
|
with self._connect() as connection:
|
|
row=connection.execute(
|
|
"SELECT * FROM lineage_releases WHERE tenant_id=? AND release_id=? AND component_id=?",
|
|
(tenant_id,release_id,component_id),
|
|
).fetchone()
|
|
if row is None:
|
|
raise KeyError("release component lineage not found")
|
|
return self._release_row(row)
|
|
|
|
@staticmethod
|
|
def _common(row):
|
|
return {key:row[key] for key in row.keys() if not key.endswith("_json")}
|
|
|
|
@classmethod
|
|
def _task_row(cls,row):
|
|
value=cls._common(row); value["metadata"]=json.loads(row["metadata_json"]); return value
|
|
|
|
@classmethod
|
|
def _effect_row(cls,row):
|
|
value=cls._common(row); value["metadata"]=json.loads(row["metadata_json"]); return value
|
|
|
|
@classmethod
|
|
def _release_row(cls,row):
|
|
value=cls._common(row); value["artifact"]=decode_artifact(row["artifact_json"]); value["metadata"]=json.loads(row["metadata_json"]); return value
|
|
|
|
@classmethod
|
|
def _deployment_row(cls,row):
|
|
value=cls._common(row); value["artifact"]=decode_artifact(row["artifact_json"]); value["metadata"]=json.loads(row["metadata_json"]); return value
|
|
|
|
@classmethod
|
|
def _acceptance_row(cls,row):
|
|
value=cls._common(row); value["artifact"]=decode_artifact(row["artifact_json"]); value["accepted"]=bool(row["accepted"]); value["evidence"]=json.loads(row["evidence_json"]); return value
|
|
|
|
|
|
__all__ = [
|
|
"LineageConflict", "LineageError", "LineageLedger", "LineageVerificationError",
|
|
"VerifiedAcceptanceEvidence", "canonical_artifact", "decode_artifact",
|
|
]
|