Ships Brief 7 (documentation + verification runbook) of the
reconcile-historical-add-scripts convoy, ~2 months post-hoc. The
6 implementer briefs (B1-B6) landed 2026-06-14 to 2026-07-06 via
PRs #148, #149, #150, #151, #152, #153. This PR closes the loop:
- Brings the architect's parent convoy file + 6 brief files onto
main (they only existed on the stale convoy/reconcile-historical-
add-scripts branch, never merged)
- Adds § As-shipped to the parent convoy file documenting all 6
squash SHAs + PR numbers + merge dates + the reservation-timestamp
rename (1781000000001-006 → 1781442330001-006 in ec9bb2b, except
B3 which kept its original) + the B6 shipped-as-tiny-migration
deviation from the collapse-to-docs plan
- Fixes docs/SCHEMA_MAP.md § user_favorites (was stale
(user_id, card_id); actual polymorphic (item_type, item_id) per
B4's migration)
- Adds docs/MIGRATION_VERIFICATION_RUNBOOK.md — manual
fresh-Neon-branch vs prod pg_dump diff runbook per architect D5
- Flips .convoys/ship-readiness.md entries:
- reconcile-historical-add-scripts → RESOLVED
- retire-graveyard-scripts-after-audit → UNBLOCKED
- Adds two new queued follow-ups surfaced by the architect:
- unify-user-avatar-column (P3 — dual avatar column smell)
- drop-dead-cards-columns (P3 — cards.quantity + cards.favorited)
No source-code changes. Docs only.
Co-authored-by: Cursor <cursoragent@cursor.com>
13 KiB
| convoy | brief_number | depends_on | files | |
|---|---|---|---|---|
| reconcile-historical-add-scripts | 2 |
|
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):
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:
await sql`
CREATE INDEX IF NOT EXISTS idx_collections_visibility
ON collections(visibility)
`;
scripts/add-collection-slugs.js lines 17-99 (relevant DDL only):
// 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):
await sql`
ALTER TABLE collections
ADD COLUMN IF NOT EXISTS image TEXT
`;
Target migration file
Path: migrations/1781000000002_reconcile-collections-columns.js
Contents:
/**
* 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 undermigrations/. Per D2 of the convoy file. - ESM exports;
"type": "module"perpackage.jsonline 5. IF NOT EXISTSon every ALTER and CREATE INDEX. CHECK constraint wraps inDO $$ ... EXCEPTION WHEN duplicate_object THEN NULL END $$;(Postgres doesn't acceptIF NOT EXISTSdirectly on CHECK constraints).
Acceptance criteria
migrations/1781000000002_reconcile-collections-columns.jsexists with the exact filename above (the timestamp1781000000002is the reservation token assigned by the architect — do NOT usenpm run migrate create).- The file's
up()adds 5 columns (visibility,tcg,tags,slug,image) viaIF NOT EXISTS, creates 2 indexes (idx_collections_visibility, uniqueidx_collections_slug) viaIF NOT EXISTS, and adds thecheck_slug_formatCHECK via aDO $$ EXCEPTIONblock. - 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.jspasses.node -e "import('./migrations/1781000000002_reconcile-collections-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"printsfunction function undefined.npm run lintmatches the baseline (no new errors).npm run test:runreports 21/21 passing.
Verification
Run from the convoy worktree BEFORE opening the PR:
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 --checkexits 0 silently.node -eprintsfunction function undefined.npm run lintmatches baseline.npm run test:runreports 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:
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 underscripts/— 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, ordocs/SCHEMA_MAP.md(B7 owns SCHEMA_MAP). - DO NOT run
npm run migrate upagainst 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 aDate.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.