docs: add Career Workspace research, teardown, strategy, and roadmap

Four discovery documents backing the Career Workspace redesign:

- cv-builder-competitor-deep-research.md: teardown of Novoresume,
  Reactive Resume, FlowCV, Teal, Enhancv, Canva, Resume.io, Kickresume
  -- positioning, UX patterns, pricing/trust failures, technical
  architecture lessons (esp. Reactive Resume's content/theme
  separation and PDF pipeline history).
- cv-builder-product-teardown.md: critical as-is audit of this app's
  CV builder -- data model, editor UX, AI workflow, rendering
  pipeline, feature gaps -- including the OAuth CV lockout bug fixed
  in a prior commit.
- career-workspace-product-strategy.md: product vision, positioning,
  personas, core object model, feature roadmap (MVP/V2/future),
  AI/monetization strategy, and the first 10 engineering tasks.
- career-workspace-implementation-roadmap.md: the execution plan --
  product boundary (Career Workspace is a bounded domain supporting
  job tracking, not replacing it), phased sequencing (F0-F6), and the
  migration mechanics specific to this repo's raw-SQL schema
  reconciler.
This commit is contained in:
cesnimda
2026-07-12 15:21:17 +02:00
parent cbd045a0d3
commit 102938c28b
4 changed files with 1094 additions and 0 deletions
@@ -0,0 +1,118 @@
# 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.