# Career Workspace — Implementation Roadmap (ADR + sequencing) **Date:** 2026-07-12 **Status:** Active. 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 this session - **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. ### Phase F1 — Career Profile as first-class data (backwards-compatible seam) 1. Add `CareerProfile` + `CareerProfileVersion` tables to the reconciler (both dialects), `DbSet`s, query filters, indexes. 2. Introduce `ICareerProfileService` — the single accessor for the user's structured profile. Initially **dual-writes**: persists to the new `CareerProfiles` table **and** keeps `ApplicationUser.ProfileCvStructureJson` in sync (so nothing that still reads the column breaks). 3. Assign stable item IDs + normalize dates when materializing a profile into the new table (one-time backfill on first read/write per user). 4. Point `ProfileCvController` read/write paths at the service (behavior identical). 5. Test: round-trip a profile through the service; assert IDs stable across saves, dates normalized, legacy column still mirrored. **Exit:** new table is authoritative; column is a mirror. Zero user-visible change. ### Phase F2 — Variants + versions (additive, opt-in) 1. `CvVariant` + `CvVersion` tables. A variant references a `CareerProfile` and holds selection/override JSON keyed by item ID. 2. `TailoredApplication` table: `(CvVariantId, JobApplicationId)` — the reference seam. Job references the tailored output; does not own the variant. 3. Backfill: each existing `TailoredCvDraft` → one `CvVariant` (job-linked) + its render options extracted toward a theme ref, wrapped in a `TailoredApplication`. Legacy `TailoredCvDrafts` retained (dual-read) until proven. 4. Endpoints under `/career/*` grow beside legacy `/profile-cv/*` and the job-scoped tailored routes. **Exit:** variants exist; regeneration writes a new `CvVersion` instead of overwriting. ### Phase F3 — Rendering as data (theme catalog) 1. Extract the `CvTemplateRenderer` template catalog into a `CvTheme` descriptor set (id, label, layout shell, font stack, palette, heading style, default accent, ATS rating). Content pipeline (`RenderMainSections` + section renderers) already theme-agnostic — formalize the boundary: **renderer consumes `(document, theme)`; theme carries no CV logic.** 2. Layout **shells** stay a small fixed set (single-column, sidebar, rail, bordered); a theme selects a shell + tokens. Adding a theme that reuses a shell = a data entry, no code. 3. Golden test: render each existing template id before/after; assert byte-identical output (pure refactor). 4. **Deferred:** external template engine (Scriban) + user/marketplace themes — only when a marketplace is real (strategy §9). Do not add the dependency now. **Exit:** adding a theme on an existing layout is data-only; PDF output unchanged. ### 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 - Diff / accept-reject on every AI mutation (rewrite, improve, generation). Trust primitive; also kills silent hallucination. - Retarget `JobCvMatchService` to read from structured profile (not raw text) — removes the dual-truth divergence; validates F1's model with an existing consumer. - Fact-constraint validator (novel named-entity/number flagging) on generation. - Persist interview-prep / fit outputs (stop regenerating). ### 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.