Vocabulary cleanup follow-up #55

Merged
varutasu merged 1 commit from chore/vocabulary-cleanup-followup into main 2026-05-29 11:01:03 -04:00
12 changed files with 71 additions and 107 deletions

View file

@ -8,14 +8,19 @@ success_metric: |
with vocabulary table. with vocabulary table.
skip: skip:
- arch - arch
status: open status: closed
created: 2026-05-27 created: 2026-05-27
closed: 2026-05-29
pr: 54
--- ---
# Convoy: rename-collections-vocabulary # Convoy: rename-collections-vocabulary
Align user-facing copy with the product taxonomy: owned cards vs curated lists. Align user-facing copy with the product taxonomy: owned cards vs curated lists.
**Shipped:** PR #54 (squash `fd78114`, 2026-05-29). Follow-up cleanup tracked in
PR after deep review (dashboard h1, marketing copy, seed description).
## Why ## Why
The scanner audit and IA review found inconsistent vocabulary: "Owned Cards", The scanner audit and IA review found inconsistent vocabulary: "Owned Cards",
@ -48,95 +53,20 @@ schema migration.
- **Schema renames** — table/column names stay; UI copy only. - **Schema renames** — table/column names stay; UI copy only.
- **URL slug changes**`/collections` path unchanged in v1. - **URL slug changes**`/collections` path unchanged in v1.
- **Architecture decisions**`skip: arch`; IA + UX run explicitly. - **Architecture decisions**`skip: arch`; IA + UX run explicitly.
- **Admin UI** — separate pass if needed.
## Roles invoked
1. `role-ia-architect` — vocabulary table + file inventory (**primary owner**).
2. `role-ux-reviewer` — scan flow + nav label consistency.
3. `role-implementer` — 2 briefs (serial: Brief 2 after Brief 1).
4. `role-reviewer` + `role-design-system-auditor` + `role-a11y-auditor`
copy changes affect screen reader strings.
Note: **`role-architect` skipped** per `skip: arch`. IA architect owns
taxonomy; implementer briefs written by IA + conductor handoff or parent
agent.
## Todos ## Todos
- [ ] IA: publish vocabulary table + grep inventory of stale strings - [x] IA: publish vocabulary table + grep inventory of stale strings
- [ ] UX: review scanner + nav + collection views for consistency - [x] UX: review scanner + nav + collection views for consistency
- [ ] Brief 1 — pages/ + components/ copy sweep - [x] Brief 1 — pages/ + components/ copy sweep
- [ ] Brief 2 — AGENTS.md + rules + SCHEMA_MAP glossary - [x] Brief 2 — AGENTS.md + rules + SCHEMA_MAP glossary
- [ ] Add `forbidden-stale-strings` CI job - [x] Add `forbidden-stale-strings` CI job
## Operator action required
**None.**
## Multitask dispatch
### Slice dependencies
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files:
- pages/**/*.js
- components/*.js
notes: exclude pages/api/**
- brief: 2
depends_on: [1]
files:
- AGENTS.md
- .cursor/rules/ui-and-theming.mdc
- docs/SCHEMA_MAP.md
- .github/workflows/ci.yml
```
Serial: Brief 2 after Brief 1 (docs reference final copy).
**Cross-convoy:** parallel with #1+#2 after `secure-scanner-gemini-key`
merges — `/multitask role-implementer` **#5 Brief 1 + #6 + #2 Brief 1**
(disjoint files).
Post-PR audit:
```
/multitask role-reviewer + role-design-system-auditor + role-a11y-auditor
```
Group id: `audit-rename-collections-vocabulary-<pr>`.
## CI impact
| Workflow / job | Behavior |
| --- | --- |
| `forbidden-stale-strings` | **New blocking job** — grep `pages/` + `components/`. |
| `visual-diff.yml` | **Likely fires** — widespread UI string changes in pages/components. |
| `preview-smoke.yml` | Fires; sign-in CTA wording must stay smoke-compatible. |
**Smoke caveat:** smoke test 2 asserts `/sign in/i` on login page — do not
rename that CTA in this convoy.
## Decisions to ratify (IA architect)
1. **"Lists" vs "Binders"** — when to use each term in nav vs empty states.
2. **Scanner button label** — replacement for "Mark Owned" (e.g. "Add to
session" vs "Confirm card").
3. **Admin UI** — out of copy sweep or separate pass?
## Acceptance criteria
1. Zero rendered occurrences of the three forbidden strings in `pages/` +
`components/`.
2. Vocabulary table committed in `AGENTS.md`.
3. `forbidden-stale-strings` CI green.
4. Schema DDL unchanged (grep `migrations/` — no new files).
5. Vitest 21/21; smoke 3/3 (sign-in CTA intact).
## Out of scope follow-ups ## Out of scope follow-ups
- **`schema-cleanup-from-scanner-audit`** — `is_system_collection` vs - **`schema-cleanup-from-scanner-audit`** — `is_system_collection` vs
`user_cards` unification (separate convoy). `user_cards` unification (separate convoy).
- **`rename-repo-and-vercel-project`** — infra naming, not UI copy. - **`rename-repo-and-vercel-project`** — infra naming, not UI copy.
- **Existing DB seed descriptions** — users registered pre-#54 retain the old
system-list description until a one-off data migration or manual edit.

View file

@ -16,6 +16,8 @@ export const VOCAB = {
ADD_TO_LISTS: 'Add to Lists', ADD_TO_LISTS: 'Add to Lists',
SYNCED_BINDER: 'Synced binder', SYNCED_BINDER: 'Synced binder',
SYSTEM_COLLECTION_SYNC_HINT: 'Automatically syncs with My Collection', SYSTEM_COLLECTION_SYNC_HINT: 'Automatically syncs with My Collection',
SYSTEM_COLLECTION_SEED_DESCRIPTION:
'Automatically syncs with My Collection. This list cannot be deleted or made public.',
}; };
/** User-facing label for a collection row (maps system collection DB name). */ /** User-facing label for a collection row (maps system collection DB name). */

View file

@ -1,6 +1,7 @@
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { sql } from '@vercel/postgres'; import { sql } from '@vercel/postgres';
import { generateUniqueSlug } from '../../../lib/slug-utils.js'; import { generateUniqueSlug } from '../../../lib/slug-utils.js';
import { SYSTEM_COLLECTION_DB_NAME, VOCAB } from '../../../lib/collection-vocabulary.js';
import { generateToken } from '../auth-utils.js'; import { generateToken } from '../auth-utils.js';
import { checkAuthRateLimit } from '../../../lib/rate-limit.js'; import { checkAuthRateLimit } from '../../../lib/rate-limit.js';
@ -90,7 +91,7 @@ export default async function handler(req, res) {
const existingSlugs = (existingSlugsData.rows || []).map(row => row.slug); const existingSlugs = (existingSlugsData.rows || []).map(row => row.slug);
// Generate unique slug for "All My Cards" // Generate unique slug for "All My Cards"
const uniqueSlug = await generateUniqueSlug("All My Cards", existingSlugs); const uniqueSlug = await generateUniqueSlug(SYSTEM_COLLECTION_DB_NAME, existingSlugs);
// Create the special collection // Create the special collection
const collectionResult = await sql` const collectionResult = await sql`
@ -106,8 +107,8 @@ export default async function handler(req, res) {
updated_at updated_at
) )
VALUES ( VALUES (
'All My Cards', ${SYSTEM_COLLECTION_DB_NAME},
'Automatically contains all cards you mark as owned. This collection cannot be deleted or made public.', ${VOCAB.SYSTEM_COLLECTION_SEED_DESCRIPTION},
'All', 'All',
false, false,
${user.id}, ${user.id},

View file

@ -26,7 +26,7 @@ function PublicCardsView() {
<div className="bg-gradient-to-r from-orange-50 to-red-50 dark:from-orange-900/20 dark:to-red-900/20 rounded-2xl p-6 border border-orange-200 dark:border-orange-800"> <div className="bg-gradient-to-r from-orange-50 to-red-50 dark:from-orange-900/20 dark:to-red-900/20 rounded-2xl p-6 border border-orange-200 dark:border-orange-800">
<h3 className="text-lg font-semibold mb-2 gradient-text-ember">Sign Up to Unlock Full Features</h3> <h3 className="text-lg font-semibold mb-2 gradient-text-ember">Sign Up to Unlock Full Features</h3>
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}> <p className="mb-4" style={{ color: 'var(--text-secondary)' }}>
Create collections, mark favorites, add cards to your inventory, and more! Create lists, add cards to {VOCAB.MY_COLLECTION}, mark favorites, and more!
</p> </p>
<div className="flex flex-col sm:flex-row gap-3 justify-center"> <div className="flex flex-col sm:flex-row gap-3 justify-center">
<Link <Link
@ -62,7 +62,7 @@ function PublicCardsView() {
</div> </div>
<h3 className="text-xl font-semibold mb-2 gradient-text-flame">Full Card Browser Coming Soon</h3> <h3 className="text-xl font-semibold mb-2 gradient-text-flame">Full Card Browser Coming Soon</h3>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
Sign up now to get early access to our complete card database and collection tools. Sign up now to get early access to our complete card database and list tools.
</p> </p>
</div> </div>
</div> </div>

View file

@ -634,7 +634,7 @@ export default function Collections() {
)} )}
</div> </div>
{/* Create Collection Modal */} {/* Create List Modal */}
{showCreateModal && ( {showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="card max-w-md w-full mx-4"> <div className="card max-w-md w-full mx-4">

View file

@ -4,7 +4,7 @@ import Layout from '../components/Layout';
import PermissionIndicator from '../components/PermissionIndicator'; import PermissionIndicator from '../components/PermissionIndicator';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
import Link from 'next/link'; import Link from 'next/link';
import { VOCAB } from '../lib/collection-vocabulary.js'; import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
export default function Dashboard() { export default function Dashboard() {
const router = useRouter(); const router = useRouter();
@ -80,10 +80,10 @@ export default function Dashboard() {
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}> <h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
{VOCAB.LISTS} {VOCAB.MY_COLLECTION}
</h1> </h1>
<p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}> <p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}>
Manage your collection of trading cards and decks Overview of your lists and owned cards
</p> </p>
</div> </div>
<div className="flex space-x-2 sm:space-x-4"> <div className="flex space-x-2 sm:space-x-4">
@ -170,7 +170,7 @@ export default function Dashboard() {
</div> </div>
<h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>No Lists Yet</h3> <h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>No Lists Yet</h3>
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}> <p className="mb-4" style={{ color: 'var(--text-secondary)' }}>
Create your first collection to start organizing your cards Create your first list to start organizing your cards
</p> </p>
<Link href="/collections"> <Link href="/collections">
<button className="px-6 py-3 font-medium rounded-xl transition-all duration-200 hover:opacity-90" style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}> <button className="px-6 py-3 font-medium rounded-xl transition-all duration-200 hover:opacity-90" style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}>
@ -191,7 +191,7 @@ export default function Dashboard() {
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-semibold truncate" style={{ color: 'var(--text-primary)' }}> <h3 className="font-semibold truncate" style={{ color: 'var(--text-primary)' }}>
{collection.name} {collectionDisplayName(collection)}
</h3> </h3>
<p className="text-sm truncate" style={{ color: 'var(--text-secondary)' }}> <p className="text-sm truncate" style={{ color: 'var(--text-secondary)' }}>
{collection.cardCount || 0} cards {collection.cardCount || 0} cards

View file

@ -96,7 +96,7 @@ export default function Home() {
<span className="gradient-text-flame">Deck Hearth</span> <span className="gradient-text-flame">Deck Hearth</span>
</h1> </h1>
<p className="text-xl md:text-2xl mb-8 max-w-3xl mx-auto leading-relaxed" style={{ color: 'var(--text-secondary)' }}> <p className="text-xl md:text-2xl mb-8 max-w-3xl mx-auto leading-relaxed" style={{ color: 'var(--text-secondary)' }}>
The ultimate hub for trading card collectors. Organize your collection, The ultimate hub for trading card collectors. Organize your cards into lists,
discover rare cards, and connect with fellow enthusiasts in one beautiful platform. discover rare cards, and connect with fellow enthusiasts in one beautiful platform.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
@ -139,7 +139,7 @@ export default function Home() {
</div> </div>
<h3 className="text-xl font-bold mb-4 gradient-text-flame">Organize Lists</h3> <h3 className="text-xl font-bold mb-4 gradient-text-flame">Organize Lists</h3>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
Create custom collections, track card values, and organize by sets, rarity, or any system that works for you. Create custom lists, track card values, and organize by sets, rarity, or any system that works for you.
</p> </p>
</div> </div>
<div className="text-center p-8 rounded-2xl" style={{ backgroundColor: 'var(--bg-primary)' }}> <div className="text-center p-8 rounded-2xl" style={{ backgroundColor: 'var(--bg-primary)' }}>
@ -150,7 +150,7 @@ export default function Home() {
</div> </div>
<h3 className="text-xl font-bold mb-4 gradient-text-gold">Discover Cards</h3> <h3 className="text-xl font-bold mb-4 gradient-text-gold">Discover Cards</h3>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
Search through thousands of cards across multiple TCGs. Find that missing piece for your collection. Search through thousands of cards across multiple TCGs. Find that missing piece for your binder.
</p> </p>
</div> </div>
<div className="text-center p-8 rounded-2xl" style={{ backgroundColor: 'var(--bg-primary)' }}> <div className="text-center p-8 rounded-2xl" style={{ backgroundColor: 'var(--bg-primary)' }}>
@ -161,7 +161,7 @@ export default function Home() {
</div> </div>
<h3 className="text-xl font-bold mb-4 gradient-text-ember">Connect & Share</h3> <h3 className="text-xl font-bold mb-4 gradient-text-ember">Connect & Share</h3>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
Share your collections with the community, collaborate with friends, and discover amazing collections from other collectors. Share your lists with the community, collaborate with friends, and discover amazing lists from other collectors.
</p> </p>
</div> </div>
</div> </div>
@ -176,7 +176,7 @@ export default function Home() {
Featured Lists Featured Lists
</h2> </h2>
<p className="text-xl" style={{ color: 'var(--text-secondary)' }}> <p className="text-xl" style={{ color: 'var(--text-secondary)' }}>
Discover amazing collections from our community Discover amazing lists from our community
</p> </p>
</div> </div>
@ -263,7 +263,7 @@ export default function Home() {
Ready to Start Your Journey? Ready to Start Your Journey?
</h2> </h2>
<p className="text-xl mb-8" style={{ color: 'var(--text-secondary)' }}> <p className="text-xl mb-8" style={{ color: 'var(--text-secondary)' }}>
Join thousands of collectors who trust Deck Hearth to manage their trading card collections. Join thousands of collectors who trust Deck Hearth to manage their cards and lists.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link <Link

View file

@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import AuthLayout from '../components/AuthLayout'; import AuthLayout from '../components/AuthLayout';
import AnimatedFireLogo from '../components/AnimatedFireLogo'; import AnimatedFireLogo from '../components/AnimatedFireLogo';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
@ -76,7 +77,7 @@ export default function Login() {
Welcome to Deck Hearth Welcome to Deck Hearth
</h2> </h2>
<p className="mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}> <p className="mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
Sign in to access your trading card collection Sign in to access {VOCAB.MY_COLLECTION}
</p> </p>
</div> </div>
</div> </div>

View file

@ -265,7 +265,7 @@ export default function MyCards() {
{VOCAB.MY_COLLECTION} {VOCAB.MY_COLLECTION}
</h1> </h1>
<p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}> <p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}>
Browse and manage cards in your collection Browse and manage cards in {VOCAB.MY_COLLECTION}
</p> </p>
</div> </div>
<div className="flex space-x-2 sm:space-x-4"> <div className="flex space-x-2 sm:space-x-4">
@ -420,7 +420,7 @@ export default function MyCards() {
{!hasMore && cards.length > 0 && ( {!hasMore && cards.length > 0 && (
<div className="text-center py-8"> <div className="text-center py-8">
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
You've reached the end of your collection! You&apos;ve reached the end of {VOCAB.MY_COLLECTION}!
</p> </p>
</div> </div>
)} )}

View file

@ -383,7 +383,7 @@ export default function Scanner() {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to add to collection'); throw new Error(`Failed to add to ${VOCAB.LIST}`);
} }
}; };
@ -758,7 +758,7 @@ export default function Scanner() {
{/* Bulk Actions Modal */} {/* Bulk Actions Modal */}
{/* This modal is no longer needed as bulk actions are in a floating toolbar */} {/* This modal is no longer needed as bulk actions are in a floating toolbar */}
{/* Create Collection Modal */} {/* Create List Modal */}
{showCreateCollection && ( {showCreateCollection && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div <div

View file

@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import AuthLayout from '../components/AuthLayout'; import AuthLayout from '../components/AuthLayout';
import AnimatedFireLogo from '../components/AnimatedFireLogo'; import AnimatedFireLogo from '../components/AnimatedFireLogo';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function Signup() { export default function Signup() {
const router = useRouter(); const router = useRouter();
@ -215,7 +216,7 @@ export default function Signup() {
Join Deck Hearth Join Deck Hearth
</h2> </h2>
<p className="mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}> <p className="mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
Create your account to start building your collection Create your account to start building {VOCAB.MY_COLLECTION}
</p> </p>
</div> </div>
</div> </div>

View file

@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import {
SYSTEM_COLLECTION_DB_NAME,
VOCAB,
collectionDisplayName,
formatProcessedDestination,
} from '../../lib/collection-vocabulary.js';
describe('collection-vocabulary', () => {
it('maps system collection rows to Synced binder', () => {
expect(collectionDisplayName({ name: SYSTEM_COLLECTION_DB_NAME, isSystemCollection: true })).toBe(
VOCAB.SYNCED_BINDER
);
expect(collectionDisplayName({ name: SYSTEM_COLLECTION_DB_NAME, is_system_collection: true })).toBe(
VOCAB.SYNCED_BINDER
);
});
it('passes through user list names', () => {
expect(collectionDisplayName({ name: 'Modern Staples' })).toBe('Modern Staples');
});
it('formats scanner processed destinations', () => {
expect(formatProcessedDestination('owned')).toBe(VOCAB.MY_COLLECTION);
expect(formatProcessedDestination('collection')).toBe(VOCAB.LIST);
expect(formatProcessedDestination('deck')).toBe('deck');
});
});