"""Durable source-held evidence and transactional outbox for the review domain. This is an internal store, not an authorization endpoint. Callers owe an access-engine decision before exposing its content or dispatching an entry. Submission state describes local delivery, never approval validity. """ from contextlib import contextmanager from dataclasses import replace from datetime import datetime, timezone import hashlib import json import os from pathlib import Path import re import sqlite3 import stat import time import uuid from .approval_client import EntryResult from .disposition import Actor, BINDING_VERBS, DispositionRefused, Verb, record from .evidence import EventClass, commit_disposition, commit_presentation, commit_stance_application, heartbeat, HEARTBEAT_CLASSES from .memo import Memo from .presentation import render from .provenance import Claim, Route, assert_human_control_dischargeable from .stance import resolve from .records import disposition_from, dumps, memo_from, presentation_from SCHEMA_VERSION = 2 _SCHEMA = """ CREATE TABLE documents (digest TEXT PRIMARY KEY, media_type TEXT NOT NULL, content BLOB NOT NULL); CREATE TABLE memos (id TEXT NOT NULL, version INTEGER NOT NULL, body TEXT NOT NULL, PRIMARY KEY(id,version)); CREATE TABLE presentations (id TEXT PRIMARY KEY, memo_id TEXT NOT NULL, version INTEGER NOT NULL, body TEXT NOT NULL, FOREIGN KEY(memo_id,version) REFERENCES memos(id,version)); CREATE TABLE acknowledgments (presentation_id TEXT NOT NULL REFERENCES presentations(id), highlight_id TEXT NOT NULL, at TEXT NOT NULL, PRIMARY KEY(presentation_id,highlight_id)); CREATE TABLE dispositions (id TEXT PRIMARY KEY, operation_id TEXT UNIQUE NOT NULL, request TEXT NOT NULL, presentation_id TEXT NOT NULL REFERENCES presentations(id), body TEXT NOT NULL); CREATE TABLE evidence (id TEXT PRIMARY KEY, class TEXT NOT NULL, at TEXT NOT NULL, envelope TEXT NOT NULL, content TEXT NOT NULL); CREATE TABLE outbox (id TEXT PRIMARY KEY REFERENCES evidence(id), state TEXT NOT NULL DEFAULT 'pending', attempts INTEGER NOT NULL DEFAULT 0, next_attempt REAL NOT NULL DEFAULT 0, lease TEXT, lease_until REAL, last_error TEXT, receiver_reference TEXT); CREATE TABLE submissions (disposition_id TEXT PRIMARY KEY REFERENCES dispositions(id), approval_id TEXT NOT NULL, subject TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'prepared', attempt TEXT, approved_at TEXT, UNIQUE(approval_id,subject)); """ class StoreError(RuntimeError): pass class Conflict(StoreError): pass class EvidenceUnavailable(StoreError): """Retrieval failure attributable to informed-decision, not an empty result.""" def _text(value, name): if not isinstance(value, str) or not value or len(value) > 256: raise ValueError(f"invalid {name}") def _now(): return datetime.now(timezone.utc).isoformat(timespec="microseconds") class Store: def __init__(self, path: str | Path): self.path = Path(path).absolute() parent = self.path.parent info = parent.lstat() if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o700): raise StoreError("evidence directory must be owned by this user with mode 0700") try: fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600) except FileExistsError: pass else: os.close(fd) info = self.path.lstat() if (not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o600): raise StoreError("evidence database must be a private, owned, non-linked 0600 file") with self._connection() as db: db.execute("PRAGMA journal_mode=WAL") version = db.execute("PRAGMA user_version").fetchone()[0] if version == 0: if db.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchone(): raise StoreError("unrecognized evidence database") triggers = "" for table in ("documents", "memos", "presentations", "acknowledgments", "dispositions", "evidence"): for operation in ("UPDATE", "DELETE"): triggers += (f"CREATE TRIGGER immutable_{table}_{operation} BEFORE {operation} ON {table} " "BEGIN SELECT RAISE(ABORT,'immutable evidence'); END;\n") db.executescript("BEGIN IMMEDIATE;" + _SCHEMA + triggers + "PRAGMA user_version=1; COMMIT;") elif version not in (1, SCHEMA_VERSION): raise StoreError("unsupported evidence schema version") if version in (0, 1): db.executescript("""BEGIN IMMEDIATE; CREATE TABLE policy_observations (id TEXT PRIMARY KEY, at TEXT NOT NULL, request TEXT NOT NULL, decision TEXT, outcome TEXT NOT NULL, usable_until REAL NOT NULL, response_sha256 TEXT, decision_attributable INTEGER NOT NULL CHECK(decision_attributable=0)); CREATE TRIGGER immutable_policy_observations_UPDATE BEFORE UPDATE ON policy_observations BEGIN SELECT RAISE(ABORT,'immutable evidence'); END; CREATE TRIGGER immutable_policy_observations_DELETE BEFORE DELETE ON policy_observations BEGIN SELECT RAISE(ABORT,'immutable evidence'); END; PRAGMA user_version=2; COMMIT;""") @contextmanager def _connection(self): db = sqlite3.connect(self.path.as_uri() + "?mode=rw", uri=True, timeout=5, isolation_level=None) db.row_factory = sqlite3.Row db.execute("PRAGMA foreign_keys=ON") db.execute("PRAGMA synchronous=FULL") try: yield db finally: db.close() @contextmanager def _transaction(self): with self._connection() as db: db.execute("BEGIN IMMEDIATE") try: yield db db.commit() except BaseException: db.rollback() raise def put_document(self, content: bytes, media_type="text/plain") -> str: if not isinstance(content, bytes) or len(content) > 16 * 1024 * 1024: raise ValueError("packet document must be bytes, at most 16 MiB") _text(media_type, "media type") digest = "sha256:" + hashlib.sha256(content).hexdigest() with self._transaction() as db: existing = db.execute("SELECT media_type,content FROM documents WHERE digest=?", (digest,)).fetchone() if existing and (existing["media_type"] != media_type or existing["content"] != content): raise Conflict("document digest already has different content or media type") if not existing: db.execute("INSERT INTO documents VALUES (?,?,?)", (digest, media_type, content)) return digest def save_memo(self, memo: Memo): _text(memo.id, "memo id") if type(memo.version) is not int or memo.version < 1: raise ValueError("memo version must be a positive integer") body = dumps(memo) if len(body.encode()) > 256 * 1024: raise ValueError("memo is too large") with self._transaction() as db: previous = db.execute("SELECT version,body FROM memos WHERE id=? ORDER BY version DESC LIMIT 1", (memo.id,)).fetchone() if previous and previous["version"] == memo.version and previous["body"] == body: return if memo.version != (previous["version"] + 1 if previous else 1): raise Conflict("memo version must advance by one; existing versions are immutable") if db.execute("SELECT 1 FROM submissions s JOIN dispositions d ON d.id=s.disposition_id " "JOIN presentations p ON p.id=d.presentation_id WHERE p.memo_id=? " "AND s.state IN ('in_flight','unresolved') LIMIT 1", (memo.id,)).fetchone(): raise Conflict("resolve the outstanding entry attempt before revising this memo") for item in memo.packet: if not db.execute("SELECT 1 FROM documents WHERE digest=?", (item.hash,)).fetchone(): raise EvidenceUnavailable("informed-decision does not hold the referenced packet content") db.execute("INSERT INTO memos VALUES (?,?,?)", (memo.id, memo.version, body)) def _memo(self, db, memo_id, version=None): if version is None: row = db.execute("SELECT body FROM memos WHERE id=? ORDER BY version DESC LIMIT 1", (memo_id,)).fetchone() else: row = db.execute("SELECT body FROM memos WHERE id=? AND version=?", (memo_id, version)).fetchone() if row is None: raise EvidenceUnavailable("informed-decision cannot produce the named memo version") return memo_from(json.loads(row["body"])) def memo(self, memo_id, version=None): with self._connection() as db: return self._memo(db, memo_id, version) def observe_policy(self, observation): request = json.loads(observation.request_json) _text(request["id"], "policy observation id") with self._transaction() as db: db.execute("INSERT INTO policy_observations VALUES (?,?,?,?,?,?,?,0)", (request["id"], _now(), observation.request_json, observation.decision_json, observation.outcome, observation.usable_until, observation.response_sha256)) return request["id"] def policy_observations(self): with self._connection() as db: return [dict(r) for r in db.execute("SELECT * FROM policy_observations ORDER BY rowid")] def _guard_policy(self, db, policy_id, memo, subject, action): if policy_id is None: return # Existing internal domain API; the browser always supplies one. row = db.execute("SELECT * FROM policy_observations WHERE id=?", (policy_id,)).fetchone() if row is None or row["outcome"] != "allow" or row["usable_until"] <= time.time(): raise Conflict("a current recorded policy observation is required") request = json.loads(row["request"]) if (request["action"] != action or request["subject"]["id"] != subject or request["resource"]["id"] != "memo:" + memo.id or request["context"] != {"memo_version": memo.version, "approval_id": memo.approval_id, "approval_binding_digest": memo.approval_binding_digest}): raise Conflict("policy observation does not name this act") def _presentation(self, db, presentation_id): row = db.execute("SELECT body FROM presentations WHERE id=?", (presentation_id,)).fetchone() if row is None: raise EvidenceUnavailable("informed-decision cannot produce the named presentation") original = presentation_from(json.loads(row["body"])) acks = db.execute("SELECT highlight_id FROM acknowledgments WHERE presentation_id=?", (presentation_id,)) return replace(original, acked_highlight_ids=frozenset(r[0] for r in acks)) def presentation(self, presentation_id): with self._connection() as db: return self._presentation(db, presentation_id) def _event(self, db, commitment, content, extra=None): # Construct the stable envelope once, in the same transaction as content # and state. Retries send these exact bytes and the same idempotency key. data = {**commitment.data, **(extra or {})} at = datetime.fromisoformat(commitment.at.replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="microseconds") envelope = {"id": commitment.id, "type": commitment.event_class.value, "source": "informed-decision", "tenant": "tenant:platform", "subject": data.get("memo_id", "informed-decision"), "correlation_id": data.get("approval_id") or commitment.id, "occurred_at": at, "data": data} encoded = dumps(envelope) if len(encoded.encode()) > 256 * 1024: raise StoreError("audit commitment exceeds the receiver limit") db.execute("INSERT INTO evidence VALUES (?,?,?,?,?)", (commitment.id, commitment.event_class.value, at, encoded, dumps(content))) db.execute("INSERT INTO outbox(id) VALUES (?)", (commitment.id,)) return commitment.id def present(self, memo_id, *, principal_sub, tenant, principal_type, awareness=None, expected_version=None, policy_id=None, approval_binding=None): _text(principal_sub, "principal") if (not isinstance(tenant, Claim) or tenant.value != "tenant:platform" or tenant.route not in (Route.DIRECTORY, Route.REGISTRATION) or not isinstance(principal_type, Claim)): raise ValueError("platform tenant and principal provenance are required") with self._transaction() as db: memo = self._memo(db, memo_id) if expected_version is not None and memo.version != expected_version: raise Conflict("memo changed after the entitlement check") self._guard_policy(db, policy_id, memo, principal_sub, "read") if approval_binding is not None and approval_binding.get("digest") != memo.approval_binding_digest: raise Conflict("observed act binding does not match this memo") presentation = render(memo, principal_sub=principal_sub, tenant=tenant, principal_type=principal_type, awareness=awareness) content = {"memo": json.loads(dumps(memo)), "presentation": json.loads(dumps(presentation)), "binding_document": memo.binding_document(), "awareness_document": memo.awareness_document(awareness)} if policy_id is not None: content["policy_observation_id"] = policy_id if approval_binding is not None: content["approval_binding"] = approval_binding # Observed act, never approval validity/state. db.execute("INSERT INTO presentations VALUES (?,?,?,?)", (presentation.id, memo.id, memo.version, dumps(presentation))) commitment = commit_presentation(presentation, custody="informed-decision:presentations:" + presentation.id) self._event(db, commitment, content) return presentation def acknowledge(self, presentation_id, actor: Actor, highlight_ids, *, policy_id=None): ids = frozenset(highlight_ids) with self._transaction() as db: p = self._presentation(db, presentation_id) memo = self._memo(db, p.memo_id) if actor.sub != p.principal_sub: raise DispositionRefused("G_ACTOR", "actor did not receive this presentation") if memo.version != p.memo_version: raise DispositionRefused("G_PRES", "presentation is stale") self._guard_policy(db, policy_id, memo, actor.sub, "acknowledge") if ids - {h.id for h in memo.highlights}: raise DispositionRefused("G_ACK", "unknown highlight") fresh = ids - p.acked_highlight_ids if not fresh: return p at = _now() for highlight_id in sorted(fresh): db.execute("INSERT INTO acknowledgments VALUES (?,?,?)", (p.id, highlight_id, at)) p = replace(p, acked_highlight_ids=p.acked_highlight_ids | fresh) c = commit_presentation(p, custody="informed-decision:presentations:" + p.id) self._event(db, c, {"presentation": json.loads(dumps(p)), "policy_observation_id": policy_id}, {"event_kind": "acknowledgment", "acknowledged_at": at}) return p def record_disposition(self, presentation_id, actor, verb, *, operation_id, reasons=(), note=None, policy_id=None): _text(operation_id, "operation id") if note is not None and (not isinstance(note, str) or len(note) > 8192): raise ValueError("disposition note is too large or invalid") if any(not isinstance(r, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", r) for r in reasons): raise ValueError("reasons must be bounded codes, not free text") request = dumps({"presentation": presentation_id, "actor": actor, "verb": verb, "reasons": reasons, "note": note}) with self._transaction() as db: existing = db.execute("SELECT request,body FROM dispositions WHERE operation_id=?", (operation_id,)).fetchone() if existing: if existing["request"] != request: raise Conflict("operation id already names a different disposition") return disposition_from(json.loads(existing["body"])) p = self._presentation(db, presentation_id) memo = self._memo(db, p.memo_id) self._guard_policy(db, policy_id, memo, actor.sub, verb.value) d = record(memo, p, verb, actor, reasons=tuple(reasons), note=note) if verb in BINDING_VERBS: if p.principal_type is None: raise DispositionRefused("G_IDENTITY", "verified human provenance required") assert_human_control_dischargeable(p.principal_type) if verb is Verb.ACCEPT: if not memo.approval_id or not memo.approval_binding_digest: raise DispositionRefused("G_BINDING", "the native approval id and carried digest are required") if db.execute("SELECT 1 FROM submissions WHERE approval_id=? AND subject=?", (memo.approval_id, actor.sub)).fetchone(): raise Conflict("this approver already has an intent for the approval; recover the original") db.execute("INSERT INTO dispositions VALUES (?,?,?,?,?)", (d.id, operation_id, request, p.id, dumps(d))) if verb is Verb.ACCEPT: db.execute("INSERT INTO submissions(disposition_id,approval_id,subject) VALUES (?,?,?)", (d.id, memo.approval_id, actor.sub)) c = commit_disposition(d, custody="informed-decision:dispositions:" + d.id) self._event(db, c, {"disposition": json.loads(dumps(d)), "presentation": json.loads(dumps(p)), "policy_observation_id": policy_id}, {"submission_state": "prepared"} if verb is Verb.ACCEPT else None) return d def begin_submission(self, disposition_id, *, policy_id=None): """Reserve one external attempt, AFTER the caller's fresh policy check. No network occurs in this store. A crash after this reservation stays unresolved; restart never silently permits another POST. """ with self._transaction() as db: row = db.execute("SELECT * FROM submissions WHERE disposition_id=?", (disposition_id,)).fetchone() if row is None or row["state"] != "prepared": raise Conflict("submission is absent, already attempted or unresolved") d = disposition_from(json.loads(db.execute("SELECT body FROM dispositions WHERE id=?", (disposition_id,)).fetchone()[0])) p = self._presentation(db, d.presentation_id) if self._memo(db, p.memo_id).version != p.memo_version: raise DispositionRefused("G_PRES", "presentation is stale") self._guard_policy(db, policy_id, self._memo(db, p.memo_id), d.actor.sub, "accept") attempt = str(uuid.uuid4()) db.execute("UPDATE submissions SET state='in_flight',attempt=? WHERE disposition_id=?", (attempt, disposition_id)) return attempt def finish_submission(self, disposition_id, attempt, result: EntryResult | None = None): with self._transaction() as db: row = db.execute("SELECT * FROM submissions WHERE disposition_id=?", (disposition_id,)).fetchone() if row is None or row["state"] != "in_flight" or row["attempt"] != attempt: raise Conflict("submission attempt does not match") if result is not None and (result.approval_id != row["approval_id"] or result.subject != row["subject"]): raise Conflict("approval entry does not match the original intent") if result is not None: at = datetime.fromisoformat(result.approved_at.replace("Z", "+00:00")) if at.tzinfo is None: raise ValueError("approval timestamp must carry its timezone") # An unknown duplicate may predate this presentation. Never attach it # to a new one. A lost response likewise cannot prove causation. state = "confirmed" if result is not None and not result.duplicate else "unresolved" db.execute("UPDATE submissions SET state=?,approved_at=? WHERE disposition_id=?", (state, result.approved_at if state == "confirmed" else None, disposition_id)) d = disposition_from(json.loads(db.execute("SELECT body FROM dispositions WHERE id=?", (disposition_id,)).fetchone()[0])) c = commit_disposition(d, custody="informed-decision:dispositions:" + d.id) extra = {"event_kind": "submission_result", "submission_state": state} if state == "confirmed": extra["entry_correlation"] = list(result.correlation) self._event(db, c, {"disposition": json.loads(dumps(d)), **extra}, extra) return state def submission(self, disposition_id): with self._connection() as db: row = db.execute("SELECT * FROM submissions WHERE disposition_id=?", (disposition_id,)).fetchone() return dict(row) if row else None def intent_for(self, approval_id, subject): with self._connection() as db: row = db.execute("SELECT s.*,d.presentation_id,d.operation_id FROM submissions s " "JOIN dispositions d ON d.id=s.disposition_id WHERE s.approval_id=? AND s.subject=?", (approval_id, subject)).fetchone() return dict(row) if row else None def dispositions_for(self, presentation_id): with self._connection() as db: return [disposition_from(json.loads(r[0])) for r in db.execute( "SELECT body FROM dispositions WHERE presentation_id=? ORDER BY rowid", (presentation_id,))] def presentation_content(self, presentation_id): with self._connection() as db: row = db.execute("SELECT content FROM evidence WHERE class=? AND " "json_extract(envelope,'$.data.presentation_id')=? ORDER BY rowid LIMIT 1", (EventClass.PRESENTATION.value, presentation_id)).fetchone() if row is None: raise EvidenceUnavailable("informed-decision cannot produce the presentation content") return json.loads(row[0]) def retrieve_presentation(self, presentation_id): """Internal custody retrieval, not a browser export/entitlement route.""" with self._connection() as db: p = self._presentation(db, presentation_id) memo = self._memo(db, p.memo_id, p.memo_version) docs = {} for item in memo.packet: row = db.execute("SELECT content FROM documents WHERE digest=?", (item.hash,)).fetchone() if row is None or "sha256:" + hashlib.sha256(row[0]).hexdigest() != item.hash: raise EvidenceUnavailable("informed-decision cannot produce intact packet content") docs[item.item_id] = row[0] return memo, p, docs def evidence(self): with self._connection() as db: return [dict(r) for r in db.execute("SELECT * FROM evidence ORDER BY rowid")] def record_unreachable(self, memo_id, dependency): if dependency not in {"access-engine", "approval-engine", "key-cape", "audit-core"}: raise ValueError("unknown dependency") with self._transaction() as db: memo = self._memo(db, memo_id) stance, state = resolve(memo.binding_level.value) c = commit_stance_application(memo_id=memo.id, memo_version=memo.version, binding_level=memo.binding_level.value, binding_level_state=state.value, stance=stance, dependency=dependency, custody="informed-decision:memos:" + memo.id) return self._event(db, c, {"memo": json.loads(dumps(memo)), "dependency": dependency}) def retrieve_disposition(self, disposition_id): with self._connection() as db: row = db.execute("SELECT body FROM dispositions WHERE id=?", (disposition_id,)).fetchone() snapshot = db.execute("SELECT content FROM evidence WHERE class=? AND " "json_extract(envelope,'$.data.disposition_id')=? ORDER BY rowid LIMIT 1", (EventClass.DISPOSITION.value, disposition_id)).fetchone() if row is None or snapshot is None: raise EvidenceUnavailable("informed-decision cannot produce the named disposition") d = disposition_from(json.loads(row[0])) original_presentation = presentation_from(json.loads(snapshot[0])["presentation"]) memo, _, documents = self.retrieve_presentation(d.presentation_id) return d, original_presentation, memo, documents def presentation_for_entry(self, approval_id, subject, approved_at): with self._connection() as db: row = db.execute("SELECT disposition_id FROM submissions WHERE state='confirmed' " "AND approval_id=? AND subject=? AND approved_at=?", (approval_id, subject, approved_at)).fetchone() if row is None: raise EvidenceUnavailable("informed-decision has no confirmed presentation correlation for this entry") return self.retrieve_disposition(row[0]) def outbox(self): with self._connection() as db: return [dict(r) for r in db.execute("SELECT * FROM outbox ORDER BY rowid")] def queue_heartbeats(self, now=None): now = time.time() if now is None else now count = 0 with self._transaction() as db: for event_class, interval in HEARTBEAT_CLASSES.items(): if db.execute("SELECT 1 FROM outbox o JOIN evidence e ON e.id=o.id WHERE o.state!='delivered' " "AND (e.class=? OR (e.class=? AND json_extract(e.envelope,'$.data.class')=?)) LIMIT 1", (event_class, EventClass.HEARTBEAT.value, event_class)).fetchone(): continue # Never let a nothing-to-report assertion mask undelivered evidence. recent = db.execute("SELECT MAX(at) FROM evidence WHERE class=? OR " "(class=? AND json_extract(envelope,'$.data.class')=?)", (event_class, EventClass.HEARTBEAT.value, event_class)).fetchone()[0] if recent and now - datetime.fromisoformat(recent.replace("Z", "+00:00")).timestamp() < interval: continue c = heartbeat(EventClass(event_class)) c = replace(c, at=datetime.fromtimestamp(now, timezone.utc).isoformat(timespec="microseconds")) self._event(db, c, {"class": event_class}) count += 1 return count def claim_delivery(self, now=None): now = time.time() if now is None else now with self._transaction() as db: row = db.execute("SELECT o.id,e.envelope FROM outbox o JOIN evidence e ON e.id=o.id " "WHERE o.state='pending' AND o.next_attempt<=? AND (o.lease IS NULL OR o.lease_until<=?) ORDER BY o.rowid LIMIT 1", (now, now)).fetchone() if row is None: return None lease = str(uuid.uuid4()) db.execute("UPDATE outbox SET lease=?,lease_until=?,attempts=attempts+1 WHERE id=?", (lease, now + 30, row["id"])) return row["id"], lease, row["envelope"] def finish_delivery(self, event_id, lease, *, reference=None, error=None, permanent=False, now=None): now = time.time() if now is None else now with self._transaction() as db: row = db.execute("SELECT attempts FROM outbox WHERE id=? AND state='pending' AND lease=?", (event_id, lease)).fetchone() if row is None: raise Conflict("delivery lease does not match") if reference: db.execute("UPDATE outbox SET state='delivered',receiver_reference=?,lease=NULL,lease_until=NULL,last_error=NULL WHERE id=?", (reference, event_id)) else: if error not in {"unavailable", "unauthorized", "rejected", "conflict", "invalid_receipt"}: raise ValueError("a bounded delivery failure code is required") delay = min(300, 2 ** min(row["attempts"], 8)) db.execute("UPDATE outbox SET state=?,last_error=?,next_attempt=?,lease=NULL,lease_until=NULL WHERE id=?", ("blocked" if permanent else "pending", error, now + delay, event_id)) def requeue_blocked(self, event_id): """Explicit operator retry after credential/config repair; same event bytes.""" with self._transaction() as db: db.execute("UPDATE outbox SET state='pending',next_attempt=0 WHERE id=? AND state='blocked'", (event_id,)) def counts_by_class(self, since=None, until=None): with self._connection() as db: if since is None and until is None: rows = db.execute("SELECT class,COUNT(*) FROM evidence GROUP BY class") else: start, end = bounded_window(since, until) rows = db.execute("SELECT class,COUNT(*) FROM evidence WHERE at>=? AND at= result[1]: raise ValueError("window must be increasing") return tuple(result)