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>
14 KiB
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
- 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. - Is production also empty on those 7 tables? I can only see the dev database.
- Does the branch's
a4c8e4a fix: unlock CV builder for Google/Microsoft-authenticated usersoverlap your current uncommitted work onProfilePage.tsx/CareerWorkspacePage.tsx(the inertcareerViewtab)? Both touch the same file. Worth checking before either is finished.