Repository navigation
feat(households): every user has a household (spec 090, #380) - #426
Merged
Merged
Conversation
Draft v0.1.0. Decisions 2 to 6 are written on the recommendations put to the maintainer. Written by the main session on Sonnet 5.5. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Supersedes the one-user half of ADR 0017. Proposed; decisions 5 and 6 await the maintainer's ruling. Written by the main session on Sonnet 5.5. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The maintainer ruled decisions 2, 3, 4 and 10. Review round 1 (Fable 5.2) fixed: delete rules, the copy's risks and the nullable fallback, all three migrations without a transaction, the harness check in two tiers, scope of shared-account rules. Written by the main session on Sonnet 5.5. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review round 2 (Fable 5.2) fixed: migration (c) is one transaction around its stragglers step, the copy and the sequence restore; the fallback comparison now states its stragglers hole and four missing facts; the README claim is corrected; the suite's bare accounts are named allowed edits. Written by the main session on Sonnet 5.5. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Take the table copy of accounts, not the nullable fallback, on two conditions: the plan review traces it against Rails 8.1.4.1, and bin/upgrade-smoke runs from the previous release built from its tag before the push. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven phases: (a) the schema, the models, (b) the backfill, nothing looks different, the upgrade proof's households check, (c) the NOT NULL copy as its own last migration phase, then documentation and evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Fable review traced migration (c) against Rails 8.1.4.1's alter_table and found the copy safe. Fixed: the user's membership check no longer reads a household on an update; (c) checks enforcement is off and that no key is broken, and has a fault test after the DROP; the shape example compares literals; phases 1 and 2 are one batch; the backfill uses the connection, one insert id and counts its transactions; the upgrade smoke runs on the commit to be pushed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…380) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d (spec 090 phase 1) Adds the migration, its spec, the schema dump, and the export format's coverage rows (plan steps 4.3 and 4.4, done here as step 1.4 says). Changed from the plan's code: the migration's keeping_sequence and the spec's sequence helper read and write main.sqlite_sequence, not the bare name. Rails' table copy builds a TEMP table with AUTOINCREMENT, which gives the connection a temp.sqlite_sequence that a bare name then resolves to, so the plan's restore wrote to the temp table and the spec read the same wrong one (it failed on the first run against a fresh test database, whose seed 34601 order puts a copy first). The assertions are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The second Fable review (phase 6 and the User validation) found no blocking issue. Fixed: the check after the copy compares row counts and new broken keys against the state before it, so a dangling row an operator left does not stop every boot; both guards have examples; the refusal checks there is no user; the validation's lambda no longer creates the association instance; the insert guard reads changes(). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#380) Checked in v0.1.0, v0.2.0 and v0.3.0: an account is inserted and deleted by the same transaction as its user, so only a hand edit leaves one. The refusal stands and the Upgrade notes say how to clear it. Phase 6 gains an example that changes the count of accounts and of each of the thirteen tables in turn. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd (spec 090 phase 2) Household, Membership, Account#household and #kind, and a user made with an account in a new household and a manager membership. The membership validation in User guards on association_cached?(:membership) first, as the coordinator's amendment to step 2.5 says, and user_spec gains the AC-4.5 example (it failed against the previous lambda, which let a second validation read memberships and households, and passes with the new one). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… 090 phase 2) Request spec for Story 1: first visitor, open sign-up, an admin's form, the operator command, invalid sign-ups, ignored household/role/kind parameters, and a deleted user's household. Changed from the plan's code, in the AC-1.7 example: the instance already has a user, so it opens sign-up (sign_up_open) before the sign-up post, and signs Ann out (delete session_path) before signing in as the admin, because SessionsController redirects a signed-in visitor. Assertions are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…380) Phase 1's implementer found that a table copy builds a TEMP table with AUTOINCREMENT, so a bare sqlite_sequence resolves to temp.sqlite_sequence. Phase 6 had the same bug. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…90 phase 3) Migration (b): every user with no membership gets a household named for them, a manager membership and their account's household_id, in batches of 500 users, each batch its own transaction. Idempotent, plain SQL, sets accounts.household_id only. Changes from the plan's code, none to an assertion: - spec: the timing example reports through RSpec.configuration.reporter (rubocop RSpec/Output forbids puts; the repo's timing specs use the reporter) and times with Process.clock_gettime, because Benchmark is not loaded under Ruby 4.0. - spec: the session example gets :aggregate_failures (it has two expectations; RSpec/MultipleExpectations). - migration: trailing "Safe:" comments on the interpolated SQL lines, as BackfillSessionsLastUsedAt does. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the round trip (spec 090 phase 4) households_invisible_spec passed on its first run (nothing visible was added; it is a guard). household_untouched_spec: its first run failed on a spec bug (target was a lazy let, created after the first household_facts call, so Household.pluck differed); target is now let!. No assertion was changed. Coverage (4.3, 4.4) was done in phase 1. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d, restored and next snapshots (spec 090 phase 5) Upgrade::Households (script/upgrade/households.rb) reads a snapshot with SQLite directly in two tiers: invariants true on every snapshot from this release on (one membership per user, a manager per household, every account in a household, a personal account in its user's household), and the backfill's own facts (count, role, kind, name), checked only when the old snapshot had no households table. The harness runs it in `compare` (the upgraded and the restored snapshots, failing with AC-6.1) and in `next_release`. Prints `households: ok` or `households: ok (backfilled)`. expected_changes.yml stays `changes: []` and seed_exceptions.yml is unchanged. Changes from the plan: in households_spec the "joined household" fixture also deletes household 2 (user 2 leaves its household of one when it joins household 1; otherwise tier 1 rightly names a household with no manager); the assertion is unchanged. The harness examples are in a new describe block with its own run_with. The seeds_spec example passed on its first run (a guard: the seed's users are made through the models, which make the households). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…row (spec 090 phase 6) Migration (c) of spec 090 decision 5: accounts.household_id becomes NOT NULL through Rails' table copy, run outside Rails' own transaction with foreign keys already off, so the thirteen tables that reference accounts (eleven ON DELETE CASCADE) keep every row. A stragglers step, the copy and the sequence's restore commit together or not at all; counts and new broken keys are checked after the copy. Re-trace of activerecord-8.1.4.1 (sqlite3_adapter.rb, sqlite3/database_statements.rb), step 6.3: 1. disable_referential_integrity runs PRAGMA foreign_keys = OFF before our transaction opens (it is a no-op after BEGIN). 2. alter_table's own disable_referential_integrity (restores to our OFF) and transaction (joins ours) neither re-enable enforcement nor commit early. 3. copy_table copies columns, the foreign key to households, the check constraints, and copy_table_indexes the plain, unique and partial indexes (the spec compares the shapes). 4. move_table creates and drops tables by name; no child is renamed, so the children's keys are untouched. 5. db/schema.rb: lots and users reference accounts with NO ACTION, the other eleven with CASCADE. 6. Rails runs no foreign_key_check after a copy (only fixtures call check_all_foreign_keys_valid!); the migration's own state check does. db/schema.rb differs by the version and `t.integer "household_id", null: false` only. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…90 phase 6)
accounts.household_id is NOT NULL now, so the specs that put a household-less account in the database take
migration (c) down first and put it back in an around ensure (backfill_households_spec), the versions list of
create_households_and_memberships_spec covers all three migrations, and the raw account inserts of the
backup writer, the snapshot spec and the backfill's batch example make a household first.
One change beyond the plan: the backfill's batch example runs `described_class.name.constantize` instead of
`described_class`. The Migrator reloads a migration's file with `load`, which makes a new class under the
same name once any example has taken (c) down; `stub_const("BackfillHouseholds::BATCH", 2)` then stubs the
new class while described_class is the old one, so under some seeds (38210) the example saw one batch
instead of three. The timing example takes (c) down before inserting its household-less accounts, so its
assertion of 10,000 households is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Scope, glossary (household, membership, personal and shared account, the second meaning of member), the instance-level tables, the invariants, the cross-account reads, the order of a user's delete, the accepted limit and the sources, each with its spec and ADR. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…(spec 090) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…090) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t (index) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, and prove it with rows in every referencing table (spec 090) The first migration's down copies accounts twice in one transaction. It now refuses when foreign keys are enforced and rolls back when a copy changed a row count, as the third migration does. Its spec fills a row in all thirteen tables that reference accounts (the helper moves to spec/support), goes down and up through all three migrations, and runs down inside an outer transaction. The AC-3.4 example compares the full shape of accounts, households and memberships before and after, and the AC-3.7 re-run now runs the migration itself. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…les' queries on every invisible page (spec 090) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…drop a history phrase (spec 090) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… exactly what holds (spec 090) An old container's delete of a user after the third migration leaves an empty household, the export format gives the reason households are not in the file, and the ADR says foreign keys are switched off before the copy's own transaction opens. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed table orders them differently (spec 090) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts: CLAUDE.md (kept main's paragraph and the households clause) and the specs index (both rows, 090 before 091).
…380) Verdict SPEC-ALIGNED WITH FINDINGS; every finding fixed or recorded. The smoke run and the full CI are to be recorded when they run. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The maintainer ruled its decisions on 2026-11-13; accepting the ADR is theirs, recorded in the plan-retirement pull request as for ADR 0029. Spec 090 v0.3.3 says so. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two lessons (a table copy of a referenced table; association(:name) creates the association) and one feedback memory (re-review a changed data-loss step). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e and deploy and data portability The area docs were written with the feature (phase 7); this adds their distillation log rows and freezes the spec's index row with its plan marked retirement owed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rtability area docs (spec 027 D4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts: the distillation log of release and deploy and the specs index (main's updated spec 091 rows kept beside the spec 090 rows).
households: ok (backfilled) after the upgraded and the restored snapshots; 28 tables compared, OK: v0.3.0 upgraded to 0a09808. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts: the memory index, CLAUDE.md (main's paragraph and the households clause) and the specs index (spec 089's row before spec 090's). Brings in spec 089's prices migrations.
…tion logs Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ec 090) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflict: the specs index (main's retired-plan row of spec 089 kept beside the spec 090 row).
…e-run (spec 090) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #380.
Every user now belongs to exactly one household, an existing instance gains one for every user on upgrade, and nothing in the app looks different yet.
What changed
households(a name) andmemberships(a user in a household asmanagerormember, one household per user, a household always has a manager), and two columns onaccounts:household_id(NOT NULL, no-action key) andkind(personalorshared; no shared account exists yet).users.account_idstays unique. An account stays the unit of ownership and privacy.Usermakes the household of one ("Ada's household", the user's first 88 characters and "'s household"), the manager membership and the account in one write, so first run, sign-up, the admin's form and the operator command all give one.accounts.household_idonly; (c)accounts.household_idNOT NULLby a table copy ofaccounts, which thirteen tables reference (eleven withON DELETE CASCADE). (c) turns enforcement off before opening one transaction of its own around the stragglers step, the copy and themain.sqlite_sequencerestore, and checks row counts and keys before and after. The maintainer ruled (c) over a nullable fallback on two conditions, both met: the plan review traced it against Rails 8.1.4.1'salter_table, andbin/upgrade-smokeran from the previous release built from its tag.script/upgrade/households.rb): invariants always (every user one membership, every household a manager, every account a household and in its user's), the backfill's facts when the old snapshot had no households.expected_changes.ymlstays empty.memberships,accounts.household_idandaccounts.kindas left out.tenancy-and-accounts.md,release-and-deploy.md,data-portability.md, the multi-tenancy rule,docs/releasing.md's Upgrade note,CLAUDE.md, the specs index.Evidence
bin/ci(full) passed in the push hook on25c6e94c;bin/ci checksgreen on the merged tree.bin/upgrade-smokefrom the v0.3.0 tag (built withgit archive) to this build: all 28 old tablessame,households: ok (backfilled)after the upgraded and after the restored snapshot,OK: v0.3.0 … 83 s. Recorded indocs/specs/090-every-user-has-a-household/verification.mdwith the implementation review (Opus 5.5, SPEC-ALIGNED WITH FINDINGS, all fixed or recorded).For the maintainer
docs/releasing.mdhas the command. Say so in the release notes of the version.🤖 Generated with Claude Code