docs: custody-schema contract revision 4 (sol r3 residuals F1/F2/F4/F6/F7/F9 + F10/F11)
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
fred
2026-08-26 21:02:12 -05:00
parent 671b872ec9
commit 269aeda193
+281 -95
View File
@@ -52,6 +52,31 @@ terminal outcome (F8). Managed deletion is an ordered git-then-database
protocol with fault-injection witnesses at both interruption points,
replacing the impossible cross-store transaction (F9).
Revision 4 (sol r3 findings F1/F2/F4/F6/F7/F9 residual, F10/F11 new):
the write fence pins `READ COMMITTED` isolation and lock-first
statement order for the reclassification migration, and the
supported-version declaration becomes a named source symbol checked
under the same lock as each decision (F1). The user foreign keys
adopt the identity store's actual key type — `users.id` is `text`
(BetterAuth identifiers), so every `user_id` column is `text` (F10).
The agent referent binds to the live platform `agents` table by
symbol, since `identity-lifecycle.md` defines no enrollment surface;
an amendment re-binds if one arrives (F2). The system auto-revocation
predicate is withdrawn entirely: grantee cessation is handled
structurally by `ON DELETE CASCADE` from the grantee referent (as
subject cessation already is from `users`), default-deny ends access,
and the audit event table keeps the durable history — which removes
the trigger/actor contradiction rather than patching it (F2, F7).
Sensitive-write routes gain a static write-set closure witness so a
derived representation cannot land in any generic platform table
(F4). Managed deletion step 2 becomes a compare-and-delete on the
hash removed in step 1, witnessed against a concurrent rewrite (F9).
Ordinary sensitive writes get an ordered content-first protocol with
deterministic locators, ack-after-pointer-commit, idempotent retry,
and fault-injection witnesses (F11). §8 discloses every rule above
plus the previously undisclosed §4.3 committed-tree/symlink rule
(F6).
This contract binds the profile-category registry (§2), the custody
placement rule (§3), the pointer schema (§4), the consent schema and its
evaluation (§5), mode application (§6), witnesses (§7), and disclosed
@@ -70,8 +95,8 @@ with contract 6 (`mode-conversion.md`), identity with
3. **Pointer**: a database record referencing sensitive content that
lives in a user brain, carrying no content (§4).
4. **Grantee**: a non-subject principal that may be granted access to a
user's sensitive content: an enrolled agent, a connector, or a
platform feature (§5.2).
user's sensitive content: a platform agent (a row of the `agents`
table, §5.2), a connector, or a platform feature (§5.2).
5. **Consent record**: a database record granting one grantee access to
one category of one user's data (§5).
6. **Registry version**: the monotonically increasing integer
@@ -152,18 +177,37 @@ with contract 6 (`mode-conversion.md`), identity with
that performs the write, and routes by the version so read. A
reclassification migration takes an **exclusive lock** on that row
(`SELECT ... FOR UPDATE`) before its scan and holds it through the
value moves and the version bump. Consequently a concurrent writer
either commits before the migration's scan begins — its value is
then swept by the scan — or blocks until the migration commits and
then routes by the new version. No interleaving exists in which a
value routed under the old version commits after the migration's
scan (witness §7.6g).
value moves and the version bump. Isolation and statement order
are part of the fence, not implementation freedom: both the
profile-write transaction and the migration transaction run at
`READ COMMITTED`, and the migration's `SELECT ... FOR UPDATE` is
the FIRST statement of its transaction — no read precedes it, so
the migration establishes no snapshot before it holds the lock.
Under `READ COMMITTED` each subsequent statement takes a fresh
snapshot, so the migration's scan — which runs only after the lock
is granted — sees every row committed by every `FOR SHARE` holder
that finished before the lock was granted. Consequently a
concurrent writer either commits before the lock is granted — its
value is then visible to and swept by the scan — or blocks until
the migration commits and then routes by the new version. No
interleaving exists in which a value routed under the old version
escapes the scan, including the writer-commits-while-migration-
waits interleaving (witness §7.6g).
7. **Supported version.** Each release declares the exact registry
version its code implements. If the declared version does not equal
`custody_config.registry_version`, every profile routing and
consent decision is refused — a readable but unsupported registry
is a refusal, never best-effort routing under either version
(witness §7.6h).
version its code implements as an exported compile-time constant
in the custody module of the shared schema package; the
implementing PR names the symbol and §7.6(h) pins it by symbol
identity, so the declaration is source, not configuration. Every
profile routing decision and every consent decision reads
`custody_config.registry_version` under a `SELECT ... FOR SHARE`
on the `custody_config` row inside the same database transaction
as the decision (write decisions share the §2.6 fence transaction;
read-only consent evaluations take the shared lock in their own
transaction), so the supported-version comparison and the decision
it gates use one atomically read version. If the declared version
does not equal the version so read, the decision is refused — a
readable but unsupported registry is a refusal, never best-effort
routing under either version (witness §7.6h).
## 3. Custody placement rule
@@ -186,7 +230,7 @@ with contract 6 (`mode-conversion.md`), identity with
only and creates no new obligation on estate brains.
5. **Non-sensitive profile table.** Non-sensitive profile answers land
in exactly one declared table, `profile_answers`, columns exactly:
`id` (uuid, primary key), `user_id` (uuid, NOT NULL, FK → users(id)
`id` (uuid, primary key), `user_id` (text, NOT NULL, FK → users(id)
ON DELETE CASCADE), `category_key` (text, NOT NULL, FK →
profile_category_registry(category_key)), `value` (text, NOT NULL),
`created_at` and `updated_at` (timestamptz, NOT NULL), and
@@ -195,6 +239,36 @@ with contract 6 (`mode-conversion.md`), identity with
table ONLY while its category's registry classification is
`non-sensitive` (§2.4 migrates the rows on reclassification). No
other platform table stores profile answer content.
6. **User-key type binding.** The identity store declares `users.id`
as `text` (BetterAuth identifiers, `packages/db` schema); every
`user_id` column in this contract (§3.5, §4.1, §5.1) is therefore
`text`, matching the referenced key's declared type exactly. The
identifiers are opaque strings, not guaranteed UUIDs; no custody
column re-types, parses, or reformats them. If a sibling contract
ever migrates the user key type, an amendment to this contract
follows it (§7.1 asserts the FK types match the live referenced
columns).
7. **Ordered sensitive-write protocol.** An ordinary sensitive write
spans two stores and is ordered, content first: step 1 commits the
content to the owning user's brain at a deterministic locator —
the implementing PR declares a locator scheme that maps (category,
answer slot) to one `brain_ref`, so a retry of the same write
resolves to the same path; step 2, only after step 1 has
committed, upserts the pointer row (insert, or update of the
existing row for that (user, category, brain_ref)) in its own
database transaction inside the §2.6 fence. The write is
acknowledged to the caller only after step 2 commits. Interruption
before step 1 leaves both stores unchanged. Interruption between
the steps leaves committed brain content with no pointer:
unpointed content is inert — it lives in the correct custody
store, no read path serves it (reads resolve through pointers
only), and nothing dangles in the database. The client's retry —
the write was never acknowledged — recommits the same locator and
completes the pointer upsert, converging to the pointed state.
The reverse order (pointer before content) is forbidden: an
ordinary write never creates a pointer whose content has not
committed. Witness §7.16 fault-injects both interruption points
and drives the retry convergence.
## 4. Pointer schema
@@ -203,7 +277,7 @@ with contract 6 (`mode-conversion.md`), identity with
| Column | Type | Constraints |
| ---------------- | ----------- | ------------------------------------------------------ |
| `id` | uuid | primary key |
| `user_id` | uuid | NOT NULL, FK → users(id) ON DELETE CASCADE |
| `user_id` | text | NOT NULL, FK → users(id) ON DELETE CASCADE (§3.6) |
| `category_key` | text | NOT NULL, FK → profile_category_registry(category_key) |
| `brain_ref` | text | NOT NULL, CHECK against the §4.3 grammar |
| `content_hash` | text | NOT NULL (§4.4 construction) |
@@ -276,32 +350,41 @@ with contract 6 (`mode-conversion.md`), identity with
7. **Managed deletion protocol.** A managed deletion of sensitive
content spans two stores and is an ordered protocol, not a single
transaction: step 1 commits the content deletion to the user's
brain repository; step 2, only after step 1 has committed, deletes
the pointer row in its own database transaction. Interruption
brain repository, recording the `content_hash` of the row it
intends to delete as read before step 1; step 2, only after step 1
has committed, deletes the pointer row in its own database
transaction as a **compare-and-delete** — the DELETE is
conditioned on (`user_id`, `category_key`, `brain_ref`) AND
`content_hash` equal to the recorded value. A concurrent §3.6
write that recreates the same locator with new content commits a
new hash on the pointer row, so step 2's condition fails, deletes
nothing, and the fresh content stays pointed — deletion never
removes a pointer for content it did not delete. Interruption
before step 1 commits leaves both stores unchanged. Interruption
between the steps leaves a dangling pointer, which §4.5 repairs
toward deletion within one reconciliation cycle. At no point does
any compensation write content into the database (witness §7.10).
any compensation write content into the database (witness §7.10,
including the concurrent-rewrite interleaving).
## 5. Consent schema and evaluation
1. **Exact columns.** Table `profile_consents`, columns exactly:
| Column | Type | Constraints |
| ---------------- | ----------- | --------------------------------------------------------------------- |
| `id` | uuid | primary key |
| `user_id` | uuid | NOT NULL, FK → users(id) ON DELETE CASCADE (the data subject) |
| `grantee_type` | text | NOT NULL, CHECK in (`agent`, `connector`, `feature`) |
| `agent_id` | uuid | NULL, FK → the enrolled-agent table (§5.2) |
| `connector_id` | text | NULL, FK → custody_connector_registry(connector_key) (§5.2) |
| `feature_key` | text | NULL, FK → custody_feature_registry(feature_key) (§5.2) |
| `grantee_ref` | text | generated stored: COALESCE(agent_id::text, connector_id, feature_key) |
| `category_key` | text | NOT NULL, FK → profile_category_registry(category_key) |
| `state` | text | NOT NULL, CHECK in (`granted`, `revoked`) |
| `granted_at` | timestamptz | NOT NULL |
| `revoked_at` | timestamptz | CHECK ((state = 'granted') = (revoked_at IS NULL)) |
| `actor` | text | NOT NULL (the authenticated principal that recorded the change) |
| `audit_event_id` | uuid | NOT NULL, FK → the §1.8 audit event table |
| Column | Type | Constraints |
| ---------------- | ----------- | ----------------------------------------------------------------------------- |
| `id` | uuid | primary key |
| `user_id` | text | NOT NULL, FK → users(id) ON DELETE CASCADE (the data subject, §3.6) |
| `grantee_type` | text | NOT NULL, CHECK in (`agent`, `connector`, `feature`) |
| `agent_id` | uuid | NULL, FK → agents(id) ON DELETE CASCADE (§5.2) |
| `connector_id` | text | NULL, FK → custody_connector_registry(connector_key) ON DELETE CASCADE (§5.2) |
| `feature_key` | text | NULL, FK → custody_feature_registry(feature_key) ON DELETE CASCADE (§5.2) |
| `grantee_ref` | text | generated stored: COALESCE(agent_id::text, connector_id, feature_key) |
| `category_key` | text | NOT NULL, FK → profile_category_registry(category_key) |
| `state` | text | NOT NULL, CHECK in (`granted`, `revoked`) |
| `granted_at` | timestamptz | NOT NULL |
| `revoked_at` | timestamptz | CHECK ((state = 'granted') = (revoked_at IS NULL)) |
| `actor` | text | NOT NULL (the authenticated principal that recorded the change) |
| `audit_event_id` | uuid | NOT NULL, FK → the §1.8 audit event table |
plus: the CHECK that exactly one referent column is non-NULL and
matches the discriminant — `(grantee_type = 'agent') = (agent_id
@@ -317,15 +400,18 @@ NULL)` — and the partial unique index UNIQUE (`user_id`,
display form is `<grantee_type>:<grantee_ref>`. The discriminant
set is closed at the three CHECK values, and every referent is a
real foreign key:
- `agent``agent_id` references the **enrolled-agent table**: the
single platform table in which agent enrollment under
`identity-lifecycle.md` (the contract 5 §3.1 rank-4 command
family) records enrolled agents. No sibling contract names that
physical table, so the binding is by identity: the custody
migration orders after that table's migration, the implementing
PR binds this FK to it, and the §7.11 witness pins the physical
name by asserting the FK's referenced table is the one the
enrollment surface writes.
- `agent``agent_id` references the platform **`agents` table**
(`agents.id`, uuid) as declared in the shared schema package
(`packages/db`). This is a binding to the live schema, named
honestly: the ratified `identity-lifecycle.md` defines no agent
enrollment surface or enrolled-agent table, so there is no
sibling-defined referent to bind to — the `agents` table is
today's only platform representation of agents, and the §7.11
witness pins the FK's referenced table to that schema symbol.
If a sibling amendment later introduces an agent-enrollment
surface with its own table, an amendment to this contract
re-binds the FK; until then, rows in `agents` are the closed
set of possible agent grantees.
- `connector``connector_id` references this contract's own
table `custody_connector_registry`, columns exactly:
`connector_key` (text, primary key), `since_version` (integer,
@@ -347,26 +433,31 @@ NULL)` — and the partial unique index UNIQUE (`user_id`,
grantee, category) means no access. There are no implicit grants,
no platform-admin bypass, and no mode in which default-deny is
suspended.
4. **Mutation authority — two disjoint predicates.** Every consent
mutation must satisfy exactly one of:
- **Subject mutation**: the authenticated actor IS the row's data
subject — the server asserts `actor` equals the principal of the
row's `user_id` before the mutation commits. This is the only
predicate under which rows are created or granted. A platform
admin has no consent-mutation capability over another user's
rows — an admin self-grant is refused at write time, closing the
write-side route around §5.3.
- **System auto-revocation** (disclosed, §8): the actor is the
single named platform system principal, the mutation is a
granted → revoked flip and nothing else, and the server has
verified the precondition that the row's grantee has ceased to
exist (e.g. agent retirement under `identity-lifecycle.md`). A
system-actor mutation whose precondition does not hold, or that
is anything other than a revocation, is refused.
The predicates are disjoint (the system principal is not a data
subject); §7.12 witnesses both and their refusal complements, and
every §7.3-enumerated consent-mutation route asserts one of them.
4. **Mutation authority — subject only.** Every consent mutation
(create, grant, revoke) must satisfy the single predicate: the
authenticated actor IS the row's data subject — the server asserts
`actor` equals the principal of the row's `user_id` before the
mutation commits. A platform admin has no consent-mutation
capability over another user's rows — an admin grant or revocation
on another user's data is refused at write time, closing the
write-side route around §5.3. There is no system-actor mutation
predicate: revision 3's system auto-revocation is withdrawn
(disclosed, §8), because grantee cessation is handled structurally
instead —
**Cessation by cascade**: when a grantee ceases to exist — the
`agents` row is deleted, or a `custody_connector_registry` /
`custody_feature_registry` row is removed by an amendment
migration — the referent FK's `ON DELETE CASCADE` removes that
grantee's consent rows in the same transaction as the referent
deletion. Access ends with the rows (§5.3 default deny: no row, no
access); no UPDATE is performed, so no actor question arises. The
durable record of the grants and their lifecycle is the audit
event table (§1.8): every mutation carried an `audit_event_id`,
and audit events are not deleted by the cascade. The same rule
already governs subject cessation via the `user_id` FK. §7.12
witnesses the subject predicate, its refusal complement, and the
cascade; every §7.3-enumerated consent-mutation route asserts the
subject predicate.
5. **Revocation and re-grant.** Revocation flips exactly one active
row to `revoked` and stamps `revoked_at`; it is effective for every
@@ -374,10 +465,17 @@ NULL)` — and the partial unique index UNIQUE (`user_id`,
retained as history and never mutated again — enforced by a
database trigger on `profile_consents` that permits UPDATE only
when it is the granted → revoked transition (the state flip plus
the `revoked_at` stamp, every other column unchanged) and rejects
every other UPDATE and every DELETE, so a revoked row cannot be
flipped back to `granted` by anyone, including through direct SQL
(witness §7.13). A re-grant after
the `revoked_at` stamp, every other column unchanged), and permits
DELETE only when the row's referenced subject or grantee no longer
exists — the trigger checks that the `users` row named by
`OLD.user_id`, or the referent row named by the grantee column, is
absent, which is true exactly during a §5.4 cessation cascade
(referential cascades delete the parent row before the dependent
rows, so the check distinguishes a cascade from a standalone
DELETE). Every other UPDATE and every DELETE with both referents
alive is rejected, so a revoked row cannot be flipped back to
`granted` and history cannot be pruned by anyone, including
through direct SQL (witness §7.13). A re-grant after
revocation inserts a NEW row (append-only history) — repeated
grant/revoke cycles are represented as successive rows, and the
§5.1 partial unique index guarantees the old revoked rows cannot
@@ -446,9 +544,13 @@ declared profile table" means `profile_answers` (§3.5).
custody tables' live column sets are exactly
§2.1/§4.1/§5.1/§5.2/§6.3 and `profile_answers` is exactly §3.5;
no platform table outside `profile_answers` carries profile
answer content; and every `audit_event_id` FK (§3.5, §4.1, §5.1,
answer content; every `audit_event_id` FK (§3.5, §4.1, §5.1,
§6.3) references the same physical table that the contract-1
(hierarchy §5.2) audit write path inserts into (§1.8 binding).
(hierarchy §5.2) audit write path inserts into (§1.8 binding);
and every FK column's declared type equals the referenced
column's live declared type — in particular each `user_id` is
`text` matching `users.id` and `agent_id` is `uuid` matching
`agents.id` (§3.6 binding).
2. **Column-type allowlist witness:** the custody tables and
`profile_answers` use only the column types named in
§2.1/§3.5/§4.1/§5.1/§5.2/§6.3 (uuid, text, integer, timestamptz,
@@ -465,9 +567,23 @@ declared profile table" means `profile_answers` (§3.5).
and runtime code construction — with the database client reachable
only through a closed importer allowlist. Every enumerated write
route implements §3.3 registry routing inside the §2.6 fence, and
every enumerated consent-mutation route asserts a §5.4 predicate;
a route outside the enumeration, or a client import outside the
allowlist, fails the assertion.
every enumerated consent-mutation route asserts the §5.4 subject
predicate; a route outside the enumeration, or a client import
outside the allowlist, fails the assertion. **Write-set closure
for sensitive paths:** for every enumerated route that performs a
sensitive write, the same static inventory (same prong set)
enumerates every database table that route's code path can write;
the set MUST equal exactly {`profile_pointers`, the §1.8 audit
event table} — the brain write goes through the declared
brain-write surface, never the database. Any additional
platform-table write statically reachable from a sensitive-write
path — an embedding insert, a cache row, any derived
representation in any generic table — fails the witness. Control:
a mutated route that adds one insert into an arbitrary existing
platform table is detected. This closes the §3.1 escape in which
a conforming-looking route also writes derived sensitive data
elsewhere: §7.5 probes for planted content; this closure bounds
what a sensitive-write route can write at all.
4. **Closed brain-read witness** (same style): the inventory enumerates
every production route that reads user-brain content on behalf of a
grantee, and every enumerated route calls the §5.6 consent
@@ -493,13 +609,25 @@ declared profile table" means `profile_answers` (§3.5).
seeded data moves the values to brains, creates pointers, and
leaves zero relational values (§2.4); (g) fence race: a profile
write transaction opened before a reclassification migration takes
its exclusive lock either commits before the scan (its value is
swept) or blocks and, on commit, routes by the new version — the
witness drives both interleavings and asserts no relational value
for the reclassified category exists after either completes
(§2.6); (h) with `custody_config.registry_version` set to a value
the release does not declare, profile writes and consent decisions
are refused (§2.7); (i) sensitive → non-sensitive: after the
its exclusive lock either commits before the lock is granted (its
value is swept) or blocks and, on commit, routes by the new
version — the witness drives both interleavings, INCLUDING the
interleaving in which the writer holds `FOR SHARE` when the
migration requests `FOR UPDATE`, the migration waits, the writer
commits its relational row, and the migration's scan (running
after lock grant, at `READ COMMITTED`, with the lock as its first
statement per §2.6) is asserted to sweep that row — and asserts no
relational value for the reclassified category exists after any
interleaving completes (§2.6); the witness also asserts the
migration implementation takes its exclusive lock as the first
statement of its transaction (a migration variant that reads
before locking fails review of this witness's static check);
(h) with `custody_config.registry_version` set to a value the
release's declared compile-time constant (§2.7, pinned by symbol)
does not equal, profile writes and consent decisions are refused,
and the witness asserts the version comparison reads the row under
the §2.7 shared lock in the same transaction as the refused
decision (§2.7); (i) sensitive → non-sensitive: after the
version bump, existing brain content and pointers remain intact
and readable, nothing is materialized into the database, and the
next write for that category lands in `profile_answers` (§2.4).
@@ -535,7 +663,14 @@ declared profile table" means `profile_answers` (§3.5).
brain commit, both stores are unchanged; killed between the brain
commit and the pointer delete, the dangling pointer is deleted by
the next reconciliation — and at no point is content written to
the database or restored to the brain.
the database or restored to the brain. **Concurrent-rewrite
interleaving:** deletion step 1 commits; before step 2 runs, a
concurrent §3.7 write recreates the same `brain_ref` with new
content and completes its pointer upsert (new `content_hash`);
step 2 then executes its compare-and-delete — the witness asserts
the DELETE matches zero rows, the fresh pointer survives, its
content resolves and verifies, and no unpointed content and no
dangling pointer exist afterward (§4.7).
11. **Default-deny and granularity witnesses:** an agent grantee with
no active row is refused; with a `granted` row for category A
only, category B is refused; with agent X granted, agent Y of the
@@ -543,8 +678,9 @@ declared profile table" means `profile_answers` (§3.5).
and `feature` are exercised via their FK refusal paths, since
both registries are empty at version 1 (§5.2); a consent insert
naming a nonexistent agent id violates the `agent_id` FK, and the
witness pins that FK's referenced table as the one the enrollment
surface writes (§5.2 binding); a platform admin requesting
witness pins that FK's referenced table by symbol identity to the
`agents` table exported by the shared schema package (§5.2
binding); a platform admin requesting
another user's sensitive content through any production read
surface is refused — admins are not a grantee type and
default-deny applies (the access-time complement of §7.12's
@@ -554,12 +690,14 @@ declared profile table" means `profile_answers` (§3.5).
12. **Mutation-authority witnesses:** a platform admin attempting to
create a grant on another user's data is refused at write time; a
hierarchy owner or manager likewise; the data subject succeeds;
`actor` equals the subject's principal on every subject-mutation
row; retiring an enrolled agent auto-revokes its active rows with
the system principal as `actor` and granted → revoked as the only
change; a system-principal mutation whose grantee still exists,
or that attempts anything other than a revocation, is refused —
the two §5.4 predicates are witnessed as disjoint (§5.4).
`actor` equals the subject's principal on every mutation row —
there is no non-subject mutation path (§5.4). **Cessation
cascade:** deleting an agent's `agents` row removes that agent's
consent rows in the same transaction via the FK cascade, the next
consent evaluation for that grantee refuses (no row), the linked
audit events survive, and no other grantee's rows are touched; a
connector-registry amendment migration removing a key cascades
identically (§5.4).
13. **Revocation/re-grant witnesses:** after revocation commits, the
next evaluation refuses and the revoked row persists unmutated; a
full grant → revoke → re-grant cycle yields two rows (one
@@ -567,8 +705,12 @@ declared profile table" means `profile_answers` (§3.5).
second concurrent grant attempt for the same key violates the
§5.1 partial unique index; a direct SQL UPDATE flipping a revoked
row back to `granted`, an UPDATE altering any other column of a
revoked row, and a DELETE of any row are each rejected by the
§5.5 trigger (§5.5).
revoked row, and a standalone DELETE of a row whose subject and
grantee both still exist are each rejected by the §5.5 trigger;
the complement controls pass: deleting the subject's `users` row
cascades the subject's consent rows, and deleting the grantee's
referent row cascades that grantee's rows — both permitted by the
trigger's absent-referent prong (§5.5).
14. **Self-access witness:** the data subject reads their own content
without consent rows (§5.6).
15. **Mode witnesses:** the custody-table schemas are byte-identical
@@ -581,6 +723,19 @@ declared profile table" means `profile_answers` (§3.5).
users, user A's grantee with a grant on user A cannot reach any of
user B's content (§6.6); inserting a second `custody_config` row
violates the §6.3 singleton constraint.
16. **Ordered-write protocol witnesses:** the §3.7 protocol is
fault-injected at both interruption points: killed before the
brain commit, both stores are unchanged and the caller receives
no acknowledgment; killed between the brain commit and the
pointer upsert, the brain holds unpointed content, no pointer row
exists, no read surface serves the content, and the caller
received no acknowledgment — then the retried write recommits the
same deterministic locator, completes the pointer upsert, and the
witness asserts the converged state (one pointer, verifying hash,
content served to the subject). A static control asserts the
implementation orders content before pointer: no enumerated
sensitive-write route creates a pointer row before its brain
commit has been confirmed (§3.6).
## 8. Drafting additions (PRD §12.1 disclosure)
@@ -591,9 +746,14 @@ ratification; none is claimed as a PRD mandate, and each is severable:
that is empty at version 1 (§5.2).
2. Append-only consent history: re-grants insert new rows; revoked
rows are retained unmutated, enforced by trigger (§5.5).
3. System-actor auto-revocation when a grantee ceases to exist, as a
precondition-checked predicate disjoint from subject mutation
(§5.4).
3. Cessation by cascade: when a grantee or subject referent row is
deleted, consent rows are removed by `ON DELETE CASCADE` in the
same transaction; access ends through default deny, and durable
history is carried by the §1.8 audit event table. The trigger's
DELETE prong permits a child-row DELETE only when the referenced
subject or grantee row is absent (§5.4, §5.5). This replaces the
revision-2 system-actor auto-revocation predicate, which is
withdrawn.
4. The `custody_config` election record for the Standalone layout,
singleton by constraint (§6.3).
5. The domain-separated keyed `content_hash` construction, its key
@@ -605,16 +765,42 @@ ratification; none is claimed as a PRD mandate, and each is severable:
unknown-key refusal, and both reclassification transition policies
(§2.1, §2.3, §2.4).
9. The custody-config write fence and the declared supported-version
refusal (§2.6, §2.7).
refusal, including the revision-4 pinning: both fence transactions
run at `READ COMMITTED` with the migration's `SELECT ... FOR
UPDATE` as the first statement of its transaction, and the
supported version is an exported compile-time constant in the
custody module of the shared schema package, read under the same
lock as the decision it gates (§2.6, §2.7).
10. Subject-only consent mutation authority (§5.4, first predicate).
11. The declared non-sensitive profile table `profile_answers` and
its closed schema (§3.5).
12. The typed grantee referent columns, the
`custody_connector_registry` and `custody_feature_registry`
tables, and the audit-table binding by identity (§5.1, §5.2,
§1.8).
tables, the audit-table binding by identity (§5.1, §5.2, §1.8),
and the agent-grantee binding by symbol identity to the `agents`
table exported by the shared schema package (§5.2, §7.11).
13. Shipping all §2–§6 schemas in v1 ahead of any sensitive write
surface (§6.4).
14. The committed-tree-only resolution rule: content reads resolve
only through the committed tree of the brain repository, and
symlink or non-tree/non-blob resolution is refused (§4.3, §7.8).
15. The text user-key binding: every `user_id` column is `text`
matching the live `users.id` declaration, values are opaque
strings not guaranteed to be UUIDs, and any sibling user-key
migration triggers amendment of this contract (§3.6, §7.1).
16. The ordered sensitive-write protocol for ordinary writes:
content-first at a deterministic locator, pointer upsert second
in its own transaction, acknowledgement only after the pointer
commit, idempotent retry convergence, and the
pointer-before-content prohibition (§3.7, §7.16).
17. The compare-and-delete managed-deletion step: deletion step 2 is
conditioned on the `content_hash` recorded in step 1, so a
concurrent recreate of the same locator is never unpointed by a
stale deletion (§4.7, §7.10).
18. The sensitive-path write-set closure: a static inventory of every
table a sensitive-write route can write, required to equal
exactly the pointer table plus the §1.8 audit event table
(§7.3).
The revision-1 "reporting" rationale for relational
background/education storage is withdrawn; the traced rationale is