Skip to content

Contract habit log slip classification schema - #764

Merged
thomasluizon merged 1 commit into
mainfrom
thomasluizon/ticket-718-slip-trigger-contract
Oct 19, 2026
Merged

thomasluizon merged 1 commit into
mainfrom
thomasluizon/ticket-718-slip-trigger-contract

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Habit logs now require the slip classification that API writers already persist. The contract migration removes the compatibility trigger and function, classifies only remaining NULL values, and requires the column without adding a default.

Closes thomasluizon/orbit-tickets#718

Approach

  • src/Orbit.Domain/Entities/HabitLog.cs restores IsSlip to bool; src/Orbit.Infrastructure/Persistence/HabitLogReader.cs filters completions with !log.IsSlip.
  • src/Orbit.Infrastructure/Migrations/20261019011031_ContractHabitLogSlip.cs drops the trigger and PostgreSQL function, backfills NULL classifications from the log value and habit flag, then applies the non-null constraint. Explicit historical classifications are preserved, including when a habit has changed type. The generated Designer and model snapshot accompany the migration.
  • tests/Orbit.Infrastructure.Tests/Persistence/ContractHabitLogSlipMigrationTests.cs exercises NULL backfill, soft-deleted rows, preservation of explicit classifications, trigger removal, rejection of an insert without classification, rollback SQL and old-schema compatibility. The existing feature-flag migration model test accounts for the additional column alteration.
  • The migration was scaffolded with dotnet ef migrations add through OrbitDbContextFactory. OpenAPI documents were regenerated by the build and had no diff.

Rolling deployment

The orchestrator supplied the production barrier evidence: Render service srv-d6tc2isr85hc739bf75g runs live deploy dep-dba7ls5g1s2s73b3lhog on 7472203c7, with the previous deploy deactivated. git merge-base --is-ancestor a1c666f1 7472203c7 also returned exit 0 locally. Every running writer therefore supplies its own classification before this contraction.

The new binary reads and writes the existing nullable column with its expand-phase trigger still installed. The expand migration already backfilled existing rows, and the trigger classifies inserts that omit the column. NewBinary_ReadsAndWritesExpandedSchema verifies full log materialization, another persisted log and the completion-date query for both good and bad habits against that schema. Startup awaits migration before serving requests; the contract transaction repairs any residual NULL values before requiring the column. Existing nullable-model binaries continue reading and writing the same column after contraction. No column is renamed or removed.

Test evidence

All verification used env -u LANG LC_ALL=en_US.UTF-8.

  • Before implementation, dotnet test tests/Orbit.Infrastructure.Tests --filter 'FullyQualifiedName~RecordHabitLogSlipMigrationTests|FullyQualifiedName~HabitLogReaderTests' --nologo passed all 23 unchanged tests while the schema still had the compatibility trigger and nullable classification.
  • Against the uncorrected scaffold, dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~ContractHabitLogSlipMigrationTests --nologo failed Up_PostgresBackfillsBeforeRequiringClassificationWithoutDefault: the generated column default was false. Up_BackfillsNullRemovesTriggerAndEnforcesNoDefault(true, 1, true) also failed because the positive bad-habit NULL log became false. These failures were observed before adding the explicit classification backfill and removing the scaffolded default.
  • After correction, dotnet test tests/Orbit.Infrastructure.Tests --filter 'FullyQualifiedName~ContractHabitLogSlipMigrationTests|FullyQualifiedName~RecordHabitLogSlipMigrationTests|FullyQualifiedName~HabitLogReaderTests|FullyQualifiedName~FeatureFlagAccountsMigrationTests' --no-build --no-restore --nologo passed all 36 tests, including both failures above.
  • dotnet build Orbit.slnx --nologo: exit 0, 0 errors.
  • dotnet test tests/Orbit.Domain.Tests --nologo: exit 0, 736 passed.
  • dotnet test tests/Orbit.Application.Tests --nologo: exit 0, 5,573 passed.
  • dotnet test tests/Orbit.Infrastructure.Tests --nologo: exit 0, 3,629 passed.
  • Commit hooks passed formatting, root allowlist, dash, timeless and suppression checks. Mutation testing remains with CI.

External interface evidence

Tests use the installed EF Core/Npgsql/SQLite providers directly, without mocked migration operations or database response fixtures. Their actual generated SQL and SQLite execution confirm the migration operation fields, provider selection and schema introspection used by these tests. The complete PostgreSQL script below was generated through the design-time factory with this command from src/Orbit.Api:

dotnet ef migrations script RecordDeletionStoppedRenewals ContractHabitLogSlip --project ../Orbit.Infrastructure --startup-project . --context OrbitDbContext --no-build --output /tmp/orbit-718-contract.sql
START TRANSACTION;
DROP TRIGGER IF EXISTS "SetHabitLogIsSlipOnInsert" ON "HabitLogs";
DROP FUNCTION IF EXISTS "SetHabitLogIsSlipOnInsert"();

UPDATE "HabitLogs" AS logs
SET "IsSlip" = logs."Value" > 0 AND
    (SELECT "IsBadHabit" FROM "Habits" WHERE "Id" = logs."HabitId")
WHERE logs."IsSlip" IS NULL;

ALTER TABLE "HabitLogs" ALTER COLUMN "IsSlip" SET NOT NULL;

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20261019011031_ContractHabitLogSlip', '10.0.13');

COMMIT;

Assumptions

  • Down restores the nullable expand schema and its old-writer classifier; rejected making the contract migration irreversible.

Manual steps

  • After merge and owner approval, use GitHub Actions > Release API > Run workflow with environment=production and branch=main. Approve its production environment deployment. Verify the release job succeeds and Render service srv-d6tc2isr85hc739bf75g > Events shows the selected commit live. The migration applies during API startup; the generated schema checks above are automated tests, and no separate manual backfill or configuration change is required.

@thomasluizon

Copy link
Copy Markdown
Owner Author

Guard proof by the orchestrator, at head aff027a2, each break alone, restored with git checkout and nothing committed (dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~ContractHabitLogSlipMigrationTests, 10 cases pass unbroken):

  • Backfill disabled (WHERE logs."IsSlip" IS NULL AND 1 = 0): exit 1, 4 failed, 6 passed; every Up_BackfillsNullRemovesTriggerAndEnforcesNoDefault case that seeds a NULL row fails, including the bad habit slip (value: 1, expected: True).
  • Trigger and function drops replaced with SELECT 1: exit 1, Up_PostgresBackfillsBeforeRequiringClassificationWithoutDefault fails.

Production barrier, read by the orchestrator from Render: srv-d6tc2isr85hc739bf75g has one live deploy, dep-dba7ls5g1s2s73b3lhog on 7472203c7 (which contains api#574 a1c666f1); the previous deploy is deactivated. This contract migration reaches production only through a manual production release.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed the schema contraction, domain/read-path changes, generated migration model, regression tests, and compatibility with the mobile consumer.

  • Schema contraction: Removes the compatibility trigger/function, backfills only NULL classifications, and requires IsSlip without adding a default; rollback restores the nullable schema and classifier.
  • Domain and reader: Makes HabitLog.IsSlip non-nullable and retains the completion query's ownership, positive-value, and soft-delete filters while excluding slips.
  • Client compatibility: Existing log, sync, and export DTOs and their sibling Zod schemas do not expose IsSlip, so this does not remove or newly require a client-facing field.
  • Verification: All 36 focused migration and reader tests passed, including historical-classification preservation, expanded-schema compatibility, and omitted-classification rejection. PostgreSQL SQL generation was exercised; live PostgreSQL execution and the documented Render deployment barrier were not independently verified.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed the schema contraction, domain and completion-reader changes, migration model, regression tests, and mobile consumer compatibility.

  • Schema contraction: Removes the compatibility trigger and function, backfills only NULL classifications, and requires IsSlip without adding a default. Rollback restores the nullable schema and classifier.
  • Domain and reader: Makes HabitLog.IsSlip non-nullable while preserving the completion query's ownership, positive-value, soft-delete, and slip-exclusion semantics.
  • Client compatibility: The existing log, sync, and export DTOs and their sibling Zod schemas do not expose IsSlip; no client-facing fields are removed or newly required.
  • Verification: All 36 focused migration and reader tests passed. PostgreSQL SQL generation was exercised; live PostgreSQL execution and the documented Render deployment barrier were not independently verified.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit b0d3ff4 into main Oct 19, 2026
24 checks passed
@thomasluizon
thomasluizon deleted the thomasluizon/ticket-718-slip-trigger-contract branch October 19, 2026 01:26
thomasluizon added a commit that referenced this pull request Oct 19, 2026
…, slip contract and the Supabase sign-in removal (#768)

* Stop Stripe and Play renewals when account deletion is confirmed (#746)

* fix: stop subscription renewal when account deletion is confirmed

* fix: restrict renewal restoration to an unexpired paid period

* fix: keep free account deletion independent of billing services

* fix: bound billing cancellation delays during account erasure

* fix: preserve renewal intent during deletion reconciliation

(cherry picked from commit 85d8824)

* docs: name Render web services in version floor preconditions (#755)

(cherry picked from commit 21615aa)

* fix(infra): use Render for production database connections (#754)

(cherry picked from commit e2909b6)

* test(habits): cover end date reactivation allowance through handler (#759)

(cherry picked from commit 16203d9)

* test(mcp): verify Pro access for OAuth and API keys (#760)

(cherry picked from commit 0a90f67)

* Cover calendar auto-sync scheduling and import reconciliation (ORB-199) (#761)

* test(calendar): cover auto-sync scheduler eligibility and batching

* test(calendar): cover import suggestion reconciliation filters

* test(calendar): isolate free plan eligibility from expiry

(cherry picked from commit 664c756)

* Clarify controller action cross-references in the agent catalog (#762)

(cherry picked from commit d9cda52)

* refactor(calendar): share legacy match key for sync and suggestions (#763)

(cherry picked from commit 8e754c4)

* Contract habit log slip classification schema (#718) (#764)

(cherry picked from commit b0d3ff4)

* fix(calendar): gate recurring event reconciliation (#765)

(cherry picked from commit c900dba)

* Gate rolling schema contractions behind deployment barriers (#766)

* feat: require deployment barriers for rolling schema contractions

* fix: keep quoted SQL comment markers from hiding contractions

* test: record the deployed habit log contract barrier

* ci: run the rolling contract gate only when its inputs change

The gate builds the infrastructure test project, which costs minutes on every
pull request. It now runs only when a migration, the barrier file or the gate
itself changes; the step before it already reads the same diff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: require barriers for unclassified ALTER TABLE actions

* test: isolate barrier unit coverage from checkout history

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 626c525)

* fix: require a session claim on access tokens (#767)

(cherry picked from commit 517ed1b)

* Deprecate legacy Google auth routes (#1528, part 1) (#753)

* Deprecate legacy Google auth routes for #1528

* Explain deprecated Google auth routes and annotate legacy tests

(cherry picked from commit 21f810f)

* Remove deleted Supabase sign-in path (#1528) (#752)

* Remove deleted Supabase sign-in path (#1528)

* Remove unused access-token validation catalog entries (#1528)

(cherry picked from commit 141a4a2)

* fix: bind carried schema barrier to the deployed redesign

Record the observed staging deployment and its drained predecessors without changing the migration gate. Regenerate the tracked gating matrix for the combined source. Refs thomasluizon/orbit-tickets#746.

* fix: retain deletion cancellation ownership across provider retries (#770)

* fix: persist deletion cancellation ownership before provider updates

* test: cover deletion intent commit and account changes

* fix: identify deletion-owned Stripe cancellations

Reconcile provider metadata before restoring renewal or retaining ownership, reject open cancellation transactions, and log skipped erasure rechecks.

Refs thomasluizon/orbit-tickets#1547

(cherry picked from commit a7d1cf2)

* test: drop the deprecation assertions the legacy Google route removal supersedes

The base merge brought #769's assertions that POST /api/Auth/google and
POST /api/Auth/operations/google stay available and deprecated. This carry
removes both routes (main's #1528 part 2), so those assertions no longer
have a subject.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant