# Phase 5 — Intelligent Application Workspace: completion report

> **Historical snapshot (2026-07-19).** Current release dependencies are tracked in `BLOCKERS.md`.

> 2026-07-19. What was built, what was decided, what is still risky, and what should come next.
> Companion to `docs/architecture/application-workspace.md` and
> `docs/infrastructure/database-ownership.md`.

## Core principle

*"The user should never ask: what should I do next?"*

The overview answers it from the checklist — the first pending item in category priority order. Not a
second ruleset, and not something that can recommend work the user already finished.

## What shipped

| Milestone | Delivered |
|---|---|
| 5.1 Foundation | `/applications/{id}` route, nav shell, one aggregate overview read, next recommended action |
| 5.2 Checklist | Persisted `ApplicationChecklistItem`, auto-completion from readiness signals, custom items, reordering, dismissal; **readiness refactored into a projection of it** |
| 5.3 Intelligence | Timeline interpretation over `JobEvent`, structured job analysis, career matching with evidence — all deterministic and read-only |
| 5.4 Assets | CV variant association, tailoring suggestions, cover letter workflow with version history, documents |
| 5.5 Interview & follow-up | User-owned `InterviewPrepItem`, follow-up over the existing `FollowUpAt` + checklist, five more timeline event types |
| Follow-up | Interview AI generation now receives the analysis and match context |
| Infrastructure | Clean MariaDB initialization fixed; database ownership model documented |

## Architecture decisions

**1. The checklist absorbed readiness rather than competing with it.**
`/readiness` keeps its DTO shape but now *projects* the checklist: score is completion percentage,
completed/missing are live items by status. One workflow surface, two views. The alternative — two
trackers kept in sync — was the thing the milestone brief explicitly forbade, and it would have
drifted within a release.

**2. Intelligence is deterministic; AI is a separate, explicit act.**
Analysis, matching, tailoring and timeline are computed from data the user already owns. The same
input always gives the same number, so the score is checkable. Opening a section costs nothing and
changes nothing. AI narrative stays in `AiWorkspaceService`, runs only when asked, and is versioned by
the append-only `AiInteraction` history.

**3. Suggestion-only is enforced by tests, not by convention.**
`Match_never_writes_to_the_career_profile`,
`Tailoring_never_writes_to_the_career_profile_or_the_variant`,
`Generating_ai_history_does_not_create_prep_items`,
`Existing_prep_items_are_never_overwritten_by_generation`.

**4. Two "interview prep" concepts exist, deliberately.**
`InterviewPrepNote` and `AiWorkspaceNote` are AI **caches** — they regenerate when their context
signature changes. `InterviewPrepItem` is the **user's** durable content. Discovering that the first
two would eventually overwrite anything a user typed is what forced the third. The DTO name
`InterviewPrepBoardDto` exists because the cache already owned `InterviewPrepDto`.

**5. Restore is always additive.**
Cover letter restore brings old text back as a *new* version, so what you restored from still exists.
Nothing in this phase destroys history.

**6. Reuse over new storage.**
Follow-up dates write `JobApplication.FollowUpAt`, the field `RulesEngine` and the reminder hosted
service already read — so reminders work with no new wiring. Follow-up tasks are checklist items.
Communication stayed entirely on `Correspondence`. Documents stayed on `Attachment`.

## Ownership verification

Audited mechanically across all six Phase 5 services:

| Service | SaveChanges | Reads profile | Writes profile | Writes variant content |
|---|---|---|---|---|
| `ApplicationWorkspaceService` | 0 | no | no | no |
| `ApplicationChecklistService` | 6 | yes | **no** | no |
| `ApplicationTimelineService` | 0 | no | no | no |
| `ApplicationIntelligenceService` | 0 | yes | **no** | no |
| `ApplicationAssetsService` | 3 | no | **no** | no |
| `InterviewPrepService` | 4 | no | **no** | no |

The rules hold:

- **`JobEvent`** — history source. Interpreted, never replaced, never written by the timeline service.
- **Checklist** — workflow guidance. Owns completion state only.
- **Readiness** — a projection of the checklist.
- **`CareerProfile`** — source of truth. Read by two services, written by none.
- **`CvVariant`** — derived output. The workspace moves a pointer; it never edits settings.
- **AI** — assistant only. Appends to `AiInteraction` and nothing else.

## Verification (local, 2026-07-19)

| Check | Result |
|---|---|
| Backend tests (Release) | 379 passed |
| Frontend tests | 128 passed, 36 suites |
| TypeScript | clean |
| Frontend production build | clean |
| Backend Docker image | built |
| Frontend Docker image | built |
| Fresh empty MariaDB 11 | 42 tables, starts, 0 exceptions |
| Existing populated MariaDB | 42 tables, starts, rows preserved |
| Existing SQLite dev DB | upgraded 34 → 44 tables, all rows preserved |

## Remaining risks

### 1. CI is red for an environmental reason — **external blocker**

Unresolved and outside the repository. The decisive evidence: run 531 on commit `8f73548` changed
**one markdown file** — no code, no test, no dependency, no workflow — and its test job failed in the
same duration band as every other run. A change that cannot affect compilation cannot fail a test job.

Everything above is therefore **local verification only**. Blocked on job logs (a read-scoped Gitea
token), `journalctl -u act_runner`, and the runner container config. See
`docs/infrastructure/runner-investigation.md`.

### 2. Production is behind — **deployment blocker**

Prod still runs the pre-Phase-4 build, because deployment is gated on CI. When it does deploy it will
apply several no-op migrations and let the reconciler build the new tables. That path is verified on
containers, **not on your data**. Take a database backup before the first deploy.

### 3. Authentication depends on a configuration flag — **defence-in-depth gap**

Authentication is enforced by a fallback policy that only activates when `Auth:Require=true`. It
defaults to `false`, and `docker-compose.yml` hardcodes `true`, so every compose deployment is
protected. But several core controllers — `JobApplicationsController`, `CompaniesController`,
`CorrespondenceController`, `RulesController`, `JobImportController` — carry **no explicit
`[Authorize]`**, so a deployment that lost that flag would expose them entirely.

Every Phase 5 controller has an explicit `[Authorize(AuthenticationSchemes = "local")]` and is safe
regardless. Adding explicit attributes to the older controllers is the right fix, but it changes local
development behaviour (where the flag is off), so it is flagged rather than applied unilaterally.

`PublicCvController` is intentionally anonymous — it serves `/cv/{slug}` only for variants the user
explicitly made public, and is `noindex` by default.

### 4. Prompt-quality is unmeasured

The interview module now receives the analysis and match context, and tests prove the context is
*present* and correctly scoped. Whether it produces better questions is a judgement no test here
makes.

## Future improvements

This dated list is superseded by the current roadmap and debt ledger. Authorization and lifecycle
event emission are implemented; the placeholder Portfolio/Notes workspace tabs were removed because
projects belong in public CVs and application notes remain in the existing application editor.

- **Resolve the runner**, then verify the full stack in CI and deploy. Everything else is downstream.
- ~~**Explicit `[Authorize]`** on the pre-Phase-5 controllers~~ — implemented.
- **AI cover-letter actions** — separate improve/shorten/expand actions remain intentionally unscoped;
  the shipped flow generates by tone and keeps user editing/version history authoritative.
- ~~**Portfolio section**~~ — removed; public CV projects/custom sections cover the current scope.
- ~~**Emit the new lifecycle events**~~ — implemented through `JobLifecycleEvents`.
- **Checklist GET writes on read** (seeding + auto-completion sync). Correct and idempotent, but it
  makes a GET non-cacheable; worth revisiting if the workspace ever needs to scale reads.
