# JobTracker programme decisions and assumptions

## DEC-001 — Programme source paths

- **Date:** 2026-08-02
- **Decision:** The source files are exactly `docs/todo/work.md` and `docs/todo/ollama.md`; no fallback path search was needed.
- **Reason/evidence:** Both exact files exist and were read completely (922 and 825 lines respectively).
- **Alternatives considered:** `docs/work-programmes/` content-based discovery, required only if names differed.
- **Consequences:** Original references in the master plan use these paths and line ranges.
- **User approval required:** No; factual discovery.
- **Reversible:** Yes, update paths if the sources move.

## DEC-002 — One authoritative plan, compatibility pointer only

- **Date:** 2026-08-02
- **Decision:** `docs/work-programmes/master-work-plan.md` is authoritative. The older requested `docs/plans/post-audit-ux-reliability-program.md` will point to it instead of duplicating the checklist.
- **Reason/evidence:** The current request explicitly requires one authoritative record; `work.md:46-59` requires the older plan path, and `docs/plans/` did not exist.
- **Alternatives considered:** duplicate both full plans; rejected because they would drift.
- **Consequences:** Legacy references remain valid without a second status source.
- **User approval required:** No; directly reconciles both instructions.
- **Reversible:** Yes.

## DEC-003 — Canonical origin precedes identity recovery

- **Date:** 2026-08-02
- **Decision:** SEC-001/SEC-002 precede Microsoft legacy relinking and email change/recovery.
- **Reason/evidence:** JT-002 security URLs currently fall back to request Host; recovery built first could send attacker-controlled links.
- **Alternatives considered:** follow the suggested identity-first order; rejected as unsafe.
- **Consequences:** SEC-001 is the first implementation package.
- **User approval required:** No; safer dependency ordering was requested.
- **Reversible:** No practical reason to reverse.

## DEC-004 — Scoped production authority

- **Date:** 2026-08-02
- **Decision:** Production mutation authority applies only to the production-local-AI inventory/benchmark/install/configuration/rollout described in `ollama.md`, and only after its backup/rollback gates. Other production deployment still requires explicit instruction.
- **Reason/evidence:** `work.md:15` prohibits production deploy; `ollama.md:3` and the current request authorize scoped AI work.
- **Alternatives considered:** treat either instruction as globally overriding the other; rejected as over-broad.
- **Consequences:** repository work continues; PROD items remain blocked without documented access.
- **User approval required:** No; this is the narrow intersection of explicit instructions.
- **Reversible:** Yes with new authority.

## DEC-005 — ADR-004 is superseded for the scoped local-first programme

- **Date:** 2026-08-02
- **Decision:** Implement one central local-first routing policy with controlled external fallback, while retaining existing models/configuration for rollback.
- **Reason/evidence:** ADR-004 and current architecture choose one deployment provider; `ollama.md:398-451,574-608` explicitly requires ordered routing/fallback and the user asked to execute it.
- **Alternatives considered:** keep one provider and ignore the new programme; scatter fallback in callers; both rejected.
- **Consequences:** ADR-004 must later be superseded/updated in its own cohesive package. No provider change occurs before privacy/entitlement/queue controls.
- **User approval required:** Already supplied by the programme request.
- **Reversible:** Yes; old provider path/config remains for rollback.

## DEC-006 — Free means no AI in the target policy

- **Date:** 2026-08-02
- **Decision:** The target external plan model is Free (no AI, core non-AI tracking) and Pro (defined AI capabilities). Preserve existing user data and internal `Premium` compatibility while migrating behavior.
- **Reason/evidence:** `work.md:636-719` is explicit; current roadmap/code instead permits limited Free AI and uses `Premium`.
- **Alternatives considered:** retain existing limited Free AI; rejected because it contradicts the new programme.
- **Consequences:** POL-001 requires a deliberate server-side policy/compatibility package; copy changes cannot precede enforcement.
- **User approval required:** Already supplied.
- **Reversible:** Product policy is reversible later; data migration should remain additive.

## DEC-007 — One durable operation foundation

- **Date:** 2026-08-02
- **Decision:** Strategy Snapshot, CV processing and other long AI work share OPS-001/AI-001; feature packages supply handlers/results/UI only.
- **Reason/evidence:** Both programmes specify the same state, idempotency, retry, notification and restart requirements and explicitly warn against duplicate implementations.
- **Alternatives considered:** separate CV and Strategy queues; rejected as duplicated infrastructure and inconsistent behavior.
- **Consequences:** AI-003/AI-004 depend on the common foundation.
- **User approval required:** No; explicitly requested.
- **Reversible:** The model can be extended; duplicate queues should not be introduced.

## DEC-008 — New-table ownership is unresolved until provider-safe design

- **Date:** 2026-08-02
- **Decision:** Do not create operation/deletion tables until their package selects one schema owner and proves fresh/upgrade SQLite and MariaDB behavior.
- **Reason/evidence:** `docs/infrastructure/database-ownership.md` mandates reconciler DDL for current cross-provider tables, while audit JT-019 identifies dual schema ownership as a risk and the Phase 0 identity design prefers a real EF migration.
- **Alternatives considered:** silently follow either document; rejected because the conflict is material.
- **Consequences:** no migration is created in SEC-001; OPS-001 records the eventual choice and rollback evidence.
- **User approval required:** No now; ask only if the evidence leaves materially different safe choices.
- **Reversible:** Yes before schema deployment.

## DEC-009 — Production access is currently blocked, not guessed

- **Date:** 2026-08-02
- **Decision:** Mark production inventory/benchmark/rollout blocked while continuing safe repository work.
- **Reason/evidence:** existing backup/verification docs say this environment has no route or production credentials; only CI secret names and `/opt/job-tracker/app` are documented.
- **Alternatives considered:** guess SSH host/user or scan; explicitly prohibited.
- **Consequences:** PROD-001/003/004 and REL-001 cannot be `DONE`; sanitized templates/harnesses can still be built.
- **User approval required:** No.
- **Reversible:** Immediately when documented access is provided.

## DEC-010 — One canonical origin replaces provider callback overrides

- **Date:** 2026-08-02
- **Decision:** `App:PublicBaseUrl` now builds Gmail and Microsoft Graph callback URLs; the old provider-specific redirect variables are removed from Compose and the example environment.
- **Reason/evidence:** Independent callback origins contradicted SEC-001's single-origin trust boundary and allowed configuration drift. Provider registrations must use the documented paths under the canonical origin.
- **Alternatives considered:** Accept overrides only after equality validation; rejected because it preserves duplicate configuration with no supported separate-origin use case.
- **Consequences:** Operators with legacy redirect variables must register/use `<APP_PUBLIC_BASE_URL>/api/gmail/oauth/callback` and `/api/microsoft-graph/oauth/callback`. Existing variables no longer affect the application.
- **User approval required:** No; this implements the approved canonical-origin prerequisite.
- **Reversible:** Yes by restoring validated overrides, but only if a real separate-origin requirement is established.

## DEC-011 — Dedicated one-hop nginx trust network

- **Date:** 2026-08-02
- **Decision:** Nginx and backend share a dedicated internal `WEB_PROXY_SUBNET`; forwarded headers are accepted for one hop only from that configured CIDR. Production Compose has no host-bound app or Ollama ports; development port bindings require `docker-compose.dev.yml` explicitly.
- **Reason/evidence:** Docker's dynamic/default/shared networks cannot safely identify nginx as the trusted hop, and the old auto-loaded override disabled proxy trust while exposing ports.
- **Alternatives considered:** clear all known proxy collections; trust the external shared network; assign a single static container IP. The first two trust too much, while a dedicated CIDR tolerates container replacement without pinning one container address.
- **Consequences:** production must inventory Docker networks and set a non-overlapping `WEB_PROXY_SUBNET` before deploy. External Traefik still requires separate operator verification.
- **User approval required:** No; this is the scoped security prerequisite.
- **Reversible:** Yes via version rollback and previous environment, but rollback must not reopen host ports unintentionally.

## DEC-012 — Nginx Host is derived, not separately configured

- **Date:** 2026-08-02
- **Decision:** The frontend container derives nginx's exact server name at startup from `APP_PUBLIC_BASE_URL`; no second host setting exists. Unknown Hosts receive nginx 444 except `/health`.
- **Reason/evidence:** Hard-coding the current domain or introducing an independent nginx-host variable would violate the single canonical-origin contract and create drift.
- **Alternatives considered:** application-only Host rejection; separate `APP_EXTERNAL_HOST`; operator-only Traefik filtering. Each leaves one repository boundary weak or duplicates authority.
- **Consequences:** the frontend container fails fast on malformed/unsupported origins; IPv6 host literals are currently outside the deploy preflight contract.
- **User approval required:** No.
- **Reversible:** Yes; broaden only with matching parser, nginx and deployment tests.

## DEC-013 — Microsoft sign-in has one tenant-qualified trust path

- **Date:** 2026-08-02
- **Decision:** Microsoft ID tokens are accepted only by the exchange/link validator, which requires the configured account mode plus exact GUID `tid`/`oid` and issuer agreement. The raw Microsoft bearer scheme is removed. Email-like claims are metadata, not verified ownership.
- **Reason/evidence:** The UI already exchanges Microsoft tokens for local sessions; maintaining a second issuer-disabled API bearer path duplicated and weakened the trust decision. `oid` alone is tenant-scoped.
- **Alternatives considered:** harden both bearer and exchange paths; retain `common` implicitly in Production; use `sub` or email as identity. Each adds duplicated policy or preserves the audited ambiguity.
- **Consequences:** undocumented raw-token API clients stop working; Production must choose a tenant mode. Legacy identity ownership remains unresolved until SEC-004 and Microsoft must not be enabled there first.
- **User approval required:** No; this is the validated P0-1A contract.
- **Reversible:** The account mode is configurable; raw bearer support should return only with a documented requirement and the same policy/tests.

## DEC-014 — Split email ownership from session revocation

- **Date:** 2026-08-02
- **Decision:** Split the original SEC-005 into SEC-005A (session/recovery revocation) and SEC-005B (registration/pending email plus migration/UI).
- **Reason/evidence:** The revocation work uses the existing schema and is independently testable/rollbackable; pending email requires a coordinated database and frontend contract. Keeping both under one active item violated the requested small-package cycle.
- **Alternatives considered:** keep one broad item; rejected because its status could not accurately distinguish verified security behavior from an unstarted migration/UI flow.
- **Consequences:** SEC-004 depends on both children. Original source/audit references remain on each, so no requirement was lost.
- **User approval required:** No; this is tracking granularity within approved scope.
- **Reversible:** Yes by presenting them as one release, but their verification remains separate.

## DEC-015 — Pending email uses Identity tokens and one provider-aware EF migration

- **Date:** 2026-08-02
- **Decision:** Store only the proposed address and request time, rotate the Identity security stamp for each replacement request, and use the built-in change-email token. Add both fields through one EF migration whose SQLite and MariaDB column types are explicit; do not duplicate these Identity columns in the startup reconciler.
- **Reason/evidence:** The active email must remain authoritative until proof. Identity already binds tokens to user, new email, purpose and security stamp; stamp rotation makes the latest request win without another token table. Dry-run SQL showed SQLite-scaffolded types were unsafe for MariaDB until the migration branched by provider. Audit/schema decision P0-4B assigns these Identity fields to EF.
- **Alternatives considered:** immediately replace `Email`; store plaintext confirmation tokens; add a request table/nonce abstraction; add the same columns to startup reconciliation. The first two are unsafe, and the latter two add duplicate state/ownership without a demonstrated need.
- **Consequences:** deployment must run `20260802205800_AddPendingEmailChange` before the new API version. SQLite uses `TEXT`; MariaDB uses `varchar(320)` and `datetime(6)`. Rolling-version and MariaDB execution still require verification.
- **User approval required:** No; this is the smallest implementation of the approved ownership contract.
- **Reversible:** Yes via the migration `Down` before data relies on pending requests; active email data is unchanged.

## DEC-016 — SEC-005B remains short of local browser verification

- **Date:** 2026-08-02
- **Decision:** Mark SEC-005B `IMPLEMENTED — NOT VERIFIED`, not `VERIFIED LOCALLY` or `DONE`.
- **Reason/evidence:** all automated suites and an isolated API runtime pass, but the in-app browser denied localhost because its administrator policy check could not be verified. SMTP and MariaDB execution are also unavailable.
- **Alternatives considered:** infer browser behavior from component tests or claim the existing Docker UI; rejected because the running containers are old images and the programme forbids inferred test claims.
- **Consequences:** SEC-004 repository work can proceed, but SEC-005B retains explicit browser/provider/production acceptance checks.
- **User approval required:** No; truthful status accounting is required.
- **Reversible:** Yes immediately after the blocked checks pass.

## DEC-017 — Canonical Microsoft ownership never backfills legacy evidence

- **Date:** 2026-08-02
- **Decision:** Add nullable bounded `MicrosoftTenantId`/`MicrosoftObjectId` with one unique composite index, use only that pair for ownership, and leave every legacy subject/email row null-canonical until explicit dual-proof relinking.
- **Reason/evidence:** `oid` is tenant-scoped, legacy subjects may be `oid` or `sub`, and provider email is mutable metadata. A disposable migration rehearsal preserved duplicate legacy values and enforced unique proven pairs. The recovery ceremony requires both a purpose-bound token delivered to the confirmed app email and a fresh Microsoft token for the same pair.
- **Alternatives considered:** backfill from `common`, legacy subject, email or next-seen token; retain email fallback; add a general identity-provider framework. Each would merge unproven identities or add unrelated abstraction.
- **Consequences:** ambiguous/unconfirmed legacy users require operator assistance; production needs a counts-only inventory and relink window. Link/unlink revoke sessions, and passwordless unlink is refused until a safe provider reauthentication path exists.
- **User approval required:** No; this implements the approved JT-001 safety contract.
- **Reversible:** Additive schema is reversible before canonical data is relied upon. Application rollback must never restore email auto-linking.

## DEC-018 — Keep MariaDB date work in SQL and bound SQLite fallback by owner/job

- **Date:** 2026-08-02
- **Decision:** Branch only at the affected `DateTimeOffset` query roots: SQLite materializes owner/job-scoped rows before ordering or range comparison; MariaDB keeps server-side ordering, filtering, aggregation and pagination.
- **Reason/evidence:** the real SQLite provider throws before execution, while Pomelo generates the required SQL. Changing all timestamp storage would require a risky migration and editing response endpoints individually would leave CV cleanup/reprocess siblings broken. Real-provider tests and fresh HTTP rehearsal pass.
- **Alternatives considered:** global timestamp conversion/schema rewrite; always materialize on every provider; modify historical migrations; catch-and-retry translation exceptions. These add migration risk, production performance cost or hide separate JT-019 ownership drift.
- **Consequences:** SQLite work is bounded by tenant/job and current entitlement limits but still happens in memory; production MariaDB behavior is unchanged. If per-user AI history grows materially, a later UTC scalar column/index migration may be measured and designed.
- **User approval required:** No; this is the smallest root-cause correction within CORE-001.
- **Reversible:** Yes; revert the provider branches and relational test, with no data rollback.

## DEC-019 — Keep the editable interview board canonical and move the generated brief

- **Date:** 2026-08-02
- **Decision:** Keep `GET /interview-prep` for the durable editable `InterviewPrepItem` board, move the distinct cached/generated `InterviewPrepNote` response to `GET /interview-prep/brief`, and delete the unused flat timeline action so the grouped/filterable timeline remains canonical.
- **Reason/evidence:** both interview representations have active repository UI callers and incompatible DTOs, while only the newer timeline has a caller. Overloading by query parameter or deleting one live feature would preserve ambiguity or break behavior. Reflection and isolated HTTP tests now show one action per method/path with owner 200, other user 404 and anonymous 401.
- **Alternatives considered:** delete either interview feature; retain the old path with a discriminator; rename the editable board; merge DTOs. Each creates unnecessary compatibility state or breaks the current workspace contract.
- **Consequences:** undocumented direct consumers of the generated brief must adopt `/brief`; repository clients/docs are updated. No data or schema changes.
- **User approval required:** No; this is the smallest cohesive repair of confirmed JT-004.
- **Reversible:** Yes by restoring the route/client and flat action, but rollback also restores the exploitable availability defect and is not recommended.

## DEC-020 — Attachment final paths are the durable operation identity

- **Date:** 2026-08-02
- **Decision:** Represent recoverable attachment mutations as `<final>.uploading` and `<final>.deleting`; do not add a separate JSON journal, schema or periodic worker. Reconcile once after database initialization and preserve unknown plain files.
- **Reason/evidence:** every final path is already generated, unique and root-bounded. The suffix plus row existence encodes every required recovery decision, and real-SQLite failure injection proves upload promotion, delete restore/purge and idempotent restart behavior. A second journal would introduce dual durable state and another crash-ordering problem.
- **Alternatives considered:** JSON operation journal; database operation table; object-store abstraction; periodic/multi-replica reconciler. None is required for the current single-filesystem deployment and each adds coordination or migration cost.
- **Consequences:** recovery retries on safe restart rather than a timer; persistent failures remain observable until restart/operator action. Multi-replica or high-volume deployments must first add a lease and measured periodic reconciliation. SEC-009 must reuse these root/quarantine rules.
- **User approval required:** No; this is the smallest implementation of the approved JT-010 recovery contract.
- **Reversible:** Yes after draining/reviewing all suffix markers. No schema rollback exists; unknown plain orphans must never be guessed away.

## DEC-021 — Worker enumeration bypasses filters once, then re-enters owner scope

- **Date:** 2026-08-02
- **Decision:** `BackgroundTenantRunner` may ignore tenant filters only to enumerate non-empty job owners. Each owner is processed sequentially in a new scope whose live `CurrentUserService` restores all normal query filters. Rules, reminders, daily export and enrichment receive separate default-false switches.
- **Reason/evidence:** hosted scopes have no HTTP identity and therefore returned zero rows. Leaving services on while fixing that root cause would unexpectedly start status mutations, file exports, email and AI calls. Real-SQLite tests prove owner filtering, per-owner settings, failure isolation and fake-only side effects.
- **Alternatives considered:** unfiltered queries throughout each worker; a privileged DbContext; automatic activation under legacy settings; a distributed scheduler. These enlarge the trust boundary, create rollout risk or solve unmeasured scale.
- **Consequences:** workers remain inert until explicitly enabled after OPS/POL prerequisites. Execution is sequential/single-instance; leasing and durable operations are the next package. Existing backup, probe and CV-run handling remain unchanged.
- **User approval required:** No; this is the requested safe foundation and does not activate production work.
- **Reversible:** Yes by keeping switches false and reverting the runner/service changes. Already-generated exports or user-visible mutations require separate reviewed rollback.

## DEC-022 — Durable operations are EF-owned and store references, not payloads

- **Date:** 2026-08-02
- **Decision:** Split OPS-001 into independently reviewable OPS-001A state/schema, OPS-001B notifications and OPS-001C APIs/UI. `UserOperations` is owned only by a provider-conditional EF migration; the startup reconciler does not create or alter it. The row stores bounded policy/subject references and no generic raw payload.
- **Reason/evidence:** a manually branched migration produces correct SQLite and bounded MariaDB DDL, avoiding the repository's old SQLite-type failure while reducing JT-019 dual ownership. CV/Strategy inputs already have durable domain IDs; copying private text into a queue row is unnecessary. Concurrent SQLite tests prove one idempotent row and one lease winner.
- **Alternatives considered:** extend `CvExtractionRun`; reconciler plus no-op migration; generic JSON payload; external queue/Redis; one large schema/UI package. These duplicate feature state, preserve dual ownership, increase private-data copies/infrastructure, or prevent small rollbackable review.
- **Consequences:** feature producers reference domain rows and must re-check entitlement/privacy before work. Notifications/API/UI follow without changing the operation identity. MariaDB execution remains a deployment gate.
- **User approval required:** No; this resolves the recorded DEC-008 conflict using repository evidence and the requested smallest reliable design.
- **Reversible:** Yes before consumers rely on rows; stop producers/drain rows before `Down`. Additive table may remain during application rollback.

## DEC-023 — One current generic notification is atomic with terminal operation state

- **Date:** 2026-08-02
- **Decision:** Store one owner-scoped `UserNotification` per current terminal operation outcome, committed in the same relational transaction. Use generic bounded text, no email delivery and no private operation/failure content. A manual retry removes the prior notification so the next terminal outcome can replace it.
- **Reason/evidence:** a unique nullable operation foreign key supplies database idempotency; a forced notification-write failure proves the terminal update rolls back. This reuses OPS-001A rather than introducing an outbox framework or a second queue.
- **Alternatives considered:** transient frontend notifications; email outbox; multiple immutable notifications per retry attempt; generic event bus. Transient state fails restart recovery, email is not authorized, and the latter two add delivery/history machinery not required by either programme.
- **Consequences:** OPS-001C can expose stable read/dismiss state without creating another notification model. Historical retry-attempt notifications are not retained; operation attempt/failure fields remain the technical state. MariaDB execution remains a deployment gate.
- **User approval required:** No; this implements the approved persistent-notification prerequisite without external side effects.
- **Reversible:** Yes before API/UI consumers rely on it. Older application versions tolerate the additive table; schema `Down` deletes notification state.

## DEC-024 — Operation APIs expose state, not worker internals

- **Date:** 2026-08-02
- **Decision:** Expose authenticated owner list/detail/cancel/retry and notification list/count/read/dismiss APIs, but no generic operation-create endpoint. Return only bounded user-facing state; omit idempotency keys, lease tokens, provider/model fields, raw failure text and result references. Use bounded polling rather than realtime infrastructure.
- **Reason/evidence:** feature producers must enforce entitlement/privacy and durable subject references before admission, so a generic create endpoint would bypass later policy. Existing Axios, MUI and browser events cover the UI without a dependency. Two-user HTTP checks prove copied identifiers return 404.
- **Alternatives considered:** WebSockets/SignalR; a generic JSON task API; exposing the full entity; merging reminders and operation notifications. These add infrastructure, unsafe authority or misleading counts without a current need.
- **Consequences:** AI-003/004 own feature admission and result navigation. The operation page polls every 15 seconds while mounted; the shell polls unread count every 60 seconds. Realtime delivery can be reconsidered only if measured UX/load requires it.
- **User approval required:** No; this is the smallest implementation of both programmes' stable status and notification contract.
- **Reversible:** Yes. UI/API removal leaves durable operation/notification data intact; application rollback can retain additive tables.

## DEC-025 — Public Pro policy uses live roles while retaining legacy billing identifiers

- **Date:** 2026-08-02
- **Decision:** Expose only `free` and `pro`; make Free AI entitlement and limits zero; authorize explicit AI actions with one live database-role policy; recheck queued/background work at execution; preserve the internal `Premium` Identity role and `Stripe:PricePremium` configuration key for billing/data compatibility.
- **Reason/evidence:** the new programme supersedes the old limited-Free-AI model. Claim-only authorization would let an already-issued session retain AI after downgrade, while renaming the persisted role/config now adds migration and rollback risk without changing user-visible behavior. Core job create/detail and deterministic enrichment were traced separately because they must still work when their optional model call is skipped.
- **Alternatives considered:** static `RequireRole`; guards copied into every controller; renaming the Identity role/config; wrapping every summarizer call in a new provider abstraction; blocking whole controllers. These leave stale-claim/worker bypasses, add scattered checks, create needless migration risk, pre-empt POL-002/AI-002, or hide existing non-AI data.
- **Consequences:** explicit locked APIs return stable `pro_required`; Admin maps to Pro; UI receives `ai`/`proThemes`; existing AI history and non-AI editing remain accessible. Usage accounting outside AI Workspace remains incomplete and blocks provider rollout/full POL-001 verification. PRODUCT-001 still owns removal of invented landing-page price/tier claims.
- **User approval required:** No; the programme explicitly requires Free=no-AI and centralized enforcement.
- **Reversible:** Yes as one repository-only policy/UI change with no schema update. Workers must remain off while rolling back to avoid restoring an entitlement bypass.

## DEC-026 — Classify synthetic AI workloads before finalizing privacy routing

- **Date:** 2026-08-02
- **Decision:** Advance PROD-002 immediately after POL-001 and before POL-002, even though the recommended list placed the general privacy policy first.
- **Reason/evidence:** POL-002 explicitly depends on task type, privacy class, payload shape, latency and fallback suitability. POL-001 produced the reachable-call inventory, and PROD-002 can safely classify it and create synthetic fixtures without production access or provider calls. Writing policy first would either duplicate this inventory or invent categories without fixtures.
- **Alternatives considered:** keep POL-002 next and revise it later; perform PROD-001 hardware inventory first; start queue implementation. The first creates churn, while the latter two are blocked by production access or need the privacy contract.
- **Consequences:** PROD-002 is the sole in-progress item; POL-002 follows with evidence-backed classes. No production/provider action is introduced.
- **User approval required:** No; the user directed dependency-aware reordering and conflict recording.
- **Reversible:** Yes; documentation/fixtures can be revised before routing code depends on them.

## DEC-027 — External AI needs two administrator/user gates and defaults to local

- **Date:** 2026-08-03
- **Decision:** Persist `AiEnabled` and `ExternalAiProcessingAllowed` per user; preserve AI-enabled behaviour for existing accounts, default external consent to false, and permit an external `/cv/*` request only when backend and sidecar administrator gates, a supported configured provider, live Pro entitlement, AI-enabled preference and explicit user consent all agree. Background calls without an authenticated request fail safe to local.
- **Reason/evidence:** the existing global sidecar `AI_PROVIDER` could route full CV data externally without a user decision. One backend policy plus a sidecar permission header closes that execution path without deleting rollback provider configuration or inventing the final AI-002 router.
- **Alternatives considered:** remove Gemini/Groq; rely on UI/local storage consent; trust one environment flag; refactor every AI interface now; silently use external when configured. These either break rollback compatibility, are bypassable, or prematurely duplicate AI-001/002.
- **Consequences:** disabling AI is enforced from live database state; external processing is off by default and needs deliberate two-sided configuration. True local-first fallback triggers, operation policy snapshots, provider/reason persistence, payload minimization and cost controls remain explicit AI-001/002 gates, so POL-002 is not overstated as fully verified.
- **User approval required:** No; this is the smallest safe implementation of the programme's explicit privacy controls and preserves provider configurations.
- **Reversible:** Yes. Application rollback should leave the additive preference columns in place; turning both administrator gates off immediately restores local-only processing without data loss.

## DEC-028 — Durable AI work extends UserOperations with a default-off typed worker

- **Date:** 2026-08-03
- **Decision:** Reuse OPS-001A/B/C for every long AI task. Add one task-handler worker and one admission service; expose no generic create endpoint. Admission stores only a domain subject reference and policy snapshot, enforces Pro/privacy/capacity/idempotency/deadline, and returns the existing stable status URL. Start at one worker and keep it disabled until real handlers and rollout checks pass.
- **Reason/evidence:** the existing operation store already supplies persistent states, atomic claims, leases, retries, cancellation, restart recovery, notifications and owner APIs/UI. A second CV/Strategy queue or Redis would duplicate proven state. A generic create API would let callers bypass task-specific ownership and payload validation.
- **Alternatives considered:** separate in-memory channel; Redis/Hangfire; one worker per feature; synchronous provider calls; generic public task creation; unbounded hosted-service parallelism. These lose restart state, duplicate infrastructure, expand authority or fail the congestion requirement.
- **Consequences:** AI-003/004 only add typed handlers/producers. Current capacity serialization is process-local for the documented single-backend deployment; database reservation is required before multiple backend replicas. Provider/model semaphores and circuit/provenance remain AI-002 responsibilities.
- **User approval required:** No; this follows both programmes' explicit instruction to reuse the smallest reliable existing infrastructure.
- **Reversible:** Yes. Keep the worker switch false, remove admission/worker registrations, and retain operation rows/API history. No new schema was added in this slice.

## DEC-029 — One sequential sidecar router owns local-first fallback

- **Date:** 2026-08-09
- **Decision:** Keep provider execution behind the existing sidecar boundary, make Ollama the default primary, permit at most one sequential external fallback, and carry the backend's rechecked privacy/task decision through the AI-001 execution scope. Reuse existing operation provider/model/progress fields for provenance; add no queue/provider schema or dependency.
- **Reason/evidence:** every generative `/cv/*` path already converges on one `_provider_generate` family, while deterministic tasks and `/summarize` must remain local. Sidecar fake-transport tests prove local success, consent/config/task/cost denials, schema fallback, circuit behavior, external failure and no parallel duplicate call. Backend tests prove policy propagation and success/failure provenance.
- **Alternatives considered:** provider selection in each controller; browser-selected providers; a second provider abstraction in .NET; simultaneous local/cloud racing; a new circuit/attempt table; increasing synchronous timeouts. These scatter policy, expose authority, duplicate the established boundary, risk double charge/output, add unneeded schema, or mask the queued-operation root cause.
- **Consequences:** `AI_ROUTING_MODE` supports `local_only`, `local_first` and explicitly gated `external_only`; invalid values fail closed. New durable task IDs stay local until allowlisted. The current circuit is process-local and one AI worker is the effective single-model concurrency limit. Per-request prompt size limits external spend/exposure, but complete monthly cross-feature accounting and model selection remain rollout gates.
- **User approval required:** No; this directly implements the approved local-first programme without invoking a provider or production service.
- **Reversible:** Yes. Set `EXTERNAL_AI_ENABLED=false` or `AI_ROUTING_MODE=local_only`; the older `AI_PROVIDER`/model configuration is retained. Existing nullable operation fields and AI history remain readable.

## DEC-030 — Strategy generation is one typed operation and one structured inference

- **Date:** 2026-08-09
- **Decision:** Make Focus Plan GET cache-only and move generation to typed `strategy.snapshot` work on the shared queue. Encode only job and at most four attachment IDs, reuse active operations, and replace four sequential model calls with one bounded JSON response whose complete shape is validated before the unique cache row is updated. Keep Strategy absent from the external fallback allowlist.
- **Reason/evidence:** the traced button/GET path owned four serial model calls and had no restart/cancel/retry identity. AI-001/002 already provide every needed state/policy/routing primitive. One structured inference reduces timeout exposure and makes publication atomic without a second queue or schema.
- **Alternatives considered:** increase HTTP/proxy timeouts; keep GET as a command; add a Strategy queue/table; store prompts/private text in operation payload; four model calls inside the worker; race external/local providers. These retain the root failure, duplicate infrastructure/private data/output, or violate established routing safety.
- **Consequences:** existing cached `FocusPlanDto` remains readable, while generation now returns 202 and the UI resumes by operation ID/context. Retry can overwrite only the same unique result row. Worker activation and real-model tuning remain rollout gates; operation records provide deduplication/provenance, not full monthly billing accounting.
- **User approval required:** No; both programmes explicitly require durable Strategy work and consolidation with AI-001/002.
- **Reversible:** Yes. Keep the worker off and revert `a621226`; no schema/dependency changed. Cancel or drain queued `strategy.snapshot` rows before removing the handler.

## DEC-031 — CV extraction runs own review data; UserOperations own execution

- **Date:** 2026-08-09
- **Decision:** Keep `CvExtractionRun` as the artifact/result/review record and make one typed `cv.process` operation reference its numeric ID. Replace the unbounded channel and separate hosted service with AI-001 admission/leases/retries/cancellation/notifications. Upload returns 202 after persistence; no raw CV payload is duplicated into operation state.
- **Reason/evidence:** the complete trace showed useful persistent review state but two competing execution mechanisms: synchronous upload and a process-local channel. Reusing both existing models gives restart-safe orchestration without a new queue/schema and preserves the mandatory accept/discard gate.
- **Alternatives considered:** raise proxy timeouts; keep synchronous upload; add a second CV queue/table; store raw CVs/prompts in operation payloads; replace extraction runs with generic operations. These retain the 504/lost-wakeup path, duplicate infrastructure/private data, or discard domain review/version history.
- **Consequences:** all four long CV actions share one default-off worker and operation UI. Parser-version/process isolation remains SEC-006/007; browser/model/MariaDB/production gates remain before rollout. Existing clients must accept the upload endpoint's 202 operation response.
- **User approval required:** No; both programmes explicitly require one durable operation foundation and preservation of human review.
- **Reversible:** Yes. Keep the worker off, revert `c3c5af8`, and retain operation/extraction rows. Cancel or drain `cv.process` rows before removing the handler.

## DEC-032 — Provider account management and signed-out authentication share logic, not presentation

- **Date:** 2026-08-09
- **Decision:** Add an explicit sign-in presentation to the existing Google and Microsoft components. It reuses provider token exchange and two-factor handling but skips signed-in account discovery, link/unlink controls and linking copy. Keep the full account presentation unchanged for authenticated profile/settings surfaces.
- **Reason/evidence:** the programme requires a conventional single sign-in card without implying account linking. Duplicating provider callback code would risk divergence from the hardened tenant/linking path, while rendering account-state panels on login creates the prohibited clutter and misleading relationship.
- **Alternatives considered:** retain tabs; create duplicate login-only provider clients; hide copy with CSS; combine provider exchange and account linking. These preserve the UX defect, duplicate sensitive logic, hide rather than remove inaccessible state, or weaken the identity boundary.
- **Consequences:** login presentation becomes simpler without changing backend identity ownership. Provider account management remains available only in its existing authenticated surfaces. Real-provider and production verification are still required.
- **User approval required:** No; this is the smallest implementation of the explicit UX-001 requirement and preserves the prior security contracts.
- **Reversible:** Yes. Reverting UX-001 restores the tabbed presentation; no provider link, session, schema or configuration data changes.

## DEC-033 — Theme preference is application-owned; MUI only renders resolved mode

- **Date:** 2026-08-09
- **Decision:** Keep the existing `themeMode:<user>` storage keys as the only preference store, resolve user → anonymous → System explicitly, and feed the resulting Light/Dark mode into MUI without MUI storage or provider/router remounts. Use a dedicated auth-user event and read-only cross-tab storage subscription; apply the same resolution in a pre-paint Next script.
- **Reason/evidence:** the trace proved the general auth event fired before the new user key was stored and no later theme event occurred. Refresh therefore changed namespace and appeared random. Provider keys/router dependencies also discarded state, while MUI's default storage could become a second source of truth.
- **Alternatives considered:** keep provider keys; emit another general auth event; store one global preference; let MUI own `mui-mode`; add a server profile migration. These retain remounts/request loops, lose user isolation, create competing precedence or add unnecessary backend scope.
- **Consequences:** users without a saved scoped choice inherit the explicit anonymous choice, otherwise the documented default is System. Explicit Light/Dark ignore OS changes. The pre-paint script must remain behaviorally aligned with `themePrefs`; tests cover both.
- **User approval required:** No; this directly implements the approved UX-002 contract without schema, dependency or production changes.
- **Reversible:** Yes. Revert `11734ee`; existing preference values remain unchanged and readable.

## DEC-034 — Important-term quality stays deterministic and on demand

- **Date:** 2026-08-09
- **Decision:** Harden the existing shared `JobCvMatchService` rather than add a model or stored analysis version. Clean HTML/chrome at the matcher boundary, use bilingual/general filler categories, rank bounded phrase runs before singleton terms, and extend the canonical skill vocabulary for punctuation-sensitive technologies. Rename UI output to “important terms.”
- **Reason/evidence:** the complete path proved the reported Norwegian words came from English-only deterministic token ranking. Imported descriptions may be clean, but manual descriptions/notes reach the same matcher. Results are recomputed and the generated learning sync already preserves user decisions, so a cache/schema migration would solve a nonexistent storage problem.
- **Alternatives considered:** hardcode the five examples; call Ollama for keywords; add a result table/version column; filter only in the UI; silently delete learning items. These are incomplete, less reliable, duplicate state, leave API consumers dirty or discard user history.
- **Consequences:** term changes appear on the next request; obsolete pending generated learning items auto-complete under existing behavior while done/dismissed decisions remain. The curated vocabulary remains intentionally bounded and test-driven.
- **User approval required:** No; this is the deterministic-first implementation explicitly required by both programmes.
- **Reversible:** Yes. Revert `da1aa8b`; no schema, cache or provider state changes.

## DEC-035 — Career Workspace derives actions from existing domain state

- **Date:** 2026-08-09
- **Decision:** Render one action-oriented overview from the completeness and durable import state already owned by `CareerProfilePage`; load only the separate recent-CV list. Link actions to the existing profile/import anchors, builder and saved-job workflow, and retain extraction Apply/Discard as the sole merge gate.
- **Reason/evidence:** duplicating `/career/profile` and `/profile-cv/runs` requests in the route wrapper would create competing loading/polling state. The existing page already has correct durable run, review and version data, while CV variants are a separate bounded list. The old wrapper duplicated headings and explanation without exposing next actions.
- **Alternatives considered:** a second workspace data loader; moving all profile state into a new global store; direct one-click job-CV creation without a job choice; rewriting the profile editor. These add inconsistent state, premature architecture or bypass the established job/application boundary.
- **Consequences:** the page presents concise state-aware navigation without changing API, profile persistence or imported-data approval semantics. Browser and production verification remain before `DONE`; deeper builder/job interaction remains in CAREER-002/JOBS-001.
- **User approval required:** No; this is the smallest cohesive implementation of the approved programme requirement.
- **Reversible:** Yes. Revert the CAREER-001 implementation commit; no data/configuration migration exists.

## DEC-036 — CV autosave uses revision ordering and explicit discard warnings

- **Date:** 2026-08-09
- **Decision:** Keep the existing debounced variant autosave API, but track the latest name/settings and a monotonic client revision. Only the newest request may update the visible save state. Offer an explicit retry and block route/unload navigation until the user chooses to discard pending or failed edits.
- **Reason/evidence:** the editor already has versioned server saves, but a route change during the debounce and overlapping responses could lose work or falsely display `Saved`. Blank names were ignored by the backend while remaining blank in the UI. The existing API is sufficient; the defect is client coordination and feedback.
- **Alternatives considered:** replace autosave with a manual-only form; add a queue/schema; extend timeouts; silently flush during unload; refactor variant versioning. These regress the working interaction, add unrelated infrastructure or cannot reliably complete during page teardown.
- **Consequences:** users receive a native discard decision for in-app navigation and browser-standard warning on unload. A blank name stays unsaved until corrected. The deeper builder redesign and browser checks remain CAREER-002 work.
- **User approval required:** No; this is a reversible data-loss safeguard within the approved programme.
- **Reversible:** Yes. Revert `b58cc19`; server variants and version history require no migration.

## DEC-037 — Variant entries override profile facts; they do not delete them

- **Date:** 2026-08-09
- **Decision:** Keep profile-backed entries read-only at the ownership boundary: a CV variant may reorder, hide or override them, but cannot delete the Career Profile item. Provide full add/edit/reorder/confirmed-delete behavior only for variant-owned custom entries using the existing `string[]` model.
- **Reason/evidence:** the architecture and resolver define variants as lenses over stable Career Profile item keys. Treating Delete in the Builder as profile deletion would violate separation and risk changing every CV. The custom-section model already owns document-specific content and needs no schema change.
- **Alternatives considered:** delete profile entries from the Builder; clone profile entries into variants; add a second custom-entry table; keep the opaque one-item-per-line textarea. These violate ownership, duplicate data/infrastructure or fail the approved interaction requirement.
- **Consequences:** original entries use Hide rather than Delete; custom entries receive explicit destructive confirmation. Existing render/public/PDF/version data remains compatible.
- **User approval required:** No; this preserves the programme's explicit Career Profile separation and existing data.
- **Reversible:** Yes. Revert `a5b74e0` and `2043349`; settings JSON remains backward-compatible.

## DEC-038 — Job email has one canonical route with filtered compatibility redirects

- **Date:** 2026-08-09
- **Decision:** Make `/correspondence` the canonical Job email hub and represent recruitment review as `?view=review`. Redirect the legacy `/correspondence/review` route into that filtered view while reusing the existing review component and APIs.
- **Reason/evidence:** the two pages were separate navigation surfaces over related linked/review workflows, while the job workspace already reuses the same correspondence domain. Route composition removes duplicate information architecture without prematurely rewriting mature Gmail decision logic.
- **Alternatives considered:** delete the old route immediately; copy review cards into the inbox; merge backend endpoints before a provider capability trace; keep both pages indefinitely. These break bookmarks, duplicate behavior, expand risk or preserve the approved UX defect.
- **Consequences:** existing links remain compatible and there is one user-facing hub. Provider-neutral thread state and explicit draft/send remain separate MAIL-001 increments and are not implied by this routing change.
- **User approval required:** No; the product decision to consolidate and preserve route compatibility is explicit.
- **Reversible:** Yes. Revert `6008b4a`; no persisted correspondence/provider data changes.

## DEC-039 — Connected email capabilities are explicit and provider-neutral

- **Date:** 2026-08-09
- **Decision:** Consume the existing `IEmailProviderRegistry` through one owner-scoped read controller and expose provider status/search/thread/plain-text detail without changing scopes or adding a second provider abstraction. Report send as unavailable until a provider has an implemented, tested send contract.
- **Reason/evidence:** Gmail, Microsoft Graph and IMAP already implement the same read seam, but only Gmail consumed it and the hub could not state actual capabilities. Their current OAuth/service contracts are read-only. The separate follow-up action uses application SMTP and cannot honestly represent connected-provider send.
- **Alternatives considered:** copy Gmail controller behavior for each provider; claim SMTP as provider send; add speculative provider-action interfaces; broaden OAuth scopes before designing re-consent/audit/uncertain delivery; return untrusted HTML through the shared detail API. These duplicate logic, mislead users, widen privileges prematurely or create an unsafe rendering path.
- **Consequences:** the hub can truthfully identify connected Gmail/Outlook/IMAP accounts and a shared API exists for later UI composition. Provider-native state changes and send remain unavailable and must be introduced with capability flags, re-consent and delivery-state tests. Message detail returns plain text only.
- **User approval required:** No; this is a local, fake-tested repository increment within MAIL-001 and invokes no provider or email service.
- **Reversible:** Yes. Revert `536d403`; no schema, dependency, OAuth scope, provider token or persisted message changes.

## DEC-040 — Message detail is plain text with an explicit saved-copy fallback

- **Date:** 2026-08-09
- **Decision:** Fetch live detail through the neutral provider endpoint when a row has a provider/message ID, but render only plain text. If provider access fails, warn and fetch the tenant-scoped persisted correspondence copy. Manual entries use the persisted path directly.
- **Reason/evidence:** imported correspondence remains useful after token expiry or provider outage, while returning/rendering provider HTML would widen the untrusted-content surface. The existing correspondence row is already the local job record and needs no duplicate store.
- **Alternatives considered:** render provider HTML; fail the whole view when reauthorization is required; silently fall back; include full message bodies in every inbox list response; add a message-cache table. These increase XSS/payload/state risk, hide stale provenance or duplicate existing data.
- **Consequences:** users can distinguish live and saved content availability, malformed legacy metadata cannot break detail, and late responses cannot populate another selected row. The saved copy may be stale and is labeled as such when live access fails.
- **User approval required:** No; this is local read-only behavior with synthetic/mocked tests and no provider invocation.
- **Reversible:** Yes. Revert `a20775c`; no schema, dependency, provider scope or persisted data changes.

## DEC-041 — Delivery idempotency is a content-free tenant ledger

- **Date:** 2026-08-09
- **Decision:** Persist one `EmailSendAttempt` per owner/client UUID with a SHA-256 payload hash and strict pending → sending → sent/failed/uncertain transitions. Terminal and uncertain attempts cannot restart; a new explicit review must use a new request UUID. Store no recipient, subject or body.
- **Reason/evidence:** SMTP/provider calls cannot be made atomic with a database commit. A crash or transport interruption after acceptance is inherently uncertain, so retrying the same attempt can duplicate email. The ledger must exist before any provider scope/button is enabled and must preserve only the metadata needed for deduplication/audit.
- **Alternatives considered:** rely on disabled buttons; store request IDs on correspondence; reuse AI operations; retry on every timeout; save full draft content in the audit row; add a message outbox that assumes provider idempotency. These do not prevent concurrent/direct-API duplicates, conflate domains, risk duplicate sends/private-data retention or promise atomicity the providers do not offer.
- **Consequences:** API integration can reserve a unique attempt before external I/O and fail closed on pending/uncertain records. A provider success followed by database failure remains reconcilable rather than blindly retried. User export must include the non-sensitive metadata; job/account deletion cascades it. The migration is additive and EF-owned with explicit SQLite/MariaDB types.
- **User approval required:** No; the approved MAIL-001 programme requires idempotent/uncertain send safety. This increment is inert and used no provider.
- **Reversible:** Yes. Before reverting `653f011`, stop send admission, reconcile/drain attempts and downgrade `20260809195014_AddEmailSendAttempts`. No production migration has been applied.

## DEC-042 — Send permission is explicit; transport uncertainty fails closed

- **Date:** 2026-08-09
- **Decision:** Request Gmail send and Graph Mail.Send scopes on new/reconnected accounts, derive `CanSend` from the stored granted scope, keep IMAP read-only, and expose one neutral delivery contract. Treat an HTTP rejection as known failed-before-delivery, but any network interruption/cancellation as uncertain.
- **Reason/evidence:** read access cannot authorize send, and existing tokens must not be assumed upgraded. Provider HTTP acceptance is outside the database transaction; after a broken transport the application cannot safely prove that no message was accepted. Mocked HTTP tests prove scope, payload and classification without contacting providers.
- **Alternatives considered:** reuse application SMTP; silently expand existing token authority; mark all errors failed/retryable; expose provider error bodies; add SMTP credentials to the IMAP connection; enable a route before adapter tests. These misrepresent identity, risk duplicate delivery/data exposure, widen secret storage or invert the required dependency order.
- **Consequences:** existing connections show read-only until explicit reconnect consent. Gmail supports its provider thread ID; Graph currently sends a new message and does not claim reply-thread semantics. The later API must reserve the ledger before calling either adapter and surface uncertain state for manual reconciliation.
- **User approval required:** No; this repository-side programme requirement used fake transports only. Real account consent/send still requires an explicitly authorized synthetic provider account.
- **Reversible:** Yes. Revert `e9937ac` to stop requesting/using send permission. Already granted provider permission is managed by the provider/user connection and is not automatically revoked by a code rollback.

## DEC-043 — Send admission reserves before delivery and never retries ambiguity

- **Date:** 2026-08-10
- **Decision:** Admit provider delivery only through an authenticated, user-rate-limited route that requires an owned job, explicit confirmation and a canonical client UUID. Reserve and begin the content-free ledger before provider I/O; persist sent correspondence, a content-free job event and the sent state in one local transaction. Return existing sent results but reject every other duplicate, especially uncertain attempts.
- **Reason/evidence:** provider acceptance cannot share the database transaction. Owner-scoped SQLite tests prove malformed, unconfirmed and cross-tenant requests do not reserve or send; duplicate, rejected and interrupted attempts call the fake provider at most once. Canonical UUID formatting closes a simple deduplication bypass.
- **Alternatives considered:** call the provider before reserving; retry timeouts; rely on a disabled button; store message content in the ledger/event; use legacy application SMTP; mark local persistence failure as sent. These can duplicate delivery, lose audit state, expose content or bypass connected-provider consent.
- **Consequences:** a successful provider call with failed local persistence is intentionally uncertain and requires manual reconciliation. A process stop after admission can leave a `sending` row; a later repository increment must age it into an explicit uncertain/manual-review state without redelivery. Existing read-only connections cannot send until re-consented.
- **User approval required:** No; MAIL-001 explicitly authorizes local implementation and fake verification. Real provider consent/send remains gated.
- **Reversible:** Yes. Disable admission/UI, then revert `123fc55`. No schema rollback is needed for this route-only increment.

## DEC-044 — Replies remain bound to their provider thread

- **Date:** 2026-08-10
- **Decision:** Offer an editable reply only for a message whose exact provider connection currently has send consent. Keep provider/from identity and thread read-only while allowing recipient, subject and body edits; require the shared app-owned confirmation before POST. Treat client/network ambiguity as uncertain and expose no retry action.
- **Reason/evidence:** a Gmail thread ID has no valid Graph meaning, and silently switching providers would misrepresent reply semantics. Mocked UI tests prove cancel invokes no API, the visible reviewed fields match the request, the UUID is stable for the attempt, and uncertainty disables resend.
- **Alternatives considered:** allow cross-provider thread switching; use browser `confirm`; hide recipient/thread; auto-retry network failures; enable manual correspondence through application SMTP; add a new draft framework before proving the flow. These weaken provenance, accessibility, consent or duplicate safety.
- **Consequences:** read-only connections require explicit reconnect consent. This increment supports replies, not durable refresh recovery or a new-message composer; those remain tracked rather than being implied. Basic email remains ungated by Pro and no AI path can send.
- **User approval required:** No; this is synthetic local MAIL-001 implementation. Real provider/email verification remains gated.
- **Reversible:** Yes. Revert `449faeb`; API, ledger, scopes and saved correspondence remain unchanged.

## DEC-045 — Interrupted sends age to terminal safety states without retry

- **Date:** 2026-08-10
- **Decision:** Run a post-readiness safety scan every five minutes. After 15 minutes, pending attempts become failed-before-provider and sending attempts become uncertain. Use conditional cross-owner updates in one local transaction and create one content-free owner notification; never invoke or enqueue provider delivery.
- **Reason/evidence:** a process can stop between ledger reservation, provider acceptance and local completion. The application can prove an old pending attempt never reached the provider, but cannot prove the same for sending. Real-SQLite two-owner tests prove classification, fresh-row preservation, notification isolation and repeat-run idempotency.
- **Alternatives considered:** retry on restart; leave rows indefinitely; mark every row failed; mark every row sent; scan provider mailboxes; add provider-specific reconciliation. These risk duplicate email, misleading state, excessive provider authority or unresolved user state.
- **Consequences:** interrupted sending requires manual Sent-folder review. Multiple replicas may select the same candidate, but the status predicate allows only one update/notification. The five-minute query intentionally avoids a new migration in this increment; large-ledger performance remains a measured rollout check.
- **User approval required:** No; this is local safety recovery with no provider/external call.
- **Reversible:** Yes. Revert `ee5ef7e`; existing terminal states and notifications remain truthful and should not be rewritten.

## DEC-046 — Resolve the audit gate with supported patches, then the smallest router major

- **Date:** 2026-08-10
- **Decision:** Update transitive js-yaml/nanoid within their existing major lines and move React Router to 7.18.2 because the later two React Router advisories have no patched 6.x release. Adapt only the obsolete RouterProvider flag and Jest encoding globals; keep the existing route model.
- **Reason/evidence:** 6.30.4 fixed the originally reported protocol-relative redirect but remained affected by two newer advisories, so `npm audit` still failed. React 19 and Node 22 satisfy v7 requirements. Audit, focused data-router tests, all frontend tests and the production build pass on 7.18.2.
- **Alternatives considered:** `npm audit fix --force` without review; suppress moderate findings; stay on 6.30.4; redesign routing. These either hide the resolution, leave the deployment gate red or expand scope unnecessarily.
- **Consequences:** the frontend now requires Node 20 or later through React Router v7. CI/live deployment and navigation smoke remain required before `DONE`.
- **User approval required:** Yes; the user explicitly requested repair of the reported live deployment audit failure and continuation.
- **Reversible:** Technically yes by reverting `b55a592`, but that restores known advisories and the failed gate. Prefer a forward compatibility fix if a deployment-only issue appears.

## DEC-047 — Follow-up drafting stays; direct SMTP delivery is retired

- **Date:** 2026-08-10
- **Decision:** Preserve grounded follow-up draft generation/edit/copy, replace its direct send control with a link to canonical Job email, and make the legacy send endpoint return 410 without an application SMTP dependency. Keep scheduled reminder notification email unchanged.
- **Reason/evidence:** the legacy action bypassed provider identity/re-consent, explicit final confirmation, the idempotency ledger and uncertain-delivery handling already implemented in MAIL-001. Drafting itself is safe and useful. Focused worker tests prove the separate scheduled reminder path remains registered and functional with fakes.
- **Alternatives considered:** route the legacy body directly into `/api/email/send`; retain both send paths; remove drafting/reminders; silently redirect the POST. These either bypass the reviewed confirmation context, preserve conflicting authority, remove unrelated functionality or misrepresent a state-changing API response.
- **Consequences:** users copy the draft or open Job email and review provider/from/recipient/content before sending. Old API callers receive an explicit terminal response and must migrate; no correspondence/follow-up date is falsely recorded as sent.
- **User approval required:** No; this is the smallest safe completion of the already active MAIL-001 programme and sends no email.
- **Reversible:** Yes by reverting `8fe3903`, but that reintroduces the unsafe SMTP bypass. Prefer forward migration of any remaining caller to `/api/email/send` with explicit confirmation.

## DEC-048 — Export delivery state, not content fingerprints

- **Date:** 2026-08-10
- **Decision:** Add one explicit send-attempt export record to both existing owner export surfaces. Include provider/request/status/provider-message/failure/timestamps, but omit the internal payload hash. Verify the existing job foreign-key cascade with real SQLite; leave complete identity/account deletion to SEC-009.
- **Reason/evidence:** users need readable delivery history, while the ledger intentionally stores no recipient, subject or body. The payload hash exists only for deduplication/conflict checks and is not meaningful portable data. Two-owner daily files and decrypted backup tests prove coverage/isolation; the cascade test proves one hard job deletion does not affect the other tenant.
- **Alternatives considered:** export the entity directly; include the payload hash; omit attempts; claim UserManager identity deletion is complete; add a second cleanup routine. These expose internal correlation data, lose audit history, overstate the current account lifecycle or duplicate the database cascade.
- **Consequences:** export schemas gain an additive `EmailSendAttempts` collection. Complete account/database/file/token/backup deletion remains an explicit SEC-009 release item rather than hidden in MAIL-001.
- **User approval required:** No; this is the approved MAIL-001 export/cascade requirement using synthetic local data only.
- **Reversible:** Yes. Revert `aff34cc`; no schema or stored data changes. Existing export files remain valid historical artifacts under their configured retention.

## DEC-049 — Keep Career Workspace smoke behavior-based

- **Date:** 2026-08-10
- **Decision:** Replace the removed marketing-copy assertion in the Career Workspace Playwright smoke with an assertion that the visible `Open CV Builder` action targets `/career/builder`.
- **Reason/evidence:** Gitea run 608 proves the dependency audit now passes and the page heading renders, but CI fails on copy that the current unit test explicitly expects to be absent. The route action is always rendered and tests the supported user journey. Full local Playwright passes 4/4.
- **Alternatives considered:** restore obsolete copy solely for the test; weaken the smoke to heading-only; increase timeout. These would contradict current product behavior, reduce journey coverage or hide no timing defect.
- **Consequences:** wording can evolve without breaking CI while the page-to-builder navigation contract remains protected. Replacement CI and live deployment verification are still required.
- **User approval required:** No; this is a focused correction to a stale test exposed by the user-authorized deployment repair.
- **Reversible:** Yes. Revert the focused smoke change, though run 608 would fail again until the expectation or page behavior is reconciled.

## DEC-050 — Share correspondence context without broadening mailbox authority

- **Date:** 2026-08-10
- **Decision:** Give the shared correspondence component a minimal company/recruiter/role context contract and supply it from both the job dialog and Application Workspace. Do not add provider mutation buttons or scopes in this increment.
- **Reason/evidence:** the workspace reused the correct correspondence domain but passed `null as any`, so its suggestion surface lost job context. Gmail currently grants readonly plus send, Graph grants read plus send, and IMAP is read-only; none has a safe read/write mutation seam. Focused tests and the full frontend/build pass.
- **Alternatives considered:** duplicate the correspondence implementation; expand the workspace aggregate with a full job object; silently request Gmail modify/Graph read-write scopes; display unsupported actions. These add coupling, authority or misleading behavior.
- **Consequences:** both application surfaces now generate the same optional contextual searches without automatic linking. Provider category mutations remain explicit remaining work that requires a separately reviewed scope/re-consent design.
- **User approval required:** No; this is a local shared-view correction inside active MAIL-001 and invokes no provider.
- **Reversible:** Yes. Revert the context-prop changes and test; stored data, scopes and provider grants are unchanged.
