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.
9.4 KiB
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 withCREATE TABLE IF NOT EXISTS+EnsureColumnguards, called in theuseSqliteBootstrapbranch (nearEnsureCvTables). - MySQL/MariaDB (prod):
if (!HasMySqlTable(...))blocks +EnsureMySqlColumn, in theelsebranch.
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):
- Stable item IDs on every profile item (jobs, bullets, skills, education, projects). Without them, variant lineage / "update everywhere" / inherit-with-override are unimplementable.
- Normalized dates (
YYYY-MM+isCurrent) — backfilled during migration by best-effort parse of the free-stringStart/End. Timeline / tenure / skills-recency depend on it. - 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 newcanEditCv(any authenticated user) instead ofisLocal. Identity/password fields remain local-only. Unblocks every Google/Microsoft user.
Phase F1 — Career Profile as first-class data (backwards-compatible seam)
- Add
CareerProfile+CareerProfileVersiontables to the reconciler (both dialects),DbSets, query filters, indexes. - Introduce
ICareerProfileService— the single accessor for the user's structured profile. Initially dual-writes: persists to the newCareerProfilestable and keepsApplicationUser.ProfileCvStructureJsonin sync (so nothing that still reads the column breaks). - Assign stable item IDs + normalize dates when materializing a profile into the new table (one-time backfill on first read/write per user).
- Point
ProfileCvControllerread/write paths at the service (behavior identical). - 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)
CvVariant+CvVersiontables. A variant references aCareerProfileand holds selection/override JSON keyed by item ID.TailoredApplicationtable:(CvVariantId, JobApplicationId)— the reference seam. Job references the tailored output; does not own the variant.- Backfill: each existing
TailoredCvDraft→ oneCvVariant(job-linked) + its render options extracted toward a theme ref, wrapped in aTailoredApplication. LegacyTailoredCvDraftsretained (dual-read) until proven. - 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)
- Extract the
CvTemplateRenderertemplate catalog into aCvThemedescriptor 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. - 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.
- Golden test: render each existing template id before/after; assert byte-identical output (pure refactor).
- 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
JobCvMatchServiceto 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.