# Career Workspace — Implementation Roadmap (ADR + sequencing)

**Date:** 2026-07-12
**Status:** Active. F0–F2 shipped; F3 and F5 partially shipped (see phase sections below for exactly what landed vs. what's still open). This is the execution plan that turns the three strategy docs into code, incrementally, without breaking the live app.
**Source of truth:** `cv-builder-competitor-deep-research.md`, `cv-builder-product-teardown.md`, `career-workspace-product-strategy.md`.

---

## Product boundary (non-negotiable, per owner directive 2026-07-12)

The **primary product stays Job Search & Application Management.** The Career Workspace is a **bounded supporting domain** that makes applications better. Rules that constrain every change below:

- Job applications **reference** career outputs; they do **not own** them.
- Career profile data is **independent of any single job**.
- Themes/templates are **independent of application workflow**.
- Do **not** remove, replace, or redesign job-tracking functionality. The goal is **integration**, not replacement.

```
Main product:  Job Search & Application Management
                 └─ tracking · applications · company research · interviews · matching
Supporting:    Career Workspace  (bounded domain)
                 └─ profile · CV variants · tailored CVs · cover letters · outputs · AI assist
Integration:   JobApplication ──references──▶ TailoredApplication ──uses──▶ CvVariant ──inherits──▶ CareerProfile
```

The tracker references a career output by id; deleting a job never deletes profile/variant data; a variant exists with zero jobs attached.

---

## Migration mechanics (how this codebase actually changes schema)

**No EF migrations in practice.** Schema is provisioned by the idempotent raw-SQL reconciler in `StartupInitializationExtensions.InitializeJobTrackerAsync`. Both dialects are hand-maintained:
- **SQLite (dev):** `Ensure*Table(conn)` helpers with `CREATE TABLE IF NOT EXISTS` + `EnsureColumn` guards, called in the `useSqliteBootstrap` branch (near `EnsureCvTables`).
- **MySQL/MariaDB (prod):** `if (!HasMySqlTable(...))` blocks + `EnsureMySqlColumn`, in the `else` branch.

**Every new table therefore needs BOTH dialect blocks + a `DbSet` + `OnModelCreating` config** (query filter + indexes). The EF `ModelSnapshot` is known-stale; do not rely on `Migrate()` to create Career tables — add them to the reconciler.

**Rule for this roadmap:** all new tables are **additive**. Nothing drops `ApplicationUser.ProfileCvStructureJson` or `TailoredCvDrafts` until the new path is proven in production and dual-read has run clean. Backwards-compatible at every step.

---

## Target entities (only what earns its place)

| Entity | Purpose | Replaces / relates | When |
|---|---|---|---|
| `CareerProfile` | The durable source of truth; one per user (N later). Structure canonical, text derived. | Lifts `ApplicationUser.ProfileCvStructureJson` off the Identity row | Phase F1 |
| `CareerProfileVersion` | Append-only history of the profile; diff/restore | New (teardown gap) | F1 (table) / F4 (UI) |
| `CvVariant` | Persistent lens on the profile: selections + per-item overrides, named ("Backend focus") | Generalizes today's single implicit CV | F2 |
| `CvVersion` | Append-only history of a variant (every generation/save) | New; kills overwrite-anxiety | F2 |
| `CvTheme` (data, not table yet) | Declarative theme descriptor; sibling input to renderer, never welded to content | Formalizes `CvTemplateRenderer` catalog | F3 |
| `TailoredApplication` | Variant × JobApplication event; gap-driven tweaks; **the integration seam** | Reworks `TailoredCvDraft`'s job-lock into a reference | F2/F5 |

**Schema amendments forced by the teardown (apply at table-creation time — cheap now, brutal later):**
1. **Stable item IDs** on every profile item (jobs, bullets, skills, education, projects). Without them, variant lineage / "update everywhere" / inherit-with-override are unimplementable.
2. **Normalized dates** (`YYYY-MM` + `isCurrent`) — backfilled during migration by best-effort parse of the free-string `Start`/`End`. Timeline / tenure / skills-recency depend on it.
3. **Structure is canonical; text is derived** — stated in code. Match scoring reads structure (F5), not raw text.

---

## Phased sequence (each phase ships independently, app stays green)

### Phase F0 — Immediate fixes (no architecture) ✅ SHIPPED
- **OAuth CV lockout fixed** (`ProfilePage.tsx`): CV controls gated on a new `canEditCv` (any authenticated user) instead of `isLocal`. Identity/password fields remain local-only. Unblocks every Google/Microsoft user.
  - Commit: `fix: unlock CV builder for Google/Microsoft-authenticated users`

### Phase F1 — Career Profile as first-class data (backwards-compatible seam) ✅ SHIPPED
1. ✅ `CareerProfile` + `CareerProfileVersion` tables in the reconciler (both dialects), `DbSet`s, query filters, indexes.
2. ✅ `ICareerProfileService` — dual-writes: persists to `CareerProfiles`/`CareerProfileVersions` on every structured-profile save (upload/rebuild/improve/reprocess/parse) while `ApplicationUser.ProfileCvStructureJson` stays the column every existing read path uses.
3. ✅ Stable item IDs (jobs/education/certifications/projects) + normalized `YYYY-MM` dates (`CvDateNormalizer`) assigned on save.
4. **Not done:** full cutover of `ProfileCvController` read paths to the service (still reads `user.ProfileCvStructureJson` directly). The service is invoked at every write site but reads haven't moved yet — deliberate: F1's own exit criteria says "new table is authoritative; column is a mirror" implies read cutover is a *later* step once the table's been proven, not this pass.
5. ✅ 11 tests (stable IDs, date normalization incl. `IsCurrent` guard, version history, dual-write). Verified against the real dev DB.
   - Commit: `feat: add career profile foundation with versioned history`

### Phase F2 — Variants + versions (additive, opt-in) ✅ SHIPPED
1. ✅ `CvVariant` + `CvVersion` tables. `CvVariant.CareerProfileId` links to the user's current `CareerProfile` (nullable, `SetNull` on delete — not owned, not cascaded).
2. ✅ `TailoredApplication` table: `(CvVariantId, JobApplicationId)`, unique per `(OwnerUserId, JobApplicationId)`. Job references the tailored output; does not own the variant. Both FKs cascade (the link is meaningless without either side).
3. ✅ Dual-write (not a one-time backfill): both `TailoredCvDraft` save paths (`SaveTailoredCvDraft`, `UpsertGeneratedTailoredCvDraftAsync`) now also upsert the variant, bump its version, append a `CvVersion` snapshot, and ensure the `TailoredApplication` link — via `SyncCvVariantFromDraftAsync`. Reuses `TailoredCvDocument` as `ContentJson` (zero new data shape). `TailoredCvDrafts` remains authoritative for every existing read path.
4. **Not done:** a one-time backfill of *pre-existing* `TailoredCvDraft` rows that predate this change (only rows saved *after* this ships get synced). **Not done:** `/career/*` endpoints — nothing reads `CvVariant`/`CvVersion` yet; this phase is pure write-side foundation, same "populate before UI" strategy as F1.
5. ✅ 2 tests (variant/version/link created on first save; same variant reused + version incremented on resave, not duplicated). Verified against the real dev DB — FK dependency order (`CareerProfiles` → `CvVariants` → `CvVersions`/`TailoredApplications`) holds in both dialects.
   - Commit: `feat: introduce CV variant schema, dual-written from tailored CV saves`

**Next actions on this phase (not started):** (a) backfill script for pre-existing `TailoredCvDraft` rows if the table shouldn't have a "before my change" gap; (b) a read endpoint exposing `CvVariant` list — the actual precondition for F4's "reuse a variant across jobs" UI to mean anything.

### Phase F3 — Rendering as data (theme catalog) — PARTIAL
1. ✅ `CvTemplateDescriptor` (backend, `ProfileCvController.GetCvTemplateDescriptors`) extended with `LayoutFamily` + `AtsRating`; surfaced as a badge in the frontend template picker (`ProfilePage.tsx`).
2. ✅ 14-test regression suite (`CvTemplateRendererTests`) locking in current renderer output — the prerequisite for a safe future extraction — landed *before* touching the renderer, per the golden-test discipline this phase calls for.
3. **Not done:** the actual extraction (renderer consumes `(document, theme)` as data; layout shells as a fixed set; theme = shell + tokens). The six `RenderXxx` HTML-string methods in `CvTemplateRenderer` are unchanged. Real work, real PDF-regression risk, correctly *not* attempted in the same pass as unrelated feature work.
4. **Known gap surfaced this pass:** the frontend never calls `GET /profile-cv/templates` — it duplicates the template catalog in a hardcoded `REWRITE_TEMPLATES` array in `ProfilePage.tsx`. Two sources of truth for template metadata. Worth fixing *as part of* the F3 extraction (single source becomes the natural output), not before.
5. **Deferred:** external template engine (Scriban) + user/marketplace themes — only when a marketplace is real (strategy §9).

**Next action on this phase:** the extraction itself (item 3) — budget a dedicated pass; the regression suite (item 2) is what makes it safe to attempt.

### Phase F4 — CV Builder UX (structured editor + tailoring workspace)
- Structured profile editor route (`/career/profile`): section forms, per-bullet reorder, provenance-flagged review queue for low-confidence fields.
- From-scratch + paste-text entry paths (removes import-only dead end).
- Tailoring workspace route (`/jobs/:id/tailor`): JD gap chips ↔ variant editor ↔ live themed preview ↔ rescore. Retire the modal editor. **Reached from a job** (integration), full page.
- **Career** becomes a top-level nav pillar (Profile · Variants); "CV" ceases to be a nav noun. Tracker nav untouched.

### Phase F5 — AI depth + integration — PARTIAL
- ✅ **Diff view for AI rewrites**: `TextDiff` component (word-level `diffWords`), wired into the master-CV rewrite preview behind a "Show changes" toggle (default off — an existing test proved diff-by-default breaks the plain-text read). Scoped to the master-CV rewrite surface only; the tailored-CV draft regenerate flow already had a confirm+reset safety net and wasn't a good fit for the same treatment (would mean diffing structured fields, which is F4 tailoring-workspace scope).
  - Commit: `feat: show diff view for AI CV rewrites`
- ✅ **Persist interview prep** (`InterviewPrepNote`, one table, per-field columns — the DTO is flat) and **candidate fit + focus plan** (`AiWorkspaceNote`, one generic table keyed by `NoteType` — those DTOs are irregular/nested, so per-field columns would've been unreasonable; introduced the generalization on the 2nd/3rd occurrence, not the 1st). All three: reuse across tab-opens, regenerate on attachment-context change, explicit "Regenerate" button as the escape hatch. Collectively these three tabs fired 9 AI calls on every single re-open before this; now 0 unless something changed.
  - Commits: `feat: persist interview prep instead of regenerating on every open`, `feat: persist candidate fit and focus plan, stop re-running on every open`
- ✅ **ATS-safety badge** on the template picker (folded into the F3 entry above — same commit touched both, since `AtsRating` is a field on the template descriptor).
- **Not done:** `JobCvMatchService` retarget — checked the live code this pass and found `BuildCvSearchCorpus` already reads structured profile *and* raw text as a hybrid (better than the teardown assumed); no change needed. Closing this line item as **resolved, not deferred**.
- **Not done:** fact-constraint validator (novel named-entity/number flagging on AI generation). Real remaining trust gap — the diff view lets a user *see* a fabrication, but nothing stops the model from producing one. Good next F5 slice.

### Phase F6+ — Career Workspace horizons (architecture-ready, not built now)
Cover letters (profile+JD+thread) · ATS plain-text view · skills-gap analytics · public profile (theme over live profile) · DOCX adapter · portfolio/LinkedIn adapters. Each ≈ one `IOutputAdapter` + optional theme. Keep the adapter boundary swap-clean (Reactive Resume abandoned server-Chromium for cost — our Playwright PDF adapter must stay replaceable without touching themes).

---

## Risk register

| Risk | Mitigation |
|---|---|
| Live-DB schema migration breaks prod | Additive-only tables; dual-write/dual-read; never drop legacy until proven; test reconciler against a prod DB copy |
| Backfill mis-parses free-string dates | Best-effort normalize, keep original string alongside normalized fields; never lose data |
| PDF output regression in theme refactor | Golden byte-identical test before merge |
| Scope creep into job-tracker redesign | Boundary rules above; tracker code out of scope |
| Two sources of truth diverge (F1 interim) | Time-box the dual-write window; F5 retargets the last consumer (match scoring) then column is dropped |

## Working-summary convention
Each session updates **## Completed / ## Current / ## Next / ## Decisions** in the response. This file is the durable plan; the running summary is the session delta.
