Feature #21
closedEnhance Matching Results and AI Assistant with Company Enrichment and Market Insights
100%
## Epic Spec — Enhance Matching Results & AI Assistant with Company Enrichment + Market Insights
### Problem
Matching results and the AI assistant do not surface enriched company data or market insights, so users lack context (company size/industry/remote policy, in-demand skills, hiring locations, work modes) to inform decisions.
### Approach
Build the data foundation first (surface enriched company data into the matching payload; add an algorithmic market-insights aggregate endpoint in MatchingAgent), expose it via a GetYourJob API proxy route, then consume it in two UI surfaces (inline expandable company card in matching results; Market Insights section in analysis tools) and in the AI assistant (company-data + market-insights tools, plus skills>locations reasoning and proactive company-type mentions). Split by surface/responsibility.
### Subtasks (split)
- #58 [Enrichment + GetYourJob API] Surface enriched company data in the matching result payload (S/M) — foundation
- #61 [MatchingAgent] Market-insights aggregate endpoint (skills/location/work-mode) (M)
- #63 [GetYourJob API] Market-insights proxy route to MatchingAgent (S)
- #64 [GetYourJob UI] Inline company-enrichment card in matching results (M)
- #66 [GetYourJob UI] Market Insights section in analysis tools (M)
- #67 [GetYourJob assistant] Company-data + market-insights assistant enhancements (M)
### Dependencies
#64 ← #58; #63 ← #61; #66 ← #63; #67 ← #58 + #63
### Acceptance criteria (epic)
- Company data displayed inline as expandable card with link to company detail (#64, data from #58)
- Assistant proactively uses company data / mentions company type (#67)
- Market Insights section: top skills + counts, location trends, work-mode trends (#66, data from #61/#63)
- Assistant prioritizes skills over locations and explains reasoning (#67)
- All features validated on a 100+ matching-result batch for perf/scalability (#61 aggregate, #64 render)
Description
### Problem
Currently, the matching results and AI assistant lack integration with enriched company data and market insights, limiting their ability to provide context-aware recommendations. Users cannot easily access company details (e.g., size, industry, remote policy) or market trends (e.g., in-demand skills, locations) to inform their decisions.
### Context
- **Company Enrichment Data**: We already enrich company data (e.g., size, industry, location, remote policy) via background workers, but this data is not surfaced in matching results or used by the AI assistant.
- **Market Insights**: The analysis tools show pros/cons for candidates but do not include market trends (e.g., top skills in demand, hiring locations, work modes).
- **AI Assistant**: The assistant can answer questions but does not proactively use company data or market insights to guide users (e.g., suggesting skills to learn or locations to target).
### Proposed Behaviour
1. **Company Data in Matching Results**:
- Display company data inline in matching results as an expandable card (e.g., "Acme (ESN, 50–99 employees, Paris)").
- Link the company name to the company detail tool for further exploration.
- Allow the AI assistant to proactively use this data (e.g., "This is an ESN, so missions may be shorter-term").
2. **Market Insights in Analysis Tools**:
- Add a dedicated "Market Insights" section to analysis tools, showing:
- Missing skills (e.g., "Top skills in demand: Java (8,974 offers), Kubernetes (7,086)").
- Location trends (e.g., "This role is most in-demand in Paris").
- Work mode trends (e.g., "80% of Frontend roles are hybrid").
3. **AI Assistant Enhancements**:
- Prioritize skills over locations in recommendations (e.g., "Learn Kubernetes (7,086 offers) before targeting Paris").
- Explain reasoning for suggestions (e.g., "I’m suggesting Paris because 60% of Frontend roles are there").
- Proactively mention company types when relevant (e.g., "This is an ESN, so missions may be shorter-term").
## Acceptance criteria
- [ ] Company data (e.g., size, industry, location) is displayed inline in matching results as an expandable card.
- [ ] The company name in matching results links to the company detail tool.
- [ ] The AI assistant proactively uses company data to adjust recommendations (e.g., "This is an ESN, so missions may be shorter-term").
- [ ] A dedicated "Market Insights" section is added to analysis tools, showing:
- Top missing skills with demand counts (e.g., "Java (8,974 offers)").
- Location trends (e.g., "Paris has the most Frontend roles").
- Work mode trends (e.g., "80% of roles are hybrid").
- [ ] The AI assistant prioritizes skills over locations in recommendations and explains its reasoning.
- [ ] The AI assistant proactively mentions company types when relevant (e.g., "This candidate prefers product companies, but this is an ESN").
- [ ] All new features are tested with a batch of 100+ matching results to ensure performance and scalability.
Files
RA Updated by Redmine Admin about 2 months ago
- Status changed from Submitted to Spec
RA Updated by Redmine Admin about 2 months ago
## Spec — Epic decomposition
**Decision:** SPLIT (large cross-surface epic spanning Enrichment Agent, MatchingAgent FastAPI, GetYourJob API routes, and GetYourJob Next.js UI/assistant).
### Approach
Build the data foundation first (surface enriched company data into the matching payload; add a market-insights aggregate endpoint in MatchingAgent), expose it through a GetYourJob API route, then consume it in two UI surfaces (inline company card in matching results, Market Insights section in analysis tools) and in the AI assistant (tools + reasoning behavior). Split by responsibility/surface, not arbitrarily.
### Acceptance-criteria → subtask mapping
- AC company card inline + link to company detail → #4
- AC assistant proactively uses company data / mentions company type → #6
- AC Market Insights section (skills/locations/work modes) → #5 (data from #2 via #3)
- AC assistant prioritizes skills over locations + explains reasoning → #6
- AC 100+ batch perf/scalability → #2 (aggregate query) + #4 (render)
### Subtasks
1. **[Enrichment + GetYourJob API] Surface enriched company data in the matching result payload** — S/M. Join enriched company fields (size, industry, location, remote policy, company type/ESN) into the matching response. Verify Enrichment Agent data availability.
2. **[MatchingAgent] Market-insights aggregate endpoint** — M. New FastAPI endpoint computing top in-demand skills (with offer counts), location trends, and work-mode distribution, scoped by role/filters. Perf-tested for 100+ result batches.
3. **[GetYourJob API] Market-insights proxy route** — S. Expose MA aggregates to the frontend (auth, caching, in-memory/Redis fallback).
4. **[GetYourJob UI] Inline company-enrichment card in matching results** — M. Expandable card (e.g. "Acme (ESN, 50–99 employees, Paris)") + company name links to company detail tool. Validated on 100+ results.
5. **[GetYourJob UI] Market Insights section in analysis tools** — M. Dedicated section rendering top missing skills + demand counts, location trends, work-mode trends, consuming #3.
6. **[GetYourJob assistant] Company-data + market-insights assistant enhancements** — M. Wire company data + market insights into assistant tool surfaces (MCP + in-app AssistantToolExecutor); prompt/behavior to prioritize skills over locations, explain reasoning, and mention company types when relevant.
### Dependencies
- #4 depends on #1
- #3 depends on #2
- #5 depends on #3
- #6 depends on #1 + #3
RA Updated by Redmine Admin about 2 months ago
- Subtask #58 added
RA Updated by Redmine Admin about 2 months ago
- Subtask #61 added
RA Updated by Redmine Admin about 2 months ago
- Subtask #63 added
RA Updated by Redmine Admin about 2 months ago
- Subtask #64 added
RA Updated by Redmine Admin about 2 months ago
- Subtask #66 added
RA Updated by Redmine Admin about 2 months ago
- Subtask #67 added
RA Updated by Redmine Admin about 2 months ago
- spec_ref updated (diff)
RA Updated by Redmine Admin about 2 months ago
RA Updated by Redmine Admin about 2 months ago
RA Updated by Redmine Admin about 2 months ago
RA Updated by Redmine Admin about 2 months ago
## Design brief — Enrichissement entreprise & tendances marché (UI/UX #64, #66, #67)
Cadrage design des **3 surfaces UI** de l'épic avant développement frontend. Enfants backend (#58 données, #61 agrégat MatchingAgent, #63 proxy) hors périmètre design. **Aucune couleur ni token nouveau** (charte GYJ v2.3). Section ajoutée au `DESIGN_SYSTEM.md` (section **CE**, additive). Mockups joints : `mockup-64-company-enrichment-card.svg`, `mockup-66-market-insights-section.svg`, `mockup-67-assistant-ux.svg`.
> **Règle données impérative (projet) : aucune donnée fictive / placeholder.** Enrichissement ou agrégat absent → on rend **rien** ou « donnée indisponible » ; jamais de chiffre inventé.
### Asymétrie à noter (dev)
La carte entreprise (#64) porte sur l'offre → son entreprise → côté **développeur** (`MatchedOfferCard` / `OfferMatchDetail`, où `offer.company` existe). Côté **recruteur** (`MatchCandidateCard` / `CandidateMatchDetail`) les résultats sont des **candidats**, pas des entreprises → pas de carte entreprise sur ces cartes. L'AC #64 « les deux UIs » se lit : là où une entreprise existe.
### #64 — Carte entreprise enrichie (inline, repliable)
- **Atomic** : `atoms/CompanyTypeBadge` (existant) + nouvelle **molecule `CompanyEnrichmentCard`** (pattern `atoms/Card`).
- **Replié** : nom (lien `/companies/<slug>` via `companySlug`, couleur `secondary`) + badge type + résumé 1 ligne « 50–99 salariés · Paris · Hybride » (champs présents seulement) + chevron.
- **Déplié** : grille 2 col. Taille / Secteur / Localisation / Politique remote (uniquement champs présents) + CTA « Voir la fiche entreprise → » (`primary-light`).
- **Donnée partielle** : seuls les champs connus s'affichent, pas de « N/A ». **Aucune donnée** : `return null` (pas de carte vide). Type inconnu/anonyme : `CompanyTypeBadge` rend déjà `null`.
- Montée : sous la ligne titre/société de `MatchedOfferCard` + enrichit le bloc « Entreprise » de `OfferMatchDetail`.
### #66 — Section « Tendances du marché » (outils d'analyse)
- **Atomic** : molecules `DemandSkillBar` + `MarketInsightsSection`, montée **après points forts / points d'attention** (réutilise carte `rounded-2xl` + `SectionHeader` de `OfferMatchDetail`). Données via `/api/market-insights` (#63→#61), scopées au rôle.
- 3 sous-blocs : **Compétences demandées** (barres `secondary` + compteur réel « Java — 8 974 ») / **Localisation** (phrase « Paris concentre le plus d'offres Frontend ») / **Mode de travail** (barre `primary` + « 80 % des rôles Frontend sont en hybride »).
- **États** : Renseigné (sous-blocs présents seulement) / Chargement (skeleton anti-CLS) / Vide « Aucune tendance disponible pour ce rôle pour le moment » (jamais de chiffre par défaut) / Erreur (encart `danger` léger + « Réessayer »).
- **Charts** : barres CSS, **pas** de recharts (toute viz temporelle future → wrapper recharts unique).
### #67 — Assistant : présentation + affordances (pas la logique LLM)
- Réutilise `atoms/AssistantMessage`. Affordances : **badge type société inline proactif** (« Acme est une [ESN]… »), **encart « PRIORITÉ : COMPÉTENCES AVANT LOCALISATION »** (`primary-light` — compétence à acquérir + compteur réel « Kubernetes (7 086 offres) », puis localisation comme levier secondaire avec raisonnement « car 60 % des rôles Frontend y sont »), **puces sources cliquables** (pattern `ToolIndicator` : « 🏢 Fiche Acme », « 📈 Tendances Frontend »).
- Tous les chiffres viennent des agrégats / enrichissement ; aucune valeur inventée.
### i18n (convention GetYourJob)
Dans ce dépôt `en.ts` et `fr.ts` sont **deux objets réels** (PAS un re-export) → chaque clé ajoutée dans **les deux** avec copie FR. Clés proposées (`companyCard.*`, `marketInsights.*`) listées dans la section CE du `DESIGN_SYSTEM.md`. Libellés de type société restent dans `CompanyTypeBadge`.
Transition Spec → **Design**.
RA Updated by Redmine Admin about 2 months ago
- Status changed from Spec to Design
RA Updated by Redmine Admin about 2 months ago
🏁 done designing — design terminé (3 surfaces UI : #64 carte entreprise enrichie, #66 section Tendances du marché, #67 UX assistant), prêt pour le dev.\nLivrables : brief posté, section « CE » ajoutée à DESIGN_SYSTEM.md, mockups joints (`mockup-64-*`, `mockup-66-*`, `mockup-67-*`). Règle « aucune donnée factice » respectée. Reste en Design jusqu'à ce qu'un dev le tire vers « In development ».
RA Updated by Redmine Admin about 2 months ago
- Status changed from Design to In development
RA Updated by Redmine Admin about 2 months ago
- Status changed from In development to QA
🔬 QA en cours — démarrage des tests de fumée (epic #21).
RA Updated by Redmine Admin about 2 months ago
RA Updated by Redmine Admin about 2 months ago
RA Updated by Redmine Admin about 2 months ago
RA Updated by Redmine Admin about 2 months ago
- Status changed from QA to In development
🚫 **QA — FAIL** (régression i18n bloquante introduite par #66)
**Résumé par surface**
- **AC1 — Carte entreprise inline (#64/#58) : OK.** `CompanyEnrichmentCard` rend uniquement les champs présents (jamais « N/A »), retourne `null` sans enrichment, lien `/companies/<slug>`, expand/collapse. `attachCompanyData.ts` construit `companyEnrichment` seulement à partir des champs non-null et l'omet si vide. NO-FAKE-DATA respecté.
- **AC2 — Section Market Insights (#66/#61/#63) : KO.** La logique d'agrégat est correcte (comptes réels par offre, % work-mode sur lignes classifiables, corpus vide → agrégats vides ; endpoint `GET /api/market-insights?role=&location=` + filtre active-offer canonique + cache). **MAIS la livraison #66 introduit une régression i18n bloquante (voir ci-dessous).**
- **AC3 — Assistant (#67) : OK.** Outils `get_company_data` / `get_market_insights` déclarés (`AssistantToolRegistry`) et implémentés (`AssistantToolExecutor`, present-only, « donnée indisponible » sinon agrégat réel). System prompt avec sections « Données entreprise (proactif) » + « Compétences AVANT localisation » et consigne « aucune valeur inventée ». Tool-wiring + prompt testés.
**Tests unitaires de l'epic : 43/43 PASS** (7 suites). `npx prisma generate` OK.
**🛑 Régression bloquante — collision de clé i18n `marketInsights` (commit d33026c, #66)**
Il existe déjà un bloc top-level `marketInsights:` (en.ts l.2771 / fr.ts l.2780) = toute la page **Market Statistics** (`title`, `subtitle`, `tabs`, `kpi`, `compensation`…). #66 a ajouté un **second** bloc top-level `marketInsights:` (en.ts l.3114 / fr.ts l.3123). En littéral objet JS, la dernière clé dupliquée **écrase** la première.
- tsc : `en.ts(3114,3) TS1117` et `fr.ts(3123,3) TS1117` (« object literal cannot have multiple properties with the same name »).
- Preuve de résolution runtime (transpile + eval) :
- EN `marketInsights.title` → `"Market insights"` (au lieu de `"Market Statistics"`)
- EN `marketInsights.tabs` → `undefined` ; EN `marketInsights.kpi` → `undefined`
- FR `marketInsights.tabs` → `undefined` ; FR `marketInsights.kpi` → `undefined`
- **Impact** : la page existante `/profile/developer/market-insights` consomme ~40+ clés `t('marketInsights.title' | '.tabs.*' | '.kpi.*' | '.compensation.*' …)` → toutes cassées (clés brutes/undefined) en EN **et** FR.
**Correctif attendu** : renommer le nouveau bloc (ex. `marketInsightsSection:`) ou fusionner ses clés dans le bloc existant l.2771 (pas un 2e top-level), puis mettre à jour `MarketInsightsSection.tsx`. À corriger dans en.ts **et** fr.ts.
**Pièces jointes**
- `qa-redmine-21-rapport-technique.txt` — commandes, ports, AC, sortie des gates (secrets filtrés)
- `qa-redmine-21-regression-i18n.png` — preuve visuelle de la collision + résolution runtime
- `qa-redmine-21-tests-pass.png` — sortie 43/43 PASS des suites de l'epic
→ Transition **QA → In development** pour correction.
RA Updated by Redmine Admin about 2 months ago
🏁 done dev (i18n fix) — ready for re-QA
Correction de la régression i18n bloquante confirmée par la QA.
**Cause** : le bloc Market Insights SECTION ajouté par #66 (d33026c) introduisait une 2e clé top-level `marketInsights:` dans `en.ts` ET `fr.ts`. Par last-key-wins JS, ce bloc écrasait le namespace pré-existant de la page « Market Statistics » (`/profile/developer/market-insights`, ~40 usages `t('marketInsights.*')`) → clés brutes/undefined en EN et FR, + erreur TS1117 (clé dupliquée) sur les deux fichiers.
**Fix** :
- Renommé le bloc #66 en `marketInsightsSection:` dans `en.ts` et `fr.ts` (le `marketInsights:` original de la page Statistiques est laissé intact).
- Mis à jour les 14 références dans `MarketInsightsSection.tsx` et le mock du test associé vers `marketInsightsSection.*`.
- Ajout d'un test garde-fou (`marketInsights-namespaces.test.ts`) qui vérifie que les deux namespaces résolvent distinctement (la page Statistiques garde title="Market Statistics" / "Statistiques Marché", la section garde son propre title) pour empêcher toute récidive silencieuse.
**Fichiers modifiés** :
- `src/infrastructure/i18n/translations/en.ts`
- `src/infrastructure/i18n/translations/fr.ts`
- `src/components/molecules/MarketInsightsSection.tsx`
- `src/components/molecules/__tests__/MarketInsightsSection.test.tsx`
- `src/infrastructure/i18n/translations/__tests__/marketInsights-namespaces.test.ts` (nouveau, garde-fou)
**Vérifs** :
- TS1117 disparu de `en.ts` et `fr.ts` (`npx tsc --noEmit` propre sur les fichiers touchés ; erreurs restantes pré-existantes dans `src/scripts/*` + `remotion/*`, hors scope).
- Exactement UNE clé top-level `marketInsights:` par fichier de locale.
- Les clés de la page existante (title, tabs, kpi, compensation…) et celles de la nouvelle section résolvent toutes.
- `npx vitest run` (suites touchées) : 10 passées. `npm run build` : OK.
- `gh pr checks 239` : branch + pr-head **pass** (verts).
PR #239 : https://github.com/omdev-tech/GetYourJob/pull/239
Commit : 549534e. Ticket laissé **In development** pour la passe de re-QA.
RA Updated by Redmine Admin about 2 months ago
RA Updated by Redmine Admin about 2 months ago
RA Updated by Redmine Admin about 2 months ago
RA Updated by Redmine Admin about 2 months ago
🏁 done QA — PASS (re-QA)
Re-QA après le correctif i18n (commit 549534e, PR #239). La régression bloquante est **levée** et les 3 surfaces de l'épic restent fonctionnelles. NO-FAKE-DATA respecté partout.
**AC1 — Régression i18n CORRIGÉE (le bloqueur) : OK**
- Exactement **une** clé top-level `marketInsights:` par fichier de locale (en.ts l.2771, fr.ts l.2780) ; le bloc #66 est bien renommé `marketInsightsSection:` (en.ts l.3114, fr.ts l.3123).
- `npx tsc --noEmit` : **0 TS1117 dans en.ts / fr.ts**. Le seul TS1117 restant (`src/scripts/cron-geocode-jobs.ts`) n'est **pas touché** par cette branche — pré-existant, hors scope (confirmé par `git diff`).
- Résolution runtime (`import { en, fr }`) : EN `marketInsights.title` = "Market Statistics", FR = "Statistiques Marché" ; `tabs` / `kpi` / `compensation` = objets réels (**plus undefined**). `marketInsightsSection.title` distinct ("Market insights" / "Tendances du marche").
- Test garde-fou `marketInsights-namespaces.test.ts` : **5/5 PASS**.
- **Rendu LIVE** de la page précédemment cassée `/profile/developer/market-insights` (port 3013) : en-tête **"Statistiques Marché"** résolu (pas de clé brute, pas d'undefined), nav OK, aucune clé `marketInsights.*` ne fuit dans le DOM, aucune erreur console i18n. Voir `qa-reqa-21-market-statistics-page.png`.
**AC2 — Section Market Insights (#66/#61/#63) : OK**
- `MarketInsightsSection.tsx` utilise 100 % le namespace `marketInsightsSection.*` (skills+compteurs réels par offre, localisation, mode de travail ; empty → EmptyState gracieux, jamais de chiffre par défaut).
- Suites : section (5) + route (8) + service (11) PASS.
**AC3 — Carte #64 + Assistant #67 (régression-check) : OK**
- #64 `CompanyEnrichmentCard` (5) : champs présents seulement, `null` sans enrichment, lien `/companies/<slug>`.
- #67 prompt (4) + tool-wiring (5) : outils `get_company_data` / `get_market_insights` (present-only), prompt skills>locations + mention type société.
**Suite complète de l'épic : 43/43 PASS (7 suites).** `npx prisma generate` OK.
**Pièces jointes**
- `qa-reqa-21-rapport-technique.txt` — commandes, sortie tsc/garde-fou, résolution runtime, mapping AC (secrets filtrés)
- `qa-reqa-21-market-statistics-page.png` — page « Statistiques Marché » rendant ses libellés correctement (preuve visuelle que la régression est levée)
→ #21 **laissé en QA** (la promotion preprod est gérée par un autre agent).
RA Updated by Redmine Admin about 2 months ago
- Status changed from QA to In development
RA Updated by Redmine Admin about 2 months ago
🏁 done dev (dev-integration merge) — ready for re-promotion
Merge `origin/dev` dans `feat/redmine-21-insights-ui` (PR #239) effectué et conflits résolus en conservant les deux côtés :
- `MatchedOfferCard.tsx` : conserve les DEUX intégrations (CompanyEnrichmentCard #21 + ActionPlanChecklist #22).
- `PrismaJobOfferRepository.ts` : auto-merge propre — changements de dev + `findActiveMarketInsightRows` (#21) conservés.
- `en.ts` / `fr.ts` : toutes les clés top-level des deux côtés conservées (`companyCard` + `marketInsightsSection` #21, `actionPlan` #22, `socialProof` #20, clés #29) ; aucun doublon — zéro TS1117 dans les fichiers de locale ; le garde i18n `marketInsights-namespaces.test.ts` passe.
Validation : `npx tsc --noEmit` (zéro erreur dans en.ts/fr.ts, zéro TS1117 sur les fichiers de locale ; bruit pré-existant inchangé), `npm run build` ✅ (compiled successfully), suite vitest `ci` complète ✅ (233 fichiers, 2701 tests, 0 échec). 3 tests pré-existants (`MatchedOfferCard.companyLink`, `MatchedOfferCard.tracking`, `OfferMatchDetail.tracking`) cassaient déjà sur le tip #21 avant le merge (la carte rend désormais des enfants qui appellent `useTranslation` sans `TranslationProvider`) — corrigés en stubbant le hook côté test uniquement (aucun changement de comportement).
PR #239 → base `dev` : `branch` ✅ + `pr-head` ✅, mergeStateStatus=CLEAN.
PR : https://github.com/omdev-tech/GetYourJob/pull/239
Ticket laissé En développement pour re-promotion.
RA Updated by Redmine Admin about 2 months ago
- Status changed from In development to Preprod
- preprod_url set to https://github.com/omdev-tech/GetYourJob/pull/240
- branch set to feat/redmine-21-insights-ui
- pr_url set to https://github.com/omdev-tech/GetYourJob/pull/239
Re-QA PASS → promu vers preprod. Mergé sur `dev` : PR #239 (CI verte après résolution des conflits d'intégration avec dev). Promotion preprod via la PR consolidée #240 (dev→preprod, les 4 épopées #20/#29/#22/#21 ensemble — les PR preprod par-feature ont été fermées car elles entrent en conflit sur les fichiers i18n partagés). Bug i18n détecté et corrigé en QA (collision de clé `marketInsights`). Suivi MatchingAgent scorer = #68. Promotion preprod→master = porte humaine.
RA Updated by Redmine Admin about 2 months ago
preprod→master mergé → livré en production. Preprod → Shipped.
RA Updated by Redmine Admin about 2 months ago
- Status changed from Preprod to Shipped
preprod→master mergé → livré en production. Preprod → Shipped.