# CV Builder Product Teardown — Jobbjakt Internal Review **Date:** 2026-07-12 **Scope:** Critical product, UX, and technical teardown of the current CV builder and related career features, ahead of the Career Workspace redesign. **Companions:** `docs/cv-builder-competitor-deep-research.md` (market context) and the CV Builder Architecture Proposal artifact (target design). This document is the honest "as-is" audit both of those build on. **Stance:** Written as a senior PM + UX designer + architect reviewing before a major redesign. Existing code is treated as evidence, not as precedent. Where something is wrong, it is called wrong. --- ## 1. Current Product Overview ### What the application is today Jobbjakt is a self-hosted, single-tenant-ish (multi-user with admin) **job application tracker** with strong email intelligence, to which a CV builder has been progressively bolted on. Live at `https://jobs.cesnimda.uk`. Stack: ASP.NET Core 9 + EF Core (SQLite dev / MariaDB prod), Next.js 16 static-export frontend (MUI), FastAPI AI sidecar (Ollama/Gemini/Groq router), Playwright PDF rendering. **Feature inventory (career-relevant):** | Area | What exists | Where | |---|---|---| | Job tracking | CRUD, kanban pipeline, drag-drop, statuses, reminders, analytics/funnel | `JobApplicationsController` (~2,900 lines), `KanbanBoard`, `DashboardView` | | Email intelligence | Gmail sync, thread↔job linking, review queue, suggested jobs, status suggestions | `GmailController`, `GmailReviewPage`, `CorrespondenceInboxPage` | | Master CV | Upload (PDF/text) → parse → normalize → structured profile; rebuild/improve/reprocess; section rewrite | `ProfileCvController` (2,265 lines), `ProfilePage.tsx` | | Tailored CV | Per-job draft: generate from master + job, edit, preview, PDF export | `JobApplicationsController:2465–2614`, `JobDetailsDialog.tsx` | | Templates | 6 hardcoded templates (ats-minimal, harvard, auckland, edinburgh, monarch, fjord), curated accent palette, section order, page mode, bullet density | `CvTemplateRenderer` (448 lines), `TailoredCvRenderOptions` | | Match scoring | CV↔JD keyword match with curated skill-tag synonyms | `JobCvMatchService`, `SkillTagger`, `match-score` endpoint | | Per-job AI | Interview prep, candidate fit, focus plan, follow-up drafts, application package, readiness | `JobDetailsDialog.tsx` (all crammed into one dialog) | | AI service | `/summarize`, `/cv/normalize`, `/cv/classify-block`, `/cv/rewrite` with prompt-injection hardening + provider router | `tools/summarizer/app.py` | ### Target user Implicitly: **the developer himself** — a technically fluent, self-hosting job seeker running an active search. Nothing in the product contradicts this: no onboarding for novices, AI knobs exposed raw (tone/language/target-role fields), extraction "runs" and "reprocess" surfaced in end-user UI, admin pages in the same nav. The recent additions (onboarding checklist, empty states, OAuth signup) are the first genuine gestures toward a second user. **This is the central product tension:** the backend is built like a multi-user SaaS (Identity, roles, registration, OAuth, per-owner scoping), but the UX is built like a personal tool. The redesign must pick: the Career Workspace vision implies real second users, which means the "developer-as-user" assumptions have to go. ### Current user journey 1. Land → sign in (email/Google/Microsoft) → dashboard. 2. Add jobs manually, via bookmarklet, or via Gmail suggested-jobs. 3. Track through kanban; Gmail sync auto-links correspondence; reminders fire. 4. Separately, on Profile page: upload a CV → extraction pipeline produces structured profile. 5. Inside a job's details dialog (a modal!): generate tailored CV draft → edit in text fields → preview → export PDF. 6. AI extras (interview prep, fit, follow-ups) live as tabs/sections in that same modal. ### Current relationships (the actual object model in practice) ``` ApplicationUser ├─ ProfileCvText (raw CV text — a column on the Identity user row) ├─ ProfileCvStructureJson (StructuredCvProfile serialized — also a user column) ├─ CvUploadArtifact ──< CvExtractionRun (parser/normalizer/prompt versions, run history) └─ JobApplication ──1:1── TailoredCvDraft ├─ TemplateId + RenderOptionsJson (presentation welded to content) ├─ SummaryJson / SelectedSkillsJson / ExperienceJson / ... (per-section JSON blobs) ├─ CanonicalProfileVersion (staleness pointer to master) └─ Status ("generated" / edited) Cover letters: DO NOT EXIST as entities. Interview prep etc.: transient AI responses, not persisted as documents. ``` ### Current product philosophy (inferred, since none is written down) - **Tracker-first:** the CV is an attachment to a job application, not a first-class product. The tailored CV lives *inside the job details modal* — the clearest possible statement of the current hierarchy. - **One master CV, ephemeral derivatives:** exactly one profile per user; tailored drafts are per-job satellites; nothing else is durable. - **AI as pipeline, not as assistant:** AI does batch transforms (parse this, rewrite that) with exposed machinery, rather than conversational or inline assistance. - **Provenance-conscious:** field-level confidence/review-state metadata shows real care about "where did this claim come from" — unusually mature for this product stage. The Career Workspace vision inverts the first two tenets. The last two are worth keeping. --- ## 2. User Experience Review ### New user **How does a user create their first CV?** They can't, in any meaningful sense — they can only *import* one. `ProfilePage` offers "Upload CV" (parse an existing document). There is no from-scratch path: no guided form, no "add your first job" flow for the profile. A user without an existing CV document is stuck. Every competitor in the research offers from-scratch creation; we are import-only. **Is onboarding clear?** The new onboarding checklist (add CV / import job / check match) is a good spine, but step one drops the user on a Profile page where the CV feature is a card among password/avatar/email-connection cards. CV building is presented as an *account setting*. That framing is wrong for what is supposed to become the product's centerpiece. **Critical bug, found during this audit:** [`ProfilePage.tsx:356`](../job-tracker-ui/src/views/ProfilePage.tsx) computes `isLocal = me?.provider === "local"` and disables CV upload/rebuild/improve (among other controls) for OAuth users. The gate was presumably meant for identity fields (can't change password on a Google account) and was blanket-applied to the CV card. **Google/Microsoft users — the exact accounts we just built auto-signup for — get a disabled CV builder.** A Google-first new user's journey dead-ends at step one of the checklist. Must-fix regardless of redesign timing. **Where do new users get confused?** - "Reprocess," "Rebuild," "Improve," "Runs" — four adjacent buttons whose distinction (re-run extraction vs. regenerate structure vs. AI-rewrite text vs. view pipeline history) is developer vocabulary. No user knows which to press. - The structured profile (the actual output of extraction) has no real editing UI — the raw text and the structure are shown, but correcting a mis-parsed date means fighting JSON or re-uploading. - Nothing explains that the master CV feeds match scores and tailored drafts; the causal chain that makes the product coherent is invisible. ### Existing user **Editing the CV:** the master CV is edited as *raw text* (`ProfileCvText` in a textarea) with AI rewrite assistance per-section. The structured profile is a *derived artifact* the user can't directly maintain. This is backwards relative to both competitors (structured forms are the primary surface everywhere) and our own architecture proposal. Consequence: every text edit desynchronizes text from structure until a rebuild; the "which is the truth?" question has no good answer today (see §4). **Creating tailored CVs:** open a job → details modal → tailored CV section → "Generate" → edit. Real problems: - It's in a **modal**. A document editor competing for space with interview prep, fit analysis, follow-ups, readiness, notes — inside a dialog over the jobs table. No room for the side-by-side JD↔CV tailoring view that the competitor research identified as the killer screen (Teal's core loop). - Bullets are edited as newline-joined blobs (`splitLines`/`joinLines` in `tailoredCvDraft.ts`) — plain textareas, no per-bullet operations, no drag-reorder, no AI-improve-this-bullet affordance at the point of editing. - **One draft per job, no variants, no history.** Regenerate overwrites; a good manual edit lost to a regenerate is unrecoverable. `Status` ("generated"/edited) and `GenerationContextHash` exist precisely because overwrite-anxiety is real — they mitigate instead of solving. **Reusing information:** the master→tailored generation is the only reuse mechanism. No way to reuse a great tailored summary across jobs, no library of alternative bullets, no second master for a different career track. `CanonicalProfileVersion` at least detects when a draft is stale relative to the master — good instinct, minimal payoff without a refresh/diff flow. **Maintaining career information over time:** effectively unsupported. Adding a new job to your history = edit raw text + rebuild, or re-upload a new document. For a product whose vision is "the structured career profile is the single source of truth," today's truth is a text blob on the user table. **Friction inventory (ranked):** 1. OAuth users locked out of CV features (bug). 2. No from-scratch creation path. 3. No structured-profile editor — raw text is the editing surface. 4. Tailored CV editor trapped in a modal. 5. Single draft, overwrite-on-regenerate, no history. 6. Pipeline vocabulary (runs/reprocess/rebuild) in end-user UI. 7. Preview is HTML-in-a-box, not a paginated document preview; template switching is a dropdown with no visual gallery. 8. CV features split across two distant locations (Profile page ↔ job modal) with no navigational thread connecting them. --- ## 3. CV Builder Analysis (vs. competitor research) ### Editing model Current: **raw-text-primary with derived structure** (master) and **form-ish JSON blob editing** (tailored). The market-winning model per the research: **structured forms + instant themed preview + drag-drop sections** (FlowCV, Reactive Resume, Teal). We have the *data model* for that (StructuredCvProfile is section-granular) but not the UI. Verdict: the editing surface must be rebuilt around structure; the raw text demotes to an import artifact and export view. ### Preview experience Rendered HTML returned by the server per-request, displayed inline. No client-side re-render on keystroke, no pagination fidelity, no zoom, no "what the ATS sees" view (which our structured pipeline could produce almost for free — competitive claim identified in research §7.4). Against FlowCV's lag-free live preview this is a clear generation behind. ### Template switching & customization Genuinely decent bones: `TemplateId` swaps freely over the same content (content/presentation separation *within* the draft works); render options offer curated accent palette, section order, page mode, bullet density, photo toggle. This matches the curated-token pattern Novoresume uses (research §4.7) — the right instinct. Falls short on: no visual template gallery, no font choice, no spacing control, six templates whose design quality is mid-tier vs. the design-led cluster, and per-CV theme settings can't be saved/reused as a named style. ### What we already do well (protect these in the redesign) 1. **Extraction pipeline with provenance** — versioned runs (parser/normalizer/prompt versions), field-level confidence + review-state + source snippet. No competitor surfaces provenance at all. This is a differentiating asset the moment a review UI exposes it ("we're 60% sure about this date — confirm?"). 2. **Match scoring with curated skill synonyms** in the same system as tracking — Teal's premium feature, already ours. 3. **Prompt-injection-hardened AI pipeline** with delimiter fencing and instruction-ignoring rules; provider router with local fallback. More mature than the market's bolt-on AI. 4. **Server-side Playwright rendering** — full CSS typography control; matches the print quality of the design-led cluster. 5. **Staleness detection** (`CanonicalProfileVersion`, `GenerationContextHash`) — the primitive that version-aware tailoring needs. ### Where we fall behind | Dimension | Market bar | Us | |---|---|---| | From-scratch creation | Universal | Absent | | Structured editing UI | FlowCV/RR/Teal forms + preview | Raw text + JSON blobs | | Live preview | Instant, paginated | Server round-trip HTML | | Variants | Unlimited (RR) / paid tiers | One per job, zero free-standing | | Version history | Rare in market (opportunity) | None (also our gap) | | Template count/quality | 30–50 good | 6 mid | | DOCX export | ~60% of market | None | | Public share link | RR, FlowCV | None | | Onboarding to first PDF | <10 min (FlowCV) | Not achievable without an existing CV document | --- ## 4. Current Data Model Review ### Entities and storage - **`ApplicationUser.ProfileCvText` + `ProfileCvStructureJson`** — the master CV as two nullable string columns *on the Identity user row*. Sins: (a) fat blobs on the most-fetched row in the system (auth reads drag CV bytes along unless carefully projected); (b) exactly-one-profile hard-coded into the schema — the CvVariant/second-career-track future requires a migration by definition; (c) no versioning — every rebuild silently destroys the previous structure (extraction *runs* are versioned; the *applied profile* is not); (d) dual representation with no single source of truth — text and structure coexist, edits touch one, rebuilds overwrite the other, and different features read different ones (match scoring builds a corpus from raw text; tailoring generates from structure). - **`StructuredCvProfile`** (JSON shape) — good: section-granular, extensible (`OtherSections`, generic `Sections`), already covers certifications/projects/languages/interests, and `Metadata.Fields` carries per-field provenance. Weak: date fields are free-strings (`Start`/`End`) so no reliable timeline math (career-timeline feature will choke); `Skills` is `List` with no proficiency/category/years; no stable IDs on items, so "this bullet in the tailored draft came from job #2 bullet #3" is unexpressible — lineage between master and tailored content is lost at generation time. - **`TailoredCvDraft`** — one row per job (`JobApplicationId` FK, effectively 1:1), section JSON blobs, `TemplateId` + `RenderOptionsJson` inline. Good: `CanonicalProfileVersion` + `GenerationContextHash` staleness primitives; JSON-blob sections are pragmatic for a document-shaped payload. Bad: **presentation welded to content** (can't render one draft in two themes without mutating it — the architecture proposal's core criticism, confirmed); no variant concept; no history; `Status` is a two-state string doing lifecycle work. - **`CvUploadArtifact` / `CvExtractionRun`** — the best-designed corner: artifacts retained, runs versioned by parser/normalizer/prompt versions, `StructuredProfileJson` snapshot per run. This IS a version history — but only for imports, and nothing lets a user diff or restore from it. - **Cover letters, interview prep, portfolios, public profiles** — no entities. Interview prep/fit/focus outputs are transient API responses; a user's best interview-prep notes evaporate. ### Verdict against the Career Workspace target The proposal's target model (CareerProfile → CvVariant → CvVersion, Theme as sibling reference, TailoredCvVersion, output entities) is confirmed necessary by this audit, and the migration is *tractable*: `ProfileCvStructureJson` lifts into a `CareerProfile` table nearly verbatim; each `TailoredCvDraft` becomes a job-linked CvVariant with its render options extracted to a theme reference. Two additions this audit forces onto the proposal: 1. **Stable item IDs** in the profile schema (jobs, bullets, skills) — without them, variant/tailoring lineage, "update everywhere," and inheritance-with-overrides are all unimplementable. 2. **Normalize dates** (`YYYY-MM` + `isCurrent`) at migration time — timeline, tenure math, and skills-recency all depend on it; migrating free-strings later means re-parsing every profile again. Also settle the source-of-truth rule explicitly: **structure is canonical; text is derived** (an export format and search corpus, regenerated on change) — and make match scoring read from structure so the two consumers stop diverging. --- ## 5. AI Workflow Review ### What exists | Feature | Flow | Persistence | |---|---|---| | CV parse/normalize | upload → PDF text → `/cv/normalize` + `/cv/classify-block` (delimiter-fenced) → StructuredCvProfile | Run snapshots ✓ | | Section rewrite / improve | section text + tone/language/target-role knobs → `/cv/rewrite` → replace text | Overwrites | | Tailored generation | master profile + job context → draft sections | Overwrites draft | | Match score | curated skill tags (synonym regex) + keyword corpus | Computed | | Job-ad summary | local distilbart `/summarize` | Stored on job | | Interview prep / fit / focus / follow-ups / package / readiness | per-job LLM calls from modal | **Transient** | ### What's genuinely valuable - The **hardened pipeline** (fencing, ignore-embedded-instructions, provider router with graceful local fallback) — infrastructure competitors lack. - **Match scoring** — the research's verdict was that JD-gap analysis is the one universally-praised AI feature; ours is real (curated synonyms beat naive keyword matching) and already wired to job data. - **Structured extraction with confidence** — the input side of every future feature. ### What's limited - **Rewrite is fire-and-forget:** no diff view, no accept/reject, no before/after. The research flagged "generic rewrite" as the gimmick tier and "improvement with visible diff/scoring" as the useful tier (BeamJobs pattern). We're on the wrong side of that line purely for lack of UI. - **Tailoring is disconnected from scoring:** generation doesn't take the match-score gaps as input, and the score doesn't update live as the user edits the draft. The two halves of the killer loop exist and don't talk. - **AI knobs are raw:** tone/language/target-role as form fields instead of intent-level actions ("make this more senior," "address this missing keyword"). - **Transient outputs:** interview prep and fit analyses regenerate (cost + latency + inconsistency) instead of persisting as reviewable documents. ### Hallucination / factual-accuracy risk — currently the biggest unmanaged AI risk Rewrites and tailored generation can fabricate: a rewrite that upgrades "assisted with migration" to "led migration" is a *career integrity* failure, invisible today because nothing constrains generation to source facts or shows the user a diff. The provenance metadata (source snippets, confidence) exists on extraction but is **not enforced on generation**. Recommendation, in priority order: 1. Every AI mutation renders as a **diff with accept/reject** (also solves the limited-rewrite problem). 2. Generation prompts constrained to *select/rephrase* profile content, never invent quantities, employers, titles, or dates; validator pass flags novel named entities/numbers that don't appear in the source profile. 3. Tailored content carries source-item references (needs the stable IDs from §4) so "where did this claim come from" is answerable per bullet. ### Missing AI opportunities (ranked by leverage of existing assets) 1. **Tailoring loop screen:** match gaps ↔ draft side-by-side, one-click "address this gap" → constrained rewrite → live rescore. Wires three existing services into the screen no competitor has with our rendering quality. 2. **Extraction review queue:** low-confidence fields surfaced as confirm/fix cards — turns existing provenance metadata into visible trust. 3. **Email-aware prep:** interview prep that reads the actual recruiter thread (unique data no competitor holds). 4. **Skills-gap analytics across tracked JDs:** "your last 15 rejections wanted X" — pure aggregation over data already stored. --- ## 6. Template & Rendering Review ### Pipeline `TailoredCvDocument` → `CvTemplateRenderer.Render(templateId, …)` → C# switch over 6 template methods building HTML strings (~450 lines total) → `PlaywrightCvPdfExporter` (headless Chromium print) → PDF. Accent resolved via `ResolveAccent` (slate/blue/emerald/plum/brick → hex). Same renderer drives HTML preview and PDF (single source of visual truth — good). ### Evaluation - **Add a new template:** write a new C# method, recompile, redeploy. Designer-inaccessible, review-heavy, untestable in isolation. Cost is why there are six. - **User customization:** limited to the curated render options; anything more means more C# branches. - **Premium/marketplace templates:** impossible — templates are compiled code; third-party code in the renderer is a non-starter (research §6 confirmed themes-as-declarative-data is how RR solves this: "Structured Style Rules"). - **Career Workspace outputs:** each new output type (public profile page, portfolio, DOCX) would today mean another hardcoded renderer. The proposal's `IOutputAdapter` + Scriban theme manifests directly answers this; this audit adds one guardrail from RR's history (they abandoned server-Chromium for cost): **keep the PDF backend swappable behind the adapter interface** — Playwright is right for us now (already built, small scale, full CSS), but the boundary must let a lighter renderer replace it without touching themes. - **Print fidelity risks present today:** page-break control is CSS-implicit (no explicit widow/orphan handling per section); "one-page" mode is a squeeze heuristic rather than a layout contract. Fine at 6 templates; codify break rules in the theme manifest schema when porting. --- ## 7. Feature Gap Analysis ### Missing entirely (vs. competitor research + vision) | Feature | Competitor bar | Vision need | Cost given our architecture | |---|---|---|---| | From-scratch CV creation | Universal | Yes | Medium (structured editor is the prerequisite) | | Multiple CV variants | RR unlimited; FlowCV's paywall seam | Core | Schema migration (planned) | | CV version history | Market gap — differentiator | Core | Medium (CvVersion planned) | | Structured profile editor + review UI | Universal (forms) | Core | Large — the main UI build | | Live paginated preview | FlowCV bar | Yes | Medium | | DOCX export | ~60% of market, loud complaints | Yes | Medium, scoped (per proposal) | | Public profile / share link | RR, FlowCV | Core (Horizon 2) | Medium (adapter + theme) | | Cover letter entity + generation | All paid competitors | Core | Small once profile model lands | | Career timeline / skills matrix | Nobody good | Differentiator | Small *after* date normalization | | Persisted interview prep | Nobody (market gap) | Differentiator | Small (persist what exists) | | ATS-view ("what the parser sees") | Nobody shows it | Trust play | Small (we have structure) | | JSON Resume interop / published schema | RR | Trust play | Small | ### Exists but needs improvement 1. **OAuth CV lockout bug** — fix now (one-line frontend condition). 2. Tailored editor out of the modal into a full-page workspace route. 3. Rewrite → diff/accept/reject. 4. Match score ↔ tailoring connection (the loop). 5. Template gallery with visual previews (data exists in `templates` endpoint descriptors). 6. Pipeline vocabulary → user vocabulary ("Update from new CV," not "Reprocess run"). 7. Section editing: per-bullet rows with reorder + inline AI, not newline blobs. 8. Extraction confidence → visible review flow instead of buried metadata. --- ## 8. Product Recommendations ### Immediate (low effort / high impact — do before or alongside Phase 1) | # | Recommendation | Problem | User value | Technical impact | Complexity | Priority | |---|---|---|---|---|---|---| | I1 | Fix OAuth `isLocal` CV lockout | Google/MS users can't use CV features | Unblocks all OAuth users | One condition split (identity-gates vs. feature-gates) | Trivial | **P0 — bug** | | I2 | Diff + accept/reject on every AI rewrite | Silent overwrites; hallucination invisible | Trust in AI edits; recoverability | Frontend diff view; keep previous text | Small | P1 | | I3 | Persist interview prep / fit outputs | Regeneration cost; lost work | Notes survive; consistent prep | One table or JSON column per job | Small | P1 | | I4 | Rename pipeline vocabulary in UI | Developer jargon confuses | Comprehensible actions | i18n strings only | Trivial | P1 | | I5 | Template gallery with thumbnails | Blind dropdown | Informed template choice | Render 6 previews once; static images | Small | P2 | | I6 | Export JSON of profile + drafts | No data-ownership story | Trust (research: fair-exit is strategy) | One endpoint, serializers exist | Small | P2 | ### Medium-term (requires the architectural work — Phases 1–4 of proposal) | # | Recommendation | Problem | User value | Technical impact | Complexity | Priority | |---|---|---|---|---|---|---| | M1 | CareerProfile + CvVariant + CvVersion migration, **with stable item IDs and normalized dates** | One profile, no variants, no history, blobs on user row | Multiple CVs, safe regeneration, update-everywhere | The Phase-1 schema migration; raw-SQL reconciler steps | Large | P0 of redesign | | M2 | Structured profile editor + extraction review queue | Raw text is the editing surface | Maintainable career data; visible trust | New primary UI; confidence metadata already present | Large | P0 of redesign | | M3 | Theme engine port (Scriban manifests, 6→best 3–4 templates) | Templates are compiled code | Theme switching, future marketplace | Per proposal; add explicit page-break rules to manifest schema | Large | P1 | | M4 | Tailoring workspace route (JD gaps ↔ draft ↔ live preview + rescore) | Modal editor; disconnected scoring | The killer screen (research MVP §9.5) | New route; wires existing services | Large | P1 | | M5 | Fact-constrained generation + novel-entity validator | Hallucinated seniority/numbers | Career integrity | Prompt + validator in FastAPI sidecar | Medium | P1 | | M6 | From-scratch creation + paste-text import | Import-only onboarding | New-grad / no-CV users can start | Falls out of M2 + existing normalize path | Medium | P2 | | M7 | Scoped DOCX adapter | Loudest export complaint in market | "Employer wants Word" solved | OpenXML `IOutputAdapter` (per proposal: scoped, not parity) | Medium | P2 | ### Long-term (Career Workspace horizons) | # | Recommendation | Value | Complexity | Priority | |---|---|---|---|---| | L1 | Cover letters as profile-derived entities | Completes application package | Small post-M1 | P1 of Horizon 2 | | L2 | Public profile / share link (theme over live profile, not doc snapshot) | RR/FlowCV parity + our live-data twist | Medium | P2 | | L3 | Email-aware interview prep (thread context) | Unique-data moat; market's empty space | Medium | P1 of Horizon 3 | | L4 | Skills matrix + gap analytics across tracked JDs | Nobody has it; pure aggregation for us | Small post-M1 | P2 | | L5 | Portfolio / personal site as output adapters | Vision endgame; each ≈ adapter + theme | Medium each | P3 | | L6 | Published profile schema + JSON Resume interop (+ MCP later) | Trust + agent-native future (RR precedent) | Small | P3 | --- ## 9. Proposed Future Architecture Confirms the architecture proposal, with this audit's amendments folded in: ``` CareerProfile (one per user now, N later; stable item IDs; normalized dates; │ provenance metadata retained; STRUCTURE canonical, text derived) │ sources: upload/extraction runs (kept), from-scratch editor, paste-text │ ├──< CvVariant (free-standing OR job-linked; inherits profile, holds │ │ selections/overrides by item ID — lineage preserved) │ ├──< CvVersion (history: every generation & manual save; diff/restore) │ └── ThemeRef ────→ Theme (SIBLING input, never welded into content; │ declarative Scriban manifest: tokens, layout, │ section styles, page-break rules; ATS rating) │ ├── Tailoring loop (JobApplication + MatchScore gaps ↔ variant edits ↔ │ constrained AI rewrites w/ diff ↔ live rescore) │ └──> IOutputAdapter (renderer boundary; PDF backend swappable — RR lesson) ├─ PDF CV (Playwright, today) ├─ DOCX CV (OpenXML, scoped) ├─ ATS plain-text (trust view — near-free) ├─ Cover letter (profile + job + thread context) ├─ Public profile (theme over live profile) ├─ Portfolio / site (Horizon 3+) ├─ LinkedIn content (Horizon 3) └─ Interview prep (persisted; email-thread-aware — unique moat) ``` **Why this redesign is right (one paragraph):** every weakness this teardown found — single profile as user-row blobs, text/structure truth conflict, presentation welded to drafts, overwrite-anxiety mitigations, compiled templates, modal-trapped editing, transient AI outputs — is a symptom of the same root cause: *documents are the primary objects and career data is trapped inside them*. The competitor research shows the market leader in each dimension solved exactly one symptom (RR: schema; FlowCV: editor; Teal: career data; Novoresume: themes) and none solved the root. Inverting the model — profile as source of truth, every artifact a themed projection — fixes all symptoms with one architecture, reuses our real assets (extraction provenance, match scoring, email intelligence, hardened AI pipeline, Playwright rendering), and each subsequent output costs one adapter + one theme instead of one product. **Sequencing note:** I1 (OAuth bug) ships now. M1+M2 before any visible redesign — the migration is invisible and everything depends on it. M3/M4 are the visible payoff. The proposal's phase plan stands; this audit adds stable IDs + date normalization as Phase-1 requirements and the diff-everywhere rule as a design principle from day one.