# KA ID v2 — personnalisation Groupe KA (module satellite)
Standard 2026-08-26. Le hub **groupe-ka.com** est le feature store du groupe :
journal d'interactions, favoris, recherches sauvegardées + alertes, éléments
masqués, **profil de préférences appris** (décroissance 30 j, signaux
négatifs, corrections utilisateur sur `/mon-ka`). Chaque satellite le
consomme par le canal s2s HMAC existant (`KA_SSO_SECRET`).
## Fichiers canoniques
- `kaid.py` — client backend (FastAPI) : `track()`, `fetch_prefs()`,
`rerank()`, `build_router()`. Copié dans le paquet de l'app par
`./sync-kaid.sh`.
- `ka-id.js` — SDK navigateur : clics positionnés, dwell de fiche,
visites de retour, `KAID.hide()`, `KAID.saveSearch()`. Copié dans
`frontend/public/` (+ `dist/`).
## Endpoints hub (s2s, sig = HMAC(secret, "client.ka_id.ts"))
- `POST /api/sso/events` — lot d'événements `{type, entity_id?, query?,
filters?, features?, dwell_ms?, position?}` (max 50)
- `GET /api/sso/prefs` — `{personalization, profile: {app, global}, hidden}`
- `GET/POST /api/sso/saved-searches`, `POST /api/sso/hide`
## Intégration type (app FastAPI)
```python
# web.py
from . import kaid
kaid.init("job-ka")
app.include_router(kaid.build_router(lambda req: current_user(req)))
# endpoint listing — APRÈS le tri de base, seulement pour le tri par défaut :
user = current_user(request)
kaid.track(user, "search", query=q, filters={"city": city, ...})
if sort == "recent": # jamais sur un tri explicite
items, personalized = kaid.rerank(
items, user,
features_of=lambda it: {"city": it.get("city"), "price": it.get("price"), ...},
active_dims={d for d in ("city",) if city}, # intention de session
)
# endpoint fiche :
kaid.track(user, "detail_view", entity_type="job", entity_id=uid,
features=_features_of(row))
# favoris (accounts.py) :
kaid.track(user, "favorite", entity_id=uid, features=_features_of(row))
```
```html
```
## Règles
1. **Jamais obligatoire** : sans session KA ID, zéro appel, zéro changement.
2. **Fail-open** : hub injoignable ou profil vide → classement de base.
3. **Pertinence d'abord** : `rerank()` reclasse APRÈS le tri de base
(blend 0,35) et uniquement le tri par défaut — un tri explicite
(prix, salaire…) n'est jamais altéré.
4. **Session > long terme** : passer les dimensions filtrées par la requête
dans `active_dims` — elles sont exclues du score personnel.
5. **Badge parcimonieux** : `item["ka_reco"]` n'apparaît que si le score
personnel ≥ 0,62, plafonné à ~1 carte sur 8. Frontend : petite
étiquette « Recommandé pour vous » + raisons (`MATCH_LOCATION`,
`MATCH_PRICE_RANGE`, `MATCH_`…).
6. **features riches** : envoyer les mêmes clés dans `track(features=…)` et
`features_of()` — c'est le vocabulaire du profil (city, price, brand,
category…). Valeurs texte OU nombres (les nombres apprennent des plages).
7. Le masquage (« Pas pour moi ») retire l'annonce des résultats de TOUTES
les sessions futures et compte comme signal négatif fort.
## Confidentialité
Réglages sur groupe-ka.com/mon-ka : Personnalisation / Historique /
Recommandations (ON par défaut, opt-out), effacement de l'historique,
réinitialisation des recommandations, export JSON (Loi 25).
`prefs.personalization=false` ⇒ `rerank()` ne fait que l'exclusion des
masqués ; Historique OFF ⇒ le hub jette les événements reçus.