Add lifecycle registration and revisions with atomic packet uploads

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-04 19:31:39 +02:00
parent 7ce1e589f7
commit ee472acc87
10 changed files with 332 additions and 35 deletions

View file

@ -101,11 +101,15 @@ class LifecycleRepository:
db.execute("INSERT INTO lifecycle_operations VALUES (?,?,?,?,?,?)", (*scope, digest, event_id))
return event_id
def register(self, context: Attribution, memo: Memo, *, key: str, guard=None) -> str:
def register(self, context: Attribution, memo: Memo, *, key: str, guard=None, documents=None) -> str:
"""Register a complete, unbound version-one memo without creating approval."""
if memo.version != 1 or memo.sealed or memo.approval_id is not None:
raise ValueError("registration requires an unbound version-one memo")
scope, digest = self._request(context, "register", key, memo)
from .lifecycle_commands import decode_documents
documents = [] if documents is None else documents
decoded = decode_documents(documents, memo)
payload = {"memo": memo, "documents": documents} if documents else memo
scope, digest = self._request(context, "register", key, payload)
with self.store._transaction() as db:
policy_id = guard(db) if guard is not None else None
replay = self._replay(db, scope, digest)
@ -114,12 +118,16 @@ class LifecycleRepository:
# Never take over an existing intake/imported memo, even with identical content.
if db.execute("SELECT 1 FROM memos WHERE id=?", (memo.id,)).fetchone():
raise Conflict("memo identifier already exists")
if {item.hash for item in memo.packet} - {item[0] for item in decoded}:
raise EvidenceUnavailable("registration requires packet content in the command")
for document in decoded:
self.store._put_document(db, *document)
self.store._save_memo(db, memo)
db.execute("INSERT INTO lifecycle_records VALUES (?,?,?)", (memo.id, context.tenant, _now()))
return self._append(db, scope, digest, memo, "registered", {"memo_version": memo.version}, policy_id)
def revise(self, context: Attribution, memo: Memo, *, expected_version: int,
summary: str, key: str, guard=None) -> str:
summary: str, key: str, guard=None, documents=None) -> str:
"""Compare-and-append; retain existing Store guards on outstanding entries."""
if type(expected_version) is not int or expected_version < 1:
raise ValueError("expected version must be a positive integer")
@ -127,8 +135,13 @@ class LifecycleRepository:
raise ValueError("revision summary required, at most 4096 characters")
if memo.version != expected_version + 1 or memo.sealed or memo.approval_id is not None:
raise ValueError("revision requires the next unbound memo version")
scope, digest = self._request(context, "revise", key,
{"memo": memo, "expected_version": expected_version, "summary": summary})
from .lifecycle_commands import decode_documents
documents = [] if documents is None else documents
decoded = decode_documents(documents, memo)
payload = {"memo": memo, "expected_version": expected_version, "summary": summary}
if documents:
payload["documents"] = documents
scope, digest = self._request(context, "revise", key, payload)
with self.store._transaction() as db:
policy_id = guard(db) if guard is not None else None
self._record(db, context.tenant, memo.id)
@ -140,6 +153,11 @@ class LifecycleRepository:
raise Conflict("memo changed since it was retrieved")
if current.sealed or current.approval_id is not None:
raise Conflict("bound memo requires the review integration path")
available = {item.hash for item in current.packet} | {item[0] for item in decoded}
if {item.hash for item in memo.packet} - available:
raise EvidenceUnavailable("revision requires new packet content in the command")
for document in decoded:
self.store._put_document(db, *document)
self.store._save_memo(db, memo)
return self._append(db, scope, digest, memo, "revised", {"summary": summary}, policy_id)