# `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.
