ADR-035: syn verify report schema + canonical logical dump
Date: 2026-06-18 Status: Accepted Author: Rômulo Febasi
Foundation for SYN-262 (the de-risking spike of the v1.0
syn verifyheadline, SYN-253). Companion to ADR-026 (audit), ADR-027 (bitemporal), ADR-030 (git substrate). Grounded indocs/researchs/2026-06-16-provable-non-corruption-syn-verify.md.
Context
The v1.0 thesis is provable non-corruption: prove the AI memory was not
silently corrupted, offline and per-fact. The keystone invariant (INV-1) is
that the disposable SQLite index is a reproducible build of the Markdown, digest(reindex(markdown)) == digest(live index). A hosted competitor
cannot offer this, because there is no user-side source to reproduce from.
The research brief flagged one real prerequisite: the .db file is not
byte-deterministic. Rowids, FTS5-internal pages, WAL framing, and float
vector blobs all vary between two indexes that are logically identical. So we
cannot hash the file; we must hash a canonical logical manifest. SYN-262
is that spike; it must produce a dump(ws) that is byte-stable across calls
and across a reindex --full, or every downstream invariant is built on sand.
The brief also fixed the proof-artifact shape: an in-toto Statement
(subject digest + predicate invariant table) signed with the user’s
existing ed25519 identity (reuse SYN-247 receipts: no new crypto). The
schema must be frozen before SYN-263 populates it.
Decision
1. Canonical logical dump. core::verify::dump(ws) emits a sorted,
line-oriented manifest. Each line is
"<table>\t<primary-key…>\t<canonical-json-of-columns>"; lines are sorted
lexically, so the output groups by table then primary key and any divergence
is a localized line diff that names the offending fact (the basis for the
SYN-269 rebuild attestation). JSON is canonicalized with recursively sorted
object keys, independent of serde_json’s preserve_order feature. The digest
is inputs_hash = sha256:<hex> of the dump bytes.
The dump covers exactly the rows that reindex deterministically reproduces
from the Markdown (plus git for last_modified_commit): entities, tags,
the deterministic frontmatter-projected links, and entity_fields.
It excludes, by construction:
entities_fts: FTS5-internal storage, fully derived fromentities;pending: the proposal queue; mutable statereindexdoes not reproduce, with a non-contentAUTOINCREMENTrowid;reindex_ledger: a machine-local(mtime, size)skip cache;sqlite_*internals and the feature-vectorfloat embedding blobs, which are model-dependent and not bit-reproducible;- enrichment links (
mentions:*): see §Consequences.
2. Report schema synapse.verify-report/1. An in-toto Statement:
{
"schema": "synapse.verify-report/1",
"subject": { "workspace": "...", "git_ref": "HEAD", "inputs_hash": "sha256:…" },
"predicate": {
"tool_version": "1.0.0",
"invariants": [ { "id": "INV-1", "title": "…", "status": "pass|fail|skip", "evidence": "…" } ],
"checkpoint": "…|null",
"trust_root": "local_only|checkpoint_witnessed|remote_witnessed",
"verified_at": "RFC-3339"
},
"signature": "base64|null",
"public_key": "base64|null"
}
trust_root is mandatory and always surfaced: the audit log is
tamper-evident, not tamper-proof: a lone local clone cannot detect its
own owner’s rewrite without a witness. The Rust data model
(core::verify::Report et al.) is defined in SYN-262; the invariant
evaluation, signing, and the syn verify command itself land in SYN-263.
Consequences
- Improves: the index becomes a provable reproducible build of the
Markdown; the digest is reproducible across machines (anyone with the repo
at
git_refrecomputes a bit-identicalinputs_hash); a divergence is localized to a named fact, not “the.dbdiffers”. - Worsens / limits:
syn verifyproves the integrity of the deterministic projection of the Markdown, not of model-derived enrichment (embeddings, NER edges). Covering enrichment would require pinningmodel@precisionand is deferred. - Real drift surfaced (open): building the dump exposed that the create
path writes
mentions:*enrichment links thatreindexdoes not regenerate, so the live index and areindex --fullindex diverge on those edges (and--fullsilently drops them). We excludementions:*from the dump (same class as float vectors: heuristic/model-derived, not a pure function ofreindex(markdown)). The underlying disposability gap,reindexnot regenerating enrichment: is a genuine finding to track as a follow-up (SYN-271), not fixed here. - Open: the
mentions:prefix is the current discriminator for enrichment edges; if future enrichment adds non-mentions:rels, the exclusion predicate must grow with it.
Alternatives considered
- Hash the
.dbbytes. Rejected: non-deterministic (rowids, FTS pages, WAL, float blobs): would report corruption on every benign rebuild. - Include enrichment links / embeddings in the dump. Rejected: they are
model-derived and not reproduced by
reindex, so the dump would never be a fixpoint. Pinmodel@precisionlater if they must be covered. - Make
reindexre-run enrichment so the dump can include it. Out of scope for the canonicalization spike, and NER output is not bit-stable across model versions regardless. Tracked separately (SYN-271). - A bespoke binary manifest / protobuf. Rejected: the line-oriented text manifest is human-inspectable and diffable (SYN-269) with zero new deps.
- A non-signed report. Rejected: the brief requires a portable, signed attestation; reusing SYN-247 receipt signing costs no new crypto.