CONTEXTE
Piloter une recherche d'emploi comme un pipeline commercial : sourcing, suivi, relances, statistiques, en temps réel.
MON RÔLE
Produit personnel conçu et développé intégralement, de l'architecture Convex à l'extension navigateur.
RÉALISATIONS
- Modèle atomique des réalisations : un fait vérifié, plusieurs formulations par angle, recomposé par offre
- Backend Convex réactif : 12 tables, kanban temps réel à 7 statuts
- Sourcing par APIs officielles (France Travail, Adzuna) + extension de capture couvrant 7 jobboards
- Worker LLM local : génération CV, lettre 3 tons, scoring ATS et audit d'entreprise
CHIFFRES
42 fonctions · 11 tables · temps réel
OUTILS
- React 19
- TypeScript
- Convex
- TanStack
CS.04.M, LES MODULES, PIÈCE PAR PIÈCE
Sous le capot
01 · Le modèle atomique
Pas de CV figés par métier. Chaque expérience se décompose en atomes (un fait vérifié) portant plusieurs formulations selon l'angle (management, dev, commercial…) et une pondération par casquette. Une offre entrante sélectionne les bons atomes, le bon angle, et recompose un CV taillé. Les chiffres vivent une seule fois, comme source de vérité : jamais contredits d'un CV à l'autre, jamais inventés.
// ATOMS. Each achievement = one fact, told through several angle-specific lenses.
achievements: defineTable({
experienceId: v.id("experiences"),
label: v.string(), // internal name, e.g. "Exosphère, direction d'équipe"
// Multiple phrasings; the offer picks the matching angle.
textVariants: v.array(
v.object({
angle: v.string(), // "management" | "dev" | "commercial" | "marketing"…
text: v.string(),
}),
),
tags: v.array(v.string()),
// Verified numbers: SINGLE SOURCE OF TRUTH. Never contradicted across CVs.
metrics: v.optional(v.array(v.object({ key: v.string(), value: v.string() }))),
keywords: v.array(v.string()), // ATS keywords this atom can satisfy
// Per-profile priority weighting (key = profiles.key, value = weight 0-10).
priorityByProfile: v.optional(v.array(v.object({ profileKey: v.string(), weight: v.number() }))),
order: v.number(),
}).index("by_experience", ["experienceId"]),02 · Le worker LLM et ses garde-fous
Un worker local polle la file de jobs Convex et pilote un LLM en ligne de commande, flux JSON parsé en direct pour afficher la progression dans l'interface. Verrou mono-instance, plafond de processus concurrents, timeout dur : les leçons d'un fork-bomb accidentel, gravées en code. Et comme un modèle hallucine parfois un identifiant, chaque composition est assainie avant d'entrer en base.
// The LLM occasionally hallucinates or mistypes an achievementId in cvComposition;
// Convex's v.id("achievements") then rejects the whole saveCv mutation. Keep only
// entries whose id is a real achievement so one bad id can't kill the CV step.
function sanitizeCvComposition(comp, ctx) {
if (!Array.isArray(comp)) return [];
const validIds = new Set((ctx.achievements ?? []).map((a) => a._id));
const clean = comp.filter((c) => c && validIds.has(c.achievementId));
const dropped = comp.length - clean.length;
if (dropped > 0) log(` ⚠ ${dropped} achievementId(s) hallucinés écartés de cvComposition`);
return clean;
}03 · Huit types de jobs de génération
La même file traite huit natures de travaux : structuration d'annonce, audit d'entreprise sur le web, scoring d'adéquation ATS avec mots-clés manquants, composition du CV, lettre en trois tons avec recommandation, préparation d'entretien, négociation salariale et comparaison d'offres. Chaque étape écrit son état dans un pipeline visible en temps réel sur la fiche candidature.
LA FACE VISIBLE, CÔTÉ STUDIO