feat(workspace): application assets workflow
CI and Deploy / test (push) Failing after 1m11s
CI and Deploy / deploy (push) Has been skipped

Phase 5.4. Connects the career outputs a user already has to one job
application, without building a second copy of any of them.

The flow is strictly one-directional — CareerProfile -> CvVariant ->
application output — and nothing writes back up. No code path in this phase
touches CareerProfile or its children.

CV integration re-points rather than duplicates. GET/PUT /{id}/cv attaches one
variant to an application via CvVariant.JobApplicationId; replacing detaches the
previous variant instead of deleting it. Creating, duplicating, editing, theming,
previewing, exporting PDF and version history all stay in the existing CV
builder, which the section links into. There is no second CV system.

Tailoring composes the Phase 5.3 analysis and match into skills to highlight,
experience to prioritise, projects to emphasise, keywords to include and gaps to
address. Deterministic and advisory: it says what the user could emphasise and
the user edits the variant themselves. Nothing auto-applies.

Cover letters gain the history they were missing. JobApplication.CoverLetterText
stays the current text with its API contract unchanged; CoverLetterVersions
records what it used to be, so an AI rewrite is never destructive. Restore is
additive — the old text comes back as a new version, so what you restored from
still exists. Source and AiAction record whether the user wrote a version or
approved it from a suggestion, and an AI generation only becomes a version once
the user saves it.

Documents are untouched: the existing Attachment system already covers CV, cover
letter, certificates and portfolio files with a Purpose field, so the workspace
mounts that component rather than adding a second upload path.

CoverLetterVersions is the only new table — reconciler-owned, no-op migration,
guarded on JobApplications, and verified on a fresh MariaDB 11: int
AUTO_INCREMENT primary key, varchar(255) owner, datetime(6), composite index
inside the key limit.

360 backend tests, 115 frontend tests, type check, Release build and the
production build all pass locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-19 16:36:27 +02:00
parent a7cecce13d
commit 02b38f7acb
16 changed files with 3867 additions and 7 deletions
+78 -2
View File
@@ -21,7 +21,8 @@ The workspace **owns no data and duplicates none**. It is an aggregate read plus
| Timeline | `JobEvent` — interpreted, never replaced |
| Analysis / Match | the advert and the master `CareerProfile`, read deterministically |
| Checklist | `ApplicationChecklistItem` — completion state only, seeded from the readiness signals |
| CV | Phase 4 `CvVariant` — a lens over the master `CareerProfile` |
| CV | Phase 4 `CvVariant` — a lens over the master `CareerProfile`; the application only points at one |
| Cover Letter | `JobApplication.CoverLetterText` + `CoverLetterVersions` history |
| Analysis / Match / Interview | Phase 5 `AiWorkspacePanel` + `AiInteraction` history |
| Documents | `Attachment` |
| Communication | `Correspondence` |
@@ -222,6 +223,80 @@ Suggestions describe what the user could change. They never change it.
With no profile it returns score 0 and asks the user to build one, rather than implying a bad match.
## Application assets (Phase 5.4)
The workspace becomes the place an application is prepared. The rule is one-directional:
```
CareerProfile → CvVariant → application-specific output
```
Nothing flows back up. No code path in this phase writes to `CareerProfile` or its children —
`Tailoring_never_writes_to_the_career_profile_or_the_variant` pins it.
### What is owned where
| Asset | Owned by | This phase adds |
|---|---|---|
| CV content | `CareerProfile` (master) | nothing |
| CV variant, preview, PDF, themes, version history | `CvVariantService` / `/api/cv` | nothing |
| Which variant an application uses | `CvVariant.JobApplicationId` | the attach/detach route |
| Documents | `Attachment` / `/api/attachments` | nothing |
| Cover letter text | `JobApplication.CoverLetterText` | version history |
| AI narrative | `AiInteraction` | nothing |
The only new table is `CoverLetterVersions` — reconciler-owned, no-op migration, guarded on
`JobApplications` (`docs/infrastructure/database-ownership.md`). Verified on MariaDB 11:
`int AUTO_INCREMENT` PK, `varchar(255)` owner, `datetime(6)`, composite index inside the key limit.
### CV integration
`GET/PUT /{id}/cv` reads and re-points. Attaching sets `CvVariant.JobApplicationId`; **one variant per
application**, so the workspace can answer "which CV am I sending". Replacing detaches the previous
variant rather than deleting it — it is still the user's to reuse. Everything else (create, duplicate,
edit, theme, preview, export PDF, versions, restore) is a link into the existing CV builder. There is
deliberately no second CV system.
### Tailoring
`GET /{id}/tailoring` composes the Phase 5.3 analysis and match into five suggestion kinds: skills to
highlight, experience to prioritise, projects to emphasise, keywords to include, gaps to address.
Deterministic and advisory. It returns what the user *could* emphasise; the user edits the variant in
the builder. Nothing auto-applies, and no suggestion mutates a variant or the profile.
### Cover letter
`JobApplication.CoverLetterText` stays the current text and its existing API contract is unchanged.
`CoverLetterVersions` records what it used to be, so an AI rewrite is never destructive.
- Every save that changes the text snapshots a new version; an unchanged save is a no-op, so autosave
never burns history.
- **Restore is additive** — the old text returns as a *new* version, so what you restored from is
still there.
- `Source` (`manual | ai | template | restore`) and `AiAction` record how each version came about, so
the history shows what the user wrote versus what they approved from a suggestion.
- An AI generation on its own is only an `AiInteraction`. It becomes a version when the user saves it
— that is what "requires approval" means here.
Creation methods: write it, start from the built-in template, or generate from the AI panel below the
editor. The editor is always the user's; generation is never triggered by opening the page.
### Documents
Unchanged. The existing `Attachments` component and `/api/attachments` already handle CV, cover
letter, certificates, portfolio and other files with a `Purpose` field, and `JobApplication.HasResume`
/ `HasCoverLetter` / `HasPortfolio` are derived from it. The workspace mounts that component; no new
storage, no duplicate upload path. Files stay private to the owning user.
### Future extension points
- **Another asset type**: add a section, compose the service that already owns it — do not add storage.
- **AI cover-letter actions** (improve, shorten, expand, tone, tailor): already modelled by
`CoverLetterVersion.AiAction`; wire a new mode in `AiWorkspaceService` and save the approved result.
- **Multiple attached variants**: relax the one-per-application rule in `AttachVariantAsync`; the DTO
already carries the full variant list.
## Extension points
- **New section**: add to `WORKSPACE_SECTIONS` and render it; nav is data-driven.
@@ -236,5 +311,6 @@ With no profile it returns score 0 and asks the user to build one, rather than i
custom items, reordering, dismissal; readiness refactored into a projection of it.
3. ✅ Application intelligence — timeline interpretation, structured job analysis, career matching
(Phase 5.3, all three deterministic and read-only).
4. CV integration.
4. ✅ Application assets — CV variant association, tailoring suggestions, cover letter workflow with
version history, documents (Phase 5.4).
7. Cover letter workflow. 8. Documents. 9. Interview preparation. 10. Dashboard improvements.
+4 -2
View File
@@ -75,7 +75,8 @@ Created by `StartupInitializationExtensions`, with a **no-op migration** holding
`CareerProfiles`, `CareerProfileVersions`, the six CareerProfile children (`CareerExperiences`,
`CareerEducations`, `CareerSkills`, `CareerProjects`, `CareerCertifications`, `CareerLanguages`),
`InterviewPrepNotes`, `AiWorkspaceNotes`, `CvVariants`, `CvVariantVersions`, `AiInteractions`,
`ApplicationChecklistItems`, `TwoFactorRecoveryCodes`, `TrustedDevices`, `UserSessions`.
`ApplicationChecklistItems`, `CoverLetterVersions`, `TwoFactorRecoveryCodes`, `TrustedDevices`,
`UserSessions`.
No-op migrations, each with a comment explaining why:
@@ -86,6 +87,7 @@ No-op migrations, each with a comment explaining why:
| `20260718131138_AddAiInteractions` | `AiInteractions` |
| `20260719085904_AddApplicationChecklistItems` | `ApplicationChecklistItems` |
| `20260719094728_SyncCareerChildKeyLengths` | snapshot sync only |
| `20260719120954_AddCoverLetterVersions` | `CoverLetterVersions` |
### Dependency guards
@@ -94,7 +96,7 @@ skips it on a fresh database and pass 2 creates it:
| Table | Waits for |
|---|---|
| `TailoredCvDrafts`, `InterviewPrepNotes`, `AiWorkspaceNotes`, `CvVariants`, `AiInteractions`, `ApplicationChecklistItems` | `JobApplications` (migration-owned) |
| `TailoredCvDrafts`, `InterviewPrepNotes`, `AiWorkspaceNotes`, `CvVariants`, `AiInteractions`, `ApplicationChecklistItems`, `CoverLetterVersions` | `JobApplications` (migration-owned) |
| `CvVariantVersions` | `CvVariants` |
| `CareerProfileVersions`, the six CareerProfile children | `CareerProfiles` |
| `CvExtractionRuns` | `CvUploadArtifacts` |