# `feature/career-workspace` — Branch Assessment Date: 2026-07-17 Trigger: your Phase 3 decision on the "orphan tables". The investigation you asked for changed the answer entirely. > **STATUS UPDATE (2026-07-17): Foundation integrated.** The F1 Career Profile foundation plus > the interview-prep / candidate-fit / focus-plan persistence were squashed onto `main` as > commit `992f89e` ("integrate Career Workspace foundation"). Decisions: **foundation only** > (CV-builder commits deferred per "do not build CV Builder yet") and **squash** (one commit). > The four recovered documents landed earlier as `aedd6e3`. 264 backend tests pass; frontend > typechecks. What remains on the branch is Phase 3/4 work — see §8. --- ## 1. The finding You asked me to treat the orphan tables as abandoned prototypes and redesign around them. **They are not prototypes.** They are the local footprint of a **complete, tested, unmerged feature branch**. `feature/career-workspace` exists **locally and on `origin`**, 10 commits, last touched **2026-07-12** — five days ago. It was never reverted. It was simply never merged. The tables appeared in the dev database because that branch was checked out and run: its schema reconciler created them. The code was then switched away, leaving the tables behind. **My discovery report got this wrong.** I concluded "Novoresume, Reactive Resume and ElegantCV are not analysed anywhere in this repo" and listed CV variants as "planned, zero code". Both are false. The audit searched the working tree, `main`, and `docs/_archive/` — **it never ran `git branch -a`.** Corrections are recorded in `docs/application-discovery-report.md` §8 and its appendix. --- ## 2. What is on the branch ### Research and strategy (~1,100 lines, none of it on `main`) | Document | Lines | Content | |---|---|---| | `cv-builder-competitor-deep-research.md` | 327 | Teardowns of **Novoresume, Reactive Resume, FlowCV, Teal, Enhancv, Canva, Resume.io, Kickresume**; feature matrix; business-model analysis | | `career-workspace-product-strategy.md` | 328 | Vision, positioning, four personas (Marcus/Dana/Sofia/Tom) | | `cv-builder-product-teardown.md` | 321 | Product teardown | | `career-workspace-implementation-roadmap.md` | 124 | ADR-grade plan, phases F0–F5, migration mechanics, product boundary | **This is the exact research roadmap task 4.9 was scheduled to produce**, and it independently reaches the same conclusions my audit did from the FlowCV files: - *"The winning editor model is structured-form + live preview, not canvas editing and not rigid wizard flows."* - Client-side live preview is *"a hard UX requirement, not a nice-to-have"*. - Themes must be declarative data, a *"sibling input to renderer, never welded to content"*. Two independent analyses converging is strong corroboration for the Phase 4 direction. It also carries pricing intelligence Phase 7 needs and `main` does not have: Resume.io holds an **F BBB rating** over trial-to-subscription billing traps despite a 4.3 Trustpilot score; Novoresume blocks re-download of CVs the user **already paid for**; Kickresume caps "unlimited" AI mid-cycle. Its conclusion — *"never hold the user's data hostage"* — independently arrives at your "gate on capability, never count" decision. ### Working code, with tests | Area | Detail | |---|---| | `CareerProfileService` (169 lines) | Dual-writes `CareerProfiles`/`CareerProfileVersions` from every existing save path. **Stable item IDs** assigned to jobs/education/certifications/projects — the stated prerequisite for variants referencing "this job" by identity rather than array position. `CvDateNormalizer` for free-text → `YYYY-MM`. | | `CvVariant` / `CvVersion` / `TailoredApplication` | Dual-written from both `TailoredCvDraft` save paths. `CvVariant` carries a **`ThemeId`** — the theme seam Phase 4 needs. | | `InterviewPrepNote` / `AiWorkspaceNote` | Persist AI output keyed by an `AttachmentContextSignature`, so interview prep and candidate fit stop regenerating on every open. | | Frontend | CV rewrite diff view (`TextDiff`), ATS-safety badge per template, **`fix: unlock CV builder for Google/Microsoft-authenticated users`**. | | Tests | **5 new backend suites** (`CareerProfileServiceTests`, `CvVariantSyncTests`, `InterviewPrepPersistenceTests`, `AiWorkspaceNotePersistenceTests`, `CvTemplateRendererTests`) + `text-diff.test.tsx`, and it **touches `profile-page.test.tsx`** — one of the two suites currently failing on `main`. | ### Data **All 7 tables are empty (0 rows) in the dev database.** There is nothing to migrate and nothing to lose. Your "do not delete until a migration strategy exists" constraint is satisfied trivially — but it is also now moot, because the right move is almost certainly not deletion. ``` CareerProfiles rows=0 Id, OwnerUserId, ProfileJson, Version, CreatedAtUtc, UpdatedAtUtc CareerProfileVersions rows=0 + CareerProfileId FK, Source (append-only history) CvVariants rows=0 + CareerProfileId FK, Name, ContentJson, ThemeId, Version CvVersions rows=0 + CvVariantId FK (append-only history) TailoredApplications rows=0 CvVariantId FK + JobApplicationId FK (the join) InterviewPrepNotes rows=0 + AttachmentContextSignature, TalkingPointsJson, LikelyQuestionsJson, WeakSpotsJson AiWorkspaceNotes rows=0 + NoteType, AttachmentContextSignature, ResultJson ``` **Caveat: this is the local dev database. I cannot see production.** Confirm prod is also empty before acting. --- ## 3. How it aligns with your decisions | Your decision (2026-07-17) | Branch position | Verdict | |---|---|---| | CareerProfile — **YES** | `CareerProfiles` + `CareerProfileService`, dual-written | ✅ Built | | CareerProfileVersions — **YES, history/versioning** | Append-only, one row per save, with `Source` | ✅ Built, exactly as you described | | CvVariants — **YES, redesign around generated variants from the master profile** | `CvVariant` hangs off `CareerProfileId`, survives job deletion, reusable across applications | ✅ Built, and already matches your redesign | | CvVersions — **YES, generated document history** | Append-only per variant | ✅ Built | | TailoredApplications — **YES, job-specific snapshots** | The join; *"a job application REFERENCES a tailored output; it does not own it"* | ✅ Built | | InterviewPrepNotes — **YES, lower priority** | Built, with cache-invalidation signature | ✅ Built | | Do not delete until a migration strategy exists | Tables empty; branch has both SQLite + MySQL reconciler dialects | ✅ Satisfied | **Every one of your six keep/rework verdicts is already implemented on that branch.** The one place it diverges from a later decision: the profile core is stored as `ProfileJson` (a blob), whereas you decided on **relational** for Experience/Education/Skills/Projects. The branch's *versioning* and *variant/tailoring* structure is unaffected by that; only the profile core's storage shape would need rework — and its **stable item IDs** work is a prerequisite for the relational model either way. It also independently states the same product boundary you have been enforcing: *"The primary product stays Job Search & Application Management. The Career Workspace is a bounded supporting domain."* --- ## 4. Cost of each option Branch is **10 commits ahead, 29 commits behind** `main`. Seven files were touched on both sides since the fork: ``` JobTrackerApi/Data/JobTrackerContext.cs JobTrackerApi/Controllers/JobApplicationsController.cs JobTrackerApi/Controllers/ProfileCvController.cs JobTrackerApi/Program.cs JobTrackerApi/Services/StartupInitializationExtensions.cs job-tracker-ui/src/components/JobDetailsDialog.tsx job-tracker-ui/src/views/ProfilePage.tsx ``` Conflicts are real but bounded, and **Phase 0 made them worse in two of those files** (`JobTrackerContext.cs`, `JobApplicationsController.cs`) — the cost grows every day main moves. | Option | Cost | Risk | |---|---|---| | **A. Rebase and merge the branch** | ~1–2 days of conflict resolution across 7 files | Recovers ~2,750 lines incl. 5 test suites and 1,100 lines of research. Conflicts grow with delay. | | **B. Cherry-pick the docs now, code later** | ~1 hour | Unblocks 4.9 and Phase 7 pricing immediately at near-zero risk; leaves the code rotting further. | | **C. Re-derive on main (the current roadmap plan)** | Weeks | Wastes the research and the tests; near-certain to produce a worse design than the one already reviewed. | | **D. Abandon the branch, drop the tables** | ~1 hour | Throws away the strongest Phase 3/4 asset in the repo. Hard to justify given the tables are empty and the code is tested. | **Recommendation: B immediately, then A.** Cherry-pick the four documents onto `main` today — they are pure additions, cannot conflict, and immediately unblock roadmap 4.9 and the Phase 7 pricing model. Then schedule the rebase before Phase 3 starts, while the conflict surface is still seven files. **I have not touched the branch.** No rebase, no cherry-pick, no merge. --- ## 5. What this changes in the roadmap - **1.9** — no longer "investigate orphan tables"; now "decide on the branch". Promoted to **P0**: it blocks honest planning of Phases 3 and 4. - **4.9** — already done, on the branch. Recover, don't redo. - **Phase 3** — F1/F2 equivalents already exist. The estimate (~2–3 wk) is wrong until the branch decision is made. - **Phase 4** — the theme seam (`CvVariant.ThemeId`) and the research backing 4.1 both exist. - **Phase 7** — the branch's pricing research independently supports the capability-not-count decision. - **1.8** (red CI) — the branch touches `profile-page.test.tsx`, one of the two failing suites. Possibly related; check before fixing blind. --- ## 6. Architecture review — Recover / Migrate / Replace Requested 2026-07-17. Verdict per table, judged against the current architecture and your 2026-07-17 decision that the profile core is **relational** for Experience/Education/Skills/Projects with a blob for the long tail. | Table | Verdict | Reasoning | |---|---|---| | **CareerProfiles** | **Recover the entity, replace the storage shape** | Lifting the profile off the Identity row (`ApplicationUser.ProfileCvStructureJson`) is right and is the whole point of the table — keep the entity, the per-user cardinality, `Version`, and the dual-write lifecycle. But `ProfileJson` is a blob, and you decided the core is relational. **Replace `ProfileJson` with relational Experience/Education/Skills/Projects + a blob column for the long tail** (awards, publications, organisations, references, custom sections). The entity survives; only its storage changes. | | **CareerProfileVersions** | **Recover as-is** | Append-only, one row per save, with a `Source` discriminator (`upload`/`rebuild`/`improve`/`reprocess`/`parse`/`manual`). A **blob snapshot is the correct shape for history even in a relational world** — a version is an immutable point-in-time record, not something you query field-by-field. Do not relationalise this one. Directly delivers your "useful for history/versioning". | | **CvVariants** | **Recover the entity, migrate the content model** | The entity already matches your redesign: it hangs off `CareerProfileId`, survives job deletion, is reusable across applications, and carries `ThemeId` (the Phase 4 theme seam). **But its own design doc says a variant is a *"persistent lens on the profile: selections + per-item overrides"*, while the shipped model stores `ContentJson` — a full serialized `TailoredCvDocument`, i.e. a *snapshot*.** The commit is explicit that this was a deliberate interim ("reuses the existing document shape… so this phase carries zero data-shape risk"). A snapshot **duplicates profile data**, which `MASTER_IMPLEMENTATION_GUIDE.md` forbids ("The CV builder is NOT the user's data… Never duplicate user data between templates"). **Migrate `ContentJson` → selections + per-item overrides referencing the stable item IDs that F1 already added for exactly this purpose.** | | **CvVersions** | **Recover as-is** | Append-only history of a variant. Same reasoning as `CareerProfileVersions`: snapshots are correct for history. Delivers your "generated document history". | | **TailoredApplications** | **Recover as-is** | The `CvVariant ↔ JobApplication` join. Encodes the product boundary you have been enforcing all along — *"a job application REFERENCES a tailored output; it does not own it"*. Delivers your "job-specific application snapshots". Note this is the same reference-not-ownership shape as Phase 0's `JobApplication.JobId`; the two are consistent. | | **InterviewPrepNotes** | **Recover as-is** (low priority, as you said) | Caches AI interview prep keyed by an `AttachmentContextSignature`, so it stops regenerating on every open. `main` recomputes live. With `AI_PROVIDER=gemini` this is **real money per open**, so it is a cost fix, not just latency. Cache invalidation is already handled by the signature. | | **AiWorkspaceNotes** | **Recover as-is** | Not in your list, but the same pattern generalised (`NoteType` + signature) and the sibling of the above — it backs the persisted candidate-fit and focus-plan commits. Recover with `InterviewPrepNotes` or neither; splitting them makes no sense. | **Nothing is Replace-and-discard. Nothing needs data migration — all 7 tables are empty in dev** (verify prod). The only genuine rework is two storage shapes: `CareerProfiles.ProfileJson` → relational core, and `CvVariants.ContentJson` → lens. ### Sequencing consequence The two reworks are the *same shape of change* and share a prerequisite: **stable item IDs**, which F1 already implements. Recovering F1 first therefore unblocks both. Recommended order: recover F1 (profile + versions + stable IDs) → replace the profile core storage → recover F2 (variants + join) → migrate variants to a lens → recover the AI-cache tables. ### One merge conflict worth naming The branch provisions tables through the **raw-SQL reconciler** (both SQLite and MySQL dialects), because when it was written "the EF ModelSnapshot is known-stale; do not rely on `Migrate()`". **That is no longer true** — Phase 0 resynced the snapshot and shipped `Jobs` via a normal EF migration, verified against the real dev database. On merge, both mechanisms will coexist. That is safe (the reconciler is idempotent) and should **not** be untangled during the rebase — do it as separate follow-up work, or the rebase becomes unreviewable. --- ## 7. Open questions 1. **Why was it never merged?** No revert exists, and it is pushed to `origin`. If it was parked for a reason I cannot see, that reason should be recorded before anyone picks it up. 2. **Is production also empty** on those 7 tables? I can only see the dev database. 3. **Does the branch's `a4c8e4a fix: unlock CV builder for Google/Microsoft-authenticated users` overlap your current uncommitted work** on `ProfilePage.tsx` / `CareerWorkspacePage.tsx` (the inert `careerView` tab)? Both touch the same file. Worth checking before either is finished. --- ## 8. What remains on the branch (for Phase 3/4) The foundation was integrated by **squash-cherry-pick**, so `main` has the *content* of `235e291`, `5916f09`, `00a035e` (as `992f89e`) but not those commits as ancestors. `git log main..feature/career-workspace` therefore still lists all ten. The commits still needed later, all CV-Builder (Phase 3/4): | Commit | What | Recover in | |---|---|---| | `a4c8e4a` | unlock CV builder for Google/Microsoft-auth users | Phase 4 (with the builder UI) | | `66384bd` | CV template renderer regression tests | Phase 4 | | `cbd045a` | AI CV-rewrite diff view (`TextDiff`) | Phase 4 | | `00c7e0b` | ATS-safety badge (the `LayoutFamily`/`AtsRating` fields I trimmed) | Phase 4 | | `47d05ba` | CvVariant / CvVersion / TailoredApplication (F2) | Phase 3 | **These will not cherry-pick cleanly.** They sit on top of the foundation commits, whose shape on `main` now differs (DTO relocation, 7-arg `CvTemplateDescriptor`, EF-migration schema). Expect the same class of semantic conflicts resolved here. When Phase 3/4 starts, treat them as a fresh foundation-style integration against the then-current `main`, not a replay. The two doc commits (`102938c`, `597191f`) are already on `main` (landed as `aedd6e3`); do not re-integrate them. **Do not delete `feature/career-workspace`** until F2 (CvVariant) is recovered — it is the only source of that code.