# 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 `/jobs/{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 |
| Application answers / recruiter draft | compatibility fields on `JobApplication`, exposed as separate workspace fields |
| 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` (`/jobs/:id`) — a left nav plus a content pane, section selected by
`?section=`, so a section is linkable and survives refresh. The whole application row/card opens this
route; the legacy job-details dialog is not part of the production navigation flow.

Cover-letter and application-package editors report dirty state to the workspace. Section changes,
Back/Forward navigation and application exit use the shared application confirmation dialog, while a
hard refresh receives the browser's unload warning. Returning through the workspace Back action
restores focus to the originating application row.

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}/interview-prep` | editable `InterviewPrepItem` board | user edits only |
| `GET /{id}/interview-prep/brief` | cached generated `InterviewPrepNote` | explicit refresh or attachment-context change |
| `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.

### Application answers and recruiter message

The Cover Letter section also owns the saved application answer and recruiter-message editors that
were previously reachable only through the retired modal. The existing database representation is
kept for compatibility: the application answer remains a marked block in `JobApplication.Notes`, but
the workspace aggregate separates it from human notes and the API owns insertion/removal. General job
edits preserve the answer, and the ordinary Notes UI never exposes the storage markers. Empty saves
can intentionally clear either draft.

### 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.
