- consolidate API ownership and remove dead vendor code - add Stripe billing, learning paths, and public CV hardening - add migration, recovery, security, audit, and browser gates
8.2 KiB
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.mdanddocs/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
AiInteractionand 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— implemented.[Authorize]on the pre-Phase-5 controllers- 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 throughJobLifecycleEvents.- 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.