Skip to content

fix(store-turso): add versioned migration ledger (ARN-242) - #389

Draft
nerdsane wants to merge 20 commits into
mainfrom
codex/arn-242-turso-migration-ledger
Draft

fix(store-turso): add versioned migration ledger (ARN-242)#389
nerdsane wants to merge 20 commits into
mainfrom
codex/arn-242-turso-migration-ledger

Conversation

@nerdsane

@nerdsane nerdsane commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Objective

Fix ARN-242 by replacing ad hoc Turso startup DDL with a durable, ordered, checksummed migration ledger that prevents readiness when the schema is incompatible or a migration cannot complete.

Root cause

TursoEventStore::new and router startup issued schema changes procedurally. Some migration errors were discarded, there was no durable version/checksum record, and startup could report healthy while required capabilities—or executable schema extensions—could not serve the first runtime write.

Design

  • ADR-0180 defines the append-only migration contract; its number is verified unique against origin/main and every open PR.
  • One seven-version catalog owns store, platform, evolution, query, key, and vector schema.
  • temper_schema_migrations records version, unique name, SHA-256 checksum, and applied time.
  • Startup validates contiguous versions, pinned checksums, gaps, incompatible newer schemas, and only the declared catalog-head capability snapshot.
  • Each pending version uses BEGIN IMMEDIATE, rechecks the ledger, applies the migration, verifies the cumulative prefix, records the version, rereads it, and commits atomically.
  • SQLite introspection verifies object kind; exact column, key, foreign-key, table-restriction, named-index, and trigger semantics; and a bounded executable-extension policy.
  • Unexpected triggers and executable partial/expression/custom-collation indexes fail readiness on catalog-owned tables. Supported legacy OTS triggers must pass rollback-only probes that execute the exact production persist/enqueue/status SQL paths.
  • Legacy OTS rebuilding preserves supported rows, indexes, and triggers and fails closed before mutation on unsupported schema.
  • Router-specific platform migration ownership is removed; one runner controls readiness.

Behavioral and durability coverage

  • Fresh install, same-database replay, and every supported version prefix from 0 through 7.
  • Interrupted migration rollback/retry and real backend DDL failure rollback with version/step diagnostics.
  • Checksum mismatch, ledger gap, and incompatible newer version rejection.
  • Semantic column/key/FK/table/index/trigger drift attributed to the owning migration.
  • A later migration can tighten an earlier-owned table and durably replay against the catalog head.
  • Unexpected triggers, expression indexes, partial indexes, and legal sqliteX... names cannot bypass inventory; plain safe non-unique indexes remain compatible.
  • Legacy OTS trigger probes cover persist, enqueue insert, enqueue conflict-update, failed, and persisted paths without durable probe side effects.
  • Independent startup processes converge on one valid ledger.
  • Public readiness regression: a view occupying a required table name is rejected before its owning version is recorded.

Evidence

  • Isolated original RED: 47a6655c and failing behavior.
  • Isolated executable-schema RED: 15c85ec1 (four behavioral/durability regressions failed before the fix).
  • Exact-current-head live E2E and local gates: incompatible view and legal sqliteX... trigger refuse readiness; fresh install and replay return 200 with the complete pinned ledger.
  • Current pushed and E2E-tested head: fe3a942d9c7dfc45f486a262afb2ba6375313879.
  • Unbypassed final-head push passed rustfmt, strict workspace clippy, the blocking readability ratchet, full cargo test --workspace, all DST suites, and doc-tests.
  • Local code-quality review: PASS/CLEAN. Local DST diff review: PASS/CLEAN; no simulation-visible crate changed.

Scope

This PR does not touch crates/temper-actor-runtime; outside-contributor sign-off is not required.

The required exact-head fresh-context GPT-5.6 review, Greptile review/remediation, final fresh review, GitHub CI, and shippable evidence remain gates. This PR stays draft until all are complete.

Greptile Summary

This PR adds a durable migration path for Turso schema startup. The main changes are:

  • New ordered migration catalog with pinned checksums.
  • New temper_schema_migrations ledger and validation flow.
  • Schema snapshot checks for tables, indexes, triggers, and legacy OTS behavior.
  • OTS rebuild and probe logic for supported legacy schemas.
  • Router startup now relies on the shared migration runner.

Confidence Score: 4/5

The Turso schema verifier needs a fix before merging.

  • Extra indexes can be hidden from validation by using a case-variant table owner.
  • The rest of the migration flow has clear transaction boundaries and strong rollback checks in the changed code.

crates/temper-store-turso/src/migrations/schema_verify.rs

Security Review

Extra executable index definitions can bypass the new Turso readiness check when they are attached through a case-variant table owner.

Important Files Changed

Filename Overview
crates/temper-store-turso/src/migrations/runner.rs Adds the transactional migration runner, ledger checks, per-version application, rollback handling, and final schema verification.
crates/temper-store-turso/src/migrations/schema_verify.rs Adds schema extension validation, but extra index discovery misses case-variant table owners.
crates/temper-store-turso/src/migrations/ots_rebuild.rs Adds guarded legacy OTS rebuild logic with preflight validation, FK checks, transactional replacement, and dependent object preservation.
crates/temper-store-turso/src/migrations/schema_ots_probe.rs Adds savepoint-based probes that run production OTS persist, enqueue, failed, and persisted SQL paths without durable side effects.
crates/temper-store-turso/src/store/mod.rs Replaces ad hoc startup DDL with the shared ordered migration runner.
crates/temper-store-turso/src/router.rs Removes router-owned platform DDL because the unified catalog now owns platform schema setup.
crates/temper-store-turso/src/migrations/catalog.rs Introduces the seven-version schema catalog and checksum material.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Store as TursoEventStore::new
    participant Runner as migrations::migrate
    participant Ledger as temper_schema_migrations
    participant Catalog as Migration catalog
    participant Verify as Schema verifier

    Store->>Runner: run startup migration
    Runner->>Ledger: create and validate ledger
    Runner->>Ledger: load versions and checksums
    loop pending versions
        Runner->>Catalog: apply steps in transaction
        Runner->>Verify: verify expected schema
        Runner->>Ledger: record version checksum
    end
    Runner->>Verify: verify catalog head
    Runner-->>Store: report ready
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Store as TursoEventStore::new
    participant Runner as migrations::migrate
    participant Ledger as temper_schema_migrations
    participant Catalog as Migration catalog
    participant Verify as Schema verifier

    Store->>Runner: run startup migration
    Runner->>Ledger: create and validate ledger
    Runner->>Ledger: load versions and checksums
    loop pending versions
        Runner->>Catalog: apply steps in transaction
        Runner->>Verify: verify expected schema
        Runner->>Ledger: record version checksum
    end
    Runner->>Verify: verify catalog head
    Runner-->>Store: report ready
Loading

Fix All in Claude Code Fix All in Codex Fix All in Cursor

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
crates/temper-store-turso/src/migrations/schema_verify.rs:95
**Case-Variant Indexes Bypass Validation**

When an extra index is created against a case-variant table name such as `EVENTS`, SQLite still attaches it to `events`, but this `tbl_name = ?1` filter does not return it while verifying `events`. That skips `is_safe_plain_index`, so expression, partial, or custom-collation indexes can pass readiness unchecked.

```suggestion
               AND name NOT GLOB 'sqlite_*' AND tbl_name COLLATE NOCASE = ?1
```

Reviews (1): Last reviewed commit: "fix(store-turso): constrain legacy OTS t..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used (3)

@nerdsane

Copy link
Copy Markdown
Owner Author

RED EVIDENCE · commit 47a6655

Command:
cargo test -p temper-store-turso --test migration_ledger incompatible_schema_object_prevents_startup -- --exact --nocapture

Result: exit 101, 0 passed, 1 failed.

Failure reason:
an incompatible schema object must prevent store readiness: TursoEventStore { ... is_remote: false }

The current store returned Ok and reached readiness even though tenant_installed_apps was a view rather than the required durable table. No implementation change is present in this commit.

@nerdsane

Copy link
Copy Markdown
Owner Author

LIVE LOCAL E2E · RED revision versus PR head

Both runs used the assigned worktree and locally built temper-cli. No alternate worktree or source mutation was used.

Before fix — committed RED revision 47a6655c

$ git switch --detach 47a6655c
HEAD is now at 47a6655c test(store-turso): reproduce migration readiness regression
$ cargo build -p temper-cli
Finished `dev` profile [unoptimized + debuginfo] target(s) in 5m 25s
$ sqlite3 /tmp/arn242-pr389-before.db "CREATE VIEW tenant_installed_apps AS SELECT 'tenant' AS tenant_id, 'app' AS app_name;"
$ TURSO_URL=file:/tmp/arn242-pr389-before.db RUST_LOG=error target/debug/temper serve --no-observe --port 31746
Storage: turso (file:/tmp/arn242-pr389-before.db)
Listening on http://0.0.0.0:31746
$ curl -sS -i http://127.0.0.1:31746/healthz
HTTP/1.1 200 OK
content-length: 0
date: Tue, 14 Jul 2026 11:35:14 GMT
$ sqlite3 /tmp/arn242-pr389-before.db "SELECT type || '|' || name FROM sqlite_master WHERE name IN ('tenant_installed_apps','temper_schema_migrations') ORDER BY name;"
view|tenant_installed_apps

Observed regression: the server reached readiness while the durable table was still an incompatible view, and no migration ledger existed.

After fix — exact PR head 8808bf19

$ git switch codex/arn-242-turso-migration-ledger
Your branch is up to date with 'origin/codex/arn-242-turso-migration-ledger'.
$ cargo build -p temper-cli
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 23s
$ sqlite3 /tmp/arn242-pr389-after-incompatible.db "CREATE VIEW tenant_installed_apps AS SELECT 'tenant' AS tenant_id, 'app' AS app_name;"
$ TURSO_URL=file:/tmp/arn242-pr389-after-incompatible.db RUST_LOG=error target/debug/temper serve --no-observe --port 31748
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 4 (apps-platform-and-secrets) step 1 schema compatibility check failed: capability 'tenant_installed_apps' must be a table, found view
$ curl -sS -i http://127.0.0.1:31748/healthz
curl: (7) Failed to connect to 127.0.0.1 port 31748 after 0 ms: Couldn't connect to server
$ sqlite3 -separator '|' /tmp/arn242-pr389-after-incompatible.db "SELECT version,name FROM temper_schema_migrations ORDER BY version; SELECT type,name FROM sqlite_master WHERE name='tenant_installed_apps';"
1|event-journal-and-snapshots
2|specs-constraints-and-integrations
3|authorization-and-artifacts
view|tenant_installed_apps

Observed fix: readiness fails closed with the owning migration and object mismatch. Earlier versions remain durably committed, while migration 4 does not replace the incompatible object.

Fresh-install readiness and complete ledger

$ sqlite3 /tmp/arn242-pr389-after-fresh.db "PRAGMA user_version;"
0
$ TURSO_URL=file:/tmp/arn242-pr389-after-fresh.db RUST_LOG=error target/debug/temper serve --no-observe --port 31750
Storage: turso (file:/tmp/arn242-pr389-after-fresh.db)
Listening on http://0.0.0.0:31750
$ curl -sS -i http://127.0.0.1:31750/healthz
HTTP/1.1 200 OK
content-length: 0
date: Tue, 14 Jul 2026 11:42:03 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-after-fresh.db "SELECT version,name,checksum FROM temper_schema_migrations ORDER BY version;"
1|event-journal-and-snapshots|45e45eb7d5a81d2382fc04b08f041753a7929d28380bb6800a6857c4f94758be
2|specs-constraints-and-integrations|e75625bdf2545ab397580f7ac5e1d926872198f8ebe3ae9a178d1a49e30cf663
3|authorization-and-artifacts|6a517aa711b33e0497c8c139d48cee5de9ca10b39783cb97431ac036a17de85d
4|apps-platform-and-secrets|c600c5407bf866e62bc071a2209913fa9c533399bf0b40602252aeda2112bd9d
5|trajectories-and-evolution|ddc573525a3df1bec5ccfd4ae9f2d93a24c90410ecb801383a2fcef02015c005
6|blob-and-query-plane|67e4590b101e15cefff6e1a75b31f217a4e7cc36d1361237bdad40e7546ae8b7
7|declared-key-and-vector-indexes|000d6833874eb0b086b2994ddd6eacf9f01dbc504bd8e188534a6743bbe61486

Final local gates at 8808bf19

  • cargo fmt --all -- --check — PASS
  • git diff --check — PASS
  • cargo clippy -p temper-store-turso --all-targets -- -D warnings — PASS
  • Code-quality review marker — PASS (2026-07-14T09:44:14Z)
  • DST review marker — PASS (2026-07-14T09:43:48Z); no simulation-visible crates changed
  • Unbypassed git push pre-push pipeline — rustfmt PASS, strict workspace clippy PASS, readability ratchet PASS, full cargo test --workspace plus doc-tests PASS
  • Final hook output: Pre-push: ALL GATES PASSED

crates/temper-actor-runtime is not touched by this PR.

@nerdsane

Copy link
Copy Markdown
Owner Author

Reviewed the complete open GitHub diff at pushed head 8808bf19033be36b36c3f92f9e1b36ca5913a555, including ADR-0171, every changed code/test file, and the existing RED/live evidence. Two blocking correctness/durability findings:

  1. [P1] Reject extra schema restrictions before recording readiness. schema_snapshot.rs:115-147 treats the expected columns, unique keys, and foreign keys as subsets: it never rejects extra actual columns/keys/FKs, and the snapshot does not model CHECK/table modifiers. A legacy events table that otherwise matches migration 1 but adds must_fill TEXT NOT NULL with no default therefore verifies and receives the ledger row. The normal event append explicitly supplies only canonical columns (event_store.rs:933-945), so the first append fails after readiness. Extra UNIQUE/FK restrictions have the same failure mode. This recreates the readiness-before-capability regression. Add fail-closed handling for unmodeled restrictions (while retaining provably harmless extensions) and a regression proving the active ledger version is not recorded.

  2. [P1] Do not bypass the OTS modifier guard merely because updated_at exists. ots_rebuild.rs:127-140 returns immediately for a current-shape table before validate_columns and validate_no_table_constraints. Because final schema verification does not encode CHECK/COLLATE/STRICT/etc., a current-shape OTS table with CHECK (data <> '{}') is accepted and migration 5 is ledgered, although {} is a supported insert in the migration coverage (tests.rs:145-152). The existing compact-CHECK test covers only the pre-upgrade/no-updated_at path. Apply the fail-closed modifier validation to the already-updated path too, and prove migration 5 remains unrecorded without mutation.

Verdict: FAIL

Copy link
Copy Markdown
Collaborator

Reviewer FAIL remediation · current head 54b7a52cc1fad7cce3ee76d5fff42b092377e237

This head addresses every blocking finding in the prior review: #389 (comment)

  1. Unexpected schema restrictions now fail closed before readiness. Schema verification now rejects unmodeled required columns, UNIQUE constraints/indexes, foreign keys, CHECK/generated semantics, and executable defaults. Provably harmless nullable extensions remain compatible. Failure preserves the table/index and does not record the owning migration.
  2. Current-shape OTS no longer bypasses modifier validation. The already-updated_at path validates columns and restricted table semantics before accepting the shape. Unsupported CHECK/generated modifiers fail without mutation and migration 5 remains unrecorded.
  3. Ledger integrity and SQL-token hardening were extended so compact/merged constraint tokens, partial predicates, and restricted semantics cannot be mistaken for a compatible schema.

Representative behavioral/durability regressions now passing:

  • unexpected_required_column_prevents_ledgering_and_preserves_table
  • unexpected_unique_index_prevents_ledgering_without_dropping_index
  • unexpected_foreign_key_prevents_ledgering_and_preserves_table
  • unexpected_check_semantics_prevent_ledgering_and_preserve_table
  • current_shape_check_constraint_fails_without_ledgering_migration_five
  • current_shape_short_generated_column_fails_without_mutation
  • current_ots_shape_preserves_harmless_inbound_reference

The unbypassed push gate on this exact head passed:

  • rustfmt
  • strict workspace clippy
  • blocking readability ratchet
  • full cargo test --workspace
  • all doc-tests

Final hook output: Pre-push: ALL GATES PASSED.

No file under crates/temper-actor-runtime is touched.

Copy link
Copy Markdown
Collaborator

Reviewed the complete open GitHub patch at exact head 54b7a52cc1fad7cce3ee76d5fff42b092377e237 (19 changed files) and the existing PR evidence/comments, including the prior blocking review.

The two prior concrete reproductions are fixed on this head: unexpected required columns/UNIQUE/FK/CHECK/generated/default semantics now fail before the owning ledger row, and the current-shape OTS path validates modifiers before returning. I still found two release-blocking durability issues:

  1. [P1] Historical prefix verification makes ordinary future migrations commit and then permanently fail readiness. build_expected_migrations captures a cumulative schema snapshot after each catalog prefix, and the final readiness loop then verifies every historical snapshot against the latest database (runner.rs:69-76). This conflicts with the exactness rules: for example, an appended migration 8 that adds events.required_value TEXT NOT NULL DEFAULT 'x' passes migration 8's in-transaction head-snapshot verification and commits its ledger row, but the subsequent migration-1 snapshot check rejects that column as omission-unsafe (schema_verify.rs:73-90). Startup now fails after the durable version advance, and every retry fails the same way; an older binary also refuses the newer ledger. The current seven groups avoid evolving objects owned by earlier groups, so the prefix-upgrade test does not expose this. Final verification needs a head/ownership-aware capability model that permits changes declared by later migrations, plus a regression with a later migration tightening an earlier table.

  2. [P1] Executable schema extensions still bypass readiness and can break the first runtime write. The verifier walks only expected tables and expected named indexes (schema_verify.rs:12-31); the snapshot model inventories no triggers, and unknown non-unique indexes are never compared (schema_snapshot.rs:73-90). A legacy events table with otherwise canonical semantics plus CREATE TRIGGER reject_events BEFORE INSERT ON events BEGIN SELECT RAISE(FAIL, 'blocked'); END therefore verifies and receives migration 1, yet the normal append at event_store.rs:930-947 fails after readiness. An unknown non-unique expression/partial index can likewise execute a failing expression during insert while remaining invisible to these comparisons. This leaves the broader fail-closed requirement from the first prior finding incomplete. Inventory and reject or explicitly validate executable extensions while preserving them on failure, and add regressions proving the owning migration is not ledgered.

Verdict: FAIL

Copy link
Copy Markdown
Collaborator

LIVE LOCAL E2E · exact current head 16ce98d6d4217a9df9bc2ff6c989b16cf6bf8c79

All commands ran in the assigned worktree on codex/arn-242-turso-migration-ledger. No Docker operation, alternate worktree, source mutation, or cleanup was used. Successful server sessions were stopped with Ctrl-C immediately after their checks; failing starts exited themselves with status 1.

Before-fix behavioral evidence

  • Original readiness regression and live RED server proof: fix(store-turso): add versioned migration ledger (ARN-242) #389 (comment)
  • The later executable-schema review regression was isolated in commit 15c85ec1. Before the fix, this command failed for the four stated durability behaviors (later migration tightening an earlier table, unexpected trigger, expression index, and partial index):
$ cargo test -p temper-store-turso migrations::schema_verify_tests::runtime_extensions -- --nocapture
test result: FAILED. 1 passed; 4 failed

At the current head the same group passes all 11 cases, including queued-only OTS triggers, legal sqliteX... names, declared-trigger mismatch/absence, rollback-only side effects, and durable 7→8 replay.

Exact-head build and remote identity

$ git status --short --branch
## codex/arn-242-turso-migration-ledger...origin/codex/arn-242-turso-migration-ledger
$ git rev-parse HEAD
16ce98d6d4217a9df9bc2ff6c989b16cf6bf8c79
$ git rev-parse origin/codex/arn-242-turso-migration-ledger
16ce98d6d4217a9df9bc2ff6c989b16cf6bf8c79
$ cargo build -p temper-cli
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 47s

Fail closed on an incompatible required object

$ sqlite3 /tmp/arn242-pr389-16ce98d6-incompatible-20260718T230954Z.db "CREATE VIEW tenant_installed_apps AS SELECT 'tenant' AS tenant_id, 'app' AS app_name;"
$ TURSO_URL=file:/tmp/arn242-pr389-16ce98d6-incompatible-20260718T230954Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39081
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 4 (apps-platform-and-secrets) step 1 schema compatibility check failed: capability 'tenant_installed_apps' must be a table, found view
$ sqlite3 -separator '|' /tmp/arn242-pr389-16ce98d6-incompatible-20260718T230954Z.db "SELECT version,name FROM temper_schema_migrations ORDER BY version; SELECT type,name FROM sqlite_master WHERE name='tenant_installed_apps';"
1|event-journal-and-snapshots
2|specs-constraints-and-integrations
3|authorization-and-artifacts
view|tenant_installed_apps

Readiness failed before migration 4 was ledgered, and the incompatible view was preserved.

Fresh install reaches readiness with the complete pinned ledger

$ TURSO_URL=file:/tmp/arn242-pr389-16ce98d6-fresh-20260718T230954Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39083
Storage: turso (file:/tmp/arn242-pr389-16ce98d6-fresh-20260718T230954Z.db)
Listening on http://0.0.0.0:39083
$ curl -sS -i http://127.0.0.1:39083/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sat, 18 Jul 2026 23:11:33 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-16ce98d6-fresh-20260718T230954Z.db "SELECT version,name,checksum FROM temper_schema_migrations ORDER BY version; SELECT 'ledger_count', COUNT(*) FROM temper_schema_migrations;"
1|event-journal-and-snapshots|0faf57ca6c414632c350bbbf5b95f557b103dfaea0de3810310653ce4908e8b4
2|specs-constraints-and-integrations|c3bc40f106b6fd792d62771499fa5309fed1cf15fc5446ccc68bf231c6b4e654
3|authorization-and-artifacts|6eb72e709229fb024023a252d436c97a09921a6514879c9379c5a220cef09fe4
4|apps-platform-and-secrets|a32934ee3488239bfd3c1caca3e58612de45a0d4df403021c1970714e399125e
5|trajectories-and-evolution|aae44c52176a0a9e5751d8a8300b0b56bba0de3964c7b66f02d58c70b0e13890
6|blob-and-query-plane|1f3d7a48d01d62afd067abbcfebd9d7f6e986c9733de831805a26cf7768b62a6
7|declared-key-and-vector-indexes|ae6952c1187610e1ec69ece56452023a3de01a85b1a8da9ee5b92789f65cc737
ledger_count|7

Same-database replay stays ready and does not append ledger rows

$ TURSO_URL=file:/tmp/arn242-pr389-16ce98d6-fresh-20260718T230954Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39085
Storage: turso (file:/tmp/arn242-pr389-16ce98d6-fresh-20260718T230954Z.db)
Restored 21 specs from Turso.
Listening on http://0.0.0.0:39085
$ curl -sS -i http://127.0.0.1:39085/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sat, 18 Jul 2026 23:12:57 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-16ce98d6-fresh-20260718T230954Z.db "SELECT 'ledger_count_after_replay', COUNT(*) FROM temper_schema_migrations;"
ledger_count_after_replay|7

Legal sqliteX... executable trigger cannot bypass readiness

After stopping the replay server:

$ sqlite3 /tmp/arn242-pr389-16ce98d6-fresh-20260718T230954Z.db "CREATE TRIGGER sqliteXreject_events BEFORE INSERT ON events BEGIN SELECT RAISE(FAIL, 'blocked'); END;"
$ TURSO_URL=file:/tmp/arn242-pr389-16ce98d6-fresh-20260718T230954Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39087
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 7 (declared-key-and-vector-indexes) failed while attempting to verify catalog head schema: storage error: Turso schema compatibility check failed: table 'events' has unexpected executable trigger 'sqliteXreject_events'
$ sqlite3 -separator '|' /tmp/arn242-pr389-16ce98d6-fresh-20260718T230954Z.db "SELECT 'ledger_count_after_trigger_rejection', COUNT(*) FROM temper_schema_migrations; SELECT type,name FROM sqlite_master WHERE name='sqliteXreject_events';"
ledger_count_after_trigger_rejection|7
trigger|sqliteXreject_events

The current head fails closed, keeps the complete ledger unchanged, and preserves the operator-owned trigger for diagnosis.

Exact-head local gates

  • cargo fmt --all -- --check — PASS
  • git diff --check — PASS
  • cargo clippy -p temper-store-turso --all-targets -- -D warnings — PASS
  • cargo test -p temper-store-turso — PASS (105 unit passed, 1 ignored; 6 integration passed)
  • Required code-quality review — PASS/CLEAN, no findings
  • Required DST diff review — PASS/CLEAN, no findings; no simulation-visible crate changed
  • Unbypassed git push — PASS: rustfmt, strict workspace clippy, readability ratchet, full cargo test --workspace, and all doc-tests
  • Final hook output: Pre-push: ALL GATES PASSED

crates/temper-actor-runtime is not touched by this PR.

Copy link
Copy Markdown
Collaborator

ADR namespace audit · exact head fe3a942d9c7dfc45f486a262afb2ba6375313879

The originally assigned ADR-0171 collided with multiple already-open PRs, so the current head renumbers this decision to ADR-0180 before independent review.

Exact checks:

$ git ls-tree -r --name-only origin/main docs/adrs | tail -n 2
docs/adrs/0155-declared-vector-access-path.md
docs/adrs/TEMPLATE.md

$ git ls-tree -r --name-only origin/main docs/adrs | rg '^docs/adrs/0180-'
# no matches

$ gh pr list --repo nerdsane/temper --state open --limit 100 --json number,files --jq '.[] | .number as $n | .files[] | select(.path | startswith("docs/adrs/")) | "\($n)\t\(.path)"' | rg 'docs/adrs/0180-'
# no matches

A repository-scoped search for "0180" across open PR metadata also returned zero results. The staged rename was independently checked as R99; ADR body lines 2–290 remained byte-identical and only the heading changed. Local code-quality and DST reviews both returned PASS/CLEAN, and the unbypassed push finished with Pre-push: ALL GATES PASSED.

Copy link
Copy Markdown
Collaborator

LIVE LOCAL E2E · exact current head fe3a942d9c7dfc45f486a262afb2ba6375313879

All commands ran in the assigned worktree on codex/arn-242-turso-migration-ledger. No Docker operation or cleanup was used. Successful servers were stopped with Ctrl-C immediately after verification; failing starts exited with status 1. A first sandboxed listener attempt was denied by the local sandbox before bind and was retried outside the sandbox on a new database.

Exact pushed identity and build

$ git status --short --branch
## codex/arn-242-turso-migration-ledger...origin/codex/arn-242-turso-migration-ledger
$ git rev-parse HEAD
fe3a942d9c7dfc45f486a262afb2ba6375313879
$ git rev-parse origin/codex/arn-242-turso-migration-ledger
fe3a942d9c7dfc45f486a262afb2ba6375313879
$ cargo build -p temper-cli
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.24s

Incompatible required object fails before its owning version is ledgered

$ sqlite3 /tmp/arn242-pr389-fe3a942d-incompatible-20260719T004320Z.db "CREATE VIEW tenant_installed_apps AS SELECT 'tenant' AS tenant_id, 'app' AS app_name;"
$ TURSO_URL=file:/tmp/arn242-pr389-fe3a942d-incompatible-20260719T004320Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39101
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 4 (apps-platform-and-secrets) step 1 schema compatibility check failed: capability 'tenant_installed_apps' must be a table, found view
$ sqlite3 -separator '|' /tmp/arn242-pr389-fe3a942d-incompatible-20260719T004320Z.db "SELECT version,name FROM temper_schema_migrations ORDER BY version; SELECT type,name FROM sqlite_master WHERE name='tenant_installed_apps';"
1|event-journal-and-snapshots
2|specs-constraints-and-integrations
3|authorization-and-artifacts
view|tenant_installed_apps

Fresh exact-head startup reaches health with the complete pinned ledger

$ TURSO_URL=file:/tmp/arn242-pr389-fe3a942d-fresh-live-20260719T004320Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39123
Storage: turso (file:/tmp/arn242-pr389-fe3a942d-fresh-live-20260719T004320Z.db)
Listening on http://0.0.0.0:39123
$ curl -sS -i http://127.0.0.1:39123/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sun, 19 Jul 2026 00:46:16 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-fe3a942d-fresh-live-20260719T004320Z.db "SELECT version,name,checksum FROM temper_schema_migrations ORDER BY version; SELECT 'ledger_count', COUNT(*) FROM temper_schema_migrations;"
1|event-journal-and-snapshots|0faf57ca6c414632c350bbbf5b95f557b103dfaea0de3810310653ce4908e8b4
2|specs-constraints-and-integrations|c3bc40f106b6fd792d62771499fa5309fed1cf15fc5446ccc68bf231c6b4e654
3|authorization-and-artifacts|6eb72e709229fb024023a252d436c97a09921a6514879c9379c5a220cef09fe4
4|apps-platform-and-secrets|a32934ee3488239bfd3c1caca3e58612de45a0d4df403021c1970714e399125e
5|trajectories-and-evolution|aae44c52176a0a9e5751d8a8300b0b56bba0de3964c7b66f02d58c70b0e13890
6|blob-and-query-plane|1f3d7a48d01d62afd067abbcfebd9d7f6e986c9733de831805a26cf7768b62a6
7|declared-key-and-vector-indexes|ae6952c1187610e1ec69ece56452023a3de01a85b1a8da9ee5b92789f65cc737
ledger_count|7

Same-database replay is healthy and does not append ledger rows

$ TURSO_URL=file:/tmp/arn242-pr389-fe3a942d-fresh-live-20260719T004320Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39125
Storage: turso (file:/tmp/arn242-pr389-fe3a942d-fresh-live-20260719T004320Z.db)
Restored 21 specs from Turso.
Listening on http://0.0.0.0:39125
$ curl -sS -i http://127.0.0.1:39125/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sun, 19 Jul 2026 00:47:18 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-fe3a942d-fresh-live-20260719T004320Z.db "SELECT 'ledger_count_after_replay', COUNT(*) FROM temper_schema_migrations;"
ledger_count_after_replay|7

Legal sqliteX... executable trigger is rejected and preserved

After stopping the replay server:

$ sqlite3 /tmp/arn242-pr389-fe3a942d-fresh-live-20260719T004320Z.db "CREATE TRIGGER sqliteXreject_events BEFORE INSERT ON events BEGIN SELECT RAISE(FAIL, 'blocked'); END;"
$ TURSO_URL=file:/tmp/arn242-pr389-fe3a942d-fresh-live-20260719T004320Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39127
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 7 (declared-key-and-vector-indexes) failed while attempting to verify catalog head schema: storage error: Turso schema compatibility check failed: table 'events' has unexpected executable trigger 'sqliteXreject_events'
$ sqlite3 -separator '|' /tmp/arn242-pr389-fe3a942d-fresh-live-20260719T004320Z.db "SELECT 'ledger_count_after_trigger_rejection', COUNT(*) FROM temper_schema_migrations; SELECT type,name FROM sqlite_master WHERE name='sqliteXreject_events';"
ledger_count_after_trigger_rejection|7
trigger|sqliteXreject_events

Exact-head local gates

  • cargo fmt --all -- --check — PASS
  • git diff --check — PASS
  • Strict workspace clippy — PASS
  • Blocking readability ratchet — PASS
  • cargo test --workspace and all doc-tests — PASS
  • Random platform DST batch — 7 passed, 0 failed (2,332.59s)
  • Required code-quality review — PASS/CLEAN
  • Required DST diff review — PASS/CLEAN; no simulation-visible crate changed
  • Unbypassed push terminal output: Pre-push: ALL GATES PASSED

crates/temper-actor-runtime is not touched by this PR.

Copy link
Copy Markdown
Collaborator

Fresh independent review of the complete open 23-file GitHub patch and PR evidence at exact head fe3a942d9c7dfc45f486a262afb2ba6375313879.

  1. [P1] Match trigger owners with SQLite identifier semantics or differently-cased triggers bypass readiness. capture_triggers scopes an owned-table inventory with tbl_name = ?1, and verify_triggers passes the catalog's lowercase table name. SQLite table identifiers are case-insensitive, but sqlite_schema.tbl_name for a trigger retains the spelling from its ON clause. Thus a canonical legacy events table plus CREATE TRIGGER reject_events BEFORE INSERT ON EVENTS BEGIN SELECT RAISE(FAIL, 'blocked'); END stores tbl_name = 'EVENTS'; the inventory for events is empty, migration 1 and final head verification can ledger successfully, and the normal INSERT INTO events still fires the trigger and fails after readiness. The same case-sensitive comparison also misses a forbidden trigger declared ON TEMPER_SCHEMA_MIGRATIONS in the ledger guard, while the legacy OTS dependent-object query misses a trigger declared ON OTS_TRAJECTORIES, allowing the rebuild to drop it instead of preserving and probing it. Use identifier-equivalent (ASCII case-insensitive) owner matching consistently, and add regressions proving a differently-cased blocking events trigger prevents ledgering, an uppercase-owner ledger trigger is rejected, and a differently-cased legacy OTS trigger is preserved/probed or causes rollback.

Verdict: FAIL

@rita-aga

Copy link
Copy Markdown
Collaborator

Reviewer FAIL remediation · SQLite trigger-owner case semantics

Remediates the exact current-review finding from #389 (comment).

RED — behavioral/durability regressions only

Commit: 50d8177

$ cargo test -p temper-store-turso differently_cased -- --nocapture
running 3 tests
test migrations::ledger_tests::differently_cased_ledger_trigger_owner_is_rejected_before_migration ... FAILED
test migrations::ots_rebuild_tests::differently_cased_ots_trigger_owner_is_preserved_when_probe_rolls_back ... FAILED
test migrations::schema_verify_tests::runtime_extensions::differently_cased_trigger_owner_cannot_bypass_inventory ... FAILED

test result: FAILED. 0 passed; 3 failed

Each regression failed at its expect_err because migration incorrectly returned Ok(()) when SQLite preserved a differently-cased trigger owner in sqlite_master.tbl_name.

GREEN — root-cause correction

Commit: d5069b6

  • Trigger-owner discovery now uses SQLite ASCII identifier semantics with COLLATE NOCASE.
  • Captured owner names are normalized before inventory comparison.
  • Declared-trigger owner comparison is ASCII case-insensitive.
  • Ledger trigger bans and OTS dependent-object preservation use the same semantics.
  • OTS rebuild and trigger-inventory algorithms were version-bumped, so all seven released migration checksums were intentionally re-pinned.
  • ADR-0180 now records the identifier-owner requirement.
$ cargo test -p temper-store-turso differently_cased -- --nocapture
test result: ok. 3 passed; 0 failed

$ cargo test -p temper-store-turso
test result: ok. 109 passed; 0 failed; 1 ignored

$ git status --short --branch
## codex/arn-242-turso-migration-ledger...origin/codex/arn-242-turso-migration-ledger

$ git rev-parse HEAD
d5069b654dcc287bb360f8b55639ff471114603a

$ git rev-parse origin/codex/arn-242-turso-migration-ledger
d5069b654dcc287bb360f8b55639ff471114603a

The unbypassed push completed with:

Pre-push: ALL GATES PASSED

That includes cargo fmt --all -- --check, strict workspace clippy, blocking readability, cargo test --workspace, all doc-tests, code-quality review PASS/CLEAN, and DST diff review PASS/CLEAN. Random platform DST: 7 passed, 0 failed in 834.80s. No simulation-visible crate and no crates/temper-actor-runtime file is touched.

@rita-aga

Copy link
Copy Markdown
Collaborator

LIVE LOCAL E2E · exact remediated head d5069b654dcc287bb360f8b55639ff471114603a

All commands ran in the assigned worktree on codex/arn-242-turso-migration-ledger. New uniquely named databases were used. No Docker operation or cleanup was used. Successful servers were stopped with Ctrl-C immediately after verification; failing starts exited with status 1. The /tmp evidence databases were intentionally retained.

This is the after-fix live counterpart to the RED behavioral regressions in #389 (comment).

Exact pushed identity and build

$ git status --short --branch
## codex/arn-242-turso-migration-ledger...origin/codex/arn-242-turso-migration-ledger
$ git rev-parse HEAD
d5069b654dcc287bb360f8b55639ff471114603a
$ git rev-parse origin/codex/arn-242-turso-migration-ledger
d5069b654dcc287bb360f8b55639ff471114603a
$ cargo build -p temper-cli
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.61s

Incompatible required object fails before its owning version is ledgered

$ sqlite3 /tmp/arn242-pr389-d5069b65-incompatible-20260719T022645Z.db "CREATE VIEW tenant_installed_apps AS SELECT 'tenant' AS tenant_id, 'app' AS app_name;"
$ TURSO_URL=file:/tmp/arn242-pr389-d5069b65-incompatible-20260719T022645Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39421
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 4 (apps-platform-and-secrets) step 1 schema compatibility check failed: capability 'tenant_installed_apps' must be a table, found view
$ sqlite3 -separator '|' /tmp/arn242-pr389-d5069b65-incompatible-20260719T022645Z.db "SELECT version,name FROM temper_schema_migrations ORDER BY version; SELECT type,name FROM sqlite_master WHERE name='tenant_installed_apps';"
1|event-journal-and-snapshots
2|specs-constraints-and-integrations
3|authorization-and-artifacts
view|tenant_installed_apps

Fresh startup reaches health with the complete pinned ledger

$ TURSO_URL=file:/tmp/arn242-pr389-d5069b65-fresh-live-20260719T022645Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39423
Storage: turso (file:/tmp/arn242-pr389-d5069b65-fresh-live-20260719T022645Z.db)
Listening on http://0.0.0.0:39423
$ curl -sS -i http://127.0.0.1:39423/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sun, 19 Jul 2026 02:27:31 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-d5069b65-fresh-live-20260719T022645Z.db "SELECT version,name,checksum FROM temper_schema_migrations ORDER BY version; SELECT 'ledger_count', COUNT(*) FROM temper_schema_migrations;"
1|event-journal-and-snapshots|6f233883540c3432ebbc8e8aa4a5e0c161f6eed155789a00291cf3e3dfaad8eb
2|specs-constraints-and-integrations|ff0b3791127a4d2299a7d5e33dd5f848c153567c138679c69e47878bb1ee93e5
3|authorization-and-artifacts|ea8f1744ccd10245d130b61a00e2c35b98951ea355525fed6af1a1705e05fb2b
4|apps-platform-and-secrets|05e2e31d6348ac6a0ebe532770e4cb644ded8fa5568eb160dd72c60d1e031b2e
5|trajectories-and-evolution|2b30a33a42a150dda03f4a7ccdde4e200729a3a0a235b0337ce4869cd187b972
6|blob-and-query-plane|954f8fd1e64867c8188df9c6afd1e2c400c74db0885b25480be67aca7098711d
7|declared-key-and-vector-indexes|bf26037a1ff101c64df14e52f11bd231630b53def29a888c881cb71f23f73837
ledger_count|7

Same-database replay is healthy and does not append ledger rows

$ TURSO_URL=file:/tmp/arn242-pr389-d5069b65-fresh-live-20260719T022645Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39425
Storage: turso (file:/tmp/arn242-pr389-d5069b65-fresh-live-20260719T022645Z.db)
Restored 21 specs from Turso.
Listening on http://0.0.0.0:39425
$ curl -sS -i http://127.0.0.1:39425/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sun, 19 Jul 2026 02:27:53 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-d5069b65-fresh-live-20260719T022645Z.db "SELECT 'ledger_count_after_replay', COUNT(*) FROM temper_schema_migrations;"
ledger_count_after_replay|7

Differently-cased events owner is inventoried, rejected, and preserved

SQLite preserves trigger owner spelling in sqlite_master.tbl_name. This is the exact behavior missed by the pre-remediation diff.

$ sqlite3 -separator '|' /tmp/arn242-pr389-d5069b65-fresh-live-20260719T022645Z.db "CREATE TRIGGER arn242CaseRejectEvents BEFORE INSERT ON EVENTS BEGIN SELECT RAISE(FAIL, 'blocked'); END; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242CaseRejectEvents';"
trigger|arn242CaseRejectEvents|EVENTS
$ TURSO_URL=file:/tmp/arn242-pr389-d5069b65-fresh-live-20260719T022645Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39427
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 7 (declared-key-and-vector-indexes) failed while attempting to verify catalog head schema: storage error: Turso schema compatibility check failed: table 'events' has unexpected executable trigger 'arn242CaseRejectEvents'
$ sqlite3 -separator '|' /tmp/arn242-pr389-d5069b65-fresh-live-20260719T022645Z.db "SELECT 'ledger_count_after_case_trigger_rejection', COUNT(*) FROM temper_schema_migrations; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242CaseRejectEvents';"
ledger_count_after_case_trigger_rejection|7
trigger|arn242CaseRejectEvents|EVENTS

Differently-cased ledger owner is rejected before replay and preserved

$ TURSO_URL=file:/tmp/arn242-pr389-d5069b65-ledger-case-20260719T022645Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39429
Storage: turso (file:/tmp/arn242-pr389-d5069b65-ledger-case-20260719T022645Z.db)
Listening on http://0.0.0.0:39429
$ curl -sS -i http://127.0.0.1:39429/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sun, 19 Jul 2026 02:28:44 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-d5069b65-ledger-case-20260719T022645Z.db "SELECT 'ledger_count_before_owner_trigger', COUNT(*) FROM temper_schema_migrations;"
ledger_count_before_owner_trigger|7
$ sqlite3 -separator '|' /tmp/arn242-pr389-d5069b65-ledger-case-20260719T022645Z.db "CREATE TRIGGER arn242CaseRejectLedger BEFORE INSERT ON TEMPER_SCHEMA_MIGRATIONS BEGIN SELECT RAISE(FAIL, 'blocked'); END; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242CaseRejectLedger';"
trigger|arn242CaseRejectLedger|TEMPER_SCHEMA_MIGRATIONS
$ TURSO_URL=file:/tmp/arn242-pr389-d5069b65-ledger-case-20260719T022645Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39431
Error: Failed to connect to Turso/libSQL: storage error: Turso migration ledger has unsupported trigger 'arn242CaseRejectLedger'
$ sqlite3 -separator '|' /tmp/arn242-pr389-d5069b65-ledger-case-20260719T022645Z.db "SELECT 'ledger_count_after_owner_trigger_rejection', COUNT(*) FROM temper_schema_migrations; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242CaseRejectLedger';"
ledger_count_after_owner_trigger_rejection|7
trigger|arn242CaseRejectLedger|TEMPER_SCHEMA_MIGRATIONS

The legacy ON OTS_TRAJECTORIES preservation/rollback path is covered by the dedicated regression differently_cased_ots_trigger_owner_is_preserved_when_probe_rolls_back, which passed both targeted and full-workspace gates. No ARN-242 server process remains. crates/temper-actor-runtime is not touched.

Copy link
Copy Markdown
Collaborator

Fresh independent review of the complete 23-file open GitHub diff from base a28fdb2ed8b0e1e0ea15a99c3b4a1dfba20e160e through exact reviewed head d5069b654dcc287bb360f8b55639ff471114603a.

  1. [P1] Probe the replacement branch of the production OTS persist statement before admitting legacy triggers. Every unexpected trigger on ots_trajectories is admitted when the runtime probe succeeds (schema_verify.rs:66-68), but the persist portion creates a fresh random persisted_id and executes the statement only once (schema_ots_probe.rs:68-100). The actual runtime SQL is INSERT OR REPLACE (store/ots.rs:10-13), so persisting an already queued/retried trajectory takes a distinct trigger-visible path that readiness never exercises. On this exact head I seeded a queued row and installed BEFORE INSERT ... WHEN NEW.persistence_status = 'persisted' AND EXISTS (SELECT 1 FROM ots_trajectories WHERE trajectory_id = NEW.trajectory_id) BEGIN SELECT RAISE(FAIL, 'persisted replacement blocked'); END. temper serve passed migration verification and reached Listening, while executing the exact production INSERT OR REPLACE for that existing ID failed with persisted replacement blocked. This is the same readiness-before-first-runtime-failure class ARN-242 is intended to eliminate. Extend the rollback-only probe to persist an already-existing ID (and verify the replaced state), with a regression proving this trigger prevents readiness and that any active migration/ledger mutation rolls back.

Verdict: FAIL

@rita-aga

Copy link
Copy Markdown
Collaborator

ARN-242 persisted-replacement remediation evidence · 2026-07-19T03:42:39Z

Reviewer finding: #389 (comment)

Current open-PR head: d2031a6a27ff1a5e5bdcb4785016d04220b10743

RED — test-only commit 0f6f474e932d93a85793061a490d83d65a4492b6

  • Added persisted_replacement_trigger_prevents_readiness_and_rolls_back_migration.
  • The regression seeds an existing persisted OTS row using the production PERSIST_OTS_TRAJECTORY_SQL, installs a trigger that rejects only the existing-ID INSERT OR REPLACE branch, and uses a synthetic active migration sentinel to prove schema + ledger rollback.
  • Exact command: cargo test -p temper-store-turso persisted_replacement_trigger_prevents_readiness_and_rolls_back_migration -- --nocapture
  • RED result: FAILED, 0 passed / 1 failed at expect_err because migrate_catalog returned Ok(()); readiness had not exercised the replacement branch. The trigger and pre-existing business row were preserved.

GREEN — root-cause commit d2031a6a27ff1a5e5bdcb4785016d04220b10743

  • The rollback-only readiness probe now executes the exact production PERSIST statement twice with the same probe ID: first as a fresh insert, then as an existing-row replacement with distinct values.
  • It verifies both persisted states before the surrounding migration transaction rolls back.
  • Trigger-policy contract advanced from v4 to v5; ADR-0180 documents both branches and rollback semantics.
  • All seven cumulative migration checksums were intentionally repinned:
    1. b426efff6f1a657ed8af8d2de772f94a399dc107a00316540757f495ea5a8e27
    2. 5407efd8198b9e2c24ba0b960b207b9d49a0e8d8ea2cc443a43d909ca1cc70f4
    3. a583a312987ed8c2befa47c1d8cb131bc91ff86e1b30758721f80fadfd6b3674
    4. cd693a25ba28f7667654429853f94cf24533d6238e40f4f1e510cccaa8870598
    5. 77943bea5ef9a96d996fcf35105668142b14baea49fcd49e321fdf13f217bb6b
    6. 9b74890b627d4e20bc8fc3da97616158f3cbefdc59cbda19c24167a18086698e
    7. 6c15b1020dacbc2ff249309889b9f1bd084d2b0117fe1dca1d257b8802ee4d41

Verification on the exact pushed head:

  • Focused regression: PASS
  • cargo test -p temper-store-turso: PASS (110 passed, 1 helper ignored; integrations and doc tests green)
  • cargo clippy -p temper-store-turso --all-targets --all-features -- -D warnings: PASS
  • cargo fmt --all -- --check: PASS
  • git diff --check: PASS
  • Unbypassed git push: Pre-push: ALL GATES PASSED, including strict workspace clippy, readability, full cargo test --workspace, DST/fault/replay suites, integrations, and doc tests.
  • Local HEAD, remote-tracking head, and open PR fix(store-turso): add versioned migration ledger (ARN-242) #389 head all equal d2031a6a27ff1a5e5bdcb4785016d04220b10743; worktree is clean.

@rita-aga

Copy link
Copy Markdown
Collaborator

LIVE LOCAL E2E · exact current head d2031a6a27ff1a5e5bdcb4785016d04220b10743

Completed at 2026-07-19T03:48:54Z in the assigned worktree on codex/arn-242-turso-migration-ledger. Every database path below was newly created and is intentionally retained. No Docker operation, cleanup, or deletion was used. Successful servers were stopped with Ctrl-C immediately after verification; failing starts exited with status 1. pgrep -fl /codex-arn-242-turso-migration-ledger/target/debug/temper returned no process afterward.

This is the exact-head after-fix evidence for the persisted-replacement reviewer finding: #389 (comment)

Exact pushed identity and build

$ git status --short --branch
## codex/arn-242-turso-migration-ledger...origin/codex/arn-242-turso-migration-ledger
$ git rev-parse HEAD
d2031a6a27ff1a5e5bdcb4785016d04220b10743
$ git rev-parse origin/codex/arn-242-turso-migration-ledger
d2031a6a27ff1a5e5bdcb4785016d04220b10743
$ gh pr view 389 --repo nerdsane/temper --json state,headRefOid
{"headRefOid":"d2031a6a27ff1a5e5bdcb4785016d04220b10743","state":"OPEN"}
$ cargo build -p temper-cli
Finished `dev` profile [unoptimized + debuginfo] target(s) in 43.39s

Incompatible required object fails before its owning version is ledgered

$ sqlite3 /tmp/arn242-pr389-d2031a6a-incompatible-20260719T034439Z.db "CREATE VIEW tenant_installed_apps AS SELECT 'tenant' AS tenant_id, 'app' AS app_name;"
$ TURSO_URL=file:/tmp/arn242-pr389-d2031a6a-incompatible-20260719T034439Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39521
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 4 (apps-platform-and-secrets) step 1 schema compatibility check failed: capability 'tenant_installed_apps' must be a table, found view
$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-incompatible-20260719T034439Z.db "SELECT version,name FROM temper_schema_migrations ORDER BY version; SELECT type,name FROM sqlite_master WHERE name='tenant_installed_apps';"
1|event-journal-and-snapshots
2|specs-constraints-and-integrations
3|authorization-and-artifacts
view|tenant_installed_apps

Fresh startup reaches health with the complete current ledger

$ TURSO_URL=file:/tmp/arn242-pr389-d2031a6a-fresh-live-escalated-20260719T034439Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39523
Storage: turso (file:/tmp/arn242-pr389-d2031a6a-fresh-live-escalated-20260719T034439Z.db)
Listening on http://0.0.0.0:39523
$ curl -sS -i http://127.0.0.1:39523/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sun, 19 Jul 2026 03:45:37 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-fresh-live-escalated-20260719T034439Z.db "SELECT version,name,checksum FROM temper_schema_migrations ORDER BY version; SELECT 'ledger_count', COUNT(*) FROM temper_schema_migrations;"
1|event-journal-and-snapshots|b426efff6f1a657ed8af8d2de772f94a399dc107a00316540757f495ea5a8e27
2|specs-constraints-and-integrations|5407efd8198b9e2c24ba0b960b207b9d49a0e8d8ea2cc443a43d909ca1cc70f4
3|authorization-and-artifacts|a583a312987ed8c2befa47c1d8cb131bc91ff86e1b30758721f80fadfd6b3674
4|apps-platform-and-secrets|cd693a25ba28f7667654429853f94cf24533d6238e40f4f1e510cccaa8870598
5|trajectories-and-evolution|77943bea5ef9a96d996fcf35105668142b14baea49fcd49e321fdf13f217bb6b
6|blob-and-query-plane|9b74890b627d4e20bc8fc3da97616158f3cbefdc59cbda19c24167a18086698e
7|declared-key-and-vector-indexes|6c15b1020dacbc2ff249309889b9f1bd084d2b0117fe1dca1d257b8802ee4d41
ledger_count|7

Same-database replay is healthy and does not append ledger rows

$ TURSO_URL=file:/tmp/arn242-pr389-d2031a6a-fresh-live-escalated-20260719T034439Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39525
Storage: turso (file:/tmp/arn242-pr389-d2031a6a-fresh-live-escalated-20260719T034439Z.db)
Restored 21 specs from Turso.
Listening on http://0.0.0.0:39525
$ curl -sS -i http://127.0.0.1:39525/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sun, 19 Jul 2026 03:46:05 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-fresh-live-escalated-20260719T034439Z.db "SELECT 'ledger_count_after_replay', COUNT(*) FROM temper_schema_migrations;"
ledger_count_after_replay|7

Differently cased events owner is inventoried, rejected, and preserved

$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-fresh-live-escalated-20260719T034439Z.db "CREATE TRIGGER arn242CaseRejectEventsD203 BEFORE INSERT ON EVENTS BEGIN SELECT RAISE(FAIL, 'blocked'); END; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242CaseRejectEventsD203';"
trigger|arn242CaseRejectEventsD203|EVENTS
$ TURSO_URL=file:/tmp/arn242-pr389-d2031a6a-fresh-live-escalated-20260719T034439Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39527
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 7 (declared-key-and-vector-indexes) failed while attempting to verify catalog head schema: storage error: Turso schema compatibility check failed: table 'events' has unexpected executable trigger 'arn242CaseRejectEventsD203'
$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-fresh-live-escalated-20260719T034439Z.db "SELECT 'ledger_count_after_case_trigger_rejection', COUNT(*) FROM temper_schema_migrations; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242CaseRejectEventsD203';"
ledger_count_after_case_trigger_rejection|7
trigger|arn242CaseRejectEventsD203|EVENTS

Differently cased ledger owner is rejected before replay and preserved

$ TURSO_URL=file:/tmp/arn242-pr389-d2031a6a-ledger-case-20260719T034439Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39529
Storage: turso (file:/tmp/arn242-pr389-d2031a6a-ledger-case-20260719T034439Z.db)
Listening on http://0.0.0.0:39529
$ curl -sS -i http://127.0.0.1:39529/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sun, 19 Jul 2026 03:46:39 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-ledger-case-20260719T034439Z.db "SELECT 'ledger_count_before_owner_trigger', COUNT(*) FROM temper_schema_migrations;"
ledger_count_before_owner_trigger|7
$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-ledger-case-20260719T034439Z.db "CREATE TRIGGER arn242CaseRejectLedgerD203 BEFORE INSERT ON TEMPER_SCHEMA_MIGRATIONS BEGIN SELECT RAISE(FAIL, 'blocked'); END; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242CaseRejectLedgerD203';"
trigger|arn242CaseRejectLedgerD203|TEMPER_SCHEMA_MIGRATIONS
$ TURSO_URL=file:/tmp/arn242-pr389-d2031a6a-ledger-case-20260719T034439Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39531
Error: Failed to connect to Turso/libSQL: storage error: Turso migration ledger has unsupported trigger 'arn242CaseRejectLedgerD203'
$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-ledger-case-20260719T034439Z.db "SELECT 'ledger_count_after_owner_trigger_rejection', COUNT(*) FROM temper_schema_migrations; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242CaseRejectLedgerD203';"
ledger_count_after_owner_trigger_rejection|7
trigger|arn242CaseRejectLedgerD203|TEMPER_SCHEMA_MIGRATIONS

Replacement-only OTS trigger now prevents readiness and the probe rolls back

The trigger below deliberately allows a fresh persisted insert but rejects the distinct existing-ID branch of production INSERT OR REPLACE. That is the precise branch missed by the previous reviewed head.

$ TURSO_URL=file:/tmp/arn242-pr389-d2031a6a-ots-replacement-20260719T034439Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39533
Storage: turso (file:/tmp/arn242-pr389-d2031a6a-ots-replacement-20260719T034439Z.db)
Listening on http://0.0.0.0:39533
$ curl -sS -i http://127.0.0.1:39533/healthz
HTTP/1.1 200 OK
content-length: 0
date: Sun, 19 Jul 2026 03:47:48 GMT
$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-ots-replacement-20260719T034439Z.db "SELECT 'ledger_count_before_replacement_trigger', COUNT(*) FROM temper_schema_migrations;"
ledger_count_before_replacement_trigger|7
$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-ots-replacement-20260719T034439Z.db "INSERT INTO ots_trajectories (trajectory_id, tenant, agent_id, session_id, outcome, turn_count, data, persistence_status, persist_attempts, last_error, created_at, updated_at) VALUES ('arn242-existing-trajectory', 'arena', 'reviewer', 'session-before', 'queued-before', 3, 'stage-before', 'queued', 0, NULL, datetime('now'), datetime('now')) ON CONFLICT(trajectory_id) DO UPDATE SET tenant = excluded.tenant, agent_id = excluded.agent_id, session_id = excluded.session_id, outcome = excluded.outcome, turn_count = excluded.turn_count, data = excluded.data, persistence_status = 'queued', last_error = NULL, updated_at = datetime('now'); CREATE TRIGGER arn242RejectPersistedReplacementD203 BEFORE INSERT ON ots_trajectories WHEN NEW.persistence_status = 'persisted' AND EXISTS (SELECT 1 FROM ots_trajectories WHERE trajectory_id = NEW.trajectory_id) BEGIN SELECT RAISE(FAIL, 'persisted replacement blocked'); END; SELECT trajectory_id,session_id,outcome,turn_count,data,persistence_status FROM ots_trajectories WHERE trajectory_id='arn242-existing-trajectory'; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242RejectPersistedReplacementD203';"
arn242-existing-trajectory|session-before|queued-before|3|stage-before|queued
trigger|arn242RejectPersistedReplacementD203|ots_trajectories
$ TURSO_URL=file:/tmp/arn242-pr389-d2031a6a-ots-replacement-20260719T034439Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39535
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 7 (declared-key-and-vector-indexes) failed while attempting to verify catalog head schema: storage error: Turso schema compatibility check failed: table 'ots_trajectories' has executable trigger extension(s) ["arn242RejectPersistedReplacementD203"] that reject a rollback-only production persist/enqueue/status-transition probe: storage error: Turso schema introspection failed while attempting to probe OTS persisted replacement: SQLite failure: `persisted replacement blocked` (SqliteFailure(1811, "persisted replacement blocked"))

The server exited with status 1 before emitting Listening. The exact production replacement SQL for the seeded ID independently reaches the same rejected branch:

$ sqlite3 /tmp/arn242-pr389-d2031a6a-ots-replacement-20260719T034439Z.db "INSERT OR REPLACE INTO ots_trajectories (trajectory_id, tenant, agent_id, session_id, outcome, turn_count, data, persistence_status, persist_attempts, last_error, created_at, updated_at) VALUES ('arn242-existing-trajectory', 'arena', 'reviewer', 'session-after', 'persisted-after', 4, 'stage-after', 'persisted', 0, NULL, datetime('now'), datetime('now'));"
Error: stepping, persisted replacement blocked (19)
$ sqlite3 -separator '|' /tmp/arn242-pr389-d2031a6a-ots-replacement-20260719T034439Z.db "SELECT 'ledger_count_after_replacement_rejection', COUNT(*) FROM temper_schema_migrations; SELECT trajectory_id,session_id,outcome,turn_count,data,persistence_status FROM ots_trajectories WHERE trajectory_id='arn242-existing-trajectory'; SELECT 'probe_rows_after_rollback', COUNT(*) FROM ots_trajectories WHERE trajectory_id LIKE '__temper_trigger_probe__-%'; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242RejectPersistedReplacementD203'; SELECT 'temporary_migration_objects', COUNT(*) FROM sqlite_master WHERE name LIKE '__temper_migration_%';"
ledger_count_after_replacement_rejection|7
arn242-existing-trajectory|session-before|queued-before|3|stage-before|queued
probe_rows_after_rollback|0
trigger|arn242RejectPersistedReplacementD203|ots_trajectories
temporary_migration_objects|0

This proves the current head blocks readiness on the exact replacement-only regression, rolls all probe effects back, preserves the user trigger and business row, and leaves the complete ledger unchanged. The test-only synthetic migration in 0f6f474e932d93a85793061a490d83d65a4492b6 separately proves active DDL and its ledger row roll back under the same failure. crates/temper-actor-runtime is not touched.

Copy link
Copy Markdown
Collaborator

Independent exact-head review of the complete GitHub base a28fdb2ed8b0e1e0ea15a99c3b4a1dfba20e160e...d2031a6a27ff1a5e5bdcb4785016d04220b10743 diff (all 24 changed files), plus the relevant head source and the locked libsql 0.9.29 transaction implementation.

  1. [P1] The final “rollback-only” OTS probe is not rollback-only on remote libSQLcrates/temper-store-turso/src/migrations/runner.rs:81, crates/temper-store-turso/src/migrations/schema_ots_probe.rs:32-43

    Per-migration verification runs on a real libsql::Transaction, but the catalog-head verification at runner line 81 runs on the bare connection after all version transactions have committed. When a supported legacy OTS trigger is present, that path sends a standalone raw SAVEPOINT. In the locked libsql 0.9.29 Hrana implementation, only TxnBegin/read-only begin marks a statement as starting transaction scope; StmtKind::Savepoint does not. On an autocommit remote connection the client therefore executes the SAVEPOINT with close_stream = true and resets/closes that server stream. The subsequent production probe DML runs on another stream/autocommit transaction, so it can commit the __temper_trigger_probe__... rows and trigger side effects; ROLLBACK TO SAVEPOINT then has no savepoint to target and readiness fails.

    Thus a benign legacy OTS trigger can make first startup/replay fail on Turso Cloud while durably polluting business/audit tables—the opposite of the advertised rollback-only guarantee. Every added migration/probe test uses Builder::new_local, whose connection-scoped SQLite SAVEPOINT behavior masks this Hrana stream boundary. Run the final ledger/schema verification through an actual libsql transaction (so the probe stays on the pinned transaction stream), and add a remote/Hrana regression that asserts both probe rows and trigger-written audit rows remain absent.

  2. [P1] Current-shape inbound FKs are admitted even when production REPLACE deletes dependent datacrates/temper-store-turso/src/migrations/ots_rebuild.rs:143,379, exercised incompletely by ots_current_tests.rs:7

    Passing !already_updated and returning early at line 379 skips every inbound-FK check once updated_at exists. But the exact production persisted path is INSERT OR REPLACE. With FK enforcement enabled, an admitted child FK using ON DELETE CASCADE observes REPLACE’s delete and permanently removes the child row; ON DELETE RESTRICT can reject the existing-ID persist. The new test covers only default NO ACTION and never executes the existing-ID production SQL, so it does not prove the schema it blesses can serve that branch without loss/failure. Either reject/characterize unsafe inbound delete actions for current tables or change the persist path to a non-deleting UPSERT, and add CASCADE/RESTRICT REDs that run the shared production SQL against an existing ID.

Verdict: FAIL

@nerdsane

Copy link
Copy Markdown
Owner Author

Reviewed head SHA: 0535156

Verdict: FAIL

  1. [P1] crates/temper-store-turso/src/migrations/schema_verify.rs:66-69 and schema_ots_probe.rs:10-12,55-81 — every undeclared ots_trajectories trigger is admitted solely by executing a small probe with publicly recognizable tenant, agent, ID-prefix, and payload values. A trigger conditioned to reject rows whose tenant is not temper_trigger_probe skips every probe, so migration and final readiness succeed while every real persist or enqueue fails. A trigger can also mutate other tables or unasserted OTS columns while the checked row still matches. This defeats the executable-schema readiness guarantee. Reject undeclared OTS triggers, or restrict them to an explicitly parsed/allowlisted contract; fixed example writes cannot prove arbitrary trigger compatibility.

Validation: cargo test -p temper-store-turso passed all 117 executed tests; the live Hrana test and helper-process test remain ignored by default.

@nerdsane

Copy link
Copy Markdown
Owner Author

ARN-242 remediation evidence · exact head a44ded0283e2f02250aa2b5ecf4a70719cf6795f

All commands ran in the assigned worktree on codex/arn-242-turso-migration-ledger, using only its normal target/. No Docker operation, alternate worktree, cleanup, or merge was used. All database files below are retained.

Exact pushed identity and full local gate

$ git status --short --branch
## codex/arn-242-turso-migration-ledger...origin/codex/arn-242-turso-migration-ledger
$ git rev-parse HEAD
a44ded0283e2f02250aa2b5ecf4a70719cf6795f
$ git rev-parse origin/codex/arn-242-turso-migration-ledger
a44ded0283e2f02250aa2b5ecf4a70719cf6795f
$ gh pr view 389 --repo nerdsane/temper --json state,headRefOid
{"headRefOid":"a44ded0283e2f02250aa2b5ecf4a70719cf6795f","state":"OPEN"}
$ git push
Pre-push: ALL GATES PASSED
To https://github.com/nerdsane/temper.git
   05351566..a44ded02  codex/arn-242-turso-migration-ledger -> codex/arn-242-turso-migration-ledger

The unbypassed push passed rustfmt, strict workspace clippy, the blocking readability ratchet, the complete workspace suite, every DST integration suite, and all doc-tests. The longest randomized platform suite passed 7/7 in 2224.47s.

Additional exact remediation checks:

$ env CARGO_INCREMENTAL=0 cargo test -p temper-store-turso migrations::schema_verify_tests::runtime_extensions -- --nocapture
test result: ok. 15 passed; 0 failed; 0 ignored
$ env CARGO_INCREMENTAL=0 cargo test -p temper-store-turso
test result: ok. 116 passed; 0 failed; 2 ignored
$ env CARGO_INCREMENTAL=0 cargo clippy -p temper-store-turso --all-targets --all-features -- -D warnings
Finished `dev` profile [unoptimized + debuginfo]
$ env CARGO_INCREMENTAL=0 TEMPER_HRANA_MIGRATION_TEST_URL=http://127.0.0.1:39641 TEMPER_HRANA_MIGRATION_TEST_TOKEN=local-test cargo test -p temper-store-turso remote_final_ots_probe_is_atomic_and_side_effect_free -- --ignored --nocapture
test remote_final_ots_probe_is_atomic_and_side_effect_free ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 117 filtered out; finished in 23.47s
$ env CARGO_INCREMENTAL=0 cargo build -p temper-cli
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 39s

Fresh local code-quality review: PASS/CLEAN. Fresh local DST diff review: PASS/CLEAN; no simulation-visible crate changed.

Incompatible required object fails before its owning version is ledgered

$ sqlite3 /tmp/arn242-pr389-a44ded02-incompatible-20260720T022822Z.db "CREATE VIEW tenant_installed_apps AS SELECT 'tenant' AS tenant_id, 'app' AS app_name;"
$ TURSO_URL=file:/tmp/arn242-pr389-a44ded02-incompatible-20260720T022822Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39747
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 4 (apps-platform-and-secrets) step 1 schema compatibility check failed: capability 'tenant_installed_apps' must be a table, found view
$ sqlite3 -separator '|' /tmp/arn242-pr389-a44ded02-incompatible-20260720T022822Z.db "SELECT version,name FROM temper_schema_migrations ORDER BY version; SELECT type,name FROM sqlite_master WHERE name='tenant_installed_apps';"
1|event-journal-and-snapshots
2|specs-constraints-and-integrations
3|authorization-and-artifacts
view|tenant_installed_apps

Fresh startup reaches readiness with the complete policy-v7 ledger

$ TURSO_URL=file:/tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39741
Storage: turso (file:/tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db)
Listening on http://0.0.0.0:39741
$ curl -sS -i http://127.0.0.1:39741/healthz
HTTP/1.1 200 OK
content-length: 0
$ sqlite3 -separator '|' /tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db "SELECT version,name,checksum FROM temper_schema_migrations ORDER BY version; SELECT 'ledger_count', COUNT(*) FROM temper_schema_migrations; SELECT 'ots_rows', COUNT(*) FROM ots_trajectories;"
1|event-journal-and-snapshots|78bafc020d87a65741a6f7c117604f693d5eb265d75b178db1737f8934da8069
2|specs-constraints-and-integrations|83bc0de0ecf597a24ebe14fc6636b9b70b3cc76b6342b326afb583715e5d18b9
3|authorization-and-artifacts|54a077e4353c6df79dce2029cded8ce148c50be90400c4893dea21752adde4ea
4|apps-platform-and-secrets|6dfcf2905113a7943f80c44da094cb5b53b35633298b1a8fdf933df127b1ee8d
5|trajectories-and-evolution|f63408461791d04d70082f996c5f7bd620d3f6af505b9c98ffa7d3a63df38d75
6|blob-and-query-plane|5347da7626a3ca311ba8295e46fc7a0a22f0f6eb1944f09aee85dabca3a7fc4d
7|declared-key-and-vector-indexes|a8b51d91118d03697d98db8a3ff55fbed5967a71e7305dcd13876a56ad206a7c
ledger_count|7
ots_rows|0

Supported audit trigger replays healthy and leaves zero durable side effects

$ sqlite3 -separator '|' /tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db "CREATE TABLE arn242_audit (trajectory_id TEXT); CREATE TRIGGER arn242_audit_trigger AFTER INSERT ON ots_trajectories BEGIN INSERT INTO arn242_audit (trajectory_id) VALUES (NEW.trajectory_id); END; SELECT type,name,tbl_name FROM sqlite_master WHERE name IN ('arn242_audit','arn242_audit_trigger') ORDER BY type;"
table|arn242_audit|arn242_audit
trigger|arn242_audit_trigger|ots_trajectories
$ TURSO_URL=file:/tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39743
Storage: turso (file:/tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db)
Restored 21 specs from Turso.
Listening on http://0.0.0.0:39743
$ curl -sS -i http://127.0.0.1:39743/healthz
HTTP/1.1 200 OK
content-length: 0
$ sqlite3 -separator '|' /tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db "SELECT 'ledger_count_after_audit_replay', COUNT(*) FROM temper_schema_migrations; SELECT 'ots_rows_after_audit_replay', COUNT(*) FROM ots_trajectories; SELECT 'audit_rows_after_audit_replay', COUNT(*) FROM arn242_audit;"
ledger_count_after_audit_replay|7
ots_rows_after_audit_replay|0
audit_rows_after_audit_replay|0

Recognizable-input bypass fails readiness before any mutation

This trigger deliberately exempts the old public probe tenant but rejects real tenants—the exact prior reviewer regression.

$ sqlite3 -separator '|' /tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db "CREATE TRIGGER arn242_probe_bypass BEFORE INSERT ON ots_trajectories WHEN NEW.tenant <> '__temper_trigger_probe__' BEGIN SELECT RAISE(FAIL, 'real tenant blocked'); END; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242_probe_bypass';"
trigger|arn242_probe_bypass|ots_trajectories
$ TURSO_URL=file:/tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39745
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 7 (declared-key-and-vector-indexes) failed while attempting to verify catalog head schema: storage error: Turso schema compatibility check failed: table 'ots_trajectories' has unsupported executable trigger extension 'arn242_probe_bypass'; legacy extensions must be an unconditional AFTER INSERT audit trigger with exactly one INSERT INTO <audit_table>(trajectory_id) VALUES (NEW.trajectory_id) statement

The same production-shaped real insert reaches the hidden rejected branch:

$ sqlite3 /tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db "INSERT INTO ots_trajectories (...) VALUES ('arn242-real-trajectory', 'arena', ...) ON CONFLICT(trajectory_id) DO UPDATE SET ...;"
Error: stepping, real tenant blocked (19)
$ sqlite3 -separator '|' /tmp/arn242-pr389-a44ded02-fresh-live-20260720T022822Z.db "SELECT 'ledger_count_after_bypass_rejection', COUNT(*) FROM temper_schema_migrations; SELECT 'ots_rows_after_bypass_rejection', COUNT(*) FROM ots_trajectories; SELECT 'audit_rows_after_bypass_rejection', COUNT(*) FROM arn242_audit; SELECT type,name,tbl_name FROM sqlite_master WHERE name IN ('arn242_audit_trigger','arn242_probe_bypass') ORDER BY name;"
ledger_count_after_bypass_rejection|7
ots_rows_after_bypass_rejection|0
audit_rows_after_bypass_rejection|0
trigger|arn242_audit_trigger|ots_trajectories
trigger|arn242_probe_bypass|ots_trajectories

The executable-schema RED is isolated in b4d76b39; the root fix is isolated in a44ded02. crates/temper-actor-runtime is not touched.

@nerdsane

Copy link
Copy Markdown
Owner Author

Fresh independent review of the complete open GitHub PR diff from base a28fdb2 through reviewed head a44ded0. GitHub reports the PR open, and the remote per-file blob SHAs match the exact local head sources inspected.

No actionable findings.

I reviewed the ordered catalog, canonical checksums and released checksum pins, ledger schema and row validation, immediate per-version transactions, retained-row checks, rollback and commit paths, concurrent replay behavior, and final catalog-head verification. I also checked the exact legacy OTS rebuild gates and dependent-object preservation, inbound FK handling, current-shape CASCADE and RESTRICT behavior under the production UPSERT, SQLite identifier owner matching, parsed unconditional audit-trigger contract, one-column sink restrictions, opaque database-generated probe identities, and savepoint rollback inside the real transaction-pinned Hrana path.

Validation on this head: cargo test -p temper-store-turso passed 116 unit tests and all package integration tests, with only the isolated-Hrana test and helper-process entrypoint intentionally ignored; cargo fmt --all -- --check and git diff --check passed. GitHub Compile and Lint, Spec Verification, Integrity and DST Patterns, Instrumentation Hygiene, Verification Contract, and platform-random checks were green when reviewed; remaining matrix jobs were still running and produced no review finding.

Verdict: PASS

@nerdsane

Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread crates/temper-store-turso/src/migrations/schema_verify.rs Outdated
@nerdsane

Copy link
Copy Markdown
Owner Author

ARN-242 final-head remediation and live E2E evidence · 29ec91207d7ecb46f22f178d559598b46dcae553

All commands ran in the assigned worktree on codex/arn-242-turso-migration-ledger, using only its normal target/. No Docker restart, alternate target, cleanup, replacement worktree, or merge was used. The evidence databases are retained:

  • /tmp/arn242-pr389-29ec9120-incompatible-20260720T035209Z.db
  • /tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db

Exact pushed identity and full unbypassed local gate

$ git rev-parse HEAD
29ec91207d7ecb46f22f178d559598b46dcae553
$ git rev-parse origin/codex/arn-242-turso-migration-ledger
29ec91207d7ecb46f22f178d559598b46dcae553
$ gh pr view 389 --repo nerdsane/temper --json state,headRefOid
{"headRefOid":"29ec91207d7ecb46f22f178d559598b46dcae553","state":"OPEN"}
$ git push
Pre-push: ALL GATES PASSED
To https://github.com/nerdsane/temper.git
   a44ded02..29ec9120  codex/arn-242-turso-migration-ledger -> codex/arn-242-turso-migration-ledger

The normal push passed rustfmt, strict workspace clippy, every blocking readability ratchet, the complete workspace test suite, every DST integration suite, and all doc-tests. The randomized platform suite passed 7/7 in 1526.45s.

Greptile remediation RED/GREEN:

$ env CARGO_INCREMENTAL=0 cargo test -p temper-store-turso migrations::schema_verify::tests::case_folded_index_owner_is_inventoried -- --exact --nocapture
running 1 test
test migrations::schema_verify::tests::case_folded_index_owner_is_inventoried ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 118 filtered out

$ env CARGO_INCREMENTAL=0 cargo test -p temper-store-turso
test result: ok. 117 passed; 0 failed; 2 ignored
test result: ok. 5 passed; 0 failed
test result: ok. 1 passed; 0 failed

$ env CARGO_INCREMENTAL=0 cargo clippy -p temper-store-turso --all-targets --all-features -- -D warnings
Finished `dev` profile [unoptimized + debuginfo]

$ cargo fmt --all -- --check
$ git diff --check

The reconnect-adjusted live Hrana regression passed before this one-line catalog-owner remediation; exact command/output is retained in the pre-Greptile final evidence. The Greptile change only changes the local sqlite_schema.tbl_name inventory predicate.

Incompatible required object fails before its owning version is ledgered

$ sqlite3 /tmp/arn242-pr389-29ec9120-incompatible-20260720T035209Z.db "CREATE VIEW tenant_installed_apps AS SELECT 'tenant' AS tenant_id, 'app' AS app_name;"
$ env TURSO_URL=file:/tmp/arn242-pr389-29ec9120-incompatible-20260720T035209Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39807
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 4 (apps-platform-and-secrets) step 1 schema compatibility check failed: capability 'tenant_installed_apps' must be a table, found view
$ sqlite3 -separator '|' /tmp/arn242-pr389-29ec9120-incompatible-20260720T035209Z.db "SELECT version,name FROM temper_schema_migrations ORDER BY version; SELECT type,name FROM sqlite_master WHERE name='tenant_installed_apps';"
1|event-journal-and-snapshots
2|specs-constraints-and-integrations
3|authorization-and-artifacts
view|tenant_installed_apps

Fresh startup reaches readiness with the complete policy-v7 ledger

$ env TURSO_URL=file:/tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39801
Storage: turso (file:/tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db)
Listening on http://0.0.0.0:39801
$ curl -sS -i http://127.0.0.1:39801/healthz
HTTP/1.1 200 OK
content-length: 0
$ sqlite3 -separator '|' /tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db "SELECT version,name,checksum FROM temper_schema_migrations ORDER BY version; SELECT 'ledger_count', COUNT(*) FROM temper_schema_migrations; SELECT 'ots_rows', COUNT(*) FROM ots_trajectories;"
1|event-journal-and-snapshots|78bafc020d87a65741a6f7c117604f693d5eb265d75b178db1737f8934da8069
2|specs-constraints-and-integrations|83bc0de0ecf597a24ebe14fc6636b9b70b3cc76b6342b326afb583715e5d18b9
3|authorization-and-artifacts|54a077e4353c6df79dce2029cded8ce148c50be90400c4893dea21752adde4ea
4|apps-platform-and-secrets|6dfcf2905113a7943f80c44da094cb5b53b35633298b1a8fdf933df127b1ee8d
5|trajectories-and-evolution|f63408461791d04d70082f996c5f7bd620d3f6af505b9c98ffa7d3a63df38d75
6|blob-and-query-plane|5347da7626a3ca311ba8295e46fc7a0a22f0f6eb1944f09aee85dabca3a7fc4d
7|declared-key-and-vector-indexes|a8b51d91118d03697d98db8a3ff55fbed5967a71e7305dcd13876a56ad206a7c
ledger_count|7
ots_rows|0

Supported audit trigger replays healthy with zero durable probe side effects

$ sqlite3 -separator '|' /tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db "CREATE TABLE arn242_final_audit (trajectory_id TEXT); CREATE TRIGGER arn242_final_audit_trigger AFTER INSERT ON ots_trajectories BEGIN INSERT INTO arn242_final_audit (trajectory_id) VALUES (NEW.trajectory_id); END; SELECT type,name,tbl_name FROM sqlite_master WHERE name IN ('arn242_final_audit','arn242_final_audit_trigger') ORDER BY type;"
table|arn242_final_audit|arn242_final_audit
trigger|arn242_final_audit_trigger|ots_trajectories
$ env TURSO_URL=file:/tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39803
Storage: turso (file:/tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db)
Restored 21 specs from Turso.
Listening on http://0.0.0.0:39803
$ curl -sS -i http://127.0.0.1:39803/healthz
HTTP/1.1 200 OK
content-length: 0
$ sqlite3 -separator '|' /tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db "SELECT 'ledger_count_after_audit_replay', COUNT(*) FROM temper_schema_migrations; SELECT 'ots_rows_after_audit_replay', COUNT(*) FROM ots_trajectories; SELECT 'audit_rows_after_audit_replay', COUNT(*) FROM arn242_final_audit;"
ledger_count_after_audit_replay|7
ots_rows_after_audit_replay|0
audit_rows_after_audit_replay|0

Recognizable-input probe bypass fails readiness before any mutation

$ sqlite3 -separator '|' /tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db "CREATE TRIGGER arn242_final_probe_bypass BEFORE INSERT ON ots_trajectories WHEN NEW.tenant <> '__temper_trigger_probe__' BEGIN SELECT RAISE(FAIL, 'real tenant blocked'); END; SELECT type,name,tbl_name FROM sqlite_master WHERE name='arn242_final_probe_bypass';"
trigger|arn242_final_probe_bypass|ots_trajectories
$ env TURSO_URL=file:/tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db RUST_LOG=error target/debug/temper serve --no-observe --port 39805
Error: Failed to connect to Turso/libSQL: storage error: Turso migration 7 (declared-key-and-vector-indexes) failed while attempting to verify catalog head schema: storage error: Turso schema compatibility check failed: table 'ots_trajectories' has unsupported executable trigger extension 'arn242_final_probe_bypass'; legacy extensions must be an unconditional AFTER INSERT audit trigger with exactly one INSERT INTO <audit_table>(trajectory_id) VALUES (NEW.trajectory_id) statement
$ sqlite3 -separator '|' /tmp/arn242-pr389-29ec9120-fresh-live-escalated-20260720T035209Z.db "SELECT 'ledger_count_after_bypass_rejection', COUNT(*) FROM temper_schema_migrations; SELECT 'ots_rows_after_bypass_rejection', COUNT(*) FROM ots_trajectories; SELECT 'audit_rows_after_bypass_rejection', COUNT(*) FROM arn242_final_audit; SELECT type,name,tbl_name FROM sqlite_master WHERE name IN ('arn242_final_audit_trigger','arn242_final_probe_bypass') ORDER BY name;"
ledger_count_after_bypass_rejection|7
ots_rows_after_bypass_rejection|0
audit_rows_after_bypass_rejection|0
trigger|arn242_final_audit_trigger|ots_trajectories
trigger|arn242_final_probe_bypass|ots_trajectories

All final-head E2E server processes were stopped after their checks. crates/temper-actor-runtime is not touched.

@nerdsane

Copy link
Copy Markdown
Owner Author

Reviewed the complete open GitHub PR diff at exact head 29ec91207d7ecb46f22f178d559598b46dcae553.

Finding

  1. [P1] Version the case-folded index-owner validation in the durable checksumcrates/temper-store-turso/src/migrations/schema_verify.rs:13-14,93-96; crates/temper-store-turso/src/migrations/schema_manifest.rs:5-10; crates/temper-store-turso/src/migrations/catalog.rs:26-32,411-425; docs/adrs/0180-versioned-turso-migration-ledger.md:69-75.

    The Greptile remediation changes index-extension compatibility validation to inventory owners with COLLATE NOCASE, but no checksum input was versioned for that behavior: EXTRA_INDEX_POLICY remains ...-v1, VALIDATION_MANIFEST_VERSION remains ...-v10, and the pinned v1-v7 checksums are byte-for-byte the same as at pre-fix head a44ded0283e2f02250aa2b5ecf4a70719cf6795f. Migration::checksum hashes only those unchanged labels, the steps, and the generated manifest; it does not hash this inventory predicate. Consequently two validator definitions identify the historical migrations with the same durable digests, contradicting ADR-0180s explicit contract that any compatibility-validation change changes the checksum. This makes validation drift invisible to the ledger. Because these are the initial release pins, version the owner-aware index policy or validation manifest now, regenerate the pinned checksums, and retain a regression that prevents this behavior change from leaving the digest unchanged.

Review summary

The per-version immediate transactions, ledger insert/retained-row checks, final pinned transaction, OTS rebuild rollback, Hrana savepoint containment, and production UPSERT preservation of inbound CASCADE/RESTRICT references are otherwise coherent in the reviewed diff. The case-folded inventory predicate itself closes the reported helper-level omission, but its durable checksum metadata is incomplete.

Verdict: FAIL

@rita-aga

Copy link
Copy Markdown
Collaborator

Fresh independent GPT-5.6 review — exact GitHub head\n\nPR state verified: OPEN\nReviewed head: cc75b4a\nReview source: complete open PR diff fetched from GitHub, not merely local files.\n\nFindings: none.\n\nThe review specifically rechecked migration-ledger durability and replay semantics; transactional rollback; Hrana/Turso behavior; CASCADE and RESTRICT preservation; case-insensitive trigger/index owner inventory; and the final remediation that versions the durable extra-index policy as sqlite-identifier-owners-v2, rotates and pins all seven released prefix checksums, adds the RED regression, and documents the compatibility consequence in ADR-0180. No correctness, durability, regression, missing-test, or ADR-alignment defect was found on this exact head.\n\nVerdict: PASS

@rita-aga

Copy link
Copy Markdown
Collaborator

Exact-head local E2E evidence\n\nPR head:\n git rev-parse HEAD\n cc75b4a\n\nRelink the existing assigned-worktree CLI target:\n CARGO_INCREMENTAL=0 cargo build -p temper-cli --bin temper\n Finished dev profile [unoptimized + debuginfo] target(s) in 2m 21s\n\nSupported audit-trigger replay and live HTTP health:\n RUST_LOG=error TURSO_URL=file:/tmp/arn242-pr389-cc75b4a9-current-live-20260720T053414Z.db target/debug/temper serve --port 43148 --no-observe\n\n Storage: turso (file:/tmp/arn242-pr389-cc75b4a9-current-live-20260720T053414Z.db)\n Restored 21 specs from Turso.\n Temper Data API: http://localhost:43148/tdata\n Listening on http://0.0.0.0:43148\n\n curl -sS -i http://127.0.0.1:43148/observe/health\n\n HTTP/1.1 200 OK\n content-type: application/json\n {"status":"healthy","uptime_seconds":13,"specs_loaded":21,"active_actors":0,"indexed_entities":0,"transitions_total":0,"errors_total":0,"event_store":"turso","cross_invariant_enforce":true,"cross_invariant_eventual_enforce":true}\n\nExact durable ledger and rollback-only trigger-probe residue:\n sqlite3 -header -column /tmp/arn242-pr389-cc75b4a9-current-live-20260720T053414Z.db "SELECT version, checksum FROM temper_schema_migrations ORDER BY version; SELECT 'ledger_rows' AS metric, COUNT() AS value FROM temper_schema_migrations UNION ALL SELECT 'ots_rows', COUNT() FROM ots_trajectories UNION ALL SELECT 'audit_rows', COUNT(*) FROM ots_probe_audit;"\n\n 1 aa159f54e46819312662448552d6ebdd56e5fe0e31d4f7619b28c3c9272521d2\n 2 5ee4fcfe1f9ff6a7b0d1d4c4081a7ec375491abc0f5ba96ec42d1eb405f137cb\n 3 1634a9cdd48acff70f20d2ef78a6b91285a016bd53dbd459b7e9c663650f691c\n 4 e25ad7da3742dc84c0a2ef1b0713b1858b82af040fd503f5cfd3aed4e7151bef\n 5 5606020fefa283e048c7a72ae8bd7db3dc91336a946e8305ac6993e606f50aeb\n 6 78aa56edc91e690e39ffeacc46c85804b3034d2368a480eae1769dfaa516b111\n 7 1e4e7af4c85e857bf0a55890d4c4142f95cc5053a252c70309794599c481603b\n ledger_rows 7\n ots_rows 0\n audit_rows 0\n\nRecognizable historical-probe bypass fails closed without mutation:\n RUST_LOG=error TURSO_URL=file:/tmp/arn242-pr389-cc75b4a9-probe-bypass-20260720T053414Z.db target/debug/temper serve --port 43146 --no-observe\n\n Error: Failed to connect to Turso/libSQL: storage error: Turso migration 7 (declared-key-and-vector-indexes) failed while attempting to verify catalog head schema: storage error: Turso schema compatibility check failed: table 'ots_trajectories' has unsupported executable trigger extension 'reject_non_probe_tenant'; legacy extensions must be an unconditional AFTER INSERT audit trigger with exactly one INSERT INTO <audit_table>(trajectory_id) VALUES (NEW.trajectory_id) statement\n\n ledger_rows 7\n ots_rows 0\n trigger_rows 1\n\nIncompatible required object fails at migration 4 and preserves the view:\n RUST_LOG=error TURSO_URL=file:/tmp/arn242-pr389-cc75b4a9-incompatible-20260720T053414Z.db target/debug/temper serve --port 43147 --no-observe\n\n Error: Failed to connect to Turso/libSQL: storage error: Turso migration 4 (apps-platform-and-secrets) step 1 schema compatibility check failed: capability 'tenant_installed_apps' must be a table, found view\n\n 1 event-journal-and-snapshots aa159f54e46819312662448552d6ebdd56e5fe0e31d4f7619b28c3c9272521d2\n 2 specs-constraints-and-integrations 5ee4fcfe1f9ff6a7b0d1d4c4081a7ec375491abc0f5ba96ec42d1eb405f137cb\n 3 authorization-and-artifacts 1634a9cdd48acff70f20d2ef78a6b91285a016bd53dbd459b7e9c663650f691c\n view tenant_installed_apps\n\nThe live server was stopped with Ctrl-C after the health and durable-state checks. All database files are retained under /tmp; no cleanup or deletion was performed.

@rita-aga

Copy link
Copy Markdown
Collaborator

ARENA SHIPPABLE · GPT-5.6 · 2026-07-20T16:18:57Z

Exact open PR head: cc75b4a

Ordered review gate:

Exact-head verification:

  • Local HTTP bind/health, all seven v2 checksum pins, rollback-only audit replay, bypass rejection, and incompatible-view evidence: fix(store-turso): add versioned migration ledger (ARN-242) #389 (comment)
  • CI run: https://github.com/nerdsane/temper/actions/runs/29719355087
  • CI passed: Compile & Lint; Tests; DST core, platform-consistency, platform-boot, and platform-random; Verification Contract; Integrity & DST Patterns; Instrumentation Hygiene; Spec Verification L0-L3. Bench Build was intentionally skipped.
  • Normal pre-push gate on this exact head passed rustfmt, strict workspace clippy, blocking readability ratchet, complete cargo test --workspace, every DST suite, and all doctests.
  • Fresh local code-quality and DST reviews passed with zero findings.

Scope audit:

  • crates/temper-actor-runtime is not touched; outside-contributor sign-off is not required.
  • No Docker restart, cleanup, deletion, replacement worktree, or alternate target was used.
  • PR remains open and unmerged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants