# Application Workspace (Phase 5) > Phase 5, Milestones 1–2 (2026-07-19). The per-application workspace: what it is, what it deliberately > is not, and how it composes existing systems. Companion to `cv-builder.md`, > `ai-career-assistant.md`, `career-profile-model.md`. ## What it is A dedicated surface for one `JobApplication` at `/applications/{id}`, so an application is a place you work rather than a row you edit in a modal. Job tracking stays the product; the workspace is the application's home. **Core principle:** the user should never ask *"what do I do next?"* — the overview always answers it. ## What it is NOT The workspace **owns no data and duplicates none**. It is an aggregate read plus a navigation shell: | Section | Backed by (existing system) | |---|---| | 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`; the application only points at one | | Cover Letter | `JobApplication.CoverLetterText` + `CoverLetterVersions` history | | Analysis / Match / Interview | Phase 5 `AiWorkspacePanel` + `AiInteraction` history | | Documents | `Attachment` | | Communication | `Correspondence` | | Activity / Timeline | `JobEvent` | | Stage semantics | `JobPipeline` | No career data is copied into the application. Nothing in this feature writes to the master profile, a CV variant, or a cover letter. ## Backend `GET /api/jobapplications/{id}/workspace` → `WorkspaceOverviewDto` (`ApplicationWorkspaceController` + `ApplicationWorkspaceService`). One aggregate read instead of the page fanning out: role/company/location/salary, status + pipeline group, applied/deadline/follow-up dates, the attached CV variant (id, name, theme), cover-letter presence, document count, AI interaction count + last run, recent `JobEvent` activity, and the computed **next recommended action**. Read-only and tenant-scoped (`OwnerUserId`), returning 404 for another user's application. ### Next recommended action Milestone 2 moved this onto the checklist: **the first pending checklist item**, ordered by category priority (`preparation` → `submission` → `follow-up` → `interview` → `custom`) then the user's own ordering. `null` means nothing is outstanding. That means the overview cannot recommend something the user has already ticked off, a dismissed item never comes back as a recommendation, and a task the user added themselves can legitimately be the next action. There is no second ruleset to keep in sync. ## The checklist `ApplicationChecklistItem` — a **workflow guidance layer**, not a store of truth. It records only "is this step done, and does the user still want it". The CV still lives in `CvVariant`, documents in `Attachment`, history in `JobEvent`, follow-up in `JobApplication.FollowUpAt`. ### System items and auto-completion Each default item carries a stable `SystemKey` and usually an `AutoSignal` — the *same* signal `/readiness` already computed. On every read the service re-syncs: - signal satisfied + item pending → **done**, `IsAutoCompleted = true` - signal no longer satisfied + item was auto-completed → back to **pending** - a **manual** tick clears `IsAutoCompleted` and therefore sticks, even against the signal So "readiness says the CV is missing" and "the checklist says Prepare a CV is pending" cannot drift apart — they read the same state. The user always wins over the signal. | System key | Category | Signal | |---|---|---| | `review-job-details` | preparation | advert text present | | `complete-career-profile` | preparation | career profile with at least one experience | | `prepare-cv` | preparation | CV variant attached, or tailored CV text | | `review-cv-match` | preparation | *manual* | | `create-cover-letter` | preparation | cover letter present | | `attach-portfolio` | preparation | `HasPortfolio` | | `attach-supporting-documents` | preparation | at least one `Attachment` | | `save-application-answers` | preparation | saved answer draft in `Notes` | | `capture-recruiter-contact` | preparation | `Company.RecruiterEmail` | | `confirm-submitted` | submission | applied date set and out of the prospect stage | | `add-follow-up-reminder` | follow-up | `FollowUpAt` set | | `set-next-action` | follow-up | `NextAction` written | | `prepare-interview-notes` | interview | prep notes present, **or** not at an interview stage | | `research-company` | interview | *manual* | Seeding is idempotent per `(JobApplicationId, SystemKey)` — enforced by a unique index, so a re-read never duplicates. Custom items have a `NULL` `SystemKey`; both SQLite and MariaDB treat NULLs as distinct in a unique index, so a user can add as many as they like. The deterministic match-score endpoint also synchronises `learning:{hash}` system items from its current missing skills. They form the first job-specific learning path without another table or an external course catalogue. Manual learned/dismissed decisions persist; only recommendations that were auto-completed because a gap disappeared reopen when the same gap returns. Deleting a **system** item dismisses it (a hard delete would be undone by the next seed); deleting a **custom** item removes the row. Dismissed items leave the progress denominator entirely. ### API `/api/jobapplications/{id}/checklist` — `GET` (seeds + syncs + returns items and progress), `POST` (custom item), `PATCH /{itemId}`, `DELETE /{itemId}`, `PUT /order` (array of ids). Tenant-scoped on `OwnerUserId`; another user's application is a 404. ### Relationship to the other systems - **Not `JobEvent`** — the checklist is forward-looking intent; `JobEvent` is the append-only history. - **Not follow-ups** — `FollowUpAt`, `RulesEngine` and the reminder hosted service still own scheduling. The checklist only asks whether a follow-up exists. - **Not `Attachment` / `CvVariant`** — it reads their presence as a signal and stores nothing of them. ### Future AI suggestions An AI-suggested task is just a checklist row with `IsSystemGenerated = false` and no `AutoSignal`, created after the user approves it. Nothing in the AI path may create, complete or delete an item without approval — same rule as everywhere else (`ai-career-assistant.md`). ### Schema provisioning `ApplicationChecklistItems` follows the established MariaDB-safe path: the scaffolded migration (`20260719085904_AddApplicationChecklistItems`) is a **no-op**, and the table is created by the idempotent reconciler in `StartupInitializationExtensions`, which has correct DDL per provider. A SQLite-scaffolded migration would emit `TEXT` datetimes and a PK without `AUTO_INCREMENT` on MariaDB — the failure that crashed prod startup for the Phase 4 tables. Verified on MariaDB 11: `int AUTO_INCREMENT` PK, `varchar`/`datetime(6)`/`tinyint(1)` columns, both indexes inside the 3072-byte key limit, cascade delete from `JobApplications`, the unique index rejecting a duplicate system key, and NULL system keys not colliding. ## Frontend `ApplicationWorkspacePage` (`/applications/:id`) — a left nav plus a content pane, section selected by `?section=`, so a section is linkable and survives refresh. Reached from the job dialog's "Open application workspace" button. The dialog passes an optional `onOpenWorkspace` callback rather than calling `useNavigate` itself: `JobDetailsDialog` must stay renderable without a `` (several suites mount it standalone), so router context belongs to the caller. The Checklist section (`ApplicationChecklist`) groups items by category, shows a completion bar, and supports tick/untick, add, remove and reorder. System items are labelled "Detected" when a signal completed them, custom items "Yours". Every mutation re-reads, because only the backend's sync knows the real post-mutation state. Implemented now: Overview, Checklist, Job Details, and the sections that reuse an existing component (Analysis/Match/Interview → `AiWorkspacePanel`, Documents → `Attachments`, Communication → `Correspondence`). Sections owned by later milestones state their milestone instead of faking functionality. ## Relationship to `/readiness` `GET /{id}/readiness` keeps its DTO shape (`score`, `level`, `completed`, `missing`, `reminders`, `workflowSignal`) and still backs the dialog's Readiness tab — but as of Milestone 2 it no longer runs its own parallel checklist. It **projects** the persisted checklist: - `completed` / `missing` — the live (non-dismissed) items by status - `score` — the checklist completion percentage - `level` — Ready ≥ 80, Needs polish ≥ 60, otherwise Needs work - `reminders` / `workflowSignal` — unchanged; `BuildWorkflowSignal` remains the health/attention view So the division is: **the checklist is the workflow the user drives, readiness is the calculation and health indicator derived from it.** One system, two projections. ## Application intelligence (Phase 5.3) Three read-only reads that answer "how suitable is this job", "how does my experience match", "what am I missing", "what happened previously". All deterministic, all owned by nothing: | Endpoint | Reads | Owns | |---|---|---| | `GET /{id}/timeline` | `JobEvent` | nothing | | `GET /{id}/analysis` | `JobApplication.Description` | nothing | | `GET /{id}/match` | `CareerProfile` + the advert | nothing | No new table, no new column. `ApplicationTimelineService` and `ApplicationIntelligenceService` write nothing at all. ### The AI boundary **The deterministic answer and the AI narrative are separate on purpose.** - The three endpoints above never call the AI. Opening the Analysis or Match section costs nothing and cannot change anything — the page renders a computed answer. - The narrative lives where it already did: `AiWorkspaceService`'s `job-analysis` and `career-match` modules, reached from `AiWorkspacePanel`, generated only when the user asks. - Every generation is appended as an `AiInteraction` — that append-only history *is* the versioning, and the user restores, compares or deletes from it. - AI output is a suggestion. Nothing in this phase writes to the `CareerProfile`, a `CvVariant`, a cover letter, or the `JobApplication`. `Match_never_writes_to_the_career_profile` pins that. So a user gets a trustworthy number for free, and pays for prose only when they want it. ### Timeline `JobEvent` stays the source of historical truth; the service is an interpretation layer over it. Each row gains a readable summary (`("StatusChanged", "Applied", "Interview")` → "Moved from Applied to Interview"), a category (`lifecycle`, `stage`, `follow-up`, `communication`, `ai`) and a milestone flag. Events group by day with relative labels. Milestones are the stages that mean something happened — applied, interview, offer, rejected, accepted, declined — plus creation and replies received. They are returned **unfiltered**: narrowing the detail below must not hide what actually happened. An unrecognised future `Type` degrades to a humanised sentence rather than disappearing. ### Job analysis Structured extraction from the advert, reusing the existing `SkillTagger` so the vocabulary matches the job importer and the CV match. Facts (role, company, location, employment type, seniority, salary), lists (technologies, skills, responsibilities, requirements, keywords, interview topics), and — deliberately — **what the advert does not say**, which is usually the more useful half. With no advert saved it degrades to the fields on the application and says so. ### Career matching Feeds the master `CareerProfile` into the same `JobCvMatchService` the CV builder uses, so one application scores identically whichever surface asks. Returns the score and band, matched and missing skills, and — the part that makes it actionable — **which experience and project entries are the evidence** for each matched keyword, ranked by hit count. 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. ## Interview and follow-up (Phase 5.5) Completes the lifecycle after submission: prepare, communicate, chase. ### Interview preparation There were already two per-application AI stores — `InterviewPrepNote` and `AiWorkspaceNote` — and **both are caches**: each regenerates when its context signature changes, so anything a user typed into them would eventually be overwritten. `InterviewPrepItem` is the durable, user-owned side. Nothing regenerates it. One table covers every category (`company-research`, `technical`, `behavioural`, `star`, `question`, `note`) — they differ only by label, and adding a category must not need a migration. Each item carries the user's own `Content`, a `Source` (`user | ai`) recording whether they wrote it or accepted a suggestion, and `IsPrepared`, which makes the section double as the preparation checklist. An accepted AI suggestion is marked `ai` for honesty, not to restrict editing — it is fully the user's afterwards. ### The AI boundary, restated Generation stays in `AiWorkspaceService`'s existing `interview` module, reached from `AiWorkspacePanel`, authenticated and ownership-scoped like every other module, with each run appended to `AiInteraction`. **A suggestion is history until the user adds it as a prep item.** Opening the section generates nothing; `Generating_ai_history_does_not_create_prep_items` pins that. ### Communication Unchanged. `Correspondence` already owns recruiter contacts, message history and notes, and the workspace already mounts that component. No second messaging or history system was added. ### Follow-up Reuses what exists rather than adding a tracker: - **The date** is `JobApplication.FollowUpAt` — the same field `RulesEngine` and `FollowUpReminderHostedService` already act on. Writing it here means reminders keep working with no new wiring. - **The task** is an `ApplicationChecklistItem` in the `follow-up` category. The follow-up section *counts* open tasks; it does not own them. - **The record** is a `FollowUpSet` `JobEvent` — the same type the rest of the app emits, so the timeline reads it unchanged. ### Timeline integration `JobEvent` remains the source of history. The interpreter learned five more types: `InterviewScheduled`, `InterviewCompleted`, `OfferReceived` (milestones) and `FollowUpCreated`, `FollowUpCompleted` (routine, deliberately kept out of the milestone spine so it stays the "what actually happened" summary). ### Ownership `InterviewPrepItems` is reconciler-owned with a no-op migration, guarded on `JobApplications` (`docs/infrastructure/database-ownership.md`). Verified on a fresh MariaDB 11: `int AUTO_INCREMENT` PK, `varchar(255)` owner, `varchar(500)` title, `tinyint(1)` flag, `datetime(6)`, composite index inside the key limit. `InterviewPrepBoardDto` is named to avoid colliding with the pre-existing `InterviewPrepDto`, which belongs to the AI cache — a reminder that the two systems are genuinely different. ## Extension points - **New section**: add to `WORKSPACE_SECTIONS` and render it; nav is data-driven. - **New next-action rule**: add one clause to `ApplicationWorkspaceService.NextStep` — ordered, so position is the priority. - **More overview data**: extend `WorkspaceOverviewDto`; the page reads one payload. ## Milestones 1. ✅ Workspace foundation — route, nav shell, aggregate overview, next recommended action. 2. ✅ Checklist and progress tracking — persisted items, auto-completion from the readiness signals, 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. ✅ Application assets — CV variant association, tailoring suggestions, cover letter workflow with version history, documents (Phase 5.4). 5. ✅ Interview and follow-up — user-owned interview preparation, follow-up over the existing FollowUpAt and checklist, five more timeline event types (Phase 5.5). 6. ✅ Completion and product readiness — ownership audit, security review, full local verification (Phase 5.6). See `docs/phase-5-completion-report.md`. **Phase 5 is feature-complete locally.** Current release and deployment dependencies are tracked in `BLOCKERS.md`; the completion report is a historical snapshot. 7. Cover letter workflow. 8. Documents. 9. Interview preparation. 10. Dashboard improvements.