b176a44627
Active docs/ was stub scaffolding while the real docs sat in docs/_archive/. Restore and correct them, and record the Phase 0 work. - docs/architecture/current.md: verified system map (from archived SYSTEM_OVERVIEW, 9 corrections against code). - docs/research/competitors.md: sourced competitor analysis (from archived PRODUCT_RESEARCH, feature matrix corrected). - docs/decisions/ADR-002-job-application-model.md: the Job/JobApplication split. - docs/application-discovery-report.md, docs/implementation-roadmap.md, docs/phase-0-foundation-report.md, docs/career-workspace-branch-assessment.md. - Remove 10 zero-byte placeholder files that advertised content that never existed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
159 lines
14 KiB
Markdown
159 lines
14 KiB
Markdown
# `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: **decision required before Phase 3 or Phase 4 can be planned honestly.**
|
||
|
||
---
|
||
|
||
## 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:
|
||
|
||
```
|
||
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.
|