Skip to content

E1-11 v2: vehicle outbox payload entityType fix with review findings - #48

Merged
davidru85 merged 5 commits into
mainfrom
story/E1-11-outbox-entitytype-fix-v2
Sep 2, 2026
Merged

E1-11 v2: vehicle outbox payload entityType fix with review findings#48
davidru85 merged 5 commits into
mainfrom
story/E1-11-outbox-entitytype-fix-v2

Conversation

@davidru85

@davidru85 davidru85 commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Re-applies the E1-11 entityType outbox fix after PR E1-11: vehicle outbox payload entityType fix #45 was merged in error and reverted by PR Revert "E1-11: vehicle outbox payload entityType fix" #47, with all six owner-review findings fixed.
  • VehicleOutboxMapper now emits "entityType":"VEHICLE" and "entityType":"FUEL_ENTRY" as the first key of the Vehicle payload and the cascade Fuel Entry tombstone payload, satisfying docs/CONTRACTS.md §8.
  • FirebaseRemoteSyncSource.toFirestoreWrite validates the payload entityType against EntitySnapshot.entityType and excludes it from FirestoreWrite.fields, keeping the closed remote schema of docs/CONTRACTS.md §16 intact. A missing, unknown or mismatched value returns RemoteError.InvalidArgument.
  • Records D-110 "Outbox entity-type token ownership" (ADR-0111) with all four mirrors.

The six review findings

  1. OutboxCoalescenceParityTest used a handcrafted payload builder — fixed. It now builds SqlDelightVehicleRepository and SqlDelightFuelEntryRepository over the same in-memory database and obtains the direct Fuel Entry outbox payload through the real create path.
  2. Only one coalescence order was proven — fixed, then corrected again in d1cf977 after the PR review. The first attempt re-applied the payload produced by the cascade itself, so it proved cascade-then-cascade while claiming cascade-then-direct. directFuelEntryWriteThenCascadeDeleteProducesTheSameCanonicalKeySet proves direct-then-cascade; cascadeDeleteThenDirectFuelEntryCoalescenceRetainsCanonicalKeySet now captures the direct payload from the real SqlDelightFuelEntryRepository create path before the cascade and re-applies that payload, so the direct writer is genuinely the last writer of the coalesced row.
  3. Backlog scope omitted the Firestore boundary — fixed. The E1-11 acceptance criteria in docs/BACKLOG.md now state that the required integration-boundary adaptation in :integration:firebase-firestore is included and that no schema or migration changed.
  4. Handoff "Files Changed" was incomplete — fixed. It now matches git diff --name-status main exactly.
  5. ADR-0111 carried a stale "no gate" claim — fixed. The options table and the Decision section declare the gated paths touched.
  6. Checkpoint was stale — fixed. docs/handoff-E1-11.md carries the current phase, verification evidence and next step.

Story

E1-11 - :feature:vehicle Outbox Payload entityType Fix

Ready Check

  • Backlog story is explicit
  • Acceptance criteria reviewed
  • Dependencies checked
  • Required decisions are not Proposed or Pending
  • Normative sections reviewed
  • Expected verification identified
  • Human review gates identified before work

In-Progress Checkpoint

  • Date: 2026-09-02
  • Branch and base: story/E1-11-outbox-entitytype-fix-v2 from main (76883c2)
  • Current phase and latest commit: complete, d1cf977
  • Push and pull-request status: pushed; this pull request is open and awaiting owner re-review after the parity-test correction; NOT merged by the agent
  • Completed since the previous checkpoint: fixed the blocking review finding on OutboxCoalescenceParityTest (it proved cascade-then-cascade, not cascade-then-direct), added the missing trailing newline to docs/handoff-E1-11.md, refreshed the handoff checkpoint / acceptance evidence / verification, and appended a correction entry to docs/PROJECT_LOG.md
  • Verification evidence and known failures: minimum verification with --rerun-tasks BUILD SUCCESSFUL (87 tasks executed); full non-instrumented command BUILD SUCCESSFUL (627 actionable tasks); contractCheck output inspected, every assertion [PASS]; git diff --check clean; no known failures
  • Open decisions or blockers: none
  • Exact next step: owner re-review and merge decision

Scope Completed

  • VehicleOutboxMapper.toVehicleOutboxPayloadOrNull emits "entityType":"VEHICLE" as the first key, covering create, update and vehicle tombstone call sites.
  • VehicleOutboxMapper.toFuelEntryTombstonePayload emits "entityType":"FUEL_ENTRY" as the first key of the cascade-delete tombstone payload.
  • FirebaseRemoteSyncSource.toFirestoreWrite requires the payload entityType to match EntitySnapshot.entityType, excludes it from FirestoreWrite.fields, and returns RemoteError.InvalidArgument for missing, unknown or mismatched values. The payload key is the file-local ENTITY_TYPE_FIELD constant, matching the existing *_FIELD convention of that file.
  • VehicleOutboxMapperTest added, including the entityTypeEnumNamesMatchTheOutboxWireValues anchor.
  • VehicleRepositoryCreateTest, VehicleRepositoryUpdateTest and VehicleRepositoryDeleteTest assert entityType on every write path with exact canonical key sets.
  • VehicleRepositoryDeleteTest.cascadeDeleteIsIdempotentWhenItIsTheLastWriterOfAFuelEntryOutboxRow proves the cascade is idempotent.
  • OutboxCoalescenceParityTest in :shared proves both coalescence orders produce the same canonical key set using the real repositories.
  • FirebaseRemoteSyncSourceEntityTypeBoundaryTest (5 tests) proves the Firestore boundary excludes entityType and rejects mismatches for both Vehicle and Fuel Entry.
  • VehicleFormStateHolderTest.vehicleOutboxPayloadWithEntityTypeReachesRemoteSyncSourceAsAValidSnapshot end-to-end regression test.
  • D-110 (ADR-0111) recorded with all four mirrors.

Acceptance Evidence

  • VehicleOutboxMapperTest.permanentOwnerVehiclePayloadIncludesEntityTypeVehicle and fuelEntryTombstonePayloadIncludesEntityTypeFuelEntry prove both mapper emissions.
  • entityType asserted for each write path: Vehicle create, Vehicle update, Vehicle tombstone, cascade Fuel Entry tombstone.
  • Coalesced (FUEL_ENTRY, entityId) outbox row keeps the same canonical key set regardless of write order, both orders driven by the real SqlDelightFuelEntryRepository:
    • OutboxCoalescenceParityTest.directFuelEntryWriteThenCascadeDeleteProducesTheSameCanonicalKeySet — direct then cascade.
    • OutboxCoalescenceParityTest.cascadeDeleteThenDirectFuelEntryCoalescenceRetainsCanonicalKeySet — cascade then direct. It captures the direct payload before the cascade, asserts both payloads carry exactly the canonical key set, asserts the two payloads differ so the re-application cannot degenerate into a repeat of the cascade write, then coalesces the direct payload onto the cascade-written row and asserts the final row equals the direct payload and keeps the canonical keys and "entityType":"FUEL_ENTRY".
    • Both run with 2 tests and 0 failures on the Android host and on iosSimulatorArm64.
  • Firestore boundary excludes entityType and validates it: FirebaseRemoteSyncSourceEntityTypeBoundaryTest (5 tests).
  • End-to-end: VehicleFormStateHolderTest.vehicleOutboxPayloadWithEntityTypeReachesRemoteSyncSourceAsAValidSnapshot.
  • LOCAL_OWNER + PENDING + no outbox invariant preserved: VehicleOutboxMapperTest.localOwnerNeverProducesAnOutboxPayload.
  • No schema or migration change.

Out of Scope / Not Done

  • DatabaseMutations (core/database/.../DatabaseMutations.kt), the :feature:fuel and :shared literals, and the .sq CHECK/SQL literals keep their string literals because :core:database cannot depend on :core:sync (docs/TECHNICAL_PLAN.md §4) and unification requires a gated relocation of EntityType. Any centralization is deferred to E3-13 (issue E3-13: Single source of truth for the outbox entityType wire value #46).

Files Changed

  • AGENTS.md
  • README.md
  • docs/BACKLOG.md
  • docs/DECISION_BOARD.md
  • docs/PROJECT_LOG.md
  • docs/SPECIFICATION.md
  • docs/TECHNICAL_PLAN.md
  • docs/adr/0111-outbox-entity-type-token-ownership.md (new)
  • docs/adr/README.md
  • docs/handoff-E1-11.md (new)
  • feature/vehicle/src/commonMain/kotlin/com/ruizurraca/carapp/feature/vehicle/data/VehicleOutboxMapper.kt
  • feature/vehicle/src/commonTest/kotlin/com/ruizurraca/carapp/feature/vehicle/data/VehicleOutboxMapperTest.kt (new)
  • feature/vehicle/src/commonTest/kotlin/com/ruizurraca/carapp/feature/vehicle/data/VehicleRepositoryCreateTest.kt
  • feature/vehicle/src/commonTest/kotlin/com/ruizurraca/carapp/feature/vehicle/data/VehicleRepositoryDeleteTest.kt
  • feature/vehicle/src/commonTest/kotlin/com/ruizurraca/carapp/feature/vehicle/data/VehicleRepositoryTestScope.kt
  • feature/vehicle/src/commonTest/kotlin/com/ruizurraca/carapp/feature/vehicle/data/VehicleRepositoryUpdateTest.kt
  • integration/firebase-firestore/src/commonMain/kotlin/com/ruizurraca/carapp/integration/firebase/firestore/FirebaseRemoteSyncSource.kt
  • integration/firebase-firestore/src/commonTest/kotlin/com/ruizurraca/carapp/integration/firebase/firestore/FirebaseRemoteSyncSourceEntityTypeBoundaryTest.kt (new)
  • integration/firebase-firestore/src/commonTest/kotlin/com/ruizurraca/carapp/integration/firebase/firestore/FirebaseRemoteSyncSourceTest.kt
  • shared/src/commonTest/kotlin/com/ruizurraca/carapp/OutboxCoalescenceParityTest.kt (new)
  • shared/src/commonTest/kotlin/com/ruizurraca/carapp/VehicleFormStateHolderTest.kt

Decisions Made

  • D-110 "Outbox entity-type token ownership" (ADR-0111): E1-11 keeps explicit contract tokens in production code. The :feature:vehicle commonTest files derive their outbox lookup keys from EntityType.*.name; payload value assertions stay as exact string literals. The entityTypeEnumNamesMatchTheOutboxWireValues anchor pins the enum to the contract. No refactorization is assigned to E3-03; E3-13 (issue E3-13: Single source of truth for the outbox entityType wire value #46) owns the future centralization.
  • The entityTypeEnumNamesMatchTheOutboxWireValues test is a characterization pin of an existing invariant and therefore has no RED phase. This is the only TDD deviation and it is declared here and in the ADR.

Verification Run

  • Relevant tests pass
  • Lint passes (ktlint, detekt)
  • Coverage thresholds hold
  • Architecture checks pass
  • Contract check passes
  • Relevant builds pass (Android, iOS simulator, Shared framework from :composition:ios)
  • Documentation updated if behaviour, decisions or models changed

Commands or checks run:

./gradlew ktlintCheck detekt architectureCheck contractCheck :build-logic:convention:test koverVerify \
  :androidApp:assembleDebug :androidApp:testDebugUnitTest testAndroidHostTest iosSimulatorArm64Test \
  -x :integration:firebase-auth:iosSimulatorArm64Test \
  -x :integration:firebase-firestore:iosSimulatorArm64Test \
  -x :wiring:firebase:iosSimulatorArm64Test \
  -x :composition:ios:iosSimulatorArm64Test
=> BUILD SUCCESSFUL, 627 actionable tasks

./gradlew :feature:vehicle:testAndroidHostTest :shared:testAndroidHostTest \
  :integration:firebase-firestore:testAndroidHostTest --rerun-tasks
=> BUILD SUCCESSFUL, 87 tasks executed

contractCheck => 111 decisions, 111 ADRs, no unresolved decisions, no PENDING assertions

git diff --check => clean

Contract Impact

  • No contract changes
  • Updated docs/CONTRACTS.md §:

docs/CONTRACTS.md §8 already mandates entityType and §16 already closes the remote schema. This story makes the code conform to both.

Decision Board Impact

  • No decision changes
  • Updated docs/DECISION_BOARD.md (D-110) and the related ADR: docs/adr/0111-outbox-entity-type-token-ownership.md

Shared-Write Modules Touched

  • None
  • core/database — confirm no other story is modifying it

Project Log Entry

  • Entry appended to docs/PROJECT_LOG.md

Human Review Gate

Gates are defined canonically in AGENTS.md.

  • Not applicable
  • Gated path (docs/SPECIFICATION.md, docs/CONTRACTS.md, docs/DECISION_BOARD.md, AGENTS.md, docs/adr/**, docs/identifiers.md, docs/versions-matrix.md, firestore/**, core/sync/**, core/auth/**, core/database/**, money types)

Applies: gated paths AGENTS.md, docs/SPECIFICATION.md, docs/DECISION_BOARD.md and docs/adr/** (the D-110 mirrors and ADR-0111). The change also touches integration/firebase-firestore production code. The gate is the owner's review and merge decision. The agent MUST NOT merge this pull request.

Risks or Follow-ups

  • Closes the E1-06 follow-up. Closes [Bug]: Vehicle cascade Fuel Entry tombstone omits entityType #36.
  • Known CI flake, owner E1-12, not E1-11. The first ios-simulator-build run on d1cf977 failed: ViewModelLifecycleTests.testFuelEntryFormViewModelModeDerivations crashed the XCTest runner during tearDown with kotlin.IllegalStateException: AndroidxDriverConnectionPool.close() called while 1 reader connection(s) still checked out, thrown from SwiftAppGraph.close() -> DefaultAppGraph.close() -> DatabaseHandle.close() -> AndroidxSqliteDriver.close(). The runner restarted and every other test in the suite passed. Re-running the job passed in 12m11s with no code change. The range 971f31e..d1cf977 touches only docs/ and shared/src/commonTest/, neither of which reaches the Shared framework the iOS host tests link against, so E1-11 is not the cause. Same graph-close defect family that E1-12 already owns; recorded as a residual risk, not fixed here.
  • docs/PROJECT_LOG.md carries a correction entry recording that the first version of the second parity test did not prove the order it named. The earlier entry is left intact per the log's append-only rule.
  • E3-13 (issue E3-13: Single source of truth for the outbox entityType wire value #46) remains the Phase 3 follow-up for a single source of truth for the entityType token.
  • DatabaseMutations, :feature:fuel, :shared and .sq literals remain independent until E3-13.

🤖 Generated with Claude Code

…dary fix

Re-apply all E1-11 work after the revert of PR #45 with six owner-review
findings fixed: (1) OutboxCoalescenceParityTest uses real
SqlDelightFuelEntryRepository; (2) both coalescence orders tested;
(3) backlog scope includes integration-boundary adaptation; (4) handoff
Files Changed complete; (5) ADR-0111 stale no-gate corrected; (6)
checkpoint refreshed. The Firestore boundary excludes entityType from
FirestoreWrite and validates it against EntitySnapshot.entityType.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@davidru85
davidru85 had a problem deploying to cloud-runtime-verification September 2, 2026 09:56 — with GitHub Actions Error
…kpoint

Continuous Progress Documentation (D-105) requires the checkpoint to reflect
the latest material state, which now includes the commit, the push and the
open pull request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@davidru85
davidru85 deployed to cloud-runtime-verification September 2, 2026 09:57 — with GitHub Actions Active
…lescence

OutboxCoalescenceParityTest.cascadeDeleteThenSubsequentFuelEntryCoalescence-
RetainsCanonicalKeySet re-applied the payload produced by the cascade itself,
so it exercised cascade-then-cascade while its name, the E1-11 acceptance
criterion and docs/handoff-E1-11.md all claimed cascade-then-direct.

The test now captures the direct payload from the real
SqlDelightFuelEntryRepository create path before the cascade delete and
re-applies that direct payload, making the direct writer the last writer of
the coalesced row. It asserts the direct and cascade payloads are exactly the
canonical key set, that they differ from each other so the re-application
cannot degenerate into a repeat of the cascade write, and that the final row
equals the direct payload and keeps the canonical keys and
"entityType":"FUEL_ENTRY". Renamed to
cascadeDeleteThenDirectFuelEntryCoalescenceRetainsCanonicalKeySet, with the
fixture helper renamed coalesceFuelEntryOutboxPayload and its comment
corrected. The direct-then-cascade test is unchanged.

Also adds the missing trailing newline to docs/handoff-E1-11.md, refreshes its
checkpoint, acceptance evidence and verification, and appends a correction
entry to docs/PROJECT_LOG.md. No contract, schema, migration, architecture or
decision change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@davidru85
davidru85 deployed to cloud-runtime-verification September 2, 2026 10:19 — with GitHub Actions Active
… owner

AGENTS.md requires the handoff checkpoint to state verification results
including failures and their established owner. The first ios-simulator-build
run on d1cf977 failed because ViewModelLifecycleTests
.testFuelEntryFormViewModelModeDerivations crashed the XCTest runner in
tearDown with "AndroidxDriverConnectionPool.close() called while 1 reader
connection(s) still checked out", thrown from SwiftAppGraph.close() through
DatabaseHandle.close(). Re-running the job passed with no code change.

The range 971f31e..d1cf977 touches only docs/ and shared/src/commonTest/,
neither of which reaches the Shared framework the iOS host tests link against,
so E1-11 is not the cause. The defect belongs to the E1-12 graph-close work and
is recorded as a residual risk, not fixed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@davidru85
davidru85 deployed to cloud-runtime-verification September 2, 2026 10:47 — with GitHub Actions Active
provider-decoupling failed on 83292e8, a documentation-only commit, with
FuelEntryStateHolderTest.unsupportedLocaleCurrencyFallsBackToEur
[iosSimulatorArm64] FAILED and "Test running process exited unexpectedly".
That is the exact test class the AGENTS.md Repository State already names as
the open E1-12 defect, "FuelEntryStateHolderTest Kotlin/Native SIGSEGV on
graph close".

Together with the ios-simulator-build crash on d1cf977, this confirms the
branch is exposed to one known Kotlin/Native graph-close defect that surfaces
nondeterministically across jobs. Recorded as a residual risk with E1-12 as the
established owner; not fixed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@davidru85
davidru85 deployed to cloud-runtime-verification September 2, 2026 10:52 — with GitHub Actions Active
@davidru85
davidru85 merged commit 64c91d6 into main Sep 2, 2026
10 checks passed
@davidru85
davidru85 deleted the story/E1-11-outbox-entitytype-fix-v2 branch September 2, 2026 11:08
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.

[Bug]: Vehicle cascade Fuel Entry tombstone omits entityType

1 participant