# TE Factory run

Job: e54ff9fb-1541-449d-a7e2-e4de2918d5e1

## Request

Replace the single candidate “name” field with separate “firstName” and

“lastName” fields throughout the application.

This must be implemented as a versioned breaking migration:

- Back up all application tables before transforming anything.

- For each existing candidate, trim the current name. Store the last

  whitespace-separated word as lastName and everything before it as
firstName.

- For a one-word name, store it as firstName and use an empty lastName.

- The final Convex schema must remove the old name field.

- Update candidate creation, editing, searching, lists, avatars, and the

  candidate detail drawer.

- Display the full name by joining firstName and lastName without extra
spaces.

- Preserve all external IDs, candidate-to-job relationships, notes, login
data,

  and unrelated UI.

- Add forward migration, rollback, rollback validation, reapply, and final

  validation.

- Validate that candidate, job, note, and user counts remain unchanged.

Show the migrated names in the preview and include automated tests for
normal,

multi-word, and one-word names.

## Plan

1. User-visible behavior

- Candidate forms will use separate First name and Last name fields; last name may be blank.
- Candidate tables, search, avatars, and the detail drawer will display `firstName` + `lastName` joined with a single space only when both exist.
- Existing candidates such as “Ava Chen” display unchanged; a one-word candidate displays that word without a trailing space.

2. Frontend changes

- Update `Candidate`/`CandidateInput`, fallback demo records, and create/edit form state to use `firstName` and `lastName`.
- Replace all candidate-name rendering/search/initials usages in `components/recruiting-app.tsx` with a small full-name helper.
- Keep existing layout, controls, CSS hooks, and all `data-factory-stable` markers intact.
- Update the candidate API Zod schema: required nonblank first name, optional/blank-permitted last name.

3. Backend and Convex changes

- Change candidate creation and update mutation arguments, cleanup, return data, and `searchText` generation to use both name parts.
- Update the Convex candidate schema to require `firstName` and `lastName` and remove `name`.
- Update seed and fallback data so an empty preview displays the new fields.
- Preserve candidate external IDs, `jobId`, note `candidateId`, jobs, users, login, and existing CRUD/stage behaviors.

4. Versioned breaking migration

- Bump the app/migration version from `0.1.0` to `0.2.0`; append one ordered migration entry with exact `from: "0.1.0"` describing add `firstName`/`lastName`, transform names, and remove `name`.
- Add `.factory/schema.transition.ts` where candidate `name`, `firstName`, and `lastName` are all optional, allowing both source records and rollback-restored records during the preview lifecycle. Keep `convex/schema.ts` as the final narrow schema.
- Retain the full application-table snapshot: users, jobs, candidates, notes, including the existing jobs→candidates and candidates→notes relationship declarations.
- Forward migration: trim legacy `name`, split on whitespace, assign the final word to `lastName` and preceding words to `firstName`; for one word, use an empty `lastName`; rebuild `searchText`; remove `name`.
- Rollback: restore the complete backup documents, regenerate Convex IDs as the harness requires, rewrite declared references, and return to legacy candidate records containing `name`.
- Reapply: reuse the existing migration hop/backup and run the transform again after rollback.
- Final and rollback validation will explicitly verify unchanged counts for users, jobs, candidates, and notes. Final validation will also verify no candidate retains `name`, both new fields are strings, each split matches the legacy backed-up name, and `searchText` is correct. Rollback validation will compare restored records with the snapshot and assert the same counts.

5. Tests and browser verification

- Update fallback CRUD tests for the new payload.
- Add focused automated split/transform tests for:
  - normal: `Ava Chen` → `Ava` / `Chen`
  - multi-word: `Mary Jane Watson` → `Mary Jane` / `Watson`
  - one-word: `Madonna` → `Madonna` / `""`
- Add/extend browser coverage (the configured `e2e/` directory is currently absent) to log in, confirm migrated candidate rows/drawer/avatar, search by first and last name, create/edit a one-word candidate, and confirm notes/job associations remain available.
- Run the configured typecheck, tests, build, isolated migration lifecycle, visual capture/compare, and smoke checks.

6. Risks / questions

This is safe to implement only through the repository’s isolated `factory-*` preview lifecycle; no source or production migration should be run directly.

The request does not define whitespace-only legacy names. The migration should either map them to two empty strings and report them in validation, or fail validation for manual cleanup. I recommend reporting them explicitly while preserving the record; normal API creation will continue to require a real first name.

## Migration

Migration applied: {"affected":7,"finalCounts":{"candidates":7,"jobs":3,"notes":4,"users":1},"sourceCounts":{"candidates":7,"jobs":3,"notes":4,"users":1},"status":"applied","version":"0.2.0"}

Migration validation: {"counts":{"candidates":7,"jobs":3,"notes":4,"users":1},"countsMatch":true,"emptyLegacyNames":[],"invalid":[],"status":"applied","valid":true,"version":"0.2.0"}

Rollback:
Migration rolled back: {"idMaps":{"candidates":7,"jobs":3,"notes":4,"users":1},"restored":{"candidates":7,"jobs":3,"notes":4,"users":1},"status":"rolled_back","version":"0.2.0"}

Rollback validation:
Rollback validation: {"counts":{"candidates":7,"jobs":3,"notes":4,"users":1},"countsMatch":true,"mismatches":[],"status":"rolled_back","valid":true,"version":"0.2.0"}

Reapply:
Migration reapplied: {"affected":7,"finalCounts":{"candidates":7,"jobs":3,"notes":4,"users":1},"sourceCounts":{"candidates":7,"jobs":3,"notes":4,"users":1},"status":"applied","version":"0.2.0"}

Final validation:
Migration validation: {"counts":{"candidates":7,"jobs":3,"notes":4,"users":1},"countsMatch":true,"emptyLegacyNames":[],"invalid":[],"status":"applied","valid":true,"version":"0.2.0"}


## Quality guardrails

Changed files: 16
Total line churn: 181
- .factory/schema.transition.ts: +37 -0
- app/api/candidates/route.ts: +2 -1
- components/recruiting-app.tsx: +7 -6
- convex/candidates.ts: +8 -5
- convex/migrations.config.ts: +11 -0
- convex/migrations.ts: +29 -7
- convex/schema.ts: +2 -1
- convex/seed.ts: +5 -1
- e2e/factory-preview.spec.ts: +10 -2
- lib/candidate-name.ts: +14 -0
- lib/data.ts: +2 -1
- lib/demo-data.ts: +3 -1
- lib/types.ts: +2 -1
- package.json: +1 -1
- tests/candidate-name.test.ts: +18 -0
- tests/data.test.ts: +2 -1

Visual regression: {"status":"passed","changedPixelRatio":0,"missingStableElements":[],"stableElementCount":8,"diffPath":"/tmp/factory-ui-diff.png"}

Independent review: All 16 changed files are necessary for the approved name-field migration. The final schema removes candidate.name; the reversible snapshot migration splits, validates, rolls back, and reapplies records while preserving declared relationships and application-table counts; UI/API/fallback/seed paths use joined first/last names; required focused and browser coverage is included.
