Grace
Architecture

Pertinence sémantique

L'algorithme de filtrage des talents par pertinence : embeddings centrés, boost structurel, double-porte.

Par défaut, grace_resolve_context compile tous les talents actifs et pertinents pour le rôle — la tâche ne sert qu'au tri. Le filtrage par pertinence (flag GRACE_RELEVANCE=embeddings) restreint la sélection aux talents pertinents pour la tâche. Côté usage, voir Activer la pertinence.

Pourquoi pas une approche naïve

Un banc d'essai (7 tâches front/back réelles, 17 talents) a tranché entre trois approches :

ApprocheVerdict
Mots-clés (triggers + tags + task_types + langage)rejetée — le langage typescript matche tout
Embeddings + seuil absolurejetée — anisotropie e5 : tout converge vers ~0.8
Embeddings centrés + boost structurel + double-porteretenue — 64–100 % d'économie de tokens, précision conservée

L'algorithme

Entrée : task (texte libre) et candidates = talents actifs et pertinents pour le rôle. Pour chaque candidat : un texte name + summary + tags + triggers et un ensemble de stems {tags + triggers + task_types}.

  1. EmbeddingsvTalent = embed("passage: " + texte), vTask = embed("query: " + task) (préfixes e5). Vecteurs talents mis en cache (texte stable).
  2. Centragemu = moyenne(vTalent) sur les candidats ; on renormalise c = normalize(vTalent − mu) et cq = normalize(vTask − mu) ; emb = cosine(cq, c). C'est l'anti-anisotropie : sans centrage, tous les talents semblent également proches.
  3. Boost structurelhits = stems communs (préfixe 5 car., ≥4 lettres, accents retirés) entre la tâche et {tags + triggers + task_types}. score = emb + BETA · hits.
  4. Porte hors-domaine — si max(emb) < FLOOR ET Σ hits == 0aucun talent retenu (le bloc note « aucun talent spécifique à cette tâche »). Une tâche backend, qui a du recouvrement lexical, ouvre la porte ; une tâche purement front la laisse fermée.
  5. Coupe — sinon garder { i : score ≥ topScore − MARGIN et score > 0 }, trié décroissant, plafonné à BUDGET tokens. Garde-fou : si l'ensemble est vide, garder le top-3.

Paramètres par défaut : FLOOR = 0.18, BETA = 0.12, MARGIN = 0.15, BUDGET = 2000 (tous configurables — voir Variables d'environnement).

Découpage hexagonal

La séparation maths de sélection (domaine) vs modèle (infra) est stricte :

ÉlémentCoucheRôle
TextEmbedder (port, symbole TEXT_EMBEDDER)domain/portsembed(texts): Promise<number[][]>
RelevanceSelectordomainstateless ; toute la logique pure (centrage, stems, porte, marge, budget)
E5TextEmbedderinfrastructureimplémente le port via Transformers.js ; cache + warm-up
ResolveContextUseCaseapplicationconstruit les ScorableTalent, appelle le sélecteur, passe la sélection au compiler
ContextCompilerdomainavec selection → restreint aux keptIds et ordonne par score ; sans → comportement inchangé

Le domaine ne dépend de rien d'externe ; E5TextEmbedder implémente le port ; le use case orchestre. La logique est testable sans le modèle (embedder factice à vecteurs déterministes).

Modèle & coûts

  • Modèle Xenova/multilingual-e5-small (~110 Mo), téléchargé au 1er boot, caché sur volume persistant. L'image reste légère.
  • Latence : embedding d'une tâche ~6–12 ms ; pré-calcul des 17 talents ~280 ms (une fois).
  • Impact RAM : ~150–250 Mo quand le flag est actif.

Limite assumée

La similarité sémantique ne capture pas parfaitement « cette règle d'archi gouverne ce code » : sur un refactor diffus, un talent d'architecture peut passer sous des talents de modélisation (dépend de MARGIN). Les paramètres sont ajustables ; le flag off garantit un repli immédiat sans régression.

Garantie : la coupe est fermée sur les requires

La sélection de pertinence score une liste plate de candidats : seule, elle n'a aucune notion du graphe. La coupe (score ≥ top − MARGIN, puis BUDGET) pourrait donc élaguer un requires dur d'un talent retenu et servir un composite sans ses briques obligatoires — contexte incohérent pour l'agent.

Pour l'éviter, ResolveContextUseCase referme keptIds sur les arêtes requires / includes des talents retenus après la sélection, via DependencyGraphResolver.requiresClosure : la réintégration est inconditionnelle (exemptée de FLOOR / MARGIN / BUDGET) et restreinte aux talents actifs (donc rendables). Les recommends, lien souple, restent soumis à la pertinence.

Exemple. Tâche UpdateProject use-case, pertinence active. architecture.hexagonal déclare requires: [dependency-rule, use-case, port-adapter, value-object]. La coupe retient hexagonal

  • use-case (déclenchés par la tâche) et écarte port-adapter, value-object, dependency-rule (sous top − MARGIN). La clôture les réintègre car ce sont des prérequis durs d'hexagonal :
flowchart LR
  classDef kept fill:#6d5ef6,stroke:#3a2f9e,color:#fff;
  classDef readded fill:#fff,stroke:#6d5ef6,color:#3a2f9e,stroke-dasharray:4 3;

  hexa["hexagonal · coupe"]:::kept
  uc["use-case · coupe"]:::kept
  pa["port-adapter · clôture"]:::readded
  vo["value-object · clôture"]:::readded
  dr["dependency-rule · clôture"]:::readded

  hexa -->|requires| uc
  hexa -->|requires| pa
  hexa -->|requires| vo
  hexa -->|requires| dr

Un talent de bas score qui n'est pas un requires d'un talent retenu reste, lui, écarté.

Cas résiduel (hors pertinence). Un requires tiré par le graphe mais non activé sur le projet n'est jamais servi (resolved.filter(r ⇒ active.includes(r.id))). Ce n'est pas une coupe de pertinence mais un prérequis manquant : il relève du grace_doctor (section Moteur de résolution), à corriger en activant le talent sur le projet.

On this page