Files
jobtrackingapp/docs/career-workspace-implementation-roadmap.md
T
cesnimda 597191f384 docs: mark shipped roadmap phases, record what's still open
F0-F2 fully shipped this session; F3 and F5 partially shipped. Each
phase section now states exactly what landed (with commit
references), what was deliberately deferred and why, and what a
future session should pick up next -- so the roadmap stays a source
of truth instead of drifting from the actual repo state.

Notable corrections made while auditing: JobCvMatchService already
reads a structured+text hybrid (the F5 "retarget" line item was based
on a stale assumption, resolved as not-needed rather than deferred);
F1's read-path cutover and F2's pre-existing-row backfill are
explicitly still open, not silently done.
2026-07-12 15:57:40 +02:00

13 KiB
Raw Blame History

Career Workspace — Implementation Roadmap (ADR + sequencing)

Date: 2026-07-12 Status: Active. F0F2 shipped; F3 and F5 partially shipped (see phase sections below for exactly what landed vs. what's still open). This is the execution plan that turns the three strategy docs into code, incrementally, without breaking the live app. Source of truth: cv-builder-competitor-deep-research.md, cv-builder-product-teardown.md, career-workspace-product-strategy.md.


Product boundary (non-negotiable, per owner directive 2026-07-12)

The primary product stays Job Search & Application Management. The Career Workspace is a bounded supporting domain that makes applications better. Rules that constrain every change below:

  • Job applications reference career outputs; they do not own them.
  • Career profile data is independent of any single job.
  • Themes/templates are independent of application workflow.
  • Do not remove, replace, or redesign job-tracking functionality. The goal is integration, not replacement.
Main product:  Job Search & Application Management
                 └─ tracking · applications · company research · interviews · matching
Supporting:    Career Workspace  (bounded domain)
                 └─ profile · CV variants · tailored CVs · cover letters · outputs · AI assist
Integration:   JobApplication ──references──▶ TailoredApplication ──uses──▶ CvVariant ──inherits──▶ CareerProfile

The tracker references a career output by id; deleting a job never deletes profile/variant data; a variant exists with zero jobs attached.


Migration mechanics (how this codebase actually changes schema)

No EF migrations in practice. Schema is provisioned by the idempotent raw-SQL reconciler in StartupInitializationExtensions.InitializeJobTrackerAsync. Both dialects are hand-maintained:

  • SQLite (dev): Ensure*Table(conn) helpers with CREATE TABLE IF NOT EXISTS + EnsureColumn guards, called in the useSqliteBootstrap branch (near EnsureCvTables).
  • MySQL/MariaDB (prod): if (!HasMySqlTable(...)) blocks + EnsureMySqlColumn, in the else branch.

Every new table therefore needs BOTH dialect blocks + a DbSet + OnModelCreating config (query filter + indexes). The EF ModelSnapshot is known-stale; do not rely on Migrate() to create Career tables — add them to the reconciler.

Rule for this roadmap: all new tables are additive. Nothing drops ApplicationUser.ProfileCvStructureJson or TailoredCvDrafts until the new path is proven in production and dual-read has run clean. Backwards-compatible at every step.


Target entities (only what earns its place)

Entity Purpose Replaces / relates When
CareerProfile The durable source of truth; one per user (N later). Structure canonical, text derived. Lifts ApplicationUser.ProfileCvStructureJson off the Identity row Phase F1
CareerProfileVersion Append-only history of the profile; diff/restore New (teardown gap) F1 (table) / F4 (UI)
CvVariant Persistent lens on the profile: selections + per-item overrides, named ("Backend focus") Generalizes today's single implicit CV F2
CvVersion Append-only history of a variant (every generation/save) New; kills overwrite-anxiety F2
CvTheme (data, not table yet) Declarative theme descriptor; sibling input to renderer, never welded to content Formalizes CvTemplateRenderer catalog F3
TailoredApplication Variant × JobApplication event; gap-driven tweaks; the integration seam Reworks TailoredCvDraft's job-lock into a reference F2/F5

Schema amendments forced by the teardown (apply at table-creation time — cheap now, brutal later):

  1. Stable item IDs on every profile item (jobs, bullets, skills, education, projects). Without them, variant lineage / "update everywhere" / inherit-with-override are unimplementable.
  2. Normalized dates (YYYY-MM + isCurrent) — backfilled during migration by best-effort parse of the free-string Start/End. Timeline / tenure / skills-recency depend on it.
  3. Structure is canonical; text is derived — stated in code. Match scoring reads structure (F5), not raw text.

Phased sequence (each phase ships independently, app stays green)

Phase F0 — Immediate fixes (no architecture) SHIPPED

  • OAuth CV lockout fixed (ProfilePage.tsx): CV controls gated on a new canEditCv (any authenticated user) instead of isLocal. Identity/password fields remain local-only. Unblocks every Google/Microsoft user.
    • Commit: fix: unlock CV builder for Google/Microsoft-authenticated users

Phase F1 — Career Profile as first-class data (backwards-compatible seam) SHIPPED

  1. CareerProfile + CareerProfileVersion tables in the reconciler (both dialects), DbSets, query filters, indexes.
  2. ICareerProfileService — dual-writes: persists to CareerProfiles/CareerProfileVersions on every structured-profile save (upload/rebuild/improve/reprocess/parse) while ApplicationUser.ProfileCvStructureJson stays the column every existing read path uses.
  3. Stable item IDs (jobs/education/certifications/projects) + normalized YYYY-MM dates (CvDateNormalizer) assigned on save.
  4. Not done: full cutover of ProfileCvController read paths to the service (still reads user.ProfileCvStructureJson directly). The service is invoked at every write site but reads haven't moved yet — deliberate: F1's own exit criteria says "new table is authoritative; column is a mirror" implies read cutover is a later step once the table's been proven, not this pass.
  5. 11 tests (stable IDs, date normalization incl. IsCurrent guard, version history, dual-write). Verified against the real dev DB.
    • Commit: feat: add career profile foundation with versioned history

Phase F2 — Variants + versions (additive, opt-in) SHIPPED

  1. CvVariant + CvVersion tables. CvVariant.CareerProfileId links to the user's current CareerProfile (nullable, SetNull on delete — not owned, not cascaded).
  2. TailoredApplication table: (CvVariantId, JobApplicationId), unique per (OwnerUserId, JobApplicationId). Job references the tailored output; does not own the variant. Both FKs cascade (the link is meaningless without either side).
  3. Dual-write (not a one-time backfill): both TailoredCvDraft save paths (SaveTailoredCvDraft, UpsertGeneratedTailoredCvDraftAsync) now also upsert the variant, bump its version, append a CvVersion snapshot, and ensure the TailoredApplication link — via SyncCvVariantFromDraftAsync. Reuses TailoredCvDocument as ContentJson (zero new data shape). TailoredCvDrafts remains authoritative for every existing read path.
  4. Not done: a one-time backfill of pre-existing TailoredCvDraft rows that predate this change (only rows saved after this ships get synced). Not done: /career/* endpoints — nothing reads CvVariant/CvVersion yet; this phase is pure write-side foundation, same "populate before UI" strategy as F1.
  5. 2 tests (variant/version/link created on first save; same variant reused + version incremented on resave, not duplicated). Verified against the real dev DB — FK dependency order (CareerProfilesCvVariantsCvVersions/TailoredApplications) holds in both dialects.
    • Commit: feat: introduce CV variant schema, dual-written from tailored CV saves

Next actions on this phase (not started): (a) backfill script for pre-existing TailoredCvDraft rows if the table shouldn't have a "before my change" gap; (b) a read endpoint exposing CvVariant list — the actual precondition for F4's "reuse a variant across jobs" UI to mean anything.

Phase F3 — Rendering as data (theme catalog) — PARTIAL

  1. CvTemplateDescriptor (backend, ProfileCvController.GetCvTemplateDescriptors) extended with LayoutFamily + AtsRating; surfaced as a badge in the frontend template picker (ProfilePage.tsx).
  2. 14-test regression suite (CvTemplateRendererTests) locking in current renderer output — the prerequisite for a safe future extraction — landed before touching the renderer, per the golden-test discipline this phase calls for.
  3. Not done: the actual extraction (renderer consumes (document, theme) as data; layout shells as a fixed set; theme = shell + tokens). The six RenderXxx HTML-string methods in CvTemplateRenderer are unchanged. Real work, real PDF-regression risk, correctly not attempted in the same pass as unrelated feature work.
  4. Known gap surfaced this pass: the frontend never calls GET /profile-cv/templates — it duplicates the template catalog in a hardcoded REWRITE_TEMPLATES array in ProfilePage.tsx. Two sources of truth for template metadata. Worth fixing as part of the F3 extraction (single source becomes the natural output), not before.
  5. Deferred: external template engine (Scriban) + user/marketplace themes — only when a marketplace is real (strategy §9).

Next action on this phase: the extraction itself (item 3) — budget a dedicated pass; the regression suite (item 2) is what makes it safe to attempt.

Phase F4 — CV Builder UX (structured editor + tailoring workspace)

  • Structured profile editor route (/career/profile): section forms, per-bullet reorder, provenance-flagged review queue for low-confidence fields.
  • From-scratch + paste-text entry paths (removes import-only dead end).
  • Tailoring workspace route (/jobs/:id/tailor): JD gap chips ↔ variant editor ↔ live themed preview ↔ rescore. Retire the modal editor. Reached from a job (integration), full page.
  • Career becomes a top-level nav pillar (Profile · Variants); "CV" ceases to be a nav noun. Tracker nav untouched.

Phase F5 — AI depth + integration — PARTIAL

  • Diff view for AI rewrites: TextDiff component (word-level diffWords), wired into the master-CV rewrite preview behind a "Show changes" toggle (default off — an existing test proved diff-by-default breaks the plain-text read). Scoped to the master-CV rewrite surface only; the tailored-CV draft regenerate flow already had a confirm+reset safety net and wasn't a good fit for the same treatment (would mean diffing structured fields, which is F4 tailoring-workspace scope).
    • Commit: feat: show diff view for AI CV rewrites
  • Persist interview prep (InterviewPrepNote, one table, per-field columns — the DTO is flat) and candidate fit + focus plan (AiWorkspaceNote, one generic table keyed by NoteType — those DTOs are irregular/nested, so per-field columns would've been unreasonable; introduced the generalization on the 2nd/3rd occurrence, not the 1st). All three: reuse across tab-opens, regenerate on attachment-context change, explicit "Regenerate" button as the escape hatch. Collectively these three tabs fired 9 AI calls on every single re-open before this; now 0 unless something changed.
    • Commits: feat: persist interview prep instead of regenerating on every open, feat: persist candidate fit and focus plan, stop re-running on every open
  • ATS-safety badge on the template picker (folded into the F3 entry above — same commit touched both, since AtsRating is a field on the template descriptor).
  • Not done: JobCvMatchService retarget — checked the live code this pass and found BuildCvSearchCorpus already reads structured profile and raw text as a hybrid (better than the teardown assumed); no change needed. Closing this line item as resolved, not deferred.
  • Not done: fact-constraint validator (novel named-entity/number flagging on AI generation). Real remaining trust gap — the diff view lets a user see a fabrication, but nothing stops the model from producing one. Good next F5 slice.

Phase F6+ — Career Workspace horizons (architecture-ready, not built now)

Cover letters (profile+JD+thread) · ATS plain-text view · skills-gap analytics · public profile (theme over live profile) · DOCX adapter · portfolio/LinkedIn adapters. Each ≈ one IOutputAdapter + optional theme. Keep the adapter boundary swap-clean (Reactive Resume abandoned server-Chromium for cost — our Playwright PDF adapter must stay replaceable without touching themes).


Risk register

Risk Mitigation
Live-DB schema migration breaks prod Additive-only tables; dual-write/dual-read; never drop legacy until proven; test reconciler against a prod DB copy
Backfill mis-parses free-string dates Best-effort normalize, keep original string alongside normalized fields; never lose data
PDF output regression in theme refactor Golden byte-identical test before merge
Scope creep into job-tracker redesign Boundary rules above; tracker code out of scope
Two sources of truth diverge (F1 interim) Time-box the dual-write window; F5 retargets the last consumer (match scoring) then column is dropped

Working-summary convention

Each session updates ## Completed / ## Current / ## Next / ## Decisions in the response. This file is the durable plan; the running summary is the session delta.