Files
jobtrackingapp/docs/career-workspace-branch-assessment.md
T
2026-07-17 19:17:37 +02:00

192 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `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 F0F5, 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:
```
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** | ~12 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 (~23 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.