Implement lifecycle workspace publication and audit delivery
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
parent
ee472acc87
commit
5fec7de4cb
21 changed files with 1022 additions and 78 deletions
|
|
@ -184,6 +184,54 @@ class LifecycleRepository:
|
|||
raise EvidenceUnavailable("evaluation source unavailable for this revision")
|
||||
return self._append(db, scope, digest, memo, kind, body, policy_id)
|
||||
|
||||
def publish(self, context, memo_id, version, *, audience, key, guard=None):
|
||||
audience = publication_audience(audience)
|
||||
if type(version) is not int or version < 1:
|
||||
raise ValueError("positive revision required")
|
||||
scope, digest = self._request(context, "publish", key,
|
||||
{"memo_id": memo_id, "version": version, "audience": audience})
|
||||
with self.store._transaction() as db:
|
||||
policy_id = guard(db) if guard is not None else None
|
||||
self._record(db, context.tenant, memo_id)
|
||||
replay = self._replay(db, scope, digest)
|
||||
if replay:
|
||||
return replay
|
||||
memo = self.store._memo(db, memo_id)
|
||||
if memo.version != version:
|
||||
raise Conflict("publish requires the current revision")
|
||||
return self._append(db, scope, digest, memo, "published", {"audience": audience}, policy_id)
|
||||
|
||||
def policy_context(self, tenant, memo_id, version=None):
|
||||
with self.store._connection() as db:
|
||||
return self._policy_context(db, tenant, memo_id, version)
|
||||
|
||||
@staticmethod
|
||||
def _policy_context(db, tenant, memo_id, version=None):
|
||||
row = db.execute("SELECT e.actor, (SELECT MAX(version) FROM memos WHERE id=l.memo_id) AS current_version "
|
||||
"FROM lifecycle_records l JOIN lifecycle_events e ON e.memo_id=l.memo_id "
|
||||
"WHERE l.tenant=? AND l.memo_id=? AND e.kind='registered'", (tenant, memo_id)).fetchone()
|
||||
if row is None:
|
||||
return {"registered": False}
|
||||
published = db.execute("SELECT body FROM lifecycle_events WHERE tenant=? AND memo_id=? "
|
||||
"AND version=? AND kind='published' ORDER BY sequence DESC LIMIT 1",
|
||||
(tenant, memo_id, version if version is not None else row["current_version"])).fetchone()
|
||||
return {"registered": True, "registered_by": json.loads(row["actor"]),
|
||||
"current_version": row["current_version"],
|
||||
"published_audience": json.loads(published["body"])["audience"] if published else None}
|
||||
|
||||
def candidates(self, tenant, *, query="", after="", limit=100):
|
||||
"""Internal bounded search candidates; callers must authorize each result."""
|
||||
if (not isinstance(query, str) or len(query) > 200 or not isinstance(after, str)
|
||||
or len(after) > 256 or type(limit) is not int or not 1 <= limit <= 100):
|
||||
raise ValueError("invalid search parameters")
|
||||
with self.store._connection() as db:
|
||||
return [row[0] for row in db.execute(
|
||||
"SELECT l.memo_id FROM lifecycle_records l JOIN memos m ON m.id=l.memo_id "
|
||||
"WHERE l.tenant=? AND l.memo_id>? AND m.version=(SELECT MAX(version) FROM memos WHERE id=l.memo_id) "
|
||||
"AND (instr(lower(l.memo_id),lower(?))>0 OR instr(lower(json_extract(m.body,'$.question')),lower(?))>0 "
|
||||
"OR instr(lower(json_extract(m.body,'$.brief')),lower(?))>0) ORDER BY l.memo_id LIMIT ?",
|
||||
(tenant, after, query, query, query, limit))]
|
||||
|
||||
@staticmethod
|
||||
def _validate_annotation(kind, body):
|
||||
fields = {
|
||||
|
|
@ -221,6 +269,19 @@ class LifecycleRepository:
|
|||
self._record(db, tenant, memo_id)
|
||||
return self.store._memo(db, memo_id, version)
|
||||
|
||||
def packet(self, tenant, memo_id, version, index):
|
||||
if type(version) is not int or version < 1 or type(index) is not int or index < 0:
|
||||
raise ValueError("invalid packet reference")
|
||||
with self.store._connection() as db:
|
||||
self._record(db, tenant, memo_id)
|
||||
memo = self.store._memo(db, memo_id, version)
|
||||
if index >= len(memo.packet):
|
||||
raise EvidenceUnavailable("packet unavailable")
|
||||
row = db.execute("SELECT content FROM documents WHERE digest=?", (memo.packet[index].hash,)).fetchone()
|
||||
if row is None:
|
||||
raise EvidenceUnavailable("packet unavailable")
|
||||
return bytes(row[0])
|
||||
|
||||
def events(self, tenant: str, memo_id: str, *, after: int = 0, limit: int = 100) -> list[dict]:
|
||||
if type(after) is not int or after < 0 or type(limit) is not int or not 1 <= limit <= 100:
|
||||
raise ValueError("invalid event page")
|
||||
|
|
@ -231,3 +292,20 @@ class LifecycleRepository:
|
|||
(tenant, memo_id, after, limit)).fetchall()
|
||||
return [{**dict(row), "actor": json.loads(row["actor"]), "body": json.loads(row["body"])}
|
||||
for row in rows]
|
||||
|
||||
|
||||
def publication_audience(value):
|
||||
if not isinstance(value, dict) or set(value) != {"subjects", "groups"}:
|
||||
raise ValueError("explicit publication audience required")
|
||||
result = {}
|
||||
for name, items in value.items():
|
||||
if not isinstance(items, list) or len(items) > 32:
|
||||
raise ValueError("invalid publication audience")
|
||||
for item in items:
|
||||
_text(item, "audience member")
|
||||
if not item.strip() or item == "*":
|
||||
raise ValueError("explicit audience member required")
|
||||
result[name] = sorted(set(items))
|
||||
if not any(result.values()):
|
||||
raise ValueError("publication audience must not be empty")
|
||||
return result
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue