convoy(architect): draft B1-B5 + B7 implementer briefs (Finding 1 → A)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
22ebef24d7
commit
1db6d4dd60
7 changed files with 2093 additions and 93 deletions
|
|
@ -88,7 +88,7 @@ Legend:
|
|||
| 3 | `add-collection-slugs.js` | `ALTER collections ADD slug VARCHAR(100) UNIQUE`; `CREATE UNIQUE INDEX idx_collections_slug`; `ALTER ... ADD CONSTRAINT check_slug_format CHECK (slug ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$' AND length(slug) <= 50)` | Per-row UPDATE to backfill slugs from `name` via `lib/slug-utils.js::generateUniqueSlug` | **M** | none |
|
||||
| 4 | `add-favorites-system.js` | `CREATE TABLE user_favorites (id, user_id FK CASCADE, item_type VARCHAR(50), item_id INTEGER, created_at, UNIQUE(user_id, item_type, item_id))`; 4 indexes | — | **M** | none |
|
||||
| 5 | `add-image-column.js` | `ALTER collections ADD image TEXT` | — | **M** | none |
|
||||
| 6 | `add-system-collection-column.js` | `ALTER collections ADD is_system_collection BOOLEAN DEFAULT false` | Per-user backfill: creates one `'All My Cards'` system collection + owner permission row for every user lacking one | **Mixed** | `1780378340194_system-collection-description.js` captures the DDL (`ADD COLUMN IF NOT EXISTS is_system_collection`) but NOT the per-user backfill DML. Backfill is an application-runtime invariant — every user-registration code path is expected to create the system collection — so leaving the historical backfill out of migrations is intentional. |
|
||||
| 6 | `add-system-collection-column.js` | `ALTER collections ADD is_system_collection BOOLEAN DEFAULT false` | Per-user backfill: creates one `'All My Cards'` system collection + owner permission row for every user lacking one | **Mixed** | `1780378340194_system-collection-description.js` captures the DDL (`ADD COLUMN IF NOT EXISTS is_system_collection`). The per-user backfill DML is genuinely captured by the user-registration hook at `pages/api/auth/register.js:97-119` (`INSERT INTO collections ... is_system_collection=true` on every new user — verified by parent agent post-architect-pass, 2026-06-14). No further work required; see Finding 4 RESOLVED below. |
|
||||
| 7 | `add-updated-at-column.js` | `ALTER cards ADD updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP` | — | **C** | `1779853647564_initial-schema.js` — `cards` `CREATE TABLE` already includes `updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP` (line 67). Script is now a no-op on fresh envs. No work required. |
|
||||
| 8 | `add-user-profile-columns.js` | `ALTER users ADD first_name, last_name VARCHAR(255), username VARCHAR(255) UNIQUE, profile_image_url TEXT` | Per-row UPDATE: defaults `first_name='User'`, `last_name=<id>`, `username='<emailprefix>_<id>'` | **M** (overlaps #9 — see Drift findings) |
|
||||
| 9 | `add-user-profile-fields.js` | `ALTER users ADD first_name, last_name VARCHAR(255), username VARCHAR(255) UNIQUE, bio TEXT, avatar_url TEXT`; `ALTER users ADD favorite_games JSONB DEFAULT '["MTG"]', collection_visibility VARCHAR(20) DEFAULT 'private', preferred_currency VARCHAR(3) DEFAULT 'USD', cards_per_page INTEGER DEFAULT 50, default_view VARCHAR(10) DEFAULT 'grid'`; `ALTER users ADD notifications_email BOOLEAN DEFAULT true, notifications_marketing BOOLEAN DEFAULT false, two_factor_enabled BOOLEAN DEFAULT false`; `ALTER users ADD theme VARCHAR(10) DEFAULT 'system', language VARCHAR(5) DEFAULT 'en'`; `CREATE TABLE user_settings (id, user_id FK CASCADE, setting_key VARCHAR(100), setting_value JSONB, UNIQUE(user_id, setting_key))`; `CREATE TABLE user_avatars (id, user_id FK CASCADE, filename, original_name, mime_type, file_size, file_path, is_active BOOLEAN DEFAULT true)`; 6 CHECK constraints (`check_collection_visibility`, `check_preferred_currency`, `check_cards_per_page`, `check_default_view`, `check_theme`, `check_language`); 6 indexes | UPDATE on users to write defaults for any row with NULLs in the new columns | **M** |
|
||||
|
|
@ -109,14 +109,42 @@ Legend:
|
|||
|
||||
### Inventory counts
|
||||
|
||||
- **Already captured (no work):** 2 — `add-updated-at-column.js` (#7) fully; `add-system-collection-column.js` (#6) DDL only
|
||||
- **Missed DDL (needs migration):** 8 — #1, #2, #3, #4, #5, #8∪#9 (deduplicate), #11 (with conflict resolution)
|
||||
- **Already captured (no work):** 2 — `add-updated-at-column.js` (#7) fully; `add-system-collection-column.js` (#6) DDL by `1780378340194` + DML backfill by `pages/api/auth/register.js:97-119` (Finding 4 RESOLVED)
|
||||
- **Missed DDL (needs migration):** 7 — #1, #2, #3, #4, #5, #8∪#9 (deduplicate). #11's DDL is NOT folded into a migration (Finding 1 RESOLVED → Outcome A; see below)
|
||||
- **DML-only (not migration material):** 3 — #10 fix-lorcana, #12 seed-alice-bob, #13 seed-with-cards
|
||||
- **Halt-and-ask:** 1 — #11's `UNIQUE(user_id, card_id)` vs `initial-schema`'s `UNIQUE(user_id, card_id, is_foil)`
|
||||
- **Halt-and-ask:** 0 (Finding 1 RESOLVED below)
|
||||
|
||||
## Drift findings
|
||||
|
||||
### Finding 1 — `user_cards` UNIQUE constraint conflict (HALT-AND-ASK)
|
||||
### Finding 1 — `user_cards` UNIQUE constraint conflict (RESOLVED — Outcome A, 2026-06-14)
|
||||
|
||||
**Resolution:** Operator ratified **Outcome A** post-architect-pass. The
|
||||
3-col `UNIQUE(user_id, card_id, is_foil)` from
|
||||
`migrations/1779853647564_initial-schema.js` is canonical and matches
|
||||
the actual prod state. `scripts/fix-user-cards-constraints.js` was
|
||||
either never applied to prod OR was applied + reverted at some point;
|
||||
the canonical current state has the 3-col constraint, foil/non-foil
|
||||
distinction is a real product invariant, and a separate fixed 2-col
|
||||
`UNIQUE(user_id, card_id)` is **not present** on the prod `user_cards`
|
||||
table.
|
||||
|
||||
**Consequence for this convoy:** B6 collapses entirely. No migration
|
||||
is written for `fix-user-cards-constraints.js`. Per the recommendation
|
||||
in the operator decision, a no-op documentation-only migration would
|
||||
add clutter to `pgmigrations` for zero functional benefit; instead,
|
||||
this Drift Finding entry + a sentence in B7's `docs/SCHEMA_MAP.md`
|
||||
update + a sentence in the As-shipped section document the reasoning.
|
||||
|
||||
The historical script remains a no-go-zone per
|
||||
`.cursor/rules/no-go-zones.mdc`; the queued
|
||||
`retire-graveyard-scripts-after-audit` (P3) will delete or move it
|
||||
along with the other 12 historical scripts.
|
||||
|
||||
The original audit narrative is preserved below for cross-reference.
|
||||
|
||||
---
|
||||
|
||||
**Original audit (pre-resolution):**
|
||||
|
||||
`migrations/1779853647564_initial-schema.js` line 82 declares:
|
||||
|
||||
|
|
@ -180,6 +208,10 @@ surface as smell). Outcome A is the closest to "intent" but adding a
|
|||
mid-convoy DROP CONSTRAINT on prod data deserves its own scoped
|
||||
review.
|
||||
|
||||
_— Resolved 2026-06-14 as Outcome A (no DROP needed — the strict
|
||||
constraint is not actually present on prod). See resolution block at
|
||||
the top of this Finding._
|
||||
|
||||
### Finding 2 — `users.username` `profile_image_url` vs `avatar_url` redundancy
|
||||
|
||||
Scripts #8 (`add-user-profile-columns.js`) and #9 (`add-user-profile-fields.js`)
|
||||
|
|
@ -208,24 +240,39 @@ drop them in prod (separate convoy). Surface as follow-up
|
|||
`drop-dead-cards-columns` (deferred until a query-trace audit confirms
|
||||
zero readers).
|
||||
|
||||
### Finding 4 — `add-system-collection-column.js` DML backfill is not captured
|
||||
### Finding 4 — `add-system-collection-column.js` DML backfill (RESOLVED — verified, 2026-06-14)
|
||||
|
||||
The script's DDL (column add) is in `1780378340194_system-collection-description`.
|
||||
The DML backfill (per-user `INSERT INTO collections ... 'All My Cards'
|
||||
... is_system_collection=true` + matching `collection_permissions`
|
||||
owner row) is NOT captured anywhere in the migration history.
|
||||
**Resolution:** Verified by parent agent post-architect-pass. The
|
||||
`pages/api/auth/register.js` handler at **lines 97-119** creates the
|
||||
system collection on every new user signup:
|
||||
|
||||
For prod: the backfill was run once and the rows exist. For fresh
|
||||
envs: per-user system-collection creation is the responsibility of the
|
||||
user-registration code path
|
||||
(`pages/api/auth/register.js` — verify the create-on-register hook
|
||||
exists). If it doesn't, fresh envs will have users without system
|
||||
collections — a runtime bug that's separate from this convoy's scope.
|
||||
```js
|
||||
const collectionResult = await sql`
|
||||
INSERT INTO collections (
|
||||
name, description, tcg, is_public, user_id, slug,
|
||||
is_system_collection, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
${SYSTEM_COLLECTION_DB_NAME},
|
||||
${VOCAB.SYSTEM_COLLECTION_SEED_DESCRIPTION},
|
||||
'All', false, ${user.id}, ${uniqueSlug},
|
||||
true, -- is_system_collection
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
)
|
||||
RETURNING id
|
||||
`;
|
||||
```
|
||||
|
||||
For this convoy: **no migration**. Note the runtime invariant in
|
||||
`docs/SCHEMA_MAP.md` § "Notes" alongside `is_system_collection`. If the
|
||||
register hook is missing, surface as `add-system-collection-on-register`
|
||||
follow-up.
|
||||
The runtime invariant is intact. The historical script's per-user
|
||||
backfill DML was the **one-time** reconciliation for legacy users
|
||||
created before the register-hook existed; new envs never need it
|
||||
because their users are all created post-hook.
|
||||
|
||||
For this convoy: **no migration, no follow-up convoy needed.** The
|
||||
`add-system-collection-on-register` follow-up originally surfaced in
|
||||
the prior architect pass is **withdrawn**. B7 will cite this
|
||||
verification in `docs/SCHEMA_MAP.md` § `collections.is_system_collection`
|
||||
notes.
|
||||
|
||||
### Finding 5 — Seed scripts wipe collection data
|
||||
|
||||
|
|
@ -240,7 +287,8 @@ below, these stay out of migrations entirely.
|
|||
|
||||
### D1 — Brief grouping: option (b), grouped by logical surface
|
||||
|
||||
**Ratified: 7 implementer briefs grouped by table/feature surface.**
|
||||
**Ratified: 6 implementer briefs grouped by table/feature surface.**
|
||||
(Originally 7; B6 collapsed post-Finding-1 resolution — see § Brief outline.)
|
||||
|
||||
Missed-DDL count is 8 (above the 1-3 threshold for option (d) and
|
||||
above the 4-thing threshold that triggered option (b/c) in the
|
||||
|
|
@ -249,7 +297,7 @@ excessive — many scripts touch the same DDL surface and ship one
|
|||
migration each would create unnecessary `pgmigrations` rows and
|
||||
make rollback narrative confusing.
|
||||
|
||||
The 7-brief plan groups every missed DDL by the table or feature it
|
||||
The 6-brief plan groups every missed DDL by the table or feature it
|
||||
touches:
|
||||
|
||||
- **B1** — Cards table reconciliation (#1, audit #7)
|
||||
|
|
@ -259,11 +307,17 @@ touches:
|
|||
- **B4** — Favorites system (#4)
|
||||
- **B5** — User profile reconciliation (#8 ∪ #9, dedup'd; +
|
||||
`user_settings`, `user_avatars`; + CHECK constraints; + indexes)
|
||||
- **B6** — `user_cards` / `collection_cards` named UNIQUE constraints
|
||||
(#11 — gated on Drift Finding 1 halt-and-ask outcome)
|
||||
- **B7** — Documentation + verification runbook (`docs/SCHEMA_MAP.md`
|
||||
refresh, AGENTS.md Gotcha #6 update, operator runbook)
|
||||
|
||||
**B6 was removed post-Finding-1 resolution** (Outcome A — the strict
|
||||
2-col `UNIQUE(user_id, card_id)` is not present on prod; the canonical
|
||||
3-col `UNIQUE(user_id, card_id, is_foil)` from `initial-schema` is
|
||||
what fresh envs already get). No migration is needed. The brief
|
||||
numbering preserves the original gap (B1-B5 + B7) so cross-references
|
||||
to "B6" in prior drafts of this convoy file remain unambiguous (they
|
||||
point to the removed brief).
|
||||
|
||||
Rationale:
|
||||
1. **Independence.** Each of B1-B6 writes a new file under `migrations/`
|
||||
with a fresh timestamp; the files don't overlap, so B1-B6 are
|
||||
|
|
@ -411,21 +465,18 @@ in timestamp order:
|
|||
8. **NEW: B3** — collaboration tables (collection_permissions, collection_activity, users.is_pending, 4 indexes)
|
||||
9. **NEW: B4** — favorites system (user_favorites + 4 indexes)
|
||||
10. **NEW: B5** — user profile reconciliation (15 ALTER-ADD-COLUMN + 2 CREATE TABLE + 6 CHECK + 6 indexes)
|
||||
11. **NEW: B6** — user_cards/collection_cards named UNIQUE constraints (pending Finding 1 outcome)
|
||||
|
||||
Each new migration is additive against the post-initial-schema state
|
||||
that prior migrations leave behind; **no inter-migration dependencies
|
||||
crossed within this convoy except B6's dependency on user_cards
|
||||
existing (provided by B0 / initial-schema) — already satisfied.**
|
||||
that prior migrations leave behind; no inter-migration dependencies
|
||||
crossed within this convoy.
|
||||
|
||||
Audited ordering risks for the new migrations:
|
||||
|
||||
- B1 depends on `cards` (initial-schema #1) ✅
|
||||
- B2 depends on `collections` (initial-schema #4) ✅
|
||||
- B3 depends on `collections` (#4) + `users` (#1) ✅
|
||||
- B4 depends on `users` (#1) ✅
|
||||
- B5 depends on `users` (#1) ✅
|
||||
- B6 depends on `user_cards` (#3) + `collection_cards` (#5) ✅
|
||||
- B1 depends on `cards` (initial-schema) ✅
|
||||
- B2 depends on `collections` (initial-schema) ✅
|
||||
- B3 depends on `collections` + `users` (initial-schema) ✅
|
||||
- B4 depends on `users` (initial-schema) ✅
|
||||
- B5 depends on `users` (initial-schema) ✅
|
||||
|
||||
For long-lived envs that already have all post-historical columns:
|
||||
every new migration is a documented no-op due to `IF NOT EXISTS`
|
||||
|
|
@ -433,23 +484,21 @@ guards. The only state change is the `pgmigrations` row insertion.
|
|||
|
||||
## Brief outline
|
||||
|
||||
Seven implementer briefs. Each B1-B6 ships exactly one new file
|
||||
under `migrations/<timestamp>_<slug>.js`. B7 updates docs only.
|
||||
Implementer briefs are **not** drafted in this convoy — they will be
|
||||
created after Human Gate 1 approves this plan.
|
||||
Six implementer briefs. Each B1-B5 ships exactly one new file under
|
||||
`migrations/<timestamp>_<slug>.js`. B7 updates docs only. Implementer
|
||||
briefs are drafted under `.convoys/reconcile-historical-add-scripts/brief-<N>-<slug>.md`.
|
||||
|
||||
| Brief | Title | Files (new) | Depends on | Est. LOC | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| B1 | Reconcile cards columns | `migrations/<ts>_add-cards-quantity-favorited.js` | — | ~40 | Captures script #1; script #7 already captured by initial-schema (verify & document) |
|
||||
| B2 | Reconcile collections columns | `migrations/<ts>_reconcile-collections-columns.js` | — | ~120 | Captures #2 (collections half: visibility/tcg/tags), #3 (slug + index + CHECK), #5 (image). Defer slug backfill DML — generating slugs on a fresh env is moot. |
|
||||
| B3 | Reconcile collaboration tables | `migrations/<ts>_reconcile-collaboration-tables.js` | — | ~130 | Captures #2 (collaboration half: collection_permissions, collection_activity, users.is_pending, 4 indexes). Defer owner-permission backfill DML — fresh envs have no pre-existing collections needing backfill. |
|
||||
| B4 | Reconcile favorites system | `migrations/<ts>_reconcile-favorites-system.js` | — | ~50 | Captures #4 (user_favorites + 4 indexes). |
|
||||
| B5 | Reconcile user profile | `migrations/<ts>_reconcile-user-profile.js` | — | ~250 | Captures #8 ∪ #9 (deduplicated; both add columns coexist in prod). Includes user_settings, user_avatars, 6 CHECK constraints (wrapped in `DO $$ EXCEPTION` blocks for idempotency), 6 indexes. Defer defaults-backfill UPDATE — column DEFAULTs handle it. |
|
||||
| B6 | Reconcile user_cards / collection_cards UNIQUE constraints | `migrations/<ts>_reconcile-user-cards-constraints.js` | **HALT — gated on Finding 1 outcome** | ~30-80 (depending on outcome) | If Outcome A: DROP `user_cards_user_card_unique` on prod (real `down()` deferred — this is one-way). If Outcome C: ADD constraint on fresh envs idempotently. Outcome B: defer entire brief to `unify-user-cards-foil-tracking` convoy. |
|
||||
| B7 | Documentation + verification | `docs/SCHEMA_MAP.md` (update); `docs/operations/RECONCILE-VERIFICATION.md` (new); `AGENTS.md` (Gotcha #6 update flipping to RESOLVED) | B1-B6 merged | ~150 | SCHEMA_MAP smell-list updates per Findings 1-3. Verification runbook (see § Verification plan below). AGENTS.md Gotcha #6 RESOLVED note. ship-readiness.md "Queued convoys" → flip entry to RESOLVED + unblock `retire-graveyard-scripts-after-audit`. |
|
||||
| B1 | Reconcile cards columns | `migrations/1781000000001_reconcile-cards-columns.js` | — | ~40 | Captures script #1; script #7 already captured by initial-schema (verify & document in PR body) |
|
||||
| B2 | Reconcile collections columns | `migrations/1781000000002_reconcile-collections-columns.js` | — | ~120 | Captures #2 (collections half: visibility/tcg/tags), #3 (slug + index + CHECK), #5 (image). Defer slug backfill DML — generating slugs on a fresh env is moot. |
|
||||
| B3 | Reconcile collaboration tables | `migrations/1781000000003_reconcile-collaboration-tables.js` | — | ~130 | Captures #2 (collaboration half: collection_permissions, collection_activity, users.is_pending, 4 indexes). Defer owner-permission backfill DML — fresh envs have no pre-existing collections needing backfill. |
|
||||
| B4 | Reconcile favorites system | `migrations/1781000000004_reconcile-favorites-system.js` | — | ~50 | Captures #4 (user_favorites + 4 indexes). |
|
||||
| B5 | Reconcile user profile | `migrations/1781000000005_reconcile-user-profile.js` | — | ~250 | Captures #8 ∪ #9 (deduplicated; both add columns coexist in prod). Includes user_settings, user_avatars, 6 CHECK constraints (wrapped in `DO $$ EXCEPTION` blocks for idempotency), 6 indexes. Defer defaults-backfill UPDATE — column DEFAULTs handle it. |
|
||||
| ~~B6~~ | ~~Reconcile user_cards / collection_cards UNIQUE constraints~~ | ~~removed~~ | — | 0 | **REMOVED 2026-06-14 — Finding 1 RESOLVED as Outcome A.** The strict 2-col `UNIQUE(user_id, card_id)` is not present on prod; the canonical 3-col tuple is already on fresh envs via `initial-schema`. Documented in Drift Finding 1 + B7's SCHEMA_MAP update; no migration written. |
|
||||
| B7 | Documentation + verification | `docs/SCHEMA_MAP.md` (update); `docs/MIGRATION_VERIFICATION_RUNBOOK.md` (new); `AGENTS.md` (Gotcha #6 audit); `.convoys/ship-readiness.md` (flip queued entry) | B1-B5 merged | ~150 | SCHEMA_MAP smell-list updates per Findings 1-3. Verification runbook (see § Verification plan below). AGENTS.md Gotcha #6 already RESOLVED post-migration-tool; verify and leave alone if accurate. ship-readiness.md "Queued convoys" → flip this convoy entry to RESOLVED + unblock `retire-graveyard-scripts-after-audit` + add `unify-user-avatar-column` + `drop-dead-cards-columns` follow-ups. |
|
||||
|
||||
Per-brief acceptance criteria (sketch — implementer briefs will
|
||||
formalize):
|
||||
Per-brief acceptance criteria (sketch — formalized in each brief file):
|
||||
|
||||
- Each migration file has a `down()` that **throws** with a clear message
|
||||
(these are reconciliation migrations; the prod schema state is the
|
||||
|
|
@ -468,49 +517,37 @@ formalize):
|
|||
slice_dependencies:
|
||||
- brief: 1
|
||||
depends_on: []
|
||||
files: [migrations/<ts1>_add-cards-quantity-favorited.js]
|
||||
files: [migrations/1781000000001_reconcile-cards-columns.js]
|
||||
- brief: 2
|
||||
depends_on: []
|
||||
files: [migrations/<ts2>_reconcile-collections-columns.js]
|
||||
files: [migrations/1781000000002_reconcile-collections-columns.js]
|
||||
- brief: 3
|
||||
depends_on: []
|
||||
files: [migrations/<ts3>_reconcile-collaboration-tables.js]
|
||||
files: [migrations/1781000000003_reconcile-collaboration-tables.js]
|
||||
- brief: 4
|
||||
depends_on: []
|
||||
files: [migrations/<ts4>_reconcile-favorites-system.js]
|
||||
files: [migrations/1781000000004_reconcile-favorites-system.js]
|
||||
- brief: 5
|
||||
depends_on: []
|
||||
files: [migrations/<ts5>_reconcile-user-profile.js]
|
||||
- brief: 6
|
||||
depends_on: [] # gated by operator decision on Finding 1, not by another brief
|
||||
files: [migrations/<ts6>_reconcile-user-cards-constraints.js]
|
||||
files: [migrations/1781000000005_reconcile-user-profile.js]
|
||||
# brief 6 removed — Finding 1 RESOLVED as Outcome A; timestamp 1781000000006 is unused
|
||||
- brief: 7
|
||||
depends_on: [1, 2, 3, 4, 5, 6]
|
||||
depends_on: [1, 2, 3, 4, 5]
|
||||
files:
|
||||
- docs/SCHEMA_MAP.md
|
||||
- docs/operations/RECONCILE-VERIFICATION.md
|
||||
- docs/MIGRATION_VERIFICATION_RUNBOOK.md
|
||||
- AGENTS.md
|
||||
- .convoys/ship-readiness.md
|
||||
```
|
||||
|
||||
**Timestamp coordination.** Each B1-B6 writes a `migrations/<timestamp>_*.js`
|
||||
file. Conductor assigns non-overlapping timestamps at dispatch (e.g.
|
||||
`npm run migrate create` rounds to millisecond, so concurrent
|
||||
generation would collide). Recommended pre-assignment:
|
||||
|
||||
| Brief | Pre-assigned timestamp prefix |
|
||||
| --- | --- |
|
||||
| B1 | 1781000000001 |
|
||||
| B2 | 1781000000002 |
|
||||
| B3 | 1781000000003 |
|
||||
| B4 | 1781000000004 |
|
||||
| B5 | 1781000000005 |
|
||||
| B6 | 1781000000006 |
|
||||
|
||||
Implementer briefs will spell out the exact filename; the timestamps
|
||||
are reservation tokens, not literal `Date.now()` values. (Pre-assigned
|
||||
**Timestamp coordination.** Each B1-B5 writes a
|
||||
`migrations/<timestamp>_*.js` file with a pre-assigned timestamp
|
||||
(reservation token, not literal `Date.now()`). Pre-assigned
|
||||
timestamps avoid the parallel-implementer collision risk documented
|
||||
in `scaffold-nextjs-app` retro recommendation #4.)
|
||||
in `scaffold-nextjs-app` retro recommendation #4. The brief frontmatter
|
||||
in each `.convoys/reconcile-historical-add-scripts/brief-<N>-*.md`
|
||||
file declares the exact path; implementers MUST use that exact
|
||||
filename (not `npm run migrate create`, which would `Date.now()`).
|
||||
|
||||
## Verification plan (D5 operator runbook)
|
||||
|
||||
|
|
@ -632,21 +669,16 @@ against prod is silent. Implementer brief will spell this out.
|
|||
|
||||
### R6 — Mid-convoy timestamp collision when implementers spawn in parallel
|
||||
|
||||
B1-B6 are mutually independent and can dispatch via `/multitask`.
|
||||
B1-B5 are mutually independent and can dispatch via `/multitask`.
|
||||
Pre-assigned timestamps (D7 table) avoid the `Date.now()`-collision
|
||||
risk that bit the `scaffold-nextjs-app` convoy. Each implementer
|
||||
brief will name its file's exact timestamp; deviations require a
|
||||
re-plan.
|
||||
|
||||
### R7 — Operator approval lag on Finding 1 (halt-and-ask)
|
||||
### R7 — Operator approval lag on Finding 1 (RESOLVED 2026-06-14)
|
||||
|
||||
B6 cannot land without operator picking Outcome A / B / C from
|
||||
Finding 1. If approval is delayed, B1-B5 + B7 can still land (B7's
|
||||
SCHEMA_MAP update can flag B6 as "deferred pending operator decision"
|
||||
and resolve in a follow-up PR once the decision arrives). Conductor
|
||||
should not block B7 on B6 specifically; B7's `depends_on: [1, 2, 3,
|
||||
4, 5, 6]` allows B6 to be deferred if necessary, with a re-PR for
|
||||
B7 once B6 lands.
|
||||
Originally a risk because B6 was gated on operator decision. Finding 1
|
||||
RESOLVED as Outcome A; B6 removed. No lag risk remains.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
|
|
@ -674,22 +706,14 @@ Surfaced by this convoy:
|
|||
by Finding 3. `cards.quantity` and `cards.favorited` are documented
|
||||
as unused. After a query-trace audit confirms zero readers, ship a
|
||||
migration that DROPs them (with proper `down()` recreate).
|
||||
- **`add-system-collection-on-register`** (priority: P1 correctness if
|
||||
the runtime hook is missing; P3 hygiene if it exists but is
|
||||
undocumented — NEW, conditional). Driven by Finding 4. Verify
|
||||
`pages/api/auth/register.js` creates an
|
||||
`is_system_collection = true` row on user registration. If not,
|
||||
fresh envs have users without system collections and the runtime
|
||||
behavior is broken.
|
||||
- ~~**`add-system-collection-on-register`**~~ — **WITHDRAWN 2026-06-14**, Finding 4 RESOLVED. Verified the register hook exists at `pages/api/auth/register.js:97-119`.
|
||||
- **`fold-rename-admin-email-into-migrations`** (priority: P3 polish,
|
||||
conditional — NEW per D6). Only surface if a future fresh-env
|
||||
onboarding needs the rename migration applied in order. Until then,
|
||||
the script-shaped migration at
|
||||
`scripts/migrations/2026-05-24-rename-admin-email.js` is left in
|
||||
place per D6.
|
||||
- **`unify-user-cards-foil-tracking`** (priority: P2 correctness, NEW,
|
||||
conditional on Finding 1 Outcome B). If operator picks B, this
|
||||
convoy designs a non-tuple foil-tracking shape on `user_cards`.
|
||||
- ~~**`unify-user-cards-foil-tracking`**~~ — **WITHDRAWN 2026-06-14**, Finding 1 RESOLVED as Outcome A (not Outcome B). The 3-col tuple stays canonical; no foil-tracking redesign needed.
|
||||
|
||||
Not surfaced (no friction yet):
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,228 @@
|
|||
---
|
||||
convoy: reconcile-historical-add-scripts
|
||||
brief_number: 1
|
||||
depends_on: []
|
||||
files:
|
||||
- migrations/1781000000001_reconcile-cards-columns.js
|
||||
---
|
||||
|
||||
# Brief 1: Reconcile `cards` columns into migration history
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Capture the DDL added by `scripts/add-card-columns.js` (the `cards.quantity` + `cards.favorited` columns) into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with both columns after `npm run setup-db`.
|
||||
|
||||
## Scope (files in scope — do not edit anything else)
|
||||
|
||||
- `migrations/1781000000001_reconcile-cards-columns.js` — **new**
|
||||
|
||||
## Source script (read-only audit reference; DO NOT EDIT — no-go-zone)
|
||||
|
||||
`scripts/add-card-columns.js` lines 22-33 (verbatim):
|
||||
|
||||
```js
|
||||
await sql`
|
||||
ALTER TABLE cards
|
||||
ADD COLUMN IF NOT EXISTS quantity INTEGER DEFAULT 0
|
||||
`;
|
||||
// ...
|
||||
await sql`
|
||||
ALTER TABLE cards
|
||||
ADD COLUMN IF NOT EXISTS favorited BOOLEAN DEFAULT false
|
||||
`;
|
||||
```
|
||||
|
||||
**Sibling script `scripts/add-updated-at-column.js`** is already captured by `migrations/1779853647564_initial-schema.js` (the `cards` `CREATE TABLE` at lines 45-69 already declares `updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`). Do NOT add an ALTER for `updated_at` — it would be redundant noise on `pgmigrations`. Mention this in the PR body so reviewers don't ask.
|
||||
|
||||
## Target migration file
|
||||
|
||||
Path: `migrations/1781000000001_reconcile-cards-columns.js`
|
||||
|
||||
Contents:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Reconcile historical `scripts/add-card-columns.js` into the migration
|
||||
* history. Adds `cards.quantity` and `cards.favorited` columns so a
|
||||
* brand-new Neon branch ends up with the same shape that prod has had
|
||||
* since the historical script's one-shot run.
|
||||
*
|
||||
* Both columns are flagged as **unused** in docs/SCHEMA_MAP.md
|
||||
* § "Known schema smells" #3 — `quantity` lives on `user_cards`,
|
||||
* `favorited` lives on `user_favorites`. They're added here for
|
||||
* fresh-env parity with prod. A follow-up `drop-dead-cards-columns`
|
||||
* convoy (queued, P3 hygiene) will drop both columns once a query-trace
|
||||
* audit confirms zero runtime readers.
|
||||
*
|
||||
* Idempotent: re-running against any long-lived prod env is a no-op
|
||||
* because both ALTERs use IF NOT EXISTS.
|
||||
*
|
||||
* Note: `scripts/add-updated-at-column.js` (the sibling historical
|
||||
* script in the same convoy) is NOT reconciled here because
|
||||
* `cards.updated_at` is already declared in
|
||||
* `migrations/1779853647564_initial-schema.js`'s `CREATE TABLE cards`
|
||||
* (line 67). No further work needed for that script.
|
||||
*
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
*/
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
ALTER TABLE cards
|
||||
ADD COLUMN IF NOT EXISTS quantity INTEGER DEFAULT 0;
|
||||
|
||||
ALTER TABLE cards
|
||||
ADD COLUMN IF NOT EXISTS favorited BOOLEAN DEFAULT false;
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Down-migration intentionally throws. Dropping these columns on
|
||||
* long-lived envs requires the `drop-dead-cards-columns` convoy's
|
||||
* query-trace audit — bypassing it via a casual rollback risks
|
||||
* dropping data on prod. Use `drop-dead-cards-columns` when ready.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export const down = () => {
|
||||
throw new Error(
|
||||
'[migration:1781000000001_reconcile-cards-columns] Down not supported. ' +
|
||||
'Dropping cards.quantity / cards.favorited belongs to the queued ' +
|
||||
'drop-dead-cards-columns convoy, which performs a query-trace audit ' +
|
||||
'before the DROP. Do not rollback this migration directly.'
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — `migrations/` is the canonical home for schema changes; one column-group per migration.
|
||||
- `.cursor/rules/no-go-zones.mdc` — `scripts/add-card-columns.js` is append-only history. **Do not edit it.**
|
||||
- Style: match the existing migrations under `migrations/`. Use raw `pgm.sql(...)` template literals (the `migration-tool` convoy ratified this in D2 of `.convoys/reconcile-historical-add-scripts.md`).
|
||||
- ESM exports (`export const up = ...`, `export const down = ...`); no `module.exports`. The repo is `"type": "module"` per `package.json` line 5.
|
||||
- Use `IF NOT EXISTS` on every ALTER — idempotent re-apply is a documented requirement of this convoy.
|
||||
- Add a JSDoc docstring at the top of the file explaining what's being reconciled, citing the source script + the convoy file.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `migrations/1781000000001_reconcile-cards-columns.js` exists with the exact filename above (the timestamp `1781000000001` is the reservation token assigned by the architect — do NOT use `npm run migrate create`, which would call `Date.now()` and assign a different timestamp).
|
||||
- [ ] The file's `up()` adds both columns via `IF NOT EXISTS`.
|
||||
- [ ] The file's `down()` throws with a clear message pointing at the `drop-dead-cards-columns` follow-up.
|
||||
- [ ] The file's docstring cites `scripts/add-card-columns.js` and `.convoys/reconcile-historical-add-scripts.md`.
|
||||
- [ ] `node --check migrations/1781000000001_reconcile-cards-columns.js` passes (syntactic validity).
|
||||
- [ ] `node -e "import('./migrations/1781000000001_reconcile-cards-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined` (module loads cleanly).
|
||||
- [ ] `npm run lint` exits clean against the baseline (no new lint errors introduced by this file).
|
||||
- [ ] `npm run test:run` reports 21/21 passing (no test surface changes).
|
||||
- [ ] The PR body documents that `add-updated-at-column.js` is already captured by `initial-schema` (per the source-script section above) and explains why no second migration is added.
|
||||
|
||||
## Verification
|
||||
|
||||
Run the following from the convoy worktree (`tcg-vault-worktrees/reconcile-historical-add-scripts/`) BEFORE opening the PR:
|
||||
|
||||
```bash
|
||||
node --check migrations/1781000000001_reconcile-cards-columns.js
|
||||
node -e "import('./migrations/1781000000001_reconcile-cards-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
|
||||
npm run lint
|
||||
npm run test:run
|
||||
```
|
||||
|
||||
Expected:
|
||||
- `node --check` exits 0 with no output.
|
||||
- The `node -e` line prints exactly: `function function undefined`.
|
||||
- `npm run lint` matches the existing baseline (no new errors).
|
||||
- `npm run test:run` reports 21/21 tests passing.
|
||||
|
||||
**Do NOT** run `npm run migrate up` against any environment as part of this brief — that's a post-merge operator step covered by Brief 7's verification runbook.
|
||||
|
||||
## Commit message
|
||||
|
||||
```
|
||||
feat(migrations): reconcile add-card-columns into migration history (brief 1/7)
|
||||
|
||||
Captures the DDL effect of scripts/add-card-columns.js (cards.quantity
|
||||
+ cards.favorited columns) into a new node-pg-migrate migration. Both
|
||||
columns are flagged as unused in docs/SCHEMA_MAP.md § "Known schema
|
||||
smells" #3 — added here for fresh-env parity with prod; a follow-up
|
||||
drop-dead-cards-columns convoy will drop them after a query-trace
|
||||
audit.
|
||||
|
||||
Sibling script add-updated-at-column.js is already captured by
|
||||
migrations/1779853647564_initial-schema.js (cards.updated_at is in the
|
||||
CREATE TABLE); no second migration needed.
|
||||
|
||||
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B1.
|
||||
Idempotent re-apply (IF NOT EXISTS guards).
|
||||
```
|
||||
|
||||
## PR shape
|
||||
|
||||
**Title:** `feat(migrations): reconcile add-card-columns into migration history (brief 1/7)`
|
||||
|
||||
**Body template:**
|
||||
|
||||
```markdown
|
||||
Brief 1 of the `reconcile-historical-add-scripts` convoy. See
|
||||
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
|
||||
for the full plan and rationale.
|
||||
|
||||
## What this PR does
|
||||
|
||||
Adds `migrations/1781000000001_reconcile-cards-columns.js` — a new
|
||||
`node-pg-migrate` migration that adds two columns to `cards` via
|
||||
`ADD COLUMN IF NOT EXISTS`:
|
||||
|
||||
- `quantity INTEGER DEFAULT 0`
|
||||
- `favorited BOOLEAN DEFAULT false`
|
||||
|
||||
Both columns already exist in long-lived prod environments (added by
|
||||
the historical `scripts/add-card-columns.js`). This migration brings
|
||||
fresh Neon branches to parity so `npm install` → `npm run setup-db`
|
||||
alone produces the prod shape, without manually replaying historical
|
||||
scripts.
|
||||
|
||||
## What this PR does NOT do
|
||||
|
||||
- Does **NOT** edit `scripts/add-card-columns.js` (no-go-zone).
|
||||
- Does **NOT** add a migration for `scripts/add-updated-at-column.js`
|
||||
— `cards.updated_at` is already declared in
|
||||
`migrations/1779853647564_initial-schema.js` line 67.
|
||||
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
|
||||
or AGENTS.md.
|
||||
- Does **NOT** run `npm run migrate up` against any environment (that's
|
||||
the post-merge operator step covered by Brief 7's runbook).
|
||||
- Does **NOT** drop the columns (deferred to follow-up
|
||||
`drop-dead-cards-columns`).
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- [ ] `node --check migrations/1781000000001_reconcile-cards-columns.js` exits 0
|
||||
- [ ] `node -e "import('./migrations/1781000000001_reconcile-cards-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
|
||||
- [ ] `npm run lint` matches baseline (no new errors)
|
||||
- [ ] `npm run test:run` reports 21/21 passing
|
||||
- [ ] Did not run `npm run migrate up` against any environment in this PR
|
||||
- [ ] Operator post-merge: run the verification runbook from Brief 7 (`docs/MIGRATION_VERIFICATION_RUNBOOK.md` once it lands)
|
||||
|
||||
## Cross-references
|
||||
|
||||
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
|
||||
- Source script (no-go-zone, audit reference only): `scripts/add-card-columns.js`
|
||||
- Follow-up after this convoy lands: `drop-dead-cards-columns` (P3 hygiene)
|
||||
```
|
||||
|
||||
## DO NOT
|
||||
|
||||
- DO NOT edit `scripts/add-card-columns.js` or any other file under `scripts/` — append-only no-go-zone per `.cursor/rules/no-go-zones.mdc`.
|
||||
- DO NOT edit any other file under `migrations/` — each migration is pinned by its `pgmigrations` row.
|
||||
- DO NOT edit `scripts/setup-neon-db.js`.
|
||||
- DO NOT edit `package.json` (no new deps).
|
||||
- DO NOT edit `AGENTS.md` or `docs/SCHEMA_MAP.md` — that's Brief 7's job.
|
||||
- DO NOT run `npm run migrate up` against any environment.
|
||||
- DO NOT call `npm run migrate create` to scaffold the file — it uses `Date.now()` for the timestamp prefix, which would collide with the architect's pre-assigned reservation tokens for parallel briefs.
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
Capturing `cards.quantity` + `cards.favorited` in a single small migration matches the per-table grouping of the convoy's D1 decision and keeps the diff easy to review. Sibling `add-updated-at-column.js` is already captured by `initial-schema`, so reconciling it would create a `pgmigrations` row for zero functional benefit. The columns themselves are dead per SCHEMA_MAP smell #3, but parity with prod is the convoy's success metric — the actual DROP is the `drop-dead-cards-columns` follow-up's responsibility.
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
---
|
||||
convoy: reconcile-historical-add-scripts
|
||||
brief_number: 2
|
||||
depends_on: []
|
||||
files:
|
||||
- migrations/1781000000002_reconcile-collections-columns.js
|
||||
---
|
||||
|
||||
# Brief 2: Reconcile `collections` columns into migration history
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Capture the `collections`-table DDL added by three historical scripts (`add-collaboration-features.js` columns half, `add-collection-slugs.js`, `add-image-column.js`) into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with `visibility`, `tcg`, `tags`, `slug` (+ index + CHECK constraint), and `image` columns on `collections` after `npm run setup-db`.
|
||||
|
||||
## Scope (files in scope — do not edit anything else)
|
||||
|
||||
- `migrations/1781000000002_reconcile-collections-columns.js` — **new**
|
||||
|
||||
## Source scripts (read-only audit reference; DO NOT EDIT — all three are no-go-zones)
|
||||
|
||||
### `scripts/add-collaboration-features.js` lines 15-20 (collections half only — the `collection_permissions` / `collection_activity` / `users.is_pending` half is Brief 3's scope):
|
||||
|
||||
```js
|
||||
await sql`
|
||||
ALTER TABLE collections
|
||||
ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) DEFAULT 'private',
|
||||
ADD COLUMN IF NOT EXISTS tcg VARCHAR(50) DEFAULT 'MTG',
|
||||
ADD COLUMN IF NOT EXISTS tags TEXT
|
||||
`;
|
||||
```
|
||||
|
||||
Plus the visibility index at lines 77-80:
|
||||
|
||||
```js
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_collections_visibility
|
||||
ON collections(visibility)
|
||||
`;
|
||||
```
|
||||
|
||||
### `scripts/add-collection-slugs.js` lines 17-99 (relevant DDL only):
|
||||
|
||||
```js
|
||||
// Step 1: Add slug column
|
||||
await sql`
|
||||
ALTER TABLE collections
|
||||
ADD COLUMN IF NOT EXISTS slug VARCHAR(100) UNIQUE
|
||||
`;
|
||||
|
||||
// Step 5: Add unique index
|
||||
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_collections_slug ON collections(slug)`;
|
||||
|
||||
// Step 6: Add format CHECK constraint
|
||||
await sql`ALTER TABLE collections ADD CONSTRAINT check_slug_format CHECK (slug ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$' AND length(slug) <= 50)`;
|
||||
```
|
||||
|
||||
The script also backfills slugs via per-row UPDATE using `lib/slug-utils.js::generateUniqueSlug` (lines 41-73). **Do NOT fold the backfill DML into this migration.** Fresh envs have no pre-existing `collections` rows to backfill; on prod, the backfill ran once historically. Future migrations that need slug generation should run that logic in application code, not in a migration.
|
||||
|
||||
### `scripts/add-image-column.js` lines 14-17 (entire DDL):
|
||||
|
||||
```js
|
||||
await sql`
|
||||
ALTER TABLE collections
|
||||
ADD COLUMN IF NOT EXISTS image TEXT
|
||||
`;
|
||||
```
|
||||
|
||||
## Target migration file
|
||||
|
||||
Path: `migrations/1781000000002_reconcile-collections-columns.js`
|
||||
|
||||
Contents:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Reconcile three historical scripts that all added columns to the
|
||||
* `collections` table:
|
||||
*
|
||||
* - scripts/add-collaboration-features.js (visibility, tcg, tags +
|
||||
* idx_collections_visibility index) — collections-table half only;
|
||||
* the collection_permissions / collection_activity / users.is_pending
|
||||
* half is reconciled by migrations/1781000000003_reconcile-collaboration-tables.js
|
||||
* - scripts/add-collection-slugs.js (slug + idx_collections_slug
|
||||
* unique index + check_slug_format CHECK constraint)
|
||||
* - scripts/add-image-column.js (image)
|
||||
*
|
||||
* Grouped into one migration per D1 of .convoys/reconcile-historical-add-scripts.md
|
||||
* (one migration per table/feature surface). All historical DDL was
|
||||
* idempotent (ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS);
|
||||
* this migration preserves that. CHECK constraint adds via a
|
||||
* DO $$ EXCEPTION block because Postgres doesn't accept
|
||||
* ADD CONSTRAINT ... IF NOT EXISTS for CHECK.
|
||||
*
|
||||
* Per-row slug backfill DML from add-collection-slugs.js is intentionally
|
||||
* NOT folded in — fresh envs have no pre-existing collections to
|
||||
* backfill; on prod, the backfill ran once historically and is baked in.
|
||||
*
|
||||
* Notes on adjacent state:
|
||||
* - `is_system_collection` was added by migrations/1780378340194_system-collection-description.js
|
||||
* (and the runtime register hook at pages/api/auth/register.js:97-119
|
||||
* creates the per-user system collection row — Finding 4 RESOLVED in
|
||||
* .convoys/reconcile-historical-add-scripts.md).
|
||||
*
|
||||
* Two-column-visibility smell: `is_public BOOLEAN` (from initial-schema)
|
||||
* and `visibility VARCHAR(20)` (from this migration) coexist on prod.
|
||||
* Surfaced as docs/SCHEMA_MAP.md § "Known schema smells" #2 → queued
|
||||
* `unify-collection-visibility` is OUT OF SCOPE here; this migration
|
||||
* adds `visibility` for parity, nothing more.
|
||||
*
|
||||
* Idempotent re-apply: every statement uses IF NOT EXISTS or the
|
||||
* exception-swallowing DO block.
|
||||
*
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
*/
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
ALTER TABLE collections
|
||||
ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) DEFAULT 'private',
|
||||
ADD COLUMN IF NOT EXISTS tcg VARCHAR(50) DEFAULT 'MTG',
|
||||
ADD COLUMN IF NOT EXISTS tags TEXT,
|
||||
ADD COLUMN IF NOT EXISTS slug VARCHAR(100) UNIQUE,
|
||||
ADD COLUMN IF NOT EXISTS image TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_collections_visibility
|
||||
ON collections (visibility);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_collections_slug
|
||||
ON collections (slug);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TABLE collections
|
||||
ADD CONSTRAINT check_slug_format
|
||||
CHECK (slug ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$' AND length(slug) <= 50);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Down-migration intentionally throws. Removing these columns on
|
||||
* long-lived envs would drop user-curated tag / slug / image data
|
||||
* and break the runtime code that reads collections.visibility,
|
||||
* collections.slug, collections.image, collections.tcg, collections.tags.
|
||||
*
|
||||
* If a future schema correction needs to mutate any of these columns,
|
||||
* write a NEW dated migration with a real `down()` — do NOT roll back
|
||||
* this one.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export const down = () => {
|
||||
throw new Error(
|
||||
'[migration:1781000000002_reconcile-collections-columns] Down not supported. ' +
|
||||
'Dropping collections.visibility / tcg / tags / slug / image would erase ' +
|
||||
'user-curated data and break runtime reads. Write a new dated migration ' +
|
||||
'for any future schema correction.'
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — one migration per table/feature surface.
|
||||
- `.cursor/rules/no-go-zones.mdc` — all three source scripts are append-only history. **Do not edit any of them.**
|
||||
- Style: raw `pgm.sql(...)` template literals, matching the other migrations under `migrations/`. Per D2 of the convoy file.
|
||||
- ESM exports; `"type": "module"` per `package.json` line 5.
|
||||
- `IF NOT EXISTS` on every ALTER and CREATE INDEX. CHECK constraint wraps in `DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL END $$;` (Postgres doesn't accept `IF NOT EXISTS` directly on CHECK constraints).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `migrations/1781000000002_reconcile-collections-columns.js` exists with the exact filename above (the timestamp `1781000000002` is the reservation token assigned by the architect — do NOT use `npm run migrate create`).
|
||||
- [ ] The file's `up()` adds 5 columns (`visibility`, `tcg`, `tags`, `slug`, `image`) via `IF NOT EXISTS`, creates 2 indexes (`idx_collections_visibility`, unique `idx_collections_slug`) via `IF NOT EXISTS`, and adds the `check_slug_format` CHECK via a `DO $$ EXCEPTION` block.
|
||||
- [ ] The file's `down()` throws with a clear message.
|
||||
- [ ] The file's docstring cites all three source scripts + the convoy file + the visibility smell (#2 in SCHEMA_MAP).
|
||||
- [ ] Per-row slug backfill DML is NOT in the migration (out of scope per the convoy plan).
|
||||
- [ ] `node --check migrations/1781000000002_reconcile-collections-columns.js` passes.
|
||||
- [ ] `node -e "import('./migrations/1781000000002_reconcile-collections-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`.
|
||||
- [ ] `npm run lint` matches the baseline (no new errors).
|
||||
- [ ] `npm run test:run` reports 21/21 passing.
|
||||
|
||||
## Verification
|
||||
|
||||
Run from the convoy worktree BEFORE opening the PR:
|
||||
|
||||
```bash
|
||||
node --check migrations/1781000000002_reconcile-collections-columns.js
|
||||
node -e "import('./migrations/1781000000002_reconcile-collections-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
|
||||
npm run lint
|
||||
npm run test:run
|
||||
```
|
||||
|
||||
Expected:
|
||||
- `node --check` exits 0 silently.
|
||||
- `node -e` prints `function function undefined`.
|
||||
- `npm run lint` matches baseline.
|
||||
- `npm run test:run` reports 21/21 passing.
|
||||
|
||||
**Do NOT** run `npm run migrate up` against any environment — that's the post-merge operator step covered by Brief 7's verification runbook.
|
||||
|
||||
## Commit message
|
||||
|
||||
```
|
||||
feat(migrations): reconcile collections columns into migration history (brief 2/7)
|
||||
|
||||
Captures the DDL effect of three historical scripts into one new
|
||||
node-pg-migrate migration:
|
||||
|
||||
- scripts/add-collaboration-features.js (collections half: visibility,
|
||||
tcg, tags + idx_collections_visibility)
|
||||
- scripts/add-collection-slugs.js (slug + idx_collections_slug unique
|
||||
+ check_slug_format CHECK constraint)
|
||||
- scripts/add-image-column.js (image)
|
||||
|
||||
The collaboration tables half (collection_permissions, collection_activity,
|
||||
users.is_pending) is reconciled separately by Brief 3.
|
||||
|
||||
Per-row slug backfill DML from add-collection-slugs.js is intentionally
|
||||
NOT folded in — fresh envs have no pre-existing collections to backfill.
|
||||
|
||||
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B2.
|
||||
Idempotent re-apply (IF NOT EXISTS guards; DO $$ EXCEPTION for CHECK).
|
||||
```
|
||||
|
||||
## PR shape
|
||||
|
||||
**Title:** `feat(migrations): reconcile collections columns into migration history (brief 2/7)`
|
||||
|
||||
**Body template:**
|
||||
|
||||
```markdown
|
||||
Brief 2 of the `reconcile-historical-add-scripts` convoy. See
|
||||
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
|
||||
for the full plan and rationale.
|
||||
|
||||
## What this PR does
|
||||
|
||||
Adds `migrations/1781000000002_reconcile-collections-columns.js` —
|
||||
captures the DDL effect of three historical scripts in a single new
|
||||
`node-pg-migrate` migration:
|
||||
|
||||
- `scripts/add-collaboration-features.js` — collections columns half
|
||||
(`visibility VARCHAR(20)`, `tcg VARCHAR(50)`, `tags TEXT`,
|
||||
`idx_collections_visibility` index)
|
||||
- `scripts/add-collection-slugs.js` — `slug VARCHAR(100) UNIQUE`,
|
||||
`idx_collections_slug` unique index, `check_slug_format` CHECK
|
||||
constraint
|
||||
- `scripts/add-image-column.js` — `image TEXT`
|
||||
|
||||
All ALTERs use `ADD COLUMN IF NOT EXISTS`. The CHECK constraint wraps
|
||||
in a `DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL END $$;`
|
||||
block because Postgres doesn't accept `ADD CONSTRAINT ... IF NOT EXISTS`
|
||||
for CHECK.
|
||||
|
||||
## What this PR does NOT do
|
||||
|
||||
- Does **NOT** edit any of the three source scripts (no-go-zones).
|
||||
- Does **NOT** capture the collaboration tables half of
|
||||
`add-collaboration-features.js` (`collection_permissions`,
|
||||
`collection_activity`, `users.is_pending`, related indexes,
|
||||
owner-permission backfill DML) — that's **Brief 3**.
|
||||
- Does **NOT** fold in the per-row slug backfill DML from
|
||||
`add-collection-slugs.js` — fresh envs have no pre-existing
|
||||
collections to backfill.
|
||||
- Does **NOT** unify the `is_public` / `visibility` redundancy
|
||||
(SCHEMA_MAP smell #2) — that's a future `unify-collection-visibility`
|
||||
follow-up.
|
||||
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
|
||||
or AGENTS.md.
|
||||
- Does **NOT** run `npm run migrate up` against any environment.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- [ ] `node --check migrations/1781000000002_reconcile-collections-columns.js` exits 0
|
||||
- [ ] `node -e "import('./migrations/1781000000002_reconcile-collections-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
|
||||
- [ ] `npm run lint` matches baseline
|
||||
- [ ] `npm run test:run` reports 21/21 passing
|
||||
- [ ] Did not run `npm run migrate up` against any environment in this PR
|
||||
|
||||
## Cross-references
|
||||
|
||||
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
|
||||
- Source scripts (no-go-zones, audit reference only):
|
||||
`scripts/add-collaboration-features.js`,
|
||||
`scripts/add-collection-slugs.js`,
|
||||
`scripts/add-image-column.js`
|
||||
```
|
||||
|
||||
## DO NOT
|
||||
|
||||
- DO NOT edit `scripts/add-collaboration-features.js`, `scripts/add-collection-slugs.js`, `scripts/add-image-column.js`, or any other file under `scripts/` — append-only no-go-zone.
|
||||
- DO NOT edit any other file under `migrations/`.
|
||||
- DO NOT edit `scripts/setup-neon-db.js`, `package.json`, README, `AGENTS.md`, or `docs/SCHEMA_MAP.md` (B7 owns SCHEMA_MAP).
|
||||
- DO NOT run `npm run migrate up` against any environment.
|
||||
- DO NOT include the per-row slug backfill DML — out of scope.
|
||||
- DO NOT add work for `collection_permissions` / `collection_activity` / `users.is_pending` — that's Brief 3.
|
||||
- DO NOT call `npm run migrate create` — it would generate a `Date.now()` timestamp colliding with B1/B3/B4/B5's reservation tokens.
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
Grouping all `collections`-table DDL into one migration matches the per-table grouping of D1 and keeps the diff focused on a single surface. Splitting `add-collaboration-features.js` between this brief (columns) and Brief 3 (tables) avoids creating an artificial dependency between two parallel briefs — each writes only its own new file. The CHECK-constraint exception block matches the historical script's try/catch pattern and is the idiomatic Postgres way to do idempotent CHECK adds.
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
---
|
||||
convoy: reconcile-historical-add-scripts
|
||||
brief_number: 3
|
||||
depends_on: []
|
||||
files:
|
||||
- migrations/1781000000003_reconcile-collaboration-tables.js
|
||||
---
|
||||
|
||||
# Brief 3: Reconcile collaboration tables into migration history
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Capture the `collection_permissions` + `collection_activity` table creation, the `users.is_pending` column, and the 3 related indexes from `scripts/add-collaboration-features.js` into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with the collaboration / sharing surface after `npm run setup-db`.
|
||||
|
||||
## Scope (files in scope — do not edit anything else)
|
||||
|
||||
- `migrations/1781000000003_reconcile-collaboration-tables.js` — **new**
|
||||
|
||||
## Source script (read-only audit reference; DO NOT EDIT — no-go-zone)
|
||||
|
||||
`scripts/add-collaboration-features.js` lines 24-81 (relevant DDL only; the collections-columns half is reconciled by Brief 2):
|
||||
|
||||
```js
|
||||
// collection_permissions table
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS collection_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
|
||||
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'pending', 'declined')),
|
||||
invite_token VARCHAR(255) UNIQUE,
|
||||
invited_by INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(collection_id, user_id)
|
||||
)
|
||||
`;
|
||||
|
||||
// collection_activity table
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS collection_activity (
|
||||
id SERIAL PRIMARY KEY,
|
||||
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
details JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`;
|
||||
|
||||
// users.is_pending column
|
||||
await sql`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS is_pending BOOLEAN DEFAULT false
|
||||
`;
|
||||
|
||||
// 3 indexes (the 4th — idx_collections_visibility — is Brief 2's scope)
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_permissions_collection_id
|
||||
ON collection_permissions(collection_id)
|
||||
`;
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_permissions_user_id
|
||||
ON collection_permissions(user_id)
|
||||
`;
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_activity_collection_id
|
||||
ON collection_activity(collection_id)
|
||||
`;
|
||||
```
|
||||
|
||||
**Owner-permission backfill DML** at lines 84-99 inserts `('owner', 'active')` rows for every pre-existing collection. **Do NOT fold the backfill DML into this migration.** Fresh envs have no pre-existing collections needing backfill; on prod, the backfill ran once historically and is baked in. The current runtime invariant for new collection creation lives in `pages/api/collections.js` (verify post-merge if needed — out of scope for this brief).
|
||||
|
||||
## Target migration file
|
||||
|
||||
Path: `migrations/1781000000003_reconcile-collaboration-tables.js`
|
||||
|
||||
Contents:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Reconcile the collaboration / sharing half of
|
||||
* scripts/add-collaboration-features.js into the migration history:
|
||||
*
|
||||
* - CREATE TABLE collection_permissions (with role/status CHECK
|
||||
* constraints inline + invite_token UNIQUE + UNIQUE(collection_id,
|
||||
* user_id))
|
||||
* - CREATE TABLE collection_activity (with JSONB details column)
|
||||
* - ALTER users ADD COLUMN is_pending BOOLEAN DEFAULT false
|
||||
* - 3 indexes (idx_collection_permissions_collection_id,
|
||||
* idx_collection_permissions_user_id,
|
||||
* idx_collection_activity_collection_id)
|
||||
*
|
||||
* The 4th index from the source script (idx_collections_visibility)
|
||||
* is reconciled by migrations/1781000000002_reconcile-collections-columns.js
|
||||
* because it indexes a column added in that brief.
|
||||
*
|
||||
* The collections-columns half (visibility, tcg, tags) of
|
||||
* add-collaboration-features.js is reconciled by
|
||||
* migrations/1781000000002_reconcile-collections-columns.js.
|
||||
*
|
||||
* The owner-permission backfill DML from the source script (INSERT
|
||||
* INTO collection_permissions ... 'owner', 'active' for every
|
||||
* pre-existing collection) is intentionally NOT folded in — fresh
|
||||
* envs have no pre-existing collections to backfill; on prod, the
|
||||
* backfill ran once historically and is baked in. The runtime
|
||||
* invariant for owner-permission creation on new collections is the
|
||||
* responsibility of pages/api/collections.js (out of scope here).
|
||||
*
|
||||
* Both CREATE TABLE statements use IF NOT EXISTS, with CHECK
|
||||
* constraints declared inline (no idempotency issue — IF NOT EXISTS
|
||||
* on the parent table makes the whole CREATE a no-op when the table
|
||||
* already exists, CHECK constraints and all).
|
||||
*
|
||||
* Idempotent re-apply: every statement uses IF NOT EXISTS.
|
||||
*
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
*/
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE IF NOT EXISTS collection_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
|
||||
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'pending', 'declined')),
|
||||
invite_token VARCHAR(255) UNIQUE,
|
||||
invited_by INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(collection_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collection_activity (
|
||||
id SERIAL PRIMARY KEY,
|
||||
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
details JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS is_pending BOOLEAN DEFAULT false;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_permissions_collection_id
|
||||
ON collection_permissions (collection_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_permissions_user_id
|
||||
ON collection_permissions (user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_activity_collection_id
|
||||
ON collection_activity (collection_id);
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Down-migration intentionally throws. Dropping collection_permissions
|
||||
* + collection_activity on a long-lived env would erase every active
|
||||
* sharing relationship + every audit trail row. The runtime in
|
||||
* pages/api/collections/[identifier]/permissions.js, pages/api/invite/*.js,
|
||||
* and lib/permission-middleware.js all read these tables; rolling back
|
||||
* would break the live app.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export const down = () => {
|
||||
throw new Error(
|
||||
'[migration:1781000000003_reconcile-collaboration-tables] Down not supported. ' +
|
||||
'Dropping collection_permissions / collection_activity would erase every ' +
|
||||
'sharing relationship and audit trail, and break runtime reads in ' +
|
||||
'pages/api/collections/[identifier]/permissions.js, pages/api/invite/*.js, ' +
|
||||
'lib/permission-middleware.js. Write a new dated migration for any future ' +
|
||||
'schema correction.'
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — `migrations/` is canonical; one migration per feature surface.
|
||||
- `.cursor/rules/no-go-zones.mdc` — `scripts/add-collaboration-features.js` is append-only history. **Do not edit it.**
|
||||
- Style: raw `pgm.sql(...)` template literals matching the other migrations. Per D2.
|
||||
- ESM exports; `"type": "module"`.
|
||||
- `IF NOT EXISTS` on every CREATE / ALTER. Inline CHECK constraints on `CREATE TABLE` are fine — `CREATE TABLE IF NOT EXISTS` skips the entire statement (constraints and all) when the table exists.
|
||||
- FK declarations match the source script verbatim (`ON DELETE CASCADE` for primary FKs, `ON DELETE SET NULL` where the source uses it).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `migrations/1781000000003_reconcile-collaboration-tables.js` exists with the exact filename above.
|
||||
- [ ] The file's `up()`:
|
||||
- Creates `collection_permissions` table with all 10 columns + role/status CHECKs + invite_token UNIQUE + UNIQUE(collection_id, user_id), all via `CREATE TABLE IF NOT EXISTS`.
|
||||
- Creates `collection_activity` table with 6 columns including JSONB `details`, via `CREATE TABLE IF NOT EXISTS`.
|
||||
- Adds `users.is_pending BOOLEAN DEFAULT false` via `ADD COLUMN IF NOT EXISTS`.
|
||||
- Creates 3 indexes via `CREATE INDEX IF NOT EXISTS`.
|
||||
- [ ] The 4th index from the source script (`idx_collections_visibility`) is NOT in this migration — it belongs to Brief 2.
|
||||
- [ ] The owner-permission backfill DML is NOT in this migration.
|
||||
- [ ] The file's `down()` throws with a clear message.
|
||||
- [ ] The file's docstring cites the source script + the convoy file + the Brief 2 split.
|
||||
- [ ] `node --check migrations/1781000000003_reconcile-collaboration-tables.js` passes.
|
||||
- [ ] `node -e "import('./migrations/1781000000003_reconcile-collaboration-tables.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`.
|
||||
- [ ] `npm run lint` matches baseline.
|
||||
- [ ] `npm run test:run` reports 21/21 passing.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
node --check migrations/1781000000003_reconcile-collaboration-tables.js
|
||||
node -e "import('./migrations/1781000000003_reconcile-collaboration-tables.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
|
||||
npm run lint
|
||||
npm run test:run
|
||||
```
|
||||
|
||||
Expected: `node --check` silent, `node -e` prints `function function undefined`, lint baseline, 21/21 tests pass.
|
||||
|
||||
**Do NOT** run `npm run migrate up` against any environment.
|
||||
|
||||
## Commit message
|
||||
|
||||
```
|
||||
feat(migrations): reconcile collaboration tables into migration history (brief 3/7)
|
||||
|
||||
Captures the collaboration / sharing half of
|
||||
scripts/add-collaboration-features.js into one new node-pg-migrate
|
||||
migration:
|
||||
|
||||
- CREATE TABLE collection_permissions (role/status CHECKs,
|
||||
invite_token UNIQUE, UNIQUE(collection_id, user_id))
|
||||
- CREATE TABLE collection_activity (JSONB details)
|
||||
- ALTER users ADD COLUMN is_pending BOOLEAN DEFAULT false
|
||||
- 3 indexes
|
||||
|
||||
The collections-columns half (visibility, tcg, tags +
|
||||
idx_collections_visibility) is reconciled by Brief 2. The 4th index
|
||||
(idx_collections_visibility) belongs to Brief 2 because it indexes a
|
||||
column added there.
|
||||
|
||||
Per-row owner-permission backfill DML from the source script is
|
||||
intentionally NOT folded in — fresh envs have no pre-existing
|
||||
collections to backfill; the runtime invariant for new-collection
|
||||
owner-perm creation lives in pages/api/collections.js.
|
||||
|
||||
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B3.
|
||||
Idempotent re-apply (IF NOT EXISTS guards).
|
||||
```
|
||||
|
||||
## PR shape
|
||||
|
||||
**Title:** `feat(migrations): reconcile collaboration tables into migration history (brief 3/7)`
|
||||
|
||||
**Body template:**
|
||||
|
||||
```markdown
|
||||
Brief 3 of the `reconcile-historical-add-scripts` convoy. See
|
||||
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
|
||||
for the full plan and rationale.
|
||||
|
||||
## What this PR does
|
||||
|
||||
Adds `migrations/1781000000003_reconcile-collaboration-tables.js` —
|
||||
captures the collaboration / sharing DDL of
|
||||
`scripts/add-collaboration-features.js` (the table-and-column half;
|
||||
the columns-on-collections half is Brief 2).
|
||||
|
||||
- `CREATE TABLE IF NOT EXISTS collection_permissions` (10 columns
|
||||
including inline role/status CHECK constraints + invite_token
|
||||
UNIQUE + UNIQUE(collection_id, user_id))
|
||||
- `CREATE TABLE IF NOT EXISTS collection_activity` (6 columns
|
||||
including JSONB `details`)
|
||||
- `ALTER TABLE users ADD COLUMN IF NOT EXISTS is_pending BOOLEAN DEFAULT false`
|
||||
- 3 indexes via `CREATE INDEX IF NOT EXISTS`
|
||||
|
||||
## What this PR does NOT do
|
||||
|
||||
- Does **NOT** edit `scripts/add-collaboration-features.js` (no-go-zone).
|
||||
- Does **NOT** capture the columns-on-collections half (`visibility`,
|
||||
`tcg`, `tags`, `idx_collections_visibility`) — that's **Brief 2**.
|
||||
- Does **NOT** fold in the per-row owner-permission backfill DML from
|
||||
the source script — fresh envs have no pre-existing collections to
|
||||
backfill.
|
||||
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
|
||||
`AGENTS.md`, or `docs/SCHEMA_MAP.md` (B7 owns SCHEMA_MAP).
|
||||
- Does **NOT** run `npm run migrate up` against any environment.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- [ ] `node --check migrations/1781000000003_reconcile-collaboration-tables.js` exits 0
|
||||
- [ ] `node -e "import('./migrations/1781000000003_reconcile-collaboration-tables.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
|
||||
- [ ] `npm run lint` matches baseline
|
||||
- [ ] `npm run test:run` reports 21/21 passing
|
||||
- [ ] Did not run `npm run migrate up` against any environment in this PR
|
||||
|
||||
## Cross-references
|
||||
|
||||
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
|
||||
- Source script (no-go-zone, audit reference only):
|
||||
`scripts/add-collaboration-features.js`
|
||||
```
|
||||
|
||||
## DO NOT
|
||||
|
||||
- DO NOT edit `scripts/add-collaboration-features.js` or any file under `scripts/`.
|
||||
- DO NOT edit any other file under `migrations/`.
|
||||
- DO NOT edit `scripts/setup-neon-db.js`, `package.json`, README, `AGENTS.md`, or `docs/SCHEMA_MAP.md`.
|
||||
- DO NOT run `npm run migrate up` against any environment.
|
||||
- DO NOT include the owner-permission backfill DML.
|
||||
- DO NOT add work for the collections-columns half (`visibility`, `tcg`, `tags`, `idx_collections_visibility`) — that's Brief 2.
|
||||
- DO NOT call `npm run migrate create`.
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
Splitting `add-collaboration-features.js` between Brief 2 (collections columns + the visibility index that indexes one of those columns) and Brief 3 (collaboration tables + the users.is_pending column + the 3 indexes that index collaboration-table columns) keeps each migration scoped to the table surface it touches, matching D1. Inline CHECK constraints on `CREATE TABLE` are idempotent for free under `CREATE TABLE IF NOT EXISTS` (the whole statement no-ops when the table exists). The backfill DML is intentionally out of scope because fresh envs need no backfill and prod's backfill already ran.
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
---
|
||||
convoy: reconcile-historical-add-scripts
|
||||
brief_number: 4
|
||||
depends_on: []
|
||||
files:
|
||||
- migrations/1781000000004_reconcile-favorites-system.js
|
||||
---
|
||||
|
||||
# Brief 4: Reconcile favorites system into migration history
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Capture the `user_favorites` table and its 4 indexes from `scripts/add-favorites-system.js` into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with the favorites surface after `npm run setup-db`.
|
||||
|
||||
## Scope (files in scope — do not edit anything else)
|
||||
|
||||
- `migrations/1781000000004_reconcile-favorites-system.js` — **new**
|
||||
|
||||
## Source script (read-only audit reference; DO NOT EDIT — no-go-zone)
|
||||
|
||||
`scripts/add-favorites-system.js` lines 11-27 (entire DDL — script has no DML beyond the table create):
|
||||
|
||||
```js
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS user_favorites (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
item_type VARCHAR(50) NOT NULL, -- 'card', 'collection', 'deck'
|
||||
item_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, item_type, item_id)
|
||||
)
|
||||
`;
|
||||
|
||||
// 4 indexes
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_user_id ON user_favorites(user_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_item_type ON user_favorites(item_type)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_item_id ON user_favorites(item_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_user_type ON user_favorites(user_id, item_type)`;
|
||||
```
|
||||
|
||||
**Note on SCHEMA_MAP drift.** `docs/SCHEMA_MAP.md` § `user_favorites` (lines 104-111) shows the table with only `(user_id, card_id)` columns — that's a **doc bug**. The actual source script (and prod schema) uses the polymorphic `(item_type, item_id)` shape that supports cards, collections, AND decks (per the script's inline comment + the runtime usage in `pages/api/favorites.js`). B7 corrects the SCHEMA_MAP entry. This brief faithfully reproduces the **script's** shape — `(item_type, item_id)` — not the doc's.
|
||||
|
||||
## Target migration file
|
||||
|
||||
Path: `migrations/1781000000004_reconcile-favorites-system.js`
|
||||
|
||||
Contents:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Reconcile scripts/add-favorites-system.js into the migration history.
|
||||
*
|
||||
* Creates user_favorites with the polymorphic (item_type, item_id)
|
||||
* shape that supports favoriting cards, collections, and decks via a
|
||||
* single table. Adds 4 indexes for the common query shapes:
|
||||
*
|
||||
* - idx_user_favorites_user_id — "all favorites for user X"
|
||||
* - idx_user_favorites_item_type — "all card favorites" / "all deck favorites"
|
||||
* - idx_user_favorites_item_id — back-link from an item to its favoriters
|
||||
* - idx_user_favorites_user_type — composite for "user X's card favorites"
|
||||
*
|
||||
* Note: docs/SCHEMA_MAP.md § user_favorites currently shows only
|
||||
* (user_id, card_id) — that's a doc bug. The actual prod shape (and
|
||||
* the source script, and the runtime in pages/api/favorites.js) uses
|
||||
* the polymorphic shape. B7 of this convoy corrects the SCHEMA_MAP
|
||||
* entry; this migration reproduces the script's shape faithfully.
|
||||
*
|
||||
* Idempotent re-apply: CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
|
||||
*
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
*/
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE IF NOT EXISTS user_favorites (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
item_type VARCHAR(50) NOT NULL,
|
||||
item_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, item_type, item_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_favorites_user_id
|
||||
ON user_favorites (user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_favorites_item_type
|
||||
ON user_favorites (item_type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_favorites_item_id
|
||||
ON user_favorites (item_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_favorites_user_type
|
||||
ON user_favorites (user_id, item_type);
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Down-migration intentionally throws. Dropping user_favorites on a
|
||||
* long-lived env would erase every user's saved favorites list and
|
||||
* break runtime reads in pages/api/favorites.js.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export const down = () => {
|
||||
throw new Error(
|
||||
'[migration:1781000000004_reconcile-favorites-system] Down not supported. ' +
|
||||
'Dropping user_favorites would erase every saved favorite and break ' +
|
||||
'pages/api/favorites.js. Write a new dated migration for any future ' +
|
||||
'schema correction.'
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — `migrations/` is canonical.
|
||||
- `.cursor/rules/no-go-zones.mdc` — `scripts/add-favorites-system.js` is append-only history.
|
||||
- Style: raw `pgm.sql(...)` template literals. Per D2.
|
||||
- ESM exports; `"type": "module"`.
|
||||
- `IF NOT EXISTS` on every CREATE.
|
||||
- Match the source script's column types, FK ON DELETE rule (`CASCADE`), and UNIQUE shape verbatim.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `migrations/1781000000004_reconcile-favorites-system.js` exists with the exact filename above.
|
||||
- [ ] The file's `up()` creates `user_favorites` with all 5 columns + UNIQUE constraint, then 4 indexes, all via `IF NOT EXISTS`.
|
||||
- [ ] The column shape is `(item_type VARCHAR(50), item_id INTEGER)` — the polymorphic shape — NOT `(card_id)`. See "Note on SCHEMA_MAP drift" above.
|
||||
- [ ] The file's `down()` throws with a clear message.
|
||||
- [ ] The file's docstring cites the source script + the convoy file + the SCHEMA_MAP doc-bug note.
|
||||
- [ ] `node --check migrations/1781000000004_reconcile-favorites-system.js` passes.
|
||||
- [ ] `node -e "import('./migrations/1781000000004_reconcile-favorites-system.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`.
|
||||
- [ ] `npm run lint` matches baseline.
|
||||
- [ ] `npm run test:run` reports 21/21 passing.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
node --check migrations/1781000000004_reconcile-favorites-system.js
|
||||
node -e "import('./migrations/1781000000004_reconcile-favorites-system.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
|
||||
npm run lint
|
||||
npm run test:run
|
||||
```
|
||||
|
||||
Expected: `node --check` silent, `node -e` prints `function function undefined`, lint baseline, 21/21 tests pass.
|
||||
|
||||
**Do NOT** run `npm run migrate up` against any environment.
|
||||
|
||||
## Commit message
|
||||
|
||||
```
|
||||
feat(migrations): reconcile favorites system into migration history (brief 4/7)
|
||||
|
||||
Captures scripts/add-favorites-system.js into one new node-pg-migrate
|
||||
migration:
|
||||
|
||||
- CREATE TABLE user_favorites (polymorphic (item_type, item_id)
|
||||
shape; UNIQUE(user_id, item_type, item_id))
|
||||
- 4 indexes (user_id, item_type, item_id, (user_id, item_type))
|
||||
|
||||
Note: docs/SCHEMA_MAP.md § user_favorites currently shows the table
|
||||
with only (user_id, card_id) — that's a doc bug; B7 of this convoy
|
||||
corrects the entry. This migration faithfully reproduces the source
|
||||
script's polymorphic shape (also what runtime in pages/api/favorites.js
|
||||
uses).
|
||||
|
||||
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B4.
|
||||
Idempotent re-apply (IF NOT EXISTS guards).
|
||||
```
|
||||
|
||||
## PR shape
|
||||
|
||||
**Title:** `feat(migrations): reconcile favorites system into migration history (brief 4/7)`
|
||||
|
||||
**Body template:**
|
||||
|
||||
```markdown
|
||||
Brief 4 of the `reconcile-historical-add-scripts` convoy. See
|
||||
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
|
||||
for the full plan and rationale.
|
||||
|
||||
## What this PR does
|
||||
|
||||
Adds `migrations/1781000000004_reconcile-favorites-system.js` —
|
||||
captures `scripts/add-favorites-system.js` into a single new
|
||||
`node-pg-migrate` migration.
|
||||
|
||||
- `CREATE TABLE IF NOT EXISTS user_favorites` with polymorphic
|
||||
`(item_type VARCHAR(50), item_id INTEGER)` shape supporting
|
||||
cards / collections / decks favorites in one table.
|
||||
- `UNIQUE(user_id, item_type, item_id)` prevents duplicate favorites.
|
||||
- 4 supporting indexes via `CREATE INDEX IF NOT EXISTS`.
|
||||
|
||||
## Note on a SCHEMA_MAP doc bug
|
||||
|
||||
`docs/SCHEMA_MAP.md` § `user_favorites` (current `main`) shows the
|
||||
table with only `(user_id, card_id)` columns. That's a doc bug — the
|
||||
actual prod shape (and the source script, and `pages/api/favorites.js`
|
||||
runtime usage) uses the polymorphic `(item_type, item_id)` shape. This
|
||||
migration faithfully reproduces the script's shape; Brief 7 of this
|
||||
convoy fixes the SCHEMA_MAP entry.
|
||||
|
||||
## What this PR does NOT do
|
||||
|
||||
- Does **NOT** edit `scripts/add-favorites-system.js` (no-go-zone).
|
||||
- Does **NOT** edit `docs/SCHEMA_MAP.md` (Brief 7's job).
|
||||
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
|
||||
or `AGENTS.md`.
|
||||
- Does **NOT** run `npm run migrate up` against any environment.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- [ ] `node --check migrations/1781000000004_reconcile-favorites-system.js` exits 0
|
||||
- [ ] `node -e "import('./migrations/1781000000004_reconcile-favorites-system.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
|
||||
- [ ] `npm run lint` matches baseline
|
||||
- [ ] `npm run test:run` reports 21/21 passing
|
||||
- [ ] Did not run `npm run migrate up` against any environment in this PR
|
||||
|
||||
## Cross-references
|
||||
|
||||
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
|
||||
- Source script (no-go-zone, audit reference only): `scripts/add-favorites-system.js`
|
||||
```
|
||||
|
||||
## DO NOT
|
||||
|
||||
- DO NOT edit `scripts/add-favorites-system.js` or any file under `scripts/`.
|
||||
- DO NOT edit any other file under `migrations/`.
|
||||
- DO NOT edit `scripts/setup-neon-db.js`, `package.json`, README, `AGENTS.md`, or `docs/SCHEMA_MAP.md` (B7 owns SCHEMA_MAP).
|
||||
- DO NOT run `npm run migrate up` against any environment.
|
||||
- DO NOT use the SCHEMA_MAP `(user_id, card_id)` shape — that doc entry is wrong; reproduce the SOURCE SCRIPT'S polymorphic shape.
|
||||
- DO NOT call `npm run migrate create`.
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
The favorites system is a single self-contained table — natural fit for one small migration that maps 1:1 with the historical script. The polymorphic shape (`item_type`, `item_id`) is what runtime code actually uses, so reproducing it from the script's verbatim DDL — rather than the stale `(user_id, card_id)` doc entry — is the safe choice. Brief 7's SCHEMA_MAP rewrite resolves the doc-bug separately so this brief stays scoped to one new file.
|
||||
|
|
@ -0,0 +1,425 @@
|
|||
---
|
||||
convoy: reconcile-historical-add-scripts
|
||||
brief_number: 5
|
||||
depends_on: []
|
||||
files:
|
||||
- migrations/1781000000005_reconcile-user-profile.js
|
||||
---
|
||||
|
||||
# Brief 5: Reconcile user profile fields into migration history
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Capture the deduplicated union of `scripts/add-user-profile-columns.js` and `scripts/add-user-profile-fields.js` (15 new `users` columns, 2 new tables `user_settings` + `user_avatars`, 6 CHECK constraints, 6 indexes) into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with the user-profile surface after `npm run setup-db`.
|
||||
|
||||
## Scope (files in scope — do not edit anything else)
|
||||
|
||||
- `migrations/1781000000005_reconcile-user-profile.js` — **new**
|
||||
|
||||
## Source scripts (read-only audit reference; DO NOT EDIT — both are no-go-zones)
|
||||
|
||||
### `scripts/add-user-profile-columns.js` lines 12-18 (the earlier, narrower script):
|
||||
|
||||
```js
|
||||
await sql`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS first_name VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS last_name VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS username VARCHAR(255) UNIQUE,
|
||||
ADD COLUMN IF NOT EXISTS profile_image_url TEXT
|
||||
`;
|
||||
```
|
||||
|
||||
Plus per-row UPDATE backfill of defaults (lines 25-42). **Do NOT fold the backfill DML into this migration** — fresh envs have no rows to backfill; column DEFAULTs handle new rows.
|
||||
|
||||
### `scripts/add-user-profile-fields.js` lines 27-115 (the later, broader script — superset of #8 plus additional columns + 2 new tables + 6 CHECK constraints + 6 indexes):
|
||||
|
||||
```js
|
||||
// Basic profile fields (overlaps add-user-profile-columns.js for first_name/last_name/username,
|
||||
// but ADDS bio + avatar_url; idempotent overlap because of IF NOT EXISTS)
|
||||
await sql`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS first_name VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS last_name VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS username VARCHAR(255) UNIQUE,
|
||||
ADD COLUMN IF NOT EXISTS bio TEXT,
|
||||
ADD COLUMN IF NOT EXISTS avatar_url TEXT
|
||||
`;
|
||||
|
||||
// Preference fields
|
||||
await sql`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS favorite_games JSONB DEFAULT '["MTG"]',
|
||||
ADD COLUMN IF NOT EXISTS collection_visibility VARCHAR(20) DEFAULT 'private',
|
||||
ADD COLUMN IF NOT EXISTS preferred_currency VARCHAR(3) DEFAULT 'USD',
|
||||
ADD COLUMN IF NOT EXISTS cards_per_page INTEGER DEFAULT 50,
|
||||
ADD COLUMN IF NOT EXISTS default_view VARCHAR(10) DEFAULT 'grid'
|
||||
`;
|
||||
|
||||
// Notification + 2FA settings
|
||||
await sql`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS notifications_email BOOLEAN DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS notifications_marketing BOOLEAN DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS two_factor_enabled BOOLEAN DEFAULT false
|
||||
`;
|
||||
|
||||
// Display settings
|
||||
await sql`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS theme VARCHAR(10) DEFAULT 'system',
|
||||
ADD COLUMN IF NOT EXISTS language VARCHAR(5) DEFAULT 'en'
|
||||
`;
|
||||
|
||||
// user_settings table
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
setting_key VARCHAR(100) NOT NULL,
|
||||
setting_value JSONB NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, setting_key)
|
||||
)
|
||||
`;
|
||||
|
||||
// user_avatars table
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS user_avatars (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
original_name VARCHAR(255),
|
||||
mime_type VARCHAR(100),
|
||||
file_size INTEGER,
|
||||
file_path TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`;
|
||||
|
||||
// 6 indexes
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_settings_key ON user_settings(setting_key)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_avatars_user_id ON user_avatars(user_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_user_avatars_active ON user_avatars(user_id, is_active)`;
|
||||
|
||||
// 6 CHECK constraints (each wrapped in JS try/catch to swallow 'already exists')
|
||||
await sql`ALTER TABLE users ADD CONSTRAINT check_collection_visibility CHECK (collection_visibility IN ('private', 'public', 'unlisted'))`;
|
||||
await sql`ALTER TABLE users ADD CONSTRAINT check_preferred_currency CHECK (preferred_currency IN ('USD', 'EUR', 'GBP', 'CAD', 'JPY'))`;
|
||||
await sql`ALTER TABLE users ADD CONSTRAINT check_cards_per_page CHECK (cards_per_page IN (25, 50, 100))`;
|
||||
await sql`ALTER TABLE users ADD CONSTRAINT check_default_view CHECK (default_view IN ('grid', 'list'))`;
|
||||
await sql`ALTER TABLE users ADD CONSTRAINT check_theme CHECK (theme IN ('light', 'dark', 'system'))`;
|
||||
await sql`ALTER TABLE users ADD CONSTRAINT check_language CHECK (language IN ('en', 'es', 'fr', 'de', 'ja'))`;
|
||||
```
|
||||
|
||||
Plus a per-row UPDATE backfill of defaults (lines 198-222). **Do NOT fold the backfill DML in** — column DEFAULTs handle new rows; fresh envs have no rows to backfill.
|
||||
|
||||
### Important note on the column dedup
|
||||
|
||||
Scripts #8 and #9 overlap on `first_name`, `last_name`, `username` — both use `ADD COLUMN IF NOT EXISTS` so on prod the second-running script no-ops those three columns. Both scripts have run on prod, so the union of their columns is what's actually present:
|
||||
|
||||
- From #8 only: `profile_image_url` (TEXT) — a column that ONLY #8 added.
|
||||
- From #9 only: `bio`, `avatar_url`, `favorite_games`, `collection_visibility`, `preferred_currency`, `cards_per_page`, `default_view`, `notifications_email`, `notifications_marketing`, `two_factor_enabled`, `theme`, `language` (12 columns) + the 2 new tables + 6 CHECK constraints + 6 indexes.
|
||||
- From both (idempotent overlap): `first_name`, `last_name`, `username`.
|
||||
|
||||
The migration must add **all 15 columns** (4 from #8 ∪ 12 from #9 with 3 in the intersection = 4 + 12 - 3 = 13 unique users columns; wait, let me recount: #8 adds 4 (first_name, last_name, username, profile_image_url); #9 adds 5 basic (first_name, last_name, username, bio, avatar_url) + 5 prefs + 3 notif + 2 display = 15. Union: first_name, last_name, username (shared) + profile_image_url (#8) + bio, avatar_url, favorite_games, collection_visibility, preferred_currency, cards_per_page, default_view, notifications_email, notifications_marketing, two_factor_enabled, theme, language (#9) = **3 + 1 + 12 = 16 columns**). So the migration adds 16 columns to `users`.
|
||||
|
||||
The redundant `profile_image_url` vs `avatar_url` pair is documented in `docs/SCHEMA_MAP.md` § "Known schema smells" #1 and surfaced as the `unify-user-avatar-column` follow-up. Both must be in fresh envs for parity.
|
||||
|
||||
## Target migration file
|
||||
|
||||
Path: `migrations/1781000000005_reconcile-user-profile.js`
|
||||
|
||||
Contents:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Reconcile two overlapping historical user-profile scripts into the
|
||||
* migration history, taking the union of their effects:
|
||||
*
|
||||
* - scripts/add-user-profile-columns.js (the earlier, narrower
|
||||
* script): first_name, last_name, username UNIQUE, profile_image_url
|
||||
*
|
||||
* - scripts/add-user-profile-fields.js (the later, broader script;
|
||||
* overlaps the earlier script on first_name / last_name / username
|
||||
* and additionally adds): bio, avatar_url, favorite_games (JSONB
|
||||
* DEFAULT '["MTG"]'), collection_visibility, preferred_currency,
|
||||
* cards_per_page, default_view, notifications_email,
|
||||
* notifications_marketing, two_factor_enabled, theme, language,
|
||||
* + the new user_settings + user_avatars tables, + 6 CHECK
|
||||
* constraints, + 6 indexes.
|
||||
*
|
||||
* Result on a fresh env: 16 new columns on `users`, 2 new tables,
|
||||
* 6 CHECK constraints, 6 indexes. On any long-lived env: every
|
||||
* statement is a no-op (IF NOT EXISTS / DO $$ EXCEPTION).
|
||||
*
|
||||
* Per-row UPDATE backfills from both scripts are intentionally NOT
|
||||
* folded in — column DEFAULTs handle new rows; fresh envs have no
|
||||
* rows to backfill.
|
||||
*
|
||||
* The profile_image_url / avatar_url redundancy is intentional for
|
||||
* parity with prod and is flagged in docs/SCHEMA_MAP.md § "Known
|
||||
* schema smells" #1; future cleanup is the queued
|
||||
* `unify-user-avatar-column` follow-up.
|
||||
*
|
||||
* CHECK constraint adds wrap in DO $$ ... EXCEPTION WHEN
|
||||
* duplicate_object THEN NULL END $$ because Postgres doesn't accept
|
||||
* ADD CONSTRAINT ... IF NOT EXISTS for CHECK. Each constraint gets
|
||||
* its own DO block so a failure in one doesn't block the rest.
|
||||
*
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
*/
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
-- 16 columns on users (union of add-user-profile-columns.js + add-user-profile-fields.js)
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS first_name VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS last_name VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS username VARCHAR(255) UNIQUE,
|
||||
ADD COLUMN IF NOT EXISTS profile_image_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS bio TEXT,
|
||||
ADD COLUMN IF NOT EXISTS avatar_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS favorite_games JSONB DEFAULT '["MTG"]',
|
||||
ADD COLUMN IF NOT EXISTS collection_visibility VARCHAR(20) DEFAULT 'private',
|
||||
ADD COLUMN IF NOT EXISTS preferred_currency VARCHAR(3) DEFAULT 'USD',
|
||||
ADD COLUMN IF NOT EXISTS cards_per_page INTEGER DEFAULT 50,
|
||||
ADD COLUMN IF NOT EXISTS default_view VARCHAR(10) DEFAULT 'grid',
|
||||
ADD COLUMN IF NOT EXISTS notifications_email BOOLEAN DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS notifications_marketing BOOLEAN DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS two_factor_enabled BOOLEAN DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS theme VARCHAR(10) DEFAULT 'system',
|
||||
ADD COLUMN IF NOT EXISTS language VARCHAR(5) DEFAULT 'en';
|
||||
|
||||
-- user_settings table
|
||||
CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
setting_key VARCHAR(100) NOT NULL,
|
||||
setting_value JSONB NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, setting_key)
|
||||
);
|
||||
|
||||
-- user_avatars table
|
||||
CREATE TABLE IF NOT EXISTS user_avatars (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
original_name VARCHAR(255),
|
||||
mime_type VARCHAR(100),
|
||||
file_size INTEGER,
|
||||
file_path TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 6 indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users (username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_settings_key ON user_settings (setting_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_avatars_user_id ON user_avatars (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_avatars_active ON user_avatars (user_id, is_active);
|
||||
|
||||
-- 6 CHECK constraints (each in its own DO block so one failure doesn't block the rest)
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE users ADD CONSTRAINT check_collection_visibility
|
||||
CHECK (collection_visibility IN ('private', 'public', 'unlisted'));
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE users ADD CONSTRAINT check_preferred_currency
|
||||
CHECK (preferred_currency IN ('USD', 'EUR', 'GBP', 'CAD', 'JPY'));
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE users ADD CONSTRAINT check_cards_per_page
|
||||
CHECK (cards_per_page IN (25, 50, 100));
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE users ADD CONSTRAINT check_default_view
|
||||
CHECK (default_view IN ('grid', 'list'));
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE users ADD CONSTRAINT check_theme
|
||||
CHECK (theme IN ('light', 'dark', 'system'));
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE users ADD CONSTRAINT check_language
|
||||
CHECK (language IN ('en', 'es', 'fr', 'de', 'ja'));
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Down-migration intentionally throws. Dropping 16 user-profile columns
|
||||
* + user_settings + user_avatars on a long-lived env would erase every
|
||||
* user's profile data, preferences, avatar history, and settings, and
|
||||
* break runtime reads in pages/api/user/{settings,profile,avatar}.js.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export const down = () => {
|
||||
throw new Error(
|
||||
'[migration:1781000000005_reconcile-user-profile] Down not supported. ' +
|
||||
'Dropping these columns + tables would erase every user profile, preference, ' +
|
||||
'avatar history, and settings row, and break runtime reads in ' +
|
||||
'pages/api/user/{settings,profile,avatar}.js. Write a new dated migration ' +
|
||||
'for any future schema correction.'
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — `migrations/` is canonical.
|
||||
- `.cursor/rules/no-go-zones.mdc` — both source scripts are append-only history.
|
||||
- Style: raw `pgm.sql(...)` template literals. Per D2.
|
||||
- ESM exports; `"type": "module"`.
|
||||
- `IF NOT EXISTS` on every ALTER + CREATE INDEX + CREATE TABLE.
|
||||
- CHECK constraints in `DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL END $$;` blocks, one block per constraint.
|
||||
- Match the source scripts' column types, defaults, FK rules, and CHECK values verbatim.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `migrations/1781000000005_reconcile-user-profile.js` exists with the exact filename above.
|
||||
- [ ] The file's `up()` adds exactly **16 columns** to `users` (per the dedup count in the source-scripts note above), creates **2 tables** (`user_settings`, `user_avatars`), creates **6 indexes**, and adds **6 CHECK constraints** each in its own `DO $$ EXCEPTION` block.
|
||||
- [ ] BOTH `profile_image_url` AND `avatar_url` are present (parity with prod; redundancy is documented).
|
||||
- [ ] The file's `down()` throws with a clear message.
|
||||
- [ ] The file's docstring cites both source scripts + the convoy file + the `unify-user-avatar-column` follow-up.
|
||||
- [ ] Per-row UPDATE backfill DML is NOT in the migration.
|
||||
- [ ] `node --check migrations/1781000000005_reconcile-user-profile.js` passes.
|
||||
- [ ] `node -e "import('./migrations/1781000000005_reconcile-user-profile.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`.
|
||||
- [ ] `npm run lint` matches baseline.
|
||||
- [ ] `npm run test:run` reports 21/21 passing.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
node --check migrations/1781000000005_reconcile-user-profile.js
|
||||
node -e "import('./migrations/1781000000005_reconcile-user-profile.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
|
||||
npm run lint
|
||||
npm run test:run
|
||||
```
|
||||
|
||||
Expected: `node --check` silent, `node -e` prints `function function undefined`, lint baseline, 21/21 tests pass.
|
||||
|
||||
**Do NOT** run `npm run migrate up` against any environment.
|
||||
|
||||
## Commit message
|
||||
|
||||
```
|
||||
feat(migrations): reconcile user profile fields into migration history (brief 5/7)
|
||||
|
||||
Captures the deduplicated union of scripts/add-user-profile-columns.js
|
||||
and scripts/add-user-profile-fields.js into one new node-pg-migrate
|
||||
migration:
|
||||
|
||||
- 16 columns on users (basic profile + preferences + notifications + display)
|
||||
- 2 new tables (user_settings, user_avatars)
|
||||
- 6 CHECK constraints (each in its own DO $$ EXCEPTION block)
|
||||
- 6 indexes
|
||||
|
||||
Both profile_image_url (from script #1) and avatar_url (from script #2)
|
||||
are added for parity with prod. The redundancy is flagged in
|
||||
docs/SCHEMA_MAP.md § "Known schema smells" #1 and queued for cleanup as
|
||||
the `unify-user-avatar-column` follow-up.
|
||||
|
||||
Per-row UPDATE backfill DML from both scripts is intentionally NOT
|
||||
folded in — column DEFAULTs handle new rows; fresh envs have no rows
|
||||
to backfill.
|
||||
|
||||
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B5.
|
||||
Idempotent re-apply (IF NOT EXISTS guards + per-CHECK DO blocks).
|
||||
```
|
||||
|
||||
## PR shape
|
||||
|
||||
**Title:** `feat(migrations): reconcile user profile fields into migration history (brief 5/7)`
|
||||
|
||||
**Body template:**
|
||||
|
||||
```markdown
|
||||
Brief 5 of the `reconcile-historical-add-scripts` convoy. See
|
||||
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
|
||||
for the full plan and rationale.
|
||||
|
||||
## What this PR does
|
||||
|
||||
Adds `migrations/1781000000005_reconcile-user-profile.js` — captures
|
||||
the deduplicated union of `scripts/add-user-profile-columns.js`
|
||||
(earlier, narrower) and `scripts/add-user-profile-fields.js` (later,
|
||||
broader) into a single new `node-pg-migrate` migration.
|
||||
|
||||
- 16 new columns on `users` (first_name, last_name, username UNIQUE,
|
||||
profile_image_url, bio, avatar_url, favorite_games JSONB, …, theme,
|
||||
language) — all `ADD COLUMN IF NOT EXISTS`
|
||||
- 2 new tables: `user_settings`, `user_avatars` — both
|
||||
`CREATE TABLE IF NOT EXISTS`
|
||||
- 6 indexes — `CREATE INDEX IF NOT EXISTS`
|
||||
- 6 CHECK constraints (each in its own
|
||||
`DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL END $$;` block)
|
||||
|
||||
## Why both `profile_image_url` and `avatar_url`?
|
||||
|
||||
Script #1 added `profile_image_url`, script #2 added `avatar_url` —
|
||||
both exist on prod and both are in this migration for fresh-env
|
||||
parity. The redundancy is flagged in
|
||||
`docs/SCHEMA_MAP.md` § "Known schema smells" #1 and queued for cleanup
|
||||
as the `unify-user-avatar-column` follow-up.
|
||||
|
||||
## What this PR does NOT do
|
||||
|
||||
- Does **NOT** edit either source script (no-go-zones).
|
||||
- Does **NOT** fold in the per-row UPDATE backfill DML from either
|
||||
source script — column DEFAULTs handle new rows.
|
||||
- Does **NOT** unify `profile_image_url` / `avatar_url` — that's the
|
||||
`unify-user-avatar-column` follow-up.
|
||||
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
|
||||
`AGENTS.md`, or `docs/SCHEMA_MAP.md` (B7 owns SCHEMA_MAP).
|
||||
- Does **NOT** run `npm run migrate up` against any environment.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- [ ] `node --check migrations/1781000000005_reconcile-user-profile.js` exits 0
|
||||
- [ ] `node -e "import('./migrations/1781000000005_reconcile-user-profile.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
|
||||
- [ ] `npm run lint` matches baseline
|
||||
- [ ] `npm run test:run` reports 21/21 passing
|
||||
- [ ] Did not run `npm run migrate up` against any environment in this PR
|
||||
|
||||
## Cross-references
|
||||
|
||||
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
|
||||
- Source scripts (no-go-zones, audit reference only):
|
||||
`scripts/add-user-profile-columns.js`, `scripts/add-user-profile-fields.js`
|
||||
```
|
||||
|
||||
## DO NOT
|
||||
|
||||
- DO NOT edit either source script or any file under `scripts/`.
|
||||
- DO NOT edit any other file under `migrations/`.
|
||||
- DO NOT edit `scripts/setup-neon-db.js`, `package.json`, README, `AGENTS.md`, or `docs/SCHEMA_MAP.md`.
|
||||
- DO NOT run `npm run migrate up` against any environment.
|
||||
- DO NOT pick one of `profile_image_url` / `avatar_url` to omit — both must be present for parity.
|
||||
- DO NOT collapse the 6 CHECK constraints into a single `DO $$ EXCEPTION` block — one block per constraint so one duplicate doesn't swallow the others.
|
||||
- DO NOT include the per-row UPDATE backfill DML.
|
||||
- DO NOT call `npm run migrate create`.
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
This is the largest single migration in the convoy because both historical scripts are tightly coupled to the `users` table surface and splitting them would create artificial boundaries (e.g., separating "users columns" from "CHECK constraints on users columns" makes no sense). Per-CHECK `DO $$ EXCEPTION` blocks mirror the historical scripts' per-statement try/catch pattern and ensure one duplicate-constraint failure doesn't block the rest. Keeping both avatar-style columns matches prod-as-is and explicitly defers the cleanup to a scoped follow-up convoy.
|
||||
|
|
@ -0,0 +1,457 @@
|
|||
---
|
||||
convoy: reconcile-historical-add-scripts
|
||||
brief_number: 7
|
||||
depends_on: [1, 2, 3, 4, 5]
|
||||
files:
|
||||
- docs/SCHEMA_MAP.md
|
||||
- docs/MIGRATION_VERIFICATION_RUNBOOK.md
|
||||
- AGENTS.md
|
||||
- .convoys/ship-readiness.md
|
||||
---
|
||||
|
||||
# Brief 7: Documentation + verification runbook
|
||||
|
||||
## Goal (1 sentence)
|
||||
|
||||
Update the four documentation surfaces that describe the post-convoy schema and onboarding state — `docs/SCHEMA_MAP.md` (correct doc bugs + cross-reference new migrations), `docs/MIGRATION_VERIFICATION_RUNBOOK.md` (new — manual operator runbook from D5), `AGENTS.md` Gotcha #6 (cross-reference this convoy as the closing follow-up), `.convoys/ship-readiness.md` (flip this convoy's queued entry to RESOLVED + unblock `retire-graveyard-scripts-after-audit` + add new follow-ups) — so the next operator onboarding a fresh Neon branch can do so by `npm install` → `npm run setup-db` alone and verify the result.
|
||||
|
||||
## Scope (files in scope — do not edit anything else)
|
||||
|
||||
- `docs/SCHEMA_MAP.md` — **modified**
|
||||
- `docs/MIGRATION_VERIFICATION_RUNBOOK.md` — **new**
|
||||
- `AGENTS.md` — **modified** (small Gotcha #6 cross-reference update only)
|
||||
- `.convoys/ship-readiness.md` — **modified** (queued convoys section)
|
||||
|
||||
This brief is **sequenced last** because it cross-references the 5 new migration files that B1-B5 add; it cannot land before B1-B5 are merged. The architect's `slice_dependencies` block in the convoy file declares `depends_on: [1, 2, 3, 4, 5]`.
|
||||
|
||||
## Per-file scope
|
||||
|
||||
### 1. `docs/SCHEMA_MAP.md` — modified
|
||||
|
||||
Targeted edits (keep all other content as-is):
|
||||
|
||||
1. **Preamble update.** The current preamble (lines 1-18) notes that *"the initial backfill captures only the post-`setup-neon-db.js` shape. … A follow-up convoy (`reconcile-historical-add-scripts`) will fold the historical effects into the migration history; until then this file remains the curated reference for the full prod shape."* Replace with a sentence noting the convoy has **landed**, the migration history now captures the full prod shape, and the operator verification runbook lives at `docs/MIGRATION_VERIFICATION_RUNBOOK.md`. Bump the "Last reviewed" date.
|
||||
|
||||
2. **`### users` notes column updates** (lines 43-57): each row references `add-user-profile-columns.js` / `add-user-profile-fields.js` — leave those references in place (they're historical context); ADD a single line at the end of the table noting *"All columns above are now captured by `migrations/1781000000005_reconcile-user-profile.js` (B5 of `reconcile-historical-add-scripts`, 2026-06-14)."*
|
||||
|
||||
3. **`### cards` notes column updates** (lines 78-79): for the `quantity` and `favorited` rows, change the "Unused; consider dropping" annotation to *"Unused; captured by `migrations/1781000000001_reconcile-cards-columns.js` for fresh-env parity. Drop tracked as queued `drop-dead-cards-columns` follow-up."*
|
||||
|
||||
4. **`### user_cards` index/constraint notes** (lines 90-102): add a brief note clarifying the canonical constraint is `UNIQUE(user_id, card_id, is_foil)` (3-col) from initial-schema, and that `scripts/fix-user-cards-constraints.js`'s stricter 2-col variant was either never-applied or reverted (Finding 1 of the reconcile convoy → Outcome A, 2026-06-14). This is the SCHEMA_MAP equivalent of the Drift Finding 1 resolution.
|
||||
|
||||
5. **`### user_favorites` table** (lines 104-111): **FIX THE DOC BUG.** Current shape lists only `(user_id, card_id)`. Replace with the actual polymorphic shape per the runtime in `pages/api/favorites.js`:
|
||||
|
||||
```markdown
|
||||
### user_favorites
|
||||
|
||||
| Column | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `id` | `SERIAL PK` | |
|
||||
| `user_id` | `INTEGER FK users(id) ON DELETE CASCADE` | |
|
||||
| `item_type` | `VARCHAR(50) NOT NULL` | `'card' | 'collection' | 'deck'` — polymorphic |
|
||||
| `item_id` | `INTEGER NOT NULL` | FK depends on `item_type`; not enforced at DB level |
|
||||
| `created_at` | `TIMESTAMP` | |
|
||||
| | | **UNIQUE(user_id, item_type, item_id)** |
|
||||
|
||||
Indexes: `idx_user_favorites_user_id`, `idx_user_favorites_item_type`,
|
||||
`idx_user_favorites_item_id`, `idx_user_favorites_user_type
|
||||
(user_id, item_type)` — all in `migrations/1781000000004_reconcile-favorites-system.js`.
|
||||
```
|
||||
|
||||
6. **`### collections` notes**: add a line at the bottom noting *"`visibility`, `tcg`, `tags`, `slug` (+ `idx_collections_slug` unique + `check_slug_format` CHECK), `image` are now captured by `migrations/1781000000002_reconcile-collections-columns.js`."*
|
||||
|
||||
7. **`### collection_permissions` + `### collection_activity` sections**: add a line at the bottom of each noting *"Captured by `migrations/1781000000003_reconcile-collaboration-tables.js`."*
|
||||
|
||||
8. **NEW SECTION: `### user_settings`** (currently a one-liner at line 217). Expand to a proper column table matching the actual schema:
|
||||
|
||||
```markdown
|
||||
### user_settings
|
||||
|
||||
Per-user key/value store for settings that don't warrant a column on `users`.
|
||||
Captured by `migrations/1781000000005_reconcile-user-profile.js`.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `id` | `SERIAL PK` | |
|
||||
| `user_id` | `INTEGER FK users(id) ON DELETE CASCADE` | |
|
||||
| `setting_key` | `VARCHAR(100) NOT NULL` | |
|
||||
| `setting_value` | `JSONB NOT NULL` | |
|
||||
| `created_at`, `updated_at` | `TIMESTAMP` default now | |
|
||||
| | | **UNIQUE(user_id, setting_key)** |
|
||||
|
||||
Indexes: `idx_user_settings_user_id`, `idx_user_settings_key`.
|
||||
```
|
||||
|
||||
9. **`### user_avatars` expansion** (currently one paragraph at lines 222-223). Replace with:
|
||||
|
||||
```markdown
|
||||
### user_avatars
|
||||
|
||||
Tracks uploaded avatar history. Captured by
|
||||
`migrations/1781000000005_reconcile-user-profile.js`. Older avatars
|
||||
are typically deleted from blob storage; verify the cleanup job runs.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `id` | `SERIAL PK` | |
|
||||
| `user_id` | `INTEGER FK users(id) ON DELETE CASCADE` | |
|
||||
| `filename` | `VARCHAR(255) NOT NULL` | |
|
||||
| `original_name` | `VARCHAR(255)` | |
|
||||
| `mime_type` | `VARCHAR(100)` | |
|
||||
| `file_size` | `INTEGER` | |
|
||||
| `file_path` | `TEXT NOT NULL` | |
|
||||
| `is_active` | `BOOLEAN DEFAULT true` | |
|
||||
| `created_at`, `updated_at` | `TIMESTAMP` default now | |
|
||||
|
||||
Indexes: `idx_user_avatars_user_id`, `idx_user_avatars_active (user_id, is_active)`.
|
||||
```
|
||||
|
||||
10. **`## Known schema smells` section updates** (lines 226-232):
|
||||
|
||||
- **Smell #2** (`is_public` vs `visibility`): add a sentence noting both columns are now captured by separate migrations (initial-schema for `is_public`, B2 for `visibility`); resolution lives in a future `unify-collection-visibility` follow-up.
|
||||
- **Smell #3** (`cards.quantity` + `cards.favorited`): add the cross-reference to the queued `drop-dead-cards-columns` follow-up.
|
||||
- **NEW: Smell #7 — `users.profile_image_url` vs `users.avatar_url`** (the parity smell that B5 perpetuates intentionally). Both columns are present for prod parity; cleanup is the queued `unify-user-avatar-column` follow-up.
|
||||
|
||||
11. **`## Regeneration` section update** (lines 234-245): the manual-regeneration instructions can be removed entirely since the migration history is now authoritative. Replace with:
|
||||
|
||||
```markdown
|
||||
## Regeneration
|
||||
|
||||
The migration history under `migrations/` is the authoritative source
|
||||
of truth. To verify this file matches a live env (prod, preview, or a
|
||||
fresh Neon branch), use the operator runbook at
|
||||
[`docs/MIGRATION_VERIFICATION_RUNBOOK.md`](MIGRATION_VERIFICATION_RUNBOOK.md).
|
||||
|
||||
When you add a new migration, update the relevant table section here
|
||||
in the same PR. New tables get a new `###` section with the same
|
||||
column-table shape.
|
||||
```
|
||||
|
||||
### 2. `docs/MIGRATION_VERIFICATION_RUNBOOK.md` — new
|
||||
|
||||
Lift verbatim from `.convoys/reconcile-historical-add-scripts.md` § Verification plan (D5), with a small intro framing it as the canonical operator runbook (not just a convoy artifact). Suggested skeleton:
|
||||
|
||||
```markdown
|
||||
# Migration verification runbook
|
||||
|
||||
How to verify that the migration history under `migrations/` produces
|
||||
the same schema as a long-lived environment (prod, preview, or
|
||||
similar). Use this runbook:
|
||||
|
||||
- **After this repo's `reconcile-historical-add-scripts` convoy** (the
|
||||
initial reconciliation), to confirm a fresh Neon branch reaches
|
||||
parity with prod via `npm install` → `npm run setup-db` alone.
|
||||
- **After any new migration lands on `main`**, to spot-check that
|
||||
applying the migration to prod (via the operator's
|
||||
`POSTGRES_URL=<prod> npm run migrate up`) produced the intended
|
||||
effect.
|
||||
- **When suspecting drift** between an env's actual schema and the
|
||||
migration history (rare; the migration history is authoritative).
|
||||
|
||||
This runbook is the manual precursor to the automated check planned
|
||||
in the queued `wire-migrate-into-ci` follow-up (see
|
||||
`.convoys/ship-readiness.md` § "Queued convoys").
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `pg_dump` (PostgreSQL 16+) installed locally.
|
||||
- `POSTGRES_URL` for the env you're verifying.
|
||||
- `ADMIN_INITIAL_PASSWORD` (for the fresh-branch onboarding step) —
|
||||
see `AGENTS.md` § 5 "Running locally".
|
||||
- A Neon account with permission to create a branch (or any other
|
||||
way to spin up a fresh Postgres DB on the same major version as
|
||||
prod).
|
||||
|
||||
## Procedure
|
||||
|
||||
### Step 1 — Snapshot the reference env's structural shape
|
||||
|
||||
Run against the env you consider canonical (usually prod):
|
||||
|
||||
\`\`\`bash
|
||||
POSTGRES_URL=<reference-url> pg_dump --schema-only --no-owner --no-acl \
|
||||
--schema=public > /tmp/reference-schema.sql
|
||||
\`\`\`
|
||||
|
||||
### Step 2 — Create a fresh DB and onboard via `setup-db`
|
||||
|
||||
Create a clean Neon branch from an **empty** parent (or any other
|
||||
fresh Postgres DB on the same major version):
|
||||
|
||||
\`\`\`bash
|
||||
POSTGRES_URL=<fresh-branch-url> \
|
||||
ADMIN_INITIAL_PASSWORD=$(openssl rand -base64 24) \
|
||||
npm run setup-db
|
||||
\`\`\`
|
||||
|
||||
`setup-db` runs `npm run migrate up` (applying every migration in
|
||||
`migrations/` in timestamp order) and seeds the admin user.
|
||||
|
||||
### Step 3 — Snapshot the fresh DB's structural shape
|
||||
|
||||
\`\`\`bash
|
||||
POSTGRES_URL=<fresh-branch-url> pg_dump --schema-only --no-owner --no-acl \
|
||||
--schema=public > /tmp/fresh-schema.sql
|
||||
\`\`\`
|
||||
|
||||
### Step 4 — Diff
|
||||
|
||||
\`\`\`bash
|
||||
diff <(sort /tmp/reference-schema.sql) <(sort /tmp/fresh-schema.sql)
|
||||
\`\`\`
|
||||
|
||||
**Expected non-material differences** (acceptable; do not chase):
|
||||
|
||||
- Constraint or index NAME differences. Prod constraints created via
|
||||
the historical `scripts/add-*` / `scripts/fix-*` jobs may have
|
||||
autogenerated tuple-UNIQUE names that differ from the migrations'
|
||||
explicit names.
|
||||
- Column-ORDER differences. Prod has columns in historical-script-ALTER
|
||||
order; fresh envs have them in migration-order.
|
||||
|
||||
**Material differences** (bug — fix before declaring verified):
|
||||
|
||||
- A column type, default, or NULL/NOT NULL state that differs.
|
||||
- A missing or extra table.
|
||||
- A missing or extra CHECK constraint that changes accepted values.
|
||||
- A missing or extra index that changes query plan shape.
|
||||
|
||||
## Supplementary spot-checks
|
||||
|
||||
For the highest-risk surfaces (frequently-edited tables), run these
|
||||
information-schema queries against both envs and confirm the
|
||||
column-count / constraint-count / index-count totals match exactly:
|
||||
|
||||
\`\`\`sql
|
||||
-- Every column on every table
|
||||
SELECT table_name, column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name, ordinal_position;
|
||||
|
||||
-- Every constraint
|
||||
SELECT table_name, constraint_name, constraint_type
|
||||
FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name, constraint_name;
|
||||
|
||||
-- Every index
|
||||
SELECT tablename, indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename, indexname;
|
||||
\`\`\`
|
||||
|
||||
A mismatch in column count, constraint count, or index count is a
|
||||
material difference and indicates a bug.
|
||||
|
||||
## What to do if you find a material difference
|
||||
|
||||
1. **Identify which env is canonical.** Usually prod. If the diff
|
||||
surfaces a missing column on prod that's in the migration history,
|
||||
the migration was never applied to prod — run
|
||||
`POSTGRES_URL=<prod> npm run migrate up` to catch up.
|
||||
2. **If the diff surfaces a column on prod that's NOT in the
|
||||
migration history**, you've found a drift bug. Write a new
|
||||
reconciliation migration (under `migrations/`) that captures the
|
||||
prod column, following the pattern in `.convoys/reconcile-historical-add-scripts/`.
|
||||
3. **Do not edit existing migrations** — they're pinned by
|
||||
`pgmigrations` rows. Always write a new dated migration to correct
|
||||
schema.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- `AGENTS.md` § 4 Gotcha #6 — migration tool adoption history.
|
||||
- `.convoys/migration-tool.md` — the convoy that adopted `node-pg-migrate`.
|
||||
- `.convoys/reconcile-historical-add-scripts.md` — the convoy that folded
|
||||
the 13 historical scripts into the migration history.
|
||||
- `.cursor/rules/db-and-schema.mdc` — schema-change conventions.
|
||||
```
|
||||
|
||||
### 3. `AGENTS.md` — modified (small Gotcha #6 cross-reference)
|
||||
|
||||
Gotcha #6 is already "RESOLVED" per `migration-tool` (2026-05-26). This brief adds **one sentence** to the end of Gotcha #6 cross-referencing this convoy as the closing follow-up. Verbatim addition (insert immediately before the `- **#7**` line at AGENTS.md line 120):
|
||||
|
||||
> Post-`reconcile-historical-add-scripts` (2026-06-14), the
|
||||
> migration history additionally captures the full effect of the 13
|
||||
> historical `scripts/add-*.js` / `scripts/fix-*.js` /
|
||||
> `scripts/seed-*.js` jobs (where applicable — pure-DML seed scripts
|
||||
> stay out per D4 of that convoy). A brand-new Neon branch can now be
|
||||
> onboarded by `npm install` → `npm run setup-db` alone. Operator
|
||||
> verification runbook at
|
||||
> [`docs/MIGRATION_VERIFICATION_RUNBOOK.md`](docs/MIGRATION_VERIFICATION_RUNBOOK.md).
|
||||
> The 13 historical scripts remain no-go-zones until the queued
|
||||
> `retire-graveyard-scripts-after-audit` (P3) cleanup convoy lands;
|
||||
> that convoy is now unblocked.
|
||||
|
||||
Do NOT flip Gotcha #6's RESOLVED marker — it's already RESOLVED by the right convoy (`migration-tool`). Just append the cross-reference.
|
||||
|
||||
### 4. `.convoys/ship-readiness.md` — modified (queued convoys section)
|
||||
|
||||
Find the existing line under § "Queued convoys" that reads:
|
||||
|
||||
> - **`reconcile-historical-add-scripts`** (priority: P1 quality — needed for fresh-env onboarding). Surfaced 2026-05-26 by `migration-tool` (PR #32). Fold the effects of the 27 historical `scripts/add-*.js` / `fix-*.js` / `seed-*.js` jobs … Documented in `.convoys/migration-tool.md` § R1.
|
||||
|
||||
Replace with:
|
||||
|
||||
> - **`reconcile-historical-add-scripts`** — **RESOLVED 2026-06-14** by [convoy](../.convoys/reconcile-historical-add-scripts.md) (PRs #XX-#XX). Captured 7 of the 13 historical scripts' effects into 5 new migrations under `migrations/` (B1-B5); 2 scripts were already captured by `initial-schema` + `1780378340194_system-collection-description`; 3 are DML-only and stay as dev fixtures; 1 (`fix-user-cards-constraints.js`) is a no-op on prod per Finding 1 (Outcome A — the canonical 3-col `UNIQUE(user_id, card_id, is_foil)` from `initial-schema` is what prod has, and `fix-user-cards-constraints.js`'s stricter 2-col variant is not present). A brand-new Neon branch now onboards via `npm install` → `npm run setup-db` alone. Operator verification at [`docs/MIGRATION_VERIFICATION_RUNBOOK.md`](../docs/MIGRATION_VERIFICATION_RUNBOOK.md). Entry kept (not deleted) for audit trail.
|
||||
|
||||
Find the existing line:
|
||||
|
||||
> - **`retire-graveyard-scripts-after-audit`** (priority: P3 polish; **blocked on `reconcile-historical-add-scripts`**). …
|
||||
|
||||
Replace the `**blocked on `reconcile-historical-add-scripts`**` marker with `**UNBLOCKED 2026-06-14**` and leave the rest of the description intact.
|
||||
|
||||
Add two new queued entries (driven by Findings 2 + 3 of the reconcile convoy):
|
||||
|
||||
> - **`unify-user-avatar-column`** (priority: P3 hygiene). Surfaced 2026-06-14 by `reconcile-historical-add-scripts` Finding 2. `users.profile_image_url` (added by `scripts/add-user-profile-columns.js`) and `users.avatar_url` (added by `scripts/add-user-profile-fields.js`) coexist on prod and in the migration history (both columns are needed for parity per B5). Pick one canonical column, migrate the other's data to it, drop the loser, and sweep runtime readers in `pages/api/user/avatar*.js` + UI surfaces. Requires a query-trace audit first.
|
||||
> - **`drop-dead-cards-columns`** (priority: P3 hygiene). Surfaced 2026-06-14 by `reconcile-historical-add-scripts` Finding 3. `cards.quantity INTEGER` and `cards.favorited BOOLEAN` are documented unused (per `docs/SCHEMA_MAP.md` § "Known schema smells" #3); reconciled into the migration history by B1 for parity, but the actual semantics live on `user_cards` / `user_favorites`. After a query-trace audit confirms zero runtime readers, ship a migration that DROPs both columns with a real `down()` that recreates them.
|
||||
|
||||
(Do NOT add the withdrawn `add-system-collection-on-register` or `unify-user-cards-foil-tracking` entries — both were withdrawn during the reconcile convoy. See `.convoys/reconcile-historical-add-scripts.md` § Follow-ups.)
|
||||
|
||||
## Conventions to follow
|
||||
|
||||
- `.cursor/rules/no-go-zones.mdc` — no edits to `scripts/add-*` / `fix-*` / `seed-*`.
|
||||
- `.cursor/rules/db-and-schema.mdc` — schema-change conventions; SCHEMA_MAP is updated alongside any migration.
|
||||
- Markdown style: match the existing tone of `docs/SCHEMA_MAP.md` (compact tables, "Notes" column explains intent not type semantics) and `AGENTS.md` (numbered Gotchas, cross-reference convoy files).
|
||||
- For `.convoys/ship-readiness.md`: match the existing entry style under § "Queued convoys" (one bullet per convoy, leading `- **`name`**`, RESOLVED entries get the "Entry kept (not deleted) for audit trail" closer when applicable).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `docs/SCHEMA_MAP.md` preamble bumped (post-convoy state); "Last reviewed" date is 2026-06-14.
|
||||
- [ ] `docs/SCHEMA_MAP.md` § `user_favorites` shape is the polymorphic `(item_type, item_id)` — NOT the previous `(card_id)` shape.
|
||||
- [ ] Every reconciled table/column has a "captured by `migrations/<file>`" cross-reference.
|
||||
- [ ] `### user_settings` and `### user_avatars` sections are expanded from one-liners to full column tables matching B5's migration.
|
||||
- [ ] New schema smell #7 (`profile_image_url` vs `avatar_url`) is added to § "Known schema smells".
|
||||
- [ ] `docs/MIGRATION_VERIFICATION_RUNBOOK.md` exists with the 4-step procedure from D5 + the spot-check queries + the "material vs non-material differences" guidance.
|
||||
- [ ] `AGENTS.md` Gotcha #6 gains a single appended paragraph cross-referencing this convoy + the new runbook + the unblocked `retire-graveyard-scripts-after-audit` follow-up. Gotcha #6's existing "RESOLVED by `migration-tool`" marker is NOT changed.
|
||||
- [ ] `.convoys/ship-readiness.md` § "Queued convoys" → `reconcile-historical-add-scripts` entry is flipped to RESOLVED 2026-06-14 with a one-paragraph summary; `retire-graveyard-scripts-after-audit` is marked UNBLOCKED 2026-06-14; two new entries (`unify-user-avatar-column`, `drop-dead-cards-columns`) are added.
|
||||
- [ ] No edits to any file under `scripts/`, `migrations/`, `pages/`, `lib/`, `components/`, `test/`, or `package.json`.
|
||||
- [ ] `npm run lint` matches baseline (these are markdown-only edits; lint should be unaffected).
|
||||
- [ ] `npm run test:run` reports 21/21 passing (these are markdown-only edits; no test surface).
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run test:run
|
||||
```
|
||||
|
||||
Both must match baseline / be green. No additional verification for this brief — the migrations themselves are exercised by Brief 7's verification runbook AFTER an operator runs it manually post-merge.
|
||||
|
||||
For the SCHEMA_MAP edits, eyeball the diff against the prior state and confirm every cross-reference to a migration file is accurate (the file actually exists, and the timestamp matches the assigned reservation).
|
||||
|
||||
## Commit message
|
||||
|
||||
```
|
||||
docs(reconcile): update SCHEMA_MAP, verification runbook, AGENTS, ship-readiness (brief 7/7)
|
||||
|
||||
Closes the reconcile-historical-add-scripts convoy:
|
||||
|
||||
- docs/SCHEMA_MAP.md: cross-reference 5 new migrations from B1-B5;
|
||||
fix user_favorites doc bug (polymorphic shape, not (card_id));
|
||||
expand user_settings + user_avatars one-liners to full column tables;
|
||||
add schema smell #7 (profile_image_url vs avatar_url redundancy).
|
||||
- docs/MIGRATION_VERIFICATION_RUNBOOK.md (new): canonical operator
|
||||
runbook for verifying fresh-env vs prod schema parity (lifted from
|
||||
D5 of the convoy).
|
||||
- AGENTS.md Gotcha #6: append cross-reference to this convoy + the
|
||||
new runbook + the now-unblocked retire-graveyard-scripts-after-audit
|
||||
follow-up. Existing "RESOLVED by migration-tool" marker unchanged.
|
||||
- .convoys/ship-readiness.md: flip reconcile-historical-add-scripts
|
||||
entry to RESOLVED 2026-06-14; mark retire-graveyard-scripts-after-audit
|
||||
UNBLOCKED; add new queued unify-user-avatar-column (Finding 2) +
|
||||
drop-dead-cards-columns (Finding 3).
|
||||
|
||||
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B7.
|
||||
Depends on B1-B5 having merged (this brief cross-references their files).
|
||||
```
|
||||
|
||||
## PR shape
|
||||
|
||||
**Title:** `docs(reconcile): update SCHEMA_MAP, verification runbook, AGENTS, ship-readiness (brief 7/7)`
|
||||
|
||||
**Body template:**
|
||||
|
||||
```markdown
|
||||
Brief 7 (final) of the `reconcile-historical-add-scripts` convoy.
|
||||
See [`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
|
||||
for the full plan and rationale.
|
||||
|
||||
This is the closing brief — depends on B1-B5 having merged (PR
|
||||
references below).
|
||||
|
||||
## What this PR does
|
||||
|
||||
- **`docs/SCHEMA_MAP.md`** — cross-reference all 5 new migrations from
|
||||
B1-B5; fix the `user_favorites` doc bug (the table uses the
|
||||
polymorphic `(item_type, item_id)` shape, not `(card_id)`); expand
|
||||
`user_settings` + `user_avatars` from one-liners to full column
|
||||
tables matching the B5 migration; add a new "Known schema smell" #7
|
||||
for the `profile_image_url` / `avatar_url` redundancy.
|
||||
- **`docs/MIGRATION_VERIFICATION_RUNBOOK.md`** (new) — canonical
|
||||
operator runbook for verifying fresh-env vs prod schema parity.
|
||||
Pulls verbatim from D5 of the convoy file. Useful both for the
|
||||
one-time reconciliation verification and for ongoing post-migration
|
||||
spot-checks.
|
||||
- **`AGENTS.md`** — append one paragraph to Gotcha #6 cross-referencing
|
||||
this convoy as the closing follow-up. The "RESOLVED by `migration-tool`"
|
||||
marker is unchanged (that's still the right resolution attribution
|
||||
for the gotcha itself).
|
||||
- **`.convoys/ship-readiness.md`** § "Queued convoys" — flip the
|
||||
`reconcile-historical-add-scripts` entry to RESOLVED 2026-06-14;
|
||||
flip `retire-graveyard-scripts-after-audit` from "blocked on
|
||||
reconcile-historical-add-scripts" to UNBLOCKED 2026-06-14; add two
|
||||
new queued entries (`unify-user-avatar-column` from Finding 2,
|
||||
`drop-dead-cards-columns` from Finding 3).
|
||||
|
||||
## Prerequisite PRs (all must be merged first)
|
||||
|
||||
- B1 #XX — reconcile cards columns
|
||||
- B2 #XX — reconcile collections columns
|
||||
- B3 #XX — reconcile collaboration tables
|
||||
- B4 #XX — reconcile favorites system
|
||||
- B5 #XX — reconcile user profile
|
||||
|
||||
## What this PR does NOT do
|
||||
|
||||
- Does **NOT** edit any file under `migrations/`, `scripts/`, `pages/`,
|
||||
`lib/`, `components/`, `test/`, or `package.json`.
|
||||
- Does **NOT** run the verification runbook itself — that's the
|
||||
operator's post-merge job; see the runbook's "Procedure" section.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- [ ] `npm run lint` matches baseline
|
||||
- [ ] `npm run test:run` reports 21/21 passing
|
||||
- [ ] Every "captured by `migrations/<file>.js`" cross-reference in
|
||||
SCHEMA_MAP points at a file that actually exists in `migrations/`
|
||||
(eyeballed against `ls migrations/`)
|
||||
- [ ] `AGENTS.md` Gotcha #6 still has its original RESOLVED marker
|
||||
(we only APPEND to it, not rewrite it)
|
||||
- [ ] `.convoys/ship-readiness.md` § "Queued convoys" has the flipped
|
||||
`reconcile-historical-add-scripts` entry, the UNBLOCKED
|
||||
`retire-graveyard-scripts-after-audit` marker, and the two new
|
||||
follow-up entries
|
||||
- [ ] Operator post-merge: run `docs/MIGRATION_VERIFICATION_RUNBOOK.md`
|
||||
against prod + a fresh Neon branch and confirm the diff is
|
||||
non-material
|
||||
|
||||
## Cross-references
|
||||
|
||||
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
|
||||
- Per-brief files: `.convoys/reconcile-historical-add-scripts/brief-{1,2,3,4,5}-*.md`
|
||||
```
|
||||
|
||||
## DO NOT
|
||||
|
||||
- DO NOT edit any file under `scripts/`, `migrations/`, `pages/`, `lib/`, `components/`, `test/`.
|
||||
- DO NOT edit `package.json`, README, `next.config.js`, or any rule under `.cursor/rules/`.
|
||||
- DO NOT change `AGENTS.md` Gotcha #6's "RESOLVED by `migration-tool`" marker — append the new paragraph; don't rewrite the existing resolution attribution.
|
||||
- DO NOT add a `B6` reference anywhere — that brief was removed (Finding 1 → Outcome A).
|
||||
- DO NOT add `add-system-collection-on-register` or `unify-user-cards-foil-tracking` to the ship-readiness queued list — both were withdrawn during this convoy.
|
||||
- DO NOT remove the existing `wire-migrate-into-ci` entry from ship-readiness — it's still queued and unrelated to this convoy.
|
||||
- DO NOT run the verification runbook itself (that's the operator's post-merge step).
|
||||
|
||||
## Rationale (≤3 sentences)
|
||||
|
||||
Sequencing this brief last lets every documentation cross-reference point at a real file under `migrations/` rather than a placeholder. Bundling all four doc surfaces into one PR keeps the convoy's "as-shipped" record consistent (the migrations, the operator runbook, the AGENTS gotcha, and the ship-readiness ledger all flip together). The `docs/MIGRATION_VERIFICATION_RUNBOOK.md` extraction promotes a one-time convoy artifact into an evergreen operator tool that future migrations can reuse.
|
||||
Loading…
Reference in a new issue