P09 first principles — data and state plane
Lane S1-L4 · run 2026-08-27-sprint-1-fable · status: drafted from local evidence, finalized after external surveys (see research-report.md for the joined view).
Objective
Let one assembled Actionist product read and write state across capabilities that were never designed together — newly-written owned code, absorbed donor code, intact donor services, and external systems — without hidden dual writes, contested migrations, or a universal-database fiction.
Constraints (evidence-backed)
- Donor data layers are expensive to move: Django/Prisma emit store-specific SQL; the
cost of forcing a store is the fork, not the database (SISOCRM, observed).
- A shared mutable schema couples independently-versioned donors at their most brittle
layer — migrations (SISOCRM, observed rejection).
- Clients ultimately receive a product Cena's team must operate; per-donor database
servers multiply backup/restore/monitoring burden (SISOCRM, observed).
- LLM-authored access policy on a directly reachable database is a known data-leak
class (Lovable teardown, observed externally). Data authority must be host-owned and deterministic.
- Research boundary: no benchmark or execution was authorized this sprint; workload
claims here come from precedent reads and first principles, not measurement.
Invariants (proposed, with falsifiers)
- INV-P09-1 One authoritative owner per stateful resource — per table, per
migration lineage, per file namespace, per event stream. Falsifier: any workflow in the Loop-2 bake-off that cannot be expressed without two writers on one table.
- INV-P09-2 Workload shape precedes store choice. The unit of decision is the
state class (owned-transactional, donor-owned, document/collab, analytics, search/vector, files, events, cache/ephemeral), not the product. Falsifier: if ≥80% of pilot state classes land on Postgres anyway AND the port abstraction costs more than it saves, the taxonomy over-engineers and Postgres-first should be the rule.
- INV-P09-3 Postgres is the default for NEW owned transactional state only (A08),
never an invariant imposed on donors (A09 rejected). Falsifier: a donor whose native store creates operational burden exceeding its fork-avoidance savings.
- INV-P09-4 Cross-owner product views are read models fed by events, never
cross-owner table mutation. Synchronous cross-owner consistency is an explicit, justified exception, not a default. Falsifier: a pilot workflow where eventual consistency measurably breaks the client outcome (e.g. approval gates reading stale state) — that discovers the sync-boundary list, it doesn't kill the invariant.
- INV-P09-5 Capabilities consume typed data ports; stores hide behind adapters.
ORM choice is adapter-local (A10 rejected as contract content). Falsifier: Loop-0 — if the port needed to cover three genuinely different reuse shapes becomes so wide it leaks store semantics anyway, thin ports fail and per-shape contracts win.
- INV-P09-6 Migration authority is release-scoped. An assembled release pins the
schema versions of every owned resource and the expected donor schema versions; a release without a rollback path for its migrations is not releasable (aligns ReleaseManifest, D18).
- INV-P09-7 Tenant identity is a first-class column/key on every owned resource
from day one; per-tenant encryption keys for credential-class data. (OpenConnector's missing tenant column is the local proof of retrofit cost.)
Workload taxonomy (the P09 deliverable shape)
Eight state classes, each with distinct ownership defaults:
| Class | Default owner | Default binding | Notes |
|---|---|---|---|
| Owned transactional | Actionist host | Postgres via typed port | INV-P09-3 |
| Donor-owned state | Donor (service) or absorbed schema | Donor API / separate schema, one server | reuse-shape-dependent (SISOCRM) |
| Document/collaboration | Donor engine usually (AFFiNE-class) | Engine API + FK linkage to owned records | absorption adds real FKs |
| Analytics/read models | Actionist host | Event-fed owned tables/views | the cross-owner join answer |
| Search/vector | Dedicated engine only when workload demands | Port over engine | introduce late, not by default |
| Files/objects | Host file capability | Object-store port (impl per deployment scale) | undecided even in SISOCRM |
| Events | Host event envelope | Outbox → bus/log | also P11 receipts substrate |
| Cache/ephemeral/local | Capability-local | SQLite/libSQL/memory | never source of truth |
Contradictions and tensions
- Absorption (one schema, real FKs) vs federation (schemas + read models) pull the
data plane in opposite directions; both are locally proven choices. Resolution: the choice is made per capability by PackagingProfile, and the data plane must price both.
- Thin ports vs expressiveness: Refine-style CRUD providers prove UI decoupling, but
transactions/bulk/subscription/search stretch a single port. Likely resolution: port FAMILY (crud, query, tx, files, search, events) with capability-declared needs.
- Read models demand event discipline donors don't natively have; retrofitting CDC on
a donor may exceed the cost of just calling its API synchronously for low-volume views. The bake-off must price CDC-per-donor honestly.
Unknowns carried to experiments
Q11-Q16 from knowledge/04 remain open; Loop 0 (contract surgery) and Loop 2 (bake-off) in knowledge/05 are the designated falsifier vehicles. This lane adds: the dual-write detection question (what mechanically prevents a capability from writing a table it does not own — schema privileges? separate roles per capability? review-time static analysis of adapters?) — proposed as a Loop-2 measured dimension.