fix(ai): meter synchronous generations
CI and Deploy / test (pull_request) Successful in 5m18s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 20:25:10 +02:00
parent dbf28b97ce
commit 25a6da951e
9 changed files with 360 additions and 21 deletions
@@ -1,6 +1,6 @@
# POL-001 Free/Pro entitlement verification
Date: 2026-08-02
Date: 2026-08-15
Status: `IMPLEMENTED — NOT VERIFIED`. The server policy, worker rechecks, Free locked states and automated regressions pass. Real-browser, Stripe lifecycle, MariaDB and production checks remain unavailable.
@@ -11,26 +11,26 @@ Status: `IMPLEMENTED — NOT VERIFIED`. The server policy, worker rechecks, Free
- Pro and Admin use AI and Pro CV themes. The persisted Identity role remains `Premium`, and `Stripe:PricePremium` remains a compatibility key; neither is exposed as a public plan name.
- Current database roles are authoritative on every explicit HTTP AI action. A stale role claim cannot preserve access after downgrade.
- A locked explicit action returns HTTP 403 with `{ "code": "pro_required", "message": "This AI feature requires Pro." }`.
- Existing 250-call/1,000,000-token Pro ceilings remain because they are defined in the existing implementation roadmap. Free ceilings are zero. A content-free `AiUsageRecord` ledger is now authoritative for AI Workspace plus durable Strategy and CV work; legacy `AiInteraction` usage is backfilled. Older synchronous AI actions still need the same admission boundary before the numeric ceilings can be described as universal.
- Existing 250-call/1,000,000-token Pro ceilings remain because they are defined in the existing implementation roadmap. Free ceilings are zero. A content-free `AiUsageRecord` ledger is authoritative for AI Workspace, durable Strategy/CV work and every user-scoped generation through the shared synchronous provider boundary; legacy `AiInteraction` usage is backfilled. Health probes and non-generative text extraction are intentionally excluded.
## Entry-point inventory
| Capability | User entry / frontend | API or worker execution path | Admission and recheck | Usage accounting | Free behavior |
|---|---|---|---|---|---|
| AI Workspace modules | Job details → AI Workspace; `AiWorkspacePanel` | `POST /api/jobapplications/{jobId}/ai/generate``AiWorkspaceService``ISummarizerService` | `Pro` policy with live role lookup | Ledger reservation before generation; actual estimate finalized on success | Generate disabled; existing history/read/delete remain available |
| Candidate fit | Job details Candidate Fit and Strategy Snapshot | `GET .../{id}/candidate-fit` → attachment/correspondence context → multiple summarizer calls | `Pro` policy | No complete shared usage row | Deterministic `match-score` remains available; AI narrative locked |
| Candidate fit | Job details Candidate Fit and Strategy Snapshot | `GET .../{id}/candidate-fit` → attachment/correspondence context → multiple summarizer calls | `Pro` policy plus shared provider admission | One ledger row per provider generation | Deterministic `match-score` remains available; AI narrative locked |
| Focus plan | Job details Focus Plan and Strategy Snapshot | Durable `strategy.snapshot` operation → summarizer | `Pro` admission plus worker recheck | Atomic operation-ledger reservation; successful input/output estimate finalized | Locked; no synthetic fallback presented as generated |
| Interview brief | Job details Interview Prep | `GET .../{id}/interview-prep/brief` → summarizer | `Pro` policy | No complete shared usage row | Editable non-AI interview board remains available; generated brief locked |
| Tailored CV generation | Add Job option and job Tailored CV tab | `POST .../{id}/generate-tailored-cv-draft` → shared generation helpers → summarizer | `Pro` policy | No complete shared usage row | Job creation and manual tailored-draft editing remain available; no operation is started |
| Application package | Job workspace drafts | `POST .../{id}/generate-application-package` → attachment/email context → summarizer | `Pro` policy | No complete shared usage row | Existing/manual package drafts remain readable and editable |
| Follow-up draft | Job Follow-up tab | `GET .../{id}/followup-draft` → context → summarizer | `Pro` policy | No complete shared usage row | Manual correspondence data remains available; AI draft is locked |
| Job summary refresh | Job overview | `POST .../{id}/refresh-ai``SummarizeAsync` | `Pro` policy | No complete shared usage row | Existing summary/tags remain visible; refresh locked |
| Automatic job summary | Job create/detail | Core `POST /jobapplications` and `GET /{id}` optional summarizer calls | Live role condition inside core action | No complete shared usage row | Core request succeeds without calling AI |
| Interview brief | Job details Interview Prep | `GET .../{id}/interview-prep/brief` → summarizer | `Pro` policy plus shared provider admission | One ledger row per provider generation | Editable non-AI interview board remains available; generated brief locked |
| Tailored CV generation | Add Job option and job Tailored CV tab | `POST .../{id}/generate-tailored-cv-draft` → shared generation helpers → summarizer | `Pro` policy plus shared provider admission | One ledger row per provider generation | Job creation and manual tailored-draft editing remain available; no operation is started |
| Application package | Job workspace drafts | `POST .../{id}/generate-application-package` → attachment/email context → summarizer | `Pro` policy plus shared provider admission | One ledger row per provider generation | Existing/manual package drafts remain readable and editable |
| Follow-up draft | Job Follow-up tab | `GET .../{id}/followup-draft` → context → summarizer | `Pro` policy plus shared provider admission | One ledger row per provider generation | Manual correspondence data remains available; AI draft is locked |
| Job summary refresh | Job overview | `POST .../{id}/refresh-ai``SummarizeAsync` | `Pro` policy plus shared provider admission | One ledger row per provider generation | Existing summary/tags remain visible; refresh locked |
| Automatic job summary | Job create/detail | Core `POST /jobapplications` and `GET /{id}` optional summarizer calls | Live role condition plus shared provider admission | One ledger row when a provider generation runs | Core request succeeds without calling AI |
| CV import/parse | Career Profile upload/parse/reprocess | `/profile-cv/upload`, `/parse`, `/reprocess` → durable `cv.process` operation | `Pro` policy before admission; queued run rechecks live roles | Atomic conservative operation-ledger reservation; no raw CV content | Manual profile editing and previous review runs remain available |
| CV rebuild/improve/rewrite/PDF | Career Profile AI buttons | `/rebuild`, `/improve`, `/rewrite-section`, `/rewrite-preview`, `/export-pdf` | `Pro` policy; queued rebuild/improve recheck live roles | CV-run state only | AI controls locked; manual profile data remains available |
| CV Builder writing aid | CV Builder AI Tools | `POST /api/cv/ai/assist` → summarizer | `Pro` policy | No complete shared usage row | AI buttons disabled; CV editing/history remain available |
| CV rebuild/improve/rewrite/PDF | Career Profile AI buttons | `/rebuild`, `/improve`, `/rewrite-section`, `/rewrite-preview`, `/export-pdf` | `Pro` policy; queued work rechecks roles and synchronous generation uses shared admission | Durable operation reservation or synchronous provider ledger row | AI controls locked; manual profile data remains available |
| CV Builder writing aid | CV Builder AI Tools | `POST /api/cv/ai/assist` → summarizer | `Pro` policy plus shared provider admission | One ledger row per provider generation | AI buttons disabled; CV editing/history remain available |
| Pro CV themes | CV Builder Customize | `GET /api/cv/themes`; create/save validates selected theme | Live role lookup in theme catalog checks | Not applicable | Pro themes identified and unavailable; existing unchanged selection can still be saved |
| Job enrichment worker | No direct UI; disabled by default | `JobEnrichmentHostedService` per owner | Live role recheck immediately before summary; deterministic tag detection still runs for Free | No complete shared usage row | No model call; core tag enrichment remains possible |
| Job enrichment worker | No direct UI; disabled by default | `JobEnrichmentHostedService` per owner | Live role recheck plus shared provider admission immediately before summary; deterministic tag detection still runs for Free | One ledger row per provider generation | No model call; core tag enrichment remains possible |
| Admin AI probe | Admin system diagnostics | `/api/admin/system/ai/probe` | Admin role; Admin maps to Pro | Health metric only | Not a Free user path |
| Periodic service probe | No user entry | summarizer health probe | No private/user payload; operational health only | Health metric only | Not a user AI capability |
| Attachment storage | Add-job/files UI | `AttachmentsController` storage check | Central Free/Pro storage entitlement | Bytes stored | 250 MB Free; 5 GB Pro (existing defined capability) |
@@ -43,7 +43,8 @@ Status: `IMPLEMENTED — NOT VERIFIED`. The server policy, worker rechecks, Free
- `AccountPlansTests`: Free zero AI, Pro/Admin AI, and only `free`/`pro` external names.
- AI Workspace UI test: Free locked state, disabled generation and upgrade link.
- `AiUsageMeterTests`, operation integration, account export/deletion and SQLite compatibility tests cover idempotent reservation, limits, owner isolation, history-independent totals, Strategy finalization, CV conservative reservation and lifecycle handling.
- Full backend after the ledger migration: 663/663.
- `MeteredSummarizerServiceTests` prove synchronous success finalization, pre-provider quota rejection, Free-user rejection, workspace/operation double-count suppression and stable HTTP 429 problem details.
- Full backend after universal provider admission: 674/674.
- Full backend: 568/568.
- Full frontend: 47/47 suites, 157/157 tests.
- Production frontend build: pass.
@@ -55,7 +56,7 @@ Status: `IMPLEMENTED — NOT VERIFIED`. The server policy, worker rechecks, Free
- Stripe webhook transitions were code-inspected and existing status tests cover active/trialing vs expired states, but no real or mocked end-to-end checkout/webhook cycle ran in this package.
- MariaDB and production were not changed or tested.
- PRODUCT-001 removed landing-page prices, the third “Bring your own key” tier, Free AI allowance and “Unlimited AI” claims. Public capability copy now comes from one two-plan catalogue; commercial terms remain in configured Stripe Checkout.
- The durable ledger now spans AI Workspace, Strategy Snapshot and CV processing, and deleting user-visible AI history no longer erases usage. Candidate Fit, Interview Prep, application-package/follow-up drafting, CV Builder assistance and automatic summary paths remain synchronous and are not yet universally admitted through this ledger; the UI must therefore avoid claiming that the displayed numeric ceiling covers every AI path.
- The durable ledger spans AI Workspace, Strategy Snapshot, CV processing and all user-scoped calls through `ISummarizerService`. Failed or empty provider attempts retain their conservative reservation because they may still have consumed provider capacity; successful generations replace it with measured input/output. Health probes and extraction-only calls are not user generation usage.
## Rollback