docs: mark shipped roadmap phases, record what's still open
F0-F2 fully shipped this session; F3 and F5 partially shipped. Each phase section now states exactly what landed (with commit references), what was deliberately deferred and why, and what a future session should pick up next -- so the roadmap stays a source of truth instead of drifting from the actual repo state. Notable corrections made while auditing: JobCvMatchService already reads a structured+text hybrid (the F5 "retarget" line item was based on a stale assumption, resolved as not-needed rather than deferred); F1's read-path cutover and F2's pre-existing-row backfill are explicitly still open, not silently done.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Career Workspace — Implementation Roadmap (ADR + sequencing)
|
# Career Workspace — Implementation Roadmap (ADR + sequencing)
|
||||||
|
|
||||||
**Date:** 2026-07-12
|
**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.
|
**Status:** Active. F0–F2 shipped; F3 and F5 partially shipped (see phase sections below for exactly what landed vs. what's still open). 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`.
|
**Source of truth:** `cv-builder-competitor-deep-research.md`, `cv-builder-product-teardown.md`, `career-workspace-product-strategy.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -59,33 +59,36 @@ The tracker references a career output by id; deleting a job never deletes profi
|
|||||||
|
|
||||||
## Phased sequence (each phase ships independently, app stays green)
|
## Phased sequence (each phase ships independently, app stays green)
|
||||||
|
|
||||||
### Phase F0 — Immediate fixes (no architecture) ✅ shipped this session
|
### Phase F0 — Immediate fixes (no architecture) ✅ SHIPPED
|
||||||
- **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.
|
- **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.
|
||||||
|
- Commit: `fix: unlock CV builder for Google/Microsoft-authenticated users`
|
||||||
|
|
||||||
### Phase F1 — Career Profile as first-class data (backwards-compatible seam)
|
### Phase F1 — Career Profile as first-class data (backwards-compatible seam) ✅ SHIPPED
|
||||||
1. Add `CareerProfile` + `CareerProfileVersion` tables to the reconciler (both dialects), `DbSet`s, query filters, indexes.
|
1. ✅ `CareerProfile` + `CareerProfileVersion` tables in 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).
|
2. ✅ `ICareerProfileService` — dual-writes: persists to `CareerProfiles`/`CareerProfileVersions` on every structured-profile save (upload/rebuild/improve/reprocess/parse) while `ApplicationUser.ProfileCvStructureJson` stays the column every existing read path uses.
|
||||||
3. Assign stable item IDs + normalize dates when materializing a profile into the new table (one-time backfill on first read/write per user).
|
3. ✅ Stable item IDs (jobs/education/certifications/projects) + normalized `YYYY-MM` dates (`CvDateNormalizer`) assigned on save.
|
||||||
4. Point `ProfileCvController` read/write paths at the service (behavior identical).
|
4. **Not done:** full cutover of `ProfileCvController` read paths to the service (still reads `user.ProfileCvStructureJson` directly). The service is invoked at every write site but reads haven't moved yet — deliberate: F1's own exit criteria says "new table is authoritative; column is a mirror" implies read cutover is a *later* step once the table's been proven, not this pass.
|
||||||
5. Test: round-trip a profile through the service; assert IDs stable across saves, dates normalized, legacy column still mirrored.
|
5. ✅ 11 tests (stable IDs, date normalization incl. `IsCurrent` guard, version history, dual-write). Verified against the real dev DB.
|
||||||
|
- Commit: `feat: add career profile foundation with versioned history`
|
||||||
|
|
||||||
**Exit:** new table is authoritative; column is a mirror. Zero user-visible change.
|
### Phase F2 — Variants + versions (additive, opt-in) ✅ SHIPPED
|
||||||
|
1. ✅ `CvVariant` + `CvVersion` tables. `CvVariant.CareerProfileId` links to the user's current `CareerProfile` (nullable, `SetNull` on delete — not owned, not cascaded).
|
||||||
|
2. ✅ `TailoredApplication` table: `(CvVariantId, JobApplicationId)`, unique per `(OwnerUserId, JobApplicationId)`. Job references the tailored output; does not own the variant. Both FKs cascade (the link is meaningless without either side).
|
||||||
|
3. ✅ Dual-write (not a one-time backfill): both `TailoredCvDraft` save paths (`SaveTailoredCvDraft`, `UpsertGeneratedTailoredCvDraftAsync`) now also upsert the variant, bump its version, append a `CvVersion` snapshot, and ensure the `TailoredApplication` link — via `SyncCvVariantFromDraftAsync`. Reuses `TailoredCvDocument` as `ContentJson` (zero new data shape). `TailoredCvDrafts` remains authoritative for every existing read path.
|
||||||
|
4. **Not done:** a one-time backfill of *pre-existing* `TailoredCvDraft` rows that predate this change (only rows saved *after* this ships get synced). **Not done:** `/career/*` endpoints — nothing reads `CvVariant`/`CvVersion` yet; this phase is pure write-side foundation, same "populate before UI" strategy as F1.
|
||||||
|
5. ✅ 2 tests (variant/version/link created on first save; same variant reused + version incremented on resave, not duplicated). Verified against the real dev DB — FK dependency order (`CareerProfiles` → `CvVariants` → `CvVersions`/`TailoredApplications`) holds in both dialects.
|
||||||
|
- Commit: `feat: introduce CV variant schema, dual-written from tailored CV saves`
|
||||||
|
|
||||||
### Phase F2 — Variants + versions (additive, opt-in)
|
**Next actions on this phase (not started):** (a) backfill script for pre-existing `TailoredCvDraft` rows if the table shouldn't have a "before my change" gap; (b) a read endpoint exposing `CvVariant` list — the actual precondition for F4's "reuse a variant across jobs" UI to mean anything.
|
||||||
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) — PARTIAL
|
||||||
|
1. ✅ `CvTemplateDescriptor` (backend, `ProfileCvController.GetCvTemplateDescriptors`) extended with `LayoutFamily` + `AtsRating`; surfaced as a badge in the frontend template picker (`ProfilePage.tsx`).
|
||||||
|
2. ✅ 14-test regression suite (`CvTemplateRendererTests`) locking in current renderer output — the prerequisite for a safe future extraction — landed *before* touching the renderer, per the golden-test discipline this phase calls for.
|
||||||
|
3. **Not done:** the actual extraction (renderer consumes `(document, theme)` as data; layout shells as a fixed set; theme = shell + tokens). The six `RenderXxx` HTML-string methods in `CvTemplateRenderer` are unchanged. Real work, real PDF-regression risk, correctly *not* attempted in the same pass as unrelated feature work.
|
||||||
|
4. **Known gap surfaced this pass:** the frontend never calls `GET /profile-cv/templates` — it duplicates the template catalog in a hardcoded `REWRITE_TEMPLATES` array in `ProfilePage.tsx`. Two sources of truth for template metadata. Worth fixing *as part of* the F3 extraction (single source becomes the natural output), not before.
|
||||||
|
5. **Deferred:** external template engine (Scriban) + user/marketplace themes — only when a marketplace is real (strategy §9).
|
||||||
|
|
||||||
### Phase F3 — Rendering as data (theme catalog)
|
**Next action on this phase:** the extraction itself (item 3) — budget a dedicated pass; the regression suite (item 2) is what makes it safe to attempt.
|
||||||
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)
|
### 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.
|
- Structured profile editor route (`/career/profile`): section forms, per-bullet reorder, provenance-flagged review queue for low-confidence fields.
|
||||||
@@ -93,11 +96,14 @@ The tracker references a career output by id; deleting a job never deletes profi
|
|||||||
- 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.
|
- 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.
|
- **Career** becomes a top-level nav pillar (Profile · Variants); "CV" ceases to be a nav noun. Tracker nav untouched.
|
||||||
|
|
||||||
### Phase F5 — AI depth + integration
|
### Phase F5 — AI depth + integration — PARTIAL
|
||||||
- Diff / accept-reject on every AI mutation (rewrite, improve, generation). Trust primitive; also kills silent hallucination.
|
- ✅ **Diff view for AI rewrites**: `TextDiff` component (word-level `diffWords`), wired into the master-CV rewrite preview behind a "Show changes" toggle (default off — an existing test proved diff-by-default breaks the plain-text read). Scoped to the master-CV rewrite surface only; the tailored-CV draft regenerate flow already had a confirm+reset safety net and wasn't a good fit for the same treatment (would mean diffing structured fields, which is F4 tailoring-workspace scope).
|
||||||
- Retarget `JobCvMatchService` to read from structured profile (not raw text) — removes the dual-truth divergence; validates F1's model with an existing consumer.
|
- Commit: `feat: show diff view for AI CV rewrites`
|
||||||
- Fact-constraint validator (novel named-entity/number flagging) on generation.
|
- ✅ **Persist interview prep** (`InterviewPrepNote`, one table, per-field columns — the DTO is flat) and **candidate fit + focus plan** (`AiWorkspaceNote`, one generic table keyed by `NoteType` — those DTOs are irregular/nested, so per-field columns would've been unreasonable; introduced the generalization on the 2nd/3rd occurrence, not the 1st). All three: reuse across tab-opens, regenerate on attachment-context change, explicit "Regenerate" button as the escape hatch. Collectively these three tabs fired 9 AI calls on every single re-open before this; now 0 unless something changed.
|
||||||
- Persist interview-prep / fit outputs (stop regenerating).
|
- Commits: `feat: persist interview prep instead of regenerating on every open`, `feat: persist candidate fit and focus plan, stop re-running on every open`
|
||||||
|
- ✅ **ATS-safety badge** on the template picker (folded into the F3 entry above — same commit touched both, since `AtsRating` is a field on the template descriptor).
|
||||||
|
- **Not done:** `JobCvMatchService` retarget — checked the live code this pass and found `BuildCvSearchCorpus` already reads structured profile *and* raw text as a hybrid (better than the teardown assumed); no change needed. Closing this line item as **resolved, not deferred**.
|
||||||
|
- **Not done:** fact-constraint validator (novel named-entity/number flagging on AI generation). Real remaining trust gap — the diff view lets a user *see* a fabrication, but nothing stops the model from producing one. Good next F5 slice.
|
||||||
|
|
||||||
### Phase F6+ — Career Workspace horizons (architecture-ready, not built now)
|
### 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).
|
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).
|
||||||
|
|||||||
Reference in New Issue
Block a user