Skip to content

feat(candidature): flux complet offre → score de match → CV généré - #21

Merged
Tbeaumont79 merged 2 commits into
mainfrom
feat/candidature
Jun 10, 2026
Merged

Tbeaumont79 merged 2 commits into
mainfrom
feat/candidature

Conversation

@Tbeaumont79

@Tbeaumont79 Tbeaumont79 commented Jun 10, 2026 •

Copy link
Copy Markdown
Owner

Le cœur du produit

La page « Nouvelle candidature » (/candidature) : l'utilisateur colle une offre, obtient un score de match honnête et justifié, puis génère un CV adapté — contenu issu de son profil réel uniquement (garde-fou provenance), dans la limite des 2 générations offertes.

⚠️ Cette branche inclut le fix #19 (écritures profil + middleware auth) — merger #19 d'abord, ou cette PR seule suffit.

Parcours (conforme au brief produit)

  1. Colle l'offre → analyse (Sonnet) + score de match.
  2. Score 0–100 justifié : raisons en français, chips « Atouts couverts » / « Manque à l'appel » — les mots-clés sont calculés par le code (déterministe, reproductible), le LLM ne donne que le score et les raisons, bornés côté serveur. Garde-fou anti-score-flatteur (score > 70 avec couverture nulle → plafonné).
  3. Si score < 40 % : alerte « tu risques un refus » avec « Générer quand même » — on protège l'utilisateur ET la marge avant de consommer un crédit.
  4. Génération : matchProfileToOffer existant (provenance vérifiée hors LLM), gate quota avant tout appel LLM (403 quota_exceeded + panneau « Recharger en crédits » → /#tarifs).
  5. Aperçu CvTemplate + export PDF + mention garde-fou anti-invention.

Infrastructure

  • Migration metering : les tables usage_events/usage_counters existaient dans le schéma mais aucune migration ne les créait — les quotas tournaient dans le vide. Corrigé ; chaque analyse/génération enregistre ses tokens.
  • anthropic.ts : callback onUsage (metering) + ANTHROPIC_BASE_URL (proxy/mock). ANTHROPIC_API_KEY documentée dans .env.example — à renseigner pour utiliser le flux en réel.
  • scripts/mock-llm.mjs : mock local de l'API Claude pour tester le flux complet sans consommer de tokens (node scripts/mock-llm.mjs + ANTHROPIC_API_KEY=mock ANTHROPIC_BASE_URL=http://localhost:8787 pnpm dev).

Vérification (E2E réel sur build de prod + Postgres + mock LLM)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added "Nouvelle candidature" application workflow with multi-step process
    • Implemented match scoring between candidate profiles and job offers
    • Added AI-powered CV generation tailored to specific job offers
    • Implemented PDF export for generated CVs
    • Added usage quota system to track generation and export activities
  • Refactor

    • Updated authentication verification to use server-side session checks

Tbeaumont79 and others added 2 commits June 10, 2026 19:25
Deux bugs qui cassaient tout le parcours profil :

1. getAuthSession utilisait toWebRequest(event), qui touche au flux du
   corps de la requête : le readBody(event) des handlers PUT/POST
   attendait ensuite un corps déjà verrouillé → toutes les écritures
   /api/profile* pendaient indéfiniment en build de prod (les GET, sans
   corps, passaient). On passe event.headers directement à
   auth.api.getSession, comme le fait déjà le middleware serveur.

2. Le middleware de navigation `auth` testait la truthiness de l'atom
   nanostores de useAuth() — toujours vrai → aucune redirection, et les
   visiteurs non connectés voyaient « Impossible de charger ton profil »
   (401) sur /profil. On vérifie désormais la session via
   GET /api/auth/get-session (cookies transférés en SSR).

Vérifié sur build de prod + Postgres : PUT/POST/DELETE profil en ~10 ms,
/profil anonyme → 302 /connexion, /profil connecté → 200.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…HI-124/125)

La page « Nouvelle candidature » (/candidature) concrétise le cœur du
produit : coller une offre, obtenir un score de match honnête et justifié,
puis générer un CV adapté — contenu issu du profil réel uniquement
(garde-fou provenance), dans la limite des 2 générations offertes.

Serveur :
- packages/shared/src/match.ts : contrats du flux (MatchReport, seuils
  40/70, matchVerdict, clampScore, computeKeywordCoverage déterministe,
  chemins API, QUOTA_EXCEEDED_CODE).
- services/match-report.ts : score LLM (Sonnet, effort low) borné côté
  code, raisons nettoyées, garde-fou anti-score-flatteur (score > 70 avec
  couverture nulle → plafonné à 60), mots-clés matched/missing calculés
  par le code (reproductibles).
- POST /api/candidature/analyze : zod 50–20000 chars, 409 profile_empty,
  metering 'extraction' avec tokens.
- POST /api/candidature/generate : gate quota AVANT tout appel LLM
  (403 quota_exceeded), génération via matchProfileToOffer (provenance),
  metering 'generation' avec tokens. ProvenanceError → 422, LlmError → 502.
- anthropic.ts : onUsage (tokens pour le metering) + ANTHROPIC_BASE_URL
  (proxy / mock de test). .env.example documente ANTHROPIC_API_KEY.
- Migration metering_usage_tables : usage_events/usage_counters existaient
  dans le schéma mais aucune migration ne les créait — les quotas
  tournaient dans le vide.

UI :
- pages/candidature.vue : stepper input → score (verdict coloré, raisons,
  chips atouts/manques, alerte < 40 % « générer quand même ? ») →
  génération (loader informatif) → aperçu CvTemplate + export PDF.
  Badge quota en tête (décompte live), panneaux profil vide / quota épuisé
  (lien /#tarifs), a11y (aria-live, focus géré).
- Nav : « Nouvelle candidature » en premier lien.

Vérifié de bout en bout (Postgres jetable + mock LLM scripts/mock-llm.mjs
via ANTHROPIC_BASE_URL) : 401/400/409 corrects, analyse → score 78 avec
mots-clés déterministes, 2 générations OK (provenance validée), 3e bloquée
403, compteurs metering exacts (tokens compris), parcours UI complet
testé au navigateur. 75 tests verts (25 nouveaux), lint, typecheck, build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR implements a complete "Nouvelle candidature" (New Application) feature: users analyze job offers, receive deterministic match scoring against their profile, and generate tailored CVs. The feature includes shared TypeScript contracts, two-step API endpoints backed by Claude, database usage tracking, a multi-step Nuxt UI with quota enforcement, and testing infrastructure. It also fixes authentication middleware and session handling bugs from issue #19.

Changes

Nouvelle candidature feature

Layer / File(s) Summary
Match-scoring contracts and API surface
packages/shared/src/match.ts, packages/shared/src/index.ts
Exports MatchReport with score, reasons, matched/missing keywords; threshold constants; MatchVerdict type; utility functions for verdict mapping, score clamping, and deterministic keyword coverage computation; JSON Schema for LLM output; and request/response interfaces for /api/candidature/analyze and /api/candidature/generate endpoints.
Authentication and session infrastructure fixes
apps/app/middleware/auth.ts, apps/app/server/utils/session.ts
Replaces useAuth() hook-based checks with server GET /api/auth/get-session endpoint calls; in SSR passes cookies via useRequestHeaders(['cookie']); updates getAuthSession to pass event.headers directly instead of converting via toWebRequest, avoiding body-locking conflicts in PUT/POST handlers (addresses #19).
LLM utilities and usage metering
apps/app/server/utils/anthropic.ts, apps/app/.env.example
Adds optional onUsage callback to LlmRequest for token reporting without exposing content; introduces configurable ANTHROPIC_BASE_URL (enabling proxy/mock use); adds ANTHROPIC_API_KEY environment variable documentation.
Usage tracking schema
apps/app/prisma/migrations/20260610203816_metering_usage_tables/migration.sql
Creates usage_event_type enum (GENERATION, EXPORT_PDF, EXTRACTION); usage_events table (per-event token counts, billable flag, period, userId, type); usage_counters table (aggregated per-user/per-period counts); indexes on (userId, period) and type for efficient querying.
Match report service with scoring guardrails
apps/app/server/services/match-report.ts
Calls Claude with system prompt and profile+offer JSON; clamps returned score to [0,100]; sanitizes reasons (trim, filter empty/non-string, limit count); computes deterministic keyword coverage; enforces guardrail capping suspiciously high scores when zero keywords match.
POST /api/candidature/analyze endpoint
apps/app/server/api/candidature/analyze.post.ts
Validates offer text bounds; loads authenticated user's profile; returns HTTP 409 if profile is missing/empty; wraps anthropicComplete with onUsage callback to aggregate token counts across two LLM calls (offer analysis + match report); records non-billable extraction usage event; returns analyzed offer with match verdict.
POST /api/candidature/generate endpoint
apps/app/server/api/candidature/generate.post.ts
Validates request schema; enforces generation quota before any LLM invocation, returning HTTP 403 if exhausted; loads user profile; wraps anthropicComplete for token metering; calls matchProfileToOffer to generate tailored CV; records billable usage event; maps ProvenanceError to 422, LlmError to 502; returns generated RenderableCv.
Multi-step frontend page
apps/app/pages/candidature.vue
Implements five-state machine (input → analyzing → scored → generating → done) with profile/quota guards; fetches profile and monthly quota usage on load; offer analysis posts to /api/candidature/analyze, handles 409/400/other errors; match verdict displayed with keyword coverage and economic warning for weak matches; generation posts to /api/candidature/generate, gated by quota; PDF export via /api/cv/export-pdf; reset clears state and refreshes usage; focus management for screen-reader announcements on state transitions.
Navigation and layout
apps/app/layouts/default.vue
Adds "Nouvelle candidature" navigation link pointing to /candidature route.
Shared contract tests and match report tests
apps/app/test/match-contracts.spec.ts, apps/app/test/match-report.spec.ts
Vitest suites validate verdict thresholds (boundary at 40/70), score clamping, NaN handling, deterministic keyword coverage (case/diacritic insensitive, deduplication, priority), reason sanitization (trim/filter/limit to 4), guardrail score capping when no keywords match, and error propagation.
Mock LLM server
scripts/mock-llm.mjs
Node.js server on port 8787 mocking Anthropic API; inspects request schema to determine step (offer analysis, match scoring, CV generation); returns hardcoded payloads for first two steps; constructs realistic CV response by parsing profile data from incoming message and mapping experiences/skills with provenance IDs.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser as Client/Browser
    participant Frontend as Frontend Page
    participant Analyze as POST /api/candidature/analyze
    participant Claude1 as Claude (Offer Analysis)
    participant Claude2 as Claude (Match Report)
    participant Generate as POST /api/candidature/generate
    participant Claude3 as Claude (CV Generation)
    participant Export as POST /api/cv/export-pdf

    User->>Browser: Navigate to /candidature
    Browser->>Frontend: Load page (auth middleware)
    Frontend->>Frontend: Fetch profile & quota
    Frontend->>Frontend: Render input form
    
    User->>Frontend: Paste job offer, click analyze
    Frontend->>Analyze: POST offerText
    Analyze->>Analyze: Validate text, load profile
    Analyze->>Claude1: Call analyzeOffer(offerText)
    Claude1-->>Analyze: AnalyzedOffer
    Analyze->>Claude2: Call buildMatchReport(profile, offer)
    Claude2-->>Analyze: MatchReport {score, reasons, keywords}
    Analyze->>Analyze: Record usage event (extraction, non-billable)
    Analyze-->>Frontend: {offer, match}
    Frontend->>Frontend: Render match verdict + keyword coverage
    
    User->>Frontend: Click generate CV
    Frontend->>Frontend: Check quota, enforce guard
    Frontend->>Generate: POST {offer}
    Generate->>Generate: Check quota, return 403 if exhausted
    Generate->>Generate: Load profile
    Generate->>Claude3: Call matchProfileToOffer(profile, offer)
    Claude3-->>Generate: RenderableCv
    Generate->>Generate: Record usage event (generation, billable)
    Generate-->>Frontend: {cv}
    Frontend->>Frontend: Render CV preview
    
    User->>Frontend: Click export PDF
    Frontend->>Export: POST /api/cv/export-pdf
    Export-->>Frontend: ArrayBuffer (PDF)
    Frontend->>Browser: Trigger download
    Browser->>User: Save PDF file
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Tbeaumont79/cv-optimizer#19: Both PRs modify apps/app/middleware/auth.ts and apps/app/server/utils/session.ts to replace hook-based session checks with server-side /api/auth/get-session endpoint verification, fixing the authentication issues reported in #19.

Poem

🐰 Hop hop, through the offer text you fly,
Match scores bloom beneath Claude's watchful eye,
With keywords found and tailored CVs to send,
The candidature flow brings journey's end!
✨ Quota guards and PDF's farewell,
A rabbit's gift: applications that sell! 🎯

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely summarizes the main feature: a complete offer-to-match-score-to-CV-generation flow, directly reflecting the core changeset.
Description check ✅ Passed The description comprehensively covers objectives, changes, infrastructure updates, and end-to-end verification, with links to issue #19 and detailed product behavior.
Linked Issues check ✅ Passed The changeset fully implements issue #19 objectives: session verification via endpoint in middleware, event.headers forwarding to prevent request locking, and restoration of profile write operations. The candidature feature builds on these fixes.
Out of Scope Changes check ✅ Passed All changes align with stated objectives: auth/middleware fixes (#19), complete candidature flow, metering infrastructure, mock LLM, and comprehensive tests. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/candidature

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Tbeaumont79
Tbeaumont79 merged commit 9489c20 into main Jun 10, 2026
0 of 3 checks passed
@netlify

netlify Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for monumental-speculoos-a69398 ready!

Name Link
🔨 Latest commit 4e71097
🔍 Latest deploy log https://app.netlify.com/projects/monumental-speculoos-a69398/deploys/6a29ced0e9700f0008f14f6b
😎 Deploy Preview https://deploy-preview-21--monumental-speculoos-a69398.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@Tbeaumont79
Tbeaumont79 deleted the feat/candidature branch July 2, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant