P09 · Host · Rendered from source

decision ledger

Data and state plane

142 lines9,881 bytessha256 a0775e283071
{
  "schema_version": "actionist.decision-ledger.v1",
  "part": "P09",
  "part_title": "Data and state plane",
  "run_id": "2026-08-27-sprint-1-fable",
  "lane": "S1-L4",
  "boundary": {
    "research_only": true,
    "implementation_authorized": false,
    "execution_status": "UNEXECUTED",
    "admission_status": "NOT_ADMITTED",
    "admitted_blocks": 0,
    "no_benchmark_run": true
  },
  "decision_table_state_ownership": {
    "note": "The unit of decision is the state class, not the product. Owner is the single authority for schema, migration and write authority.",
    "rows": [
      {
        "state_class": "owned_transactional",
        "definition": "New business state Actionist writes for the client product",
        "default_owner": "actionist_host",
        "default_binding": "postgres_via_typed_port",
        "tenancy": "tenant_id column plus RLS enforced in-engine",
        "migration_owner": "actionist",
        "rationale": "A08 inferred, ecosystem fit, donor-stack compatibility, platform-owned policy removes the LLM-authored-RLS class",
        "confidence": "inferred",
        "falsifier": "Loop 2 shows typed-port overhead exceeds direct access value for the pilot workload"
      },
      {
        "state_class": "donor_owned_intact_service",
        "definition": "State inside a mature donor kept as a running service",
        "default_owner": "donor",
        "default_binding": "donor_native_store_plus_api",
        "tenancy": "donor-native, host maps canonical to donor IDs explicitly",
        "migration_owner": "donor_upstream",
        "rationale": "The cost is the fork, not the database (SISOCRM observed). Upstream patches are the reason to stay downstream.",
        "confidence": "observed_principle",
        "falsifier": "A donor whose operational burden exceeds its fork-avoidance saving"
      },
      {
        "state_class": "donor_owned_absorbed",
        "definition": "Donor code copied and owned; storage repointed at host Postgres",
        "default_owner": "actionist_host",
        "default_binding": "host_postgres_own_schema_or_merged",
        "tenancy": "host tenant model applies directly",
        "migration_owner": "actionist_single_owner",
        "rationale": "SISOCRM 2026-07-31 absorption decision; enables real foreign keys between donor and owned records",
        "confidence": "observed_local_decision",
        "falsifier": "Data-layer rewrite cost per donor measured in Loop 3 exceeds federation cost"
      },
      {
        "state_class": "document_collaboration",
        "definition": "Rich documents, blocks, whiteboards, realtime co-editing",
        "default_owner": "donor_engine",
        "default_binding": "engine_native_plus_fk_link_to_owned_records",
        "tenancy": "engine-native, host holds the linkage",
        "migration_owner": "engine",
        "rationale": "Deeply engineered; liability to own. Absorption adds the FK the donor lacks (page belongs to a deal permanently, not by folder convention).",
        "confidence": "inferred",
        "falsifier": "Collaboration engine cannot expose a stable linkage key"
      },
      {
        "state_class": "analytics_read_models",
        "definition": "Cross-owner product views and rollups",
        "default_owner": "actionist_host",
        "default_binding": "event_fed_owned_tables",
        "tenancy": "host tenant model",
        "migration_owner": "actionist",
        "rationale": "A12; the documented answer to losing cross-schema joins. Cross-owner mutation is never the mechanism.",
        "confidence": "inferred",
        "falsifier": "A workflow where eventual consistency measurably breaks the client outcome"
      },
      {
        "state_class": "search_vector",
        "definition": "Full-text and semantic retrieval",
        "default_owner": "actionist_host",
        "default_binding": "postgres_plus_pgvector_first",
        "tenancy": "same tenant predicate as relational",
        "migration_owner": "actionist",
        "rationale": "Search and vector have converged commercially; one retrieval plane chosen for licence safety. Dedicated engines are licence-mined (Meilisearch MIT AND BUSL, Typesense GPL-3.0, Elasticsearch document-level security inside ELv2-only x-pack).",
        "confidence": "inferred",
        "falsifier": "Pilot retrieval quality or latency demands a dedicated engine"
      },
      {
        "state_class": "files_objects",
        "definition": "Uploads, generated artifacts, exports",
        "default_owner": "actionist_host",
        "default_binding": "file_object_port_impl_by_deployment_scale",
        "tenancy": "naming and credential discipline only; no query semantics",
        "migration_owner": "not_applicable",
        "rationale": "Object storage OSS is effectively closed (MinIO AGPL and archived, Garage AGPL); SeaweedFS is Apache-2.0 by elimination, a named concentration risk. SISOCRM left this undecided even at single-VPS scale.",
        "confidence": "unresolved",
        "falsifier": "n/a - this row is an open decision, not a settled default"
      },
      {
        "state_class": "events",
        "definition": "Domain events, change feeds, integration triggers",
        "default_owner": "actionist_host",
        "default_binding": "transactional_outbox_then_bus",
        "tenancy": "subject or topic hierarchy mirroring the tenant boundary",
        "migration_owner": "actionist",
        "rationale": "Outbox removes the enqueue-vs-commit race. NATS Apache-2.0 preferred over Redpanda BSL, whose Additional Use Grant excludes a Streaming or Queuing Service.",
        "confidence": "inferred",
        "falsifier": "Event volume or ordering needs exceed a Postgres-outbox substrate"
      },
      {
        "state_class": "cache_ephemeral_local",
        "definition": "Sessions, locks, derived caches, local tool state",
        "default_owner": "capability_local",
        "default_binding": "sqlite_libsql_or_memory",
        "tenancy": "never a source of truth, so isolation is by lifetime",
        "migration_owner": "capability",
        "rationale": "SQLite is correct for bounded state owned end to end (SISOCRM observed)",
        "confidence": "observed_principle",
        "falsifier": "Cache state becomes authoritative anywhere, which is a defect not a variant"
      }
    ]
  },
  "invariants": [
    {
      "id": "INV-P09-1",
      "statement": "One authoritative owner per stateful resource, per table, per migration lineage, per file namespace, per event stream",
      "state": "observed_principle",
      "source": "A11 plus SISOCRM"
    },
    {
      "id": "INV-P09-2",
      "statement": "Workload shape precedes store choice",
      "state": "proposed",
      "falsifier": "80 percent of pilot state classes land on Postgres anyway and the port costs more than it saves"
    },
    {
      "id": "INV-P09-3",
      "statement": "Postgres is the default for new owned transactional state only, never imposed on donors",
      "state": "inferred",
      "source": "A08 accepted, A09 rejected"
    },
    {
      "id": "INV-P09-4",
      "statement": "Cross-owner product views are event-fed read models, never cross-owner table mutation",
      "state": "inferred",
      "source": "A12"
    },
    {
      "id": "INV-P09-5",
      "statement": "Capabilities consume typed data ports; ORM choice is adapter-local",
      "state": "proposed",
      "source": "A10 rejected as contract content"
    },
    {
      "id": "INV-P09-6",
      "statement": "Migration authority is release-scoped; no release without a migration rollback path",
      "state": "proposed"
    },
    {
      "id": "INV-P09-7",
      "statement": "Tenant identity is a first-class key on every owned resource from the first migration, with per-tenant key derivation for credential-class data",
      "state": "proposed",
      "source": "OpenConnector retrofit cost demonstrated"
    }
  ],
  "open_decisions": [
    {
      "id": "P09-D-01",
      "question": "One database, schemas, service stores or hybrid per client",
      "status": "open",
      "depends_on": "reuse shape mix in the chosen pilot",
      "note": "SISOCRM answered schemas-per-donor for federation and one-schema for absorption; both were correct in context"
    },
    {
      "id": "P09-D-02",
      "question": "What operations must the v1 typed data port support",
      "status": "open",
      "candidate": "port family (crud, query, tx, files, search, events) with capability-declared needs rather than one wide port"
    },
    {
      "id": "P09-D-03",
      "question": "What mechanically prevents a capability writing a table it does not own",
      "status": "open",
      "candidates": [
        "per-capability database roles with grants matching declared tables",
        "RLS as the single authorization source binding host service and any API layer alike",
        "static analysis of adapter declarations at admission"
      ],
      "note": "raised by this lane, proposed as a measured Loop-2 dimension"
    },
    {
      "id": "P09-D-04",
      "question": "Object storage implementation",
      "status": "open",
      "note": "unresolved locally and license-constrained externally"
    },
    {
      "id": "P09-D-05",
      "question": "Host runtime language",
      "status": "open",
      "blocks": [
        "River vs pg-boss",
        "sqlc vs Drizzle"
      ],
      "note": "ARCHITECTURE.md argues TypeScript control plane from a stratified 45-repo sample; not re-litigated here"
    }
  ],
  "rejected_here": [
    {
      "claim": "Force every donor onto Postgres and one ORM",
      "reason": "A09 and A10 already rejected; externally confirmed that the cost is the fork"
    },
    {
      "claim": "Adopt a multi-tenancy framework off the shelf for a TypeScript host",
      "reason": "None exist with real adoption; the only maintained ones are language-locked (Laravel, Django). Tenant isolation is bespoke work Actionist must own and test."
    },
    {
      "claim": "Trust a licence badge or the GitHub API SPDX field",
      "reason": "Twelve-plus corrections this run; five surfaces where the API would mislead an automated pipeline"
    }
  ],
  "feeds_experiments": [
    "knowledge/05-EXPERIMENT-ROADMAP.md Loop 0 contract surgery",
    "Loop 2 data-plane bake-off",
    "Loop 3 host absorption pilot"
  ]
}