Bring the four Career Workspace documents onto main as the target architecture for Phases 2-4, and point MASTER_IMPLEMENTATION_GUIDE.md at them. Taken from the branch tip (later commits refined them). Pure additions — none previously existed on main. - cv-builder-competitor-deep-research.md (Novoresume, Reactive Resume, FlowCV, Teal, Enhancv, Canva, Resume.io, Kickresume; matrix; pricing intelligence). - cv-builder-product-teardown.md - career-workspace-product-strategy.md - career-workspace-implementation-roadmap.md (F0-F5) MASTER_IMPLEMENTATION_GUIDE.md v1.1: adds a Source-Of-Truth Documents section and restates the "profile is the source of truth; documents reference snapshots" rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
30 KiB
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
- Land → sign in (email/Google/Microsoft) → dashboard.
- Add jobs manually, via bookmarklet, or via Gmail suggested-jobs.
- Track through kanban; Gmail sync auto-links correspondence; reminders fire.
- Separately, on Profile page: upload a CV → extraction pipeline produces structured profile.
- Inside a job's details dialog (a modal!): generate tailored CV draft → edit in text fields → preview → export PDF.
- 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 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/joinLinesintailoredCvDraft.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) andGenerationContextHashexist 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):
- OAuth users locked out of CV features (bug).
- No from-scratch creation path.
- No structured-profile editor — raw text is the editing surface.
- Tailored CV editor trapped in a modal.
- Single draft, overwrite-on-regenerate, no history.
- Pipeline vocabulary (runs/reprocess/rebuild) in end-user UI.
- Preview is HTML-in-a-box, not a paginated document preview; template switching is a dropdown with no visual gallery.
- 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)
- 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?").
- Match scoring with curated skill synonyms in the same system as tracking — Teal's premium feature, already ours.
- 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.
- Server-side Playwright rendering — full CSS typography control; matches the print quality of the design-led cluster.
- 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, genericSections), already covers certifications/projects/languages/interests, andMetadata.Fieldscarries per-field provenance. Weak: date fields are free-strings (Start/End) so no reliable timeline math (career-timeline feature will choke);SkillsisList<string>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 (JobApplicationIdFK, effectively 1:1), section JSON blobs,TemplateId+RenderOptionsJsoninline. Good:CanonicalProfileVersion+GenerationContextHashstaleness 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;Statusis a two-state string doing lifecycle work.CvUploadArtifact/CvExtractionRun— the best-designed corner: artifacts retained, runs versioned by parser/normalizer/prompt versions,StructuredProfileJsonsnapshot 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:
- Stable item IDs in the profile schema (jobs, bullets, skills) — without them, variant/tailoring lineage, "update everywhere," and inheritance-with-overrides are all unimplementable.
- 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:
- Every AI mutation renders as a diff with accept/reject (also solves the limited-rewrite problem).
- 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.
- 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)
- 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.
- Extraction review queue: low-confidence fields surfaced as confirm/fix cards — turns existing provenance metadata into visible trust.
- Email-aware prep: interview prep that reads the actual recruiter thread (unique data no competitor holds).
- 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
- OAuth CV lockout bug — fix now (one-line frontend condition).
- Tailored editor out of the modal into a full-page workspace route.
- Rewrite → diff/accept/reject.
- Match score ↔ tailoring connection (the loop).
- Template gallery with visual previews (data exists in
templatesendpoint descriptors). - Pipeline vocabulary → user vocabulary ("Update from new CV," not "Reprocess run").
- Section editing: per-bullet rows with reorder + inline AI, not newline blobs.
- 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<T> (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.