Files
jobtrackingapp/docs/phase-5-completion-report.md
T
cesnimda a3735299ec
CI and Deploy / test (push) Failing after 1m7s
CI and Deploy / deploy (push) Has been skipped
docs: complete application workspace phase
Phase 5.6 — refinement and validation, no new features.

Audited the ownership rules mechanically across all six Phase 5 services rather
than asserting them in prose. None writes to CareerProfile or its children, none
writes CV variant content, and the two services that read the profile
(ApplicationChecklistService, ApplicationIntelligenceService) never save it. The
rules hold: JobEvent is the history source, the checklist is workflow guidance,
readiness is a projection of it, CareerProfile is the source of truth, CvVariant
is derived output, and AI only appends to AiInteraction.

Verified locally end to end: 379 backend tests in Release, 128 frontend tests
across 36 suites, TypeScript clean, frontend production build, both Docker
images, a fresh empty MariaDB 11 (42 tables, no exceptions), a restart against
the populated database (rows preserved), and the existing SQLite dev database.

The security review found one genuine gap, reported rather than silently
changed: authentication is enforced by a fallback policy gated on Auth:Require,
which defaults to false. docker-compose.yml hardcodes it true so every compose
deployment is protected, and every Phase 5 controller carries an explicit
Authorize attribute, but several pre-Phase-5 controllers do not — a deployment
that lost the flag would expose them. Adding explicit attributes changes local
development behaviour, so it is flagged for a deliberate decision instead of
applied unilaterally.

docs/phase-5-completion-report.md records the milestones, the architecture
decisions and their reasoning, the ownership audit, the verification matrix, and
four remaining risks: CI red for an environmental reason (a docs-only commit
fails identically), production behind and needing a backup before first deploy,
the authentication configuration gap, and prompt quality being unmeasured.

Phase 5 is feature-complete locally. It is not deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:17:56 +02:00

148 lines
8.0 KiB
Markdown

# Phase 5 — Intelligent Application Workspace: completion report
> 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
- **Resolve the runner**, then verify the full stack in CI and deploy. Everything else is downstream.
- **Explicit `[Authorize]`** on the pre-Phase-5 controllers, paired with a dev-mode decision.
- **AI cover-letter actions** — improve, shorten, expand, tone, tailor. `CoverLetterVersion.AiAction`
already models them; only the modes and UI are missing.
- **Portfolio section** (workspace milestone 8) is still a placeholder.
- **Emit the new lifecycle events.** `InterviewScheduled`, `InterviewCompleted`, `OfferReceived`,
`FollowUpCompleted` are *interpreted* by the timeline but only `FollowUpSet` and `StatusChanged` are
currently emitted. Wiring the emitters is small and would make the timeline noticeably richer.
- **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.