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
Showing only changes of commit 4bc0cfecfc - Show all commits

View file

@ -8,14 +8,19 @@ success_metric: |
with vocabulary table.
skip:
- arch
status: open
status: closed
created: 2026-05-27
closed: 2026-05-29
pr: 54
---
# Convoy: rename-collections-vocabulary
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
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.
- **URL slug changes**`/collections` path unchanged in v1.
- **Architecture decisions**`skip: arch`; IA + UX run explicitly.
## 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.
- **Admin UI** — separate pass if needed.
## Todos
- [ ] IA: publish vocabulary table + grep inventory of stale strings
- [ ] UX: review scanner + nav + collection views for consistency
- [ ] Brief 1 — pages/ + components/ copy sweep
- [ ] Brief 2 — AGENTS.md + rules + SCHEMA_MAP glossary
- [ ] 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).
- [x] IA: publish vocabulary table + grep inventory of stale strings
- [x] UX: review scanner + nav + collection views for consistency
- [x] Brief 1 — pages/ + components/ copy sweep
- [x] Brief 2 — AGENTS.md + rules + SCHEMA_MAP glossary
- [x] Add `forbidden-stale-strings` CI job
## Out of scope follow-ups
- **`schema-cleanup-from-scanner-audit`** — `is_system_collection` vs
`user_cards` unification (separate convoy).
- **`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',
SYNCED_BINDER: 'Synced binder',
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). */

View file

@ -1,6 +1,7 @@
import bcrypt from 'bcryptjs';
import { sql } from '@vercel/postgres';
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 { 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);
// 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
const collectionResult = await sql`
@ -106,8 +107,8 @@ export default async function handler(req, res) {
updated_at
)
VALUES (
'All My Cards',
'Automatically contains all cards you mark as owned. This collection cannot be deleted or made public.',
${SYSTEM_COLLECTION_DB_NAME},
${VOCAB.SYSTEM_COLLECTION_SEED_DESCRIPTION},
'All',
false,
${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">
<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)' }}>
Create collections, mark favorites, add cards to your inventory, and more!
Create lists, add cards to {VOCAB.MY_COLLECTION}, mark favorites, and more!
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Link
@ -62,7 +62,7 @@ function PublicCardsView() {
</div>
<h3 className="text-xl font-semibold mb-2 gradient-text-flame">Full Card Browser Coming Soon</h3>
<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>
</div>
</div>

View file

@ -634,7 +634,7 @@ export default function Collections() {
)}
</div>
{/* Create Collection Modal */}
{/* Create List Modal */}
{showCreateModal && (
<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">

View file

@ -4,7 +4,7 @@ import Layout from '../components/Layout';
import PermissionIndicator from '../components/PermissionIndicator';
import { useAuth } from '../lib/use-auth';
import Link from 'next/link';
import { VOCAB } from '../lib/collection-vocabulary.js';
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
export default function Dashboard() {
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>
<h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
{VOCAB.LISTS}
{VOCAB.MY_COLLECTION}
</h1>
<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>
</div>
<div className="flex space-x-2 sm:space-x-4">
@ -170,7 +170,7 @@ export default function Dashboard() {
</div>
<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)' }}>
Create your first collection to start organizing your cards
Create your first list to start organizing your cards
</p>
<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' }}>
@ -191,7 +191,7 @@ export default function Dashboard() {
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold truncate" style={{ color: 'var(--text-primary)' }}>
{collection.name}
{collectionDisplayName(collection)}
</h3>
<p className="text-sm truncate" style={{ color: 'var(--text-secondary)' }}>
{collection.cardCount || 0} cards

View file

@ -96,7 +96,7 @@ export default function Home() {
<span className="gradient-text-flame">Deck Hearth</span>
</h1>
<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.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
@ -139,7 +139,7 @@ export default function Home() {
</div>
<h3 className="text-xl font-bold mb-4 gradient-text-flame">Organize Lists</h3>
<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>
</div>
<div className="text-center p-8 rounded-2xl" style={{ backgroundColor: 'var(--bg-primary)' }}>
@ -150,7 +150,7 @@ export default function Home() {
</div>
<h3 className="text-xl font-bold mb-4 gradient-text-gold">Discover Cards</h3>
<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>
</div>
<div className="text-center p-8 rounded-2xl" style={{ backgroundColor: 'var(--bg-primary)' }}>
@ -161,7 +161,7 @@ export default function Home() {
</div>
<h3 className="text-xl font-bold mb-4 gradient-text-ember">Connect & Share</h3>
<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>
</div>
</div>
@ -176,7 +176,7 @@ export default function Home() {
Featured Lists
</h2>
<p className="text-xl" style={{ color: 'var(--text-secondary)' }}>
Discover amazing collections from our community
Discover amazing lists from our community
</p>
</div>
@ -263,7 +263,7 @@ export default function Home() {
Ready to Start Your Journey?
</h2>
<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>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link

View file

@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import Link from 'next/link';
import AuthLayout from '../components/AuthLayout';
import AnimatedFireLogo from '../components/AnimatedFireLogo';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function Login() {
const router = useRouter();
@ -76,7 +77,7 @@ export default function Login() {
Welcome to Deck Hearth
</h2>
<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>
</div>
</div>

View file

@ -265,7 +265,7 @@ export default function MyCards() {
{VOCAB.MY_COLLECTION}
</h1>
<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>
</div>
<div className="flex space-x-2 sm:space-x-4">
@ -420,7 +420,7 @@ export default function MyCards() {
{!hasMore && cards.length > 0 && (
<div className="text-center py-8">
<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>
</div>
)}

View file

@ -383,7 +383,7 @@ export default function Scanner() {
});
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 */}
{/* This modal is no longer needed as bulk actions are in a floating toolbar */}
{/* Create Collection Modal */}
{/* Create List Modal */}
{showCreateCollection && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div

View file

@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import Link from 'next/link';
import AuthLayout from '../components/AuthLayout';
import AnimatedFireLogo from '../components/AnimatedFireLogo';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function Signup() {
const router = useRouter();
@ -215,7 +216,7 @@ export default function Signup() {
Join Deck Hearth
</h2>
<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>
</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');
});
});