Spaces:
Runtime error
Runtime error
Commit History
21 days ago
-
feat(embeddings): voyage-4-large remplace EmbeddingGemma (API Voyage, halfvec 1024)
Mesure sur tout le corpus (32 303 morceaux) et le jeu relu de 102 questions : en recherche dense, MRR@20 0,758 contre 0,537 pour EmbeddingGemma en francais, 0,680 contre 0,537 en anglais. Par le chemin du chat lui-meme (mots-cles en plein texte, question entiere vectorisee, 8 extraits envoyes au modele), l'article attendu figure parmi les extraits 92 % du temps en francais contre 61 % avec Gemma (MRR@8 0,504 contre 0,336), et 87 % en anglais contre 56 % (0,538 contre 0,282). Vectoriser la question entiere plutot que ses mots-cles gagne a lui seul +0,087 de MRR@8 en francais [IC 95 % +0,035 ; +0,143] et +0,127 en anglais [+0,067 ; +0,191]. - embedding_service.py : VoyageProvider (httpx, connexion gardee : 0,42 s par question contre 1,14 s avec une connexion neuve). Son corps de requete reproduit les vecteurs deja calcules du corpus : un test voyage_live le prouve, cosinus >= 0,999 sur deux questions et deux documents. Lots de 900 textes et 90 000 jetons estimes, refus definit…57c8117
26 days ago
-
fix(analytique): le profil general a ses statistiques, les alias du chat sont admis
Le chat repond desormais en profil general quand aucun profil n'est choisi. Seule la repartition d'usage le comptait : l'agregation quotidienne, /stats/all, /compare, les avis par profil et toutes les routes /personas/{persona}/... l'ignoraient ou repondaient 400. Les conversations sans profil disparaissaient des statistiques, et le total des avis ne valait plus la somme par profil. - validate_persona rend la forme canonique, avec les alias du chat (« etudiant » donnait 400 ici alors que le chat l'accepte) ; general=True admet le profil general la ou il a des donnees. La fiche descriptive (/personas/{persona}) reste reservee aux 4 profils. - Agregation, statistiques, comparaison et avis parcourent les 5 profils ; le profil general s'affiche « Profil general ». Releve par la revue adversariale du lot personas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>0b43949 -
fix(compte): l'enregistrement du profil est prouve, la garde de 015 vise users
- Le test du PATCH relit le profil par une AUTRE connexion. Le client partage sa session avec la route : il voyait une modification jamais validee, et un commit remplace par flush passait inapercu (verifie par mutation : le test echoue desormais). - 015 : la contrainte n'est cherchee que sur la table users. Une contrainte du meme nom ailleurs (copie de table) faisait croire a sa presence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>9c5085a -
fix(tests): le filet des routes reservees ne verifiait plus aucune route
Depuis FastAPI 0.141, app.routes contient des _IncludedRouter sans attribut `routes` : le parcours ne descendait plus dans les routeurs inclus, ne voyait que / et /health, et le test passait sans avoir verifie une seule route d'administration. - Le parcours descend par effective_candidates(), qui porte le prefixe et les dependances du routeur. - La garde exigee est celle d'un administrateur (get_current_admin_user ou get_current_superadmin_user), cherchee dans tout l'arbre des dependances. L'ancien critere, « current » dans le NOM du parametre, aurait accepte get_current_user_optional et refuse `_`. - Les deux routes personas reservees aux administrateurs sont ajoutees ; /ocr/health rejoint /admin/health parmi les sondes publiques. - Le test echoue s'il ne voit plus les routes reservees : il ne peut plus redevenir vide en silence. Verifie par mutation : une garde remplacee par get_current_user_optional sur /personas/{persona}/questions fait echouer le test. Co-Authored-B…3f06b9e -
feat(compte): le profil choisi suit le compte d'un appareil a l'autre
- Migration 015 (f7a8b9c0d1e2) : users.persona VARCHAR(50) NULL, et la contrainte ck_users_persona_connu, qui n'admet que les quatre profils (« étudiant » en NFC). NULL : aucun choix ; un refus de repondre n'est jamais stocke sur un compte. Rejouable, comme 013 et 014 : le test d'idempotence de 013 recule la version par stamp puis rejoue la suite. Instantanee sous PostgreSQL 16 (colonne nullable). - UserResponse.persona : /auth/me, la connexion, l'inscription, Google et la liste admin le renvoient. Pas dans UserBase : UserCreate en heriterait et la creation admin l'ignorerait. - PATCH /auth/me : le profil, seul champ modifiable en libre-service. MiseAJourDuCompte n'herite pas de UserUpdate (email, mot de passe, role : escalade de privileges) et refuse toute autre cle (422). Journal : l'identifiant du compte seulement. Jamais dans le JWT. - README : la route et les regles de synchronisation du front. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>4808bed -
feat(chat): le profil choisi pilote la reponse, une conversation garde le sien
- persona absent, null ou vide vaut desormais « general » (l'utilisateur n'a pas repondu) et non plus « citoyen » : les deux etaient indiscernables, dans la reponse comme dans les statistiques. La casse et les accents sont toleres (normaliser_profil). - Une conversation garde un seul profil. Un session_id existant envoye avec un autre profil ouvre une NOUVELLE conversation (nouveau session_id, historique vide) ; l'ancienne n'est ni modifiee ni adoptee. Le controle d'appartenance passe avant : la conversation d'autrui reste refusee quel que soit le profil. Ni 409 (un profil change entre deux visites ferait echouer la premiere question), ni silence (on repondrait avec un profil que l'utilisateur vient de quitter). - Le dernier evenement du flux porte `persona`, le profil effectif. - Les reponses conversationnelles et le message « aucun resultat » suivent le profil. - _handle_no_results recevait sa conversation en la rechargeant : sans session_id, il en creait une de…2c463a5 -
fix(confidentialite): questions et avis des utilisateurs reserves aux administrateurs
GET /personas/{p}/questions rendait a n'importe qui le texte exact des questions posees au chat par tous les utilisateurs sur 90 jours (21 questions citoyen dans jurix_dev), et GET /personas/feedback/{id} les commentaires libres des avis. Les deux exigent desormais un compte administrateur : 401 sans jeton, 403 pour un compte ordinaire. Les agregats sans texte restent publics. POST /personas/feedback reste public : hors de ce lot, signale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>4a7363d -
fix(analytique): compteurs exacts des conversations reprises, le profil general compte
- update_interaction_metrics compte en SQL. Il lisait Conversation.messages par selectinload, mais la collection, deja chargee par _load_or_create_conversation avant l'ajout des messages du tour, etait relue perimee (expire_on_commit=False) : chaque conversation reprise comptait un echange de retard (conversation 1 de jurix_dev : 3 questions reelles, 2 comptees). Le contenu des messages n'est plus charge. - personas_used est une liste neuve : modifiee en place, une colonne JSON simple ne voyait pas le changement, et l'ajout n'etait jamais ecrit. - La repartition d'usage compte « general » : la part des visiteurs qui n'ont pas dit qui ils sont, sans laquelle les pourcentages ne totalisaient pas 100. Les trois tests ajoutes echouent sur l'ancien code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>08a963c
30 days ago
-
fix(reclassement): la simulation montre tout ce que --force ecrira
Issus de la revue de la branche : - `appliquer --dry-run` sans --force n'inscrivait pas dans changements.csv les categories deja posees que le verdict contredit : sur le corpus, ou toutes les lois en ont une, on relisait un fichier presque vide avant un --force qui les reecrivait toutes. changements.csv liste desormais toute loi dont la categorie changerait, avec sa suite (appliquee, ou gardee faute de --force), et le README recommande `--dry-run --force`. - Une relance `--incertains` apres un arret sur quota repayait les lois deja retentees avec un extrait : elle les saute. - Une ligne de journal coupee par un arret brutal absorbait la ligne suivante : le journal est termine par un saut de ligne avant d'ecrire. - Le journal est indexe par l'identifiant de la loi : chaque verdict porte desormais sa reference, et celui d'une autre loi (base reconstruite) n'est ni repris ni applique. - L'anomalie « Classement indisponible » posee par le pipeline est effacee une foi…f508b99 -
feat(groq): classement des lois par lots, sans repli silencieux, et suite isolee du reseau
GroqService (app/services/groq_service.py) : - chaque appel passe par le limiteur de son modele (les quotas Groq sont par modele) ; un 429 est respecte (Retry-After) puis rejoue ; une attente imposee de plus de GROQ_ATTENTE_MAX_429_S, ou un message de quota journalier, ferme le modele jusqu'a l'echeance (GroqQuotaError, sans requete) ; 5xx et pannes reseau : 4 essais ; - erreurs typees au lieu de None : GroqLimiteError, GroqQuotaError, GroqIndisponibleError, GroqReponseInvalideError ; - schema JSON strict, max_completion_tokens, temperature 0, reflexion coupee (qwen : « none ») ou reduite et masquee (gpt-oss) ; - la sonde de sante lit /models/{modele} au lieu de generer. Classement des lois (legal_domain_classifier.py) : - par LOTS de documents numerotes, codes courts en sortie : 2 226 lois une par une demandaient 11 jours de quota ; un lot incomplet est redemande (les manquants, ou le lot coupe en deux), un document qui manque encore est « a revoir » ; - modele prop…1a3d3f5 -
test: fixtures aux 14 domaines canoniques
Les fixtures designaient des categories par leurs anciens noms (« Droit Civil », « Droit Pénal », « Droit des Affaires et OHADA ») : 62 tests tombaient en erreur au setup depuis la migration 013. Elles passent par les constantes CIVIL, PENAL et AFFAIRES de legal_domain_classifier, et les comptes par len(CANONICAL_DOMAINS) : le prochain renommage ne cassera plus rien. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>5ae5d91 -
fix(imports): un seul choix du fournisseur, et chaque fichier importe ce qu'il utilise
La comparaison et l'explication d'article lisaient settings.LLM_PROVIDER sans importer settings. Rien n'echouait a l'import : le nom n'etait resolu qu'a la construction du service, donc chaque /compare et chaque /explain-article repondait 500 des qu'aucune doublure n'etait injectee — en production, jamais dans les tests. Le choix du fournisseur, recopie trois fois, vit desormais dans app/services/llm.py : get_llm_service() rend Mistral ou Gemini selon LLM_PROVIDER, et les erreurs sont regroupees par nature (ERREURS_QUOTA, ERREURS_SATURATION, ERREURS_LLM), quel que soit le fournisseur. Les messages « Please set up Gemini API » deviennent neutres. ruff rendait 18 signalements (2 noms inconnus, 14 imports inutiles, 2 blocs mal ordonnes) ; il en rend zero. Les __import__("sqlalchemy") et __import__("datetime") deviennent de vrais imports. Garde permanente, tests/test_qualite/test_imports.py : ruff doit sortir a zero sur app, scripts, tests et alembic, et chaque module de app doit s'impo…e77f0a8
-
fix(chat): le flux refuse la conversation d'autrui en 404, plus en 500
La route /rag/ask/stream verifie l'appartenance de la conversation avant d'ouvrir le flux, mais sa clause generale `except Exception` attrapait le HTTPException 404 et le renvoyait en 500. L'interface, qui emprunte desormais le flux, ne pouvait pas repartir sur une conversation neuve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>89d5b4d
-
fix(uploads): le nettoyage ne supprime plus le PDF des lois
La route de nettoyage effacait tout fichier de plus de 24 h dans data/uploads, y compris le PDF de chaque loi publiee, que laws.file_id designe : son affichage cassait, et toute re-extraction devenait impossible. Seuls les fichiers qu'aucune loi ne reference partent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>19a2ab9 -
feat(embeddings): EmbeddingGemma par defaut, et une panne qui se voit
EMBEDDING_PROVIDER vaut desormais gemma : les embeddings sont calcules en local, gratuitement, et Gemini ne sert plus qu'au chat. `gemini` reste disponible, a 768 dimensions lui aussi. LA PANNE LA PLUS PROBABLE DU SEMANTIQUE ETAIT LA SEULE INVISIBLE. Modele absent, fichier tronque, vecteurs d'un autre modele : dans chacun de ces cas, la recherche hybride avalait l'erreur et repondait en plein texte seul, pendant que GET /search/health disait « healthy ». Trois filets : - le modele est charge AU DEMARRAGE, et une question est reellement encodee. Un echec n'empeche pas l'API de demarrer — plein texte, chat et comptes s'en passent — mais il est journalise, et le service est ECARTE : garde, il retenterait de charger 300 Mo a chaque question ; - la sante passe en « degraded » si le service manque, si des vecteurs d'un AUTRE fournisseur sont en base (leurs cosinus ne veulent rien dire), ou si aucun vecteur n'est exploitable alors qu'il y a des articles a vectoriser. Ce dernier …7a1d560 -
fix(tests): imposer l'absence de cle Brevo au lieu de l'esperer du .env
test_sans_cle_brevo_l_inscription_est_inchangee affirmait que settings.BREVO_API_KEY etait vide : il ne prouvait son invariant que sur une machine ou Brevo n'etait pas configure. Des qu'une cle est posee dans .env — le cas normal en production, et desormais en developpement — le test tombait, alors que le comportement protege, lui, n'avait pas bouge. La fixture sans_brevo, symetrique de brevo, impose l'absence de configuration d'envoi par monkeypatch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>aad088e -
feat(chat): aiguillage d'intention, le RAG seulement pour le droit
Toute question partait au pipeline RAG, sans exception. A « comment vas tu ? », le modele rendait un pave a rubriques (« Source legale : l'information n'est pas presente dans les documents fournis ») sous un bloc Sources citant un article du Code Minier sans rapport. UN CLASSIFICATEUR DECIDE AVANT LA RECHERCHE. Quatre intentions — juridique, smalltalk, meta, hors_sujet — chacune avec sa reponse. Hors juridique : ni embedding, ni recherche hybride, ni source fabriquee. Le defaut sur toute panne est « juridique », donc le comportement anterieur : une saturation du fournisseur ne fait pas basculer la plateforme en bavardage. DEUX COURT-CIRCUITS SANS APPEL. Un numero d'article, et une salutation dont le message ENTIER est une formule de politesse. Egalite stricte, jamais par sous-chaine : « bonjour, puis-je divorcer ? » part au modele. LES REGLAGES SONT MESURES. INTENT_TIMEOUT_S passe de 3 a 10 s : sur six classifications reelles, mediane 3,3 s, maximum 6,5 s. A 3 s, cinq appels sur six…153dd7c
-
feat: deployment on free tiers, object-storage documents, and email
Enables the project to run on a free hosting tier — ephemeral disk, managed Postgres behind a connection pooler, no outbound SMTP — and adds the password reset flow that was previously a dead end. Two blocking deployment bugs, both reproduced rather than deduced: DATABASE_URL with ?sslmode=require TypeError: connect() got an unexpected keyword argument 'sslmode' SQLAlchemy's asyncpg dialect forwards unknown query parameters straight to asyncpg.connect(), whose signature accepts neither sslmode nor channel_binding and has no **kwargs. The failure surfaces only on the first database access, long after an apparently successful start — and that is exactly the URL the "copy" buttons of managed Postgres providers hand out, and what .env.example recommended. Settings now refuse such a URL at load time, naming PGSSLMODE as the remedy. alembic/env.py, swapping asyncpg for psycopg2 but keeping the query string ProgrammingError: invalid dsn: invalid URI query parameter prepared_s…6429ad3
-
feat(auth): inscription publique, connexion Google, et conversations rattachees a un compte
L'utilisateur peut desormais creer un compte — nom, adresse, mot de passe — ou se connecter par Google, afin de retrouver ses conversations d'une visite a l'autre. PAS DE FIREBASE. Le navigateur obtient un jeton d'identite de Google, ce serveur le verifie avec google-auth, puis emet le JWT maison. Une dependance, zero paquet npm, et surtout UN SEUL fichier d'utilisateurs — Firebase en imposerait deux, a garder synchronises. L'ESCALADE DE PRIVILEGE EST FERMEE PAR CONSTRUCTION. `UserCreate` porte un champ `role` fourni par le client ; le reutiliser pour une route publique aurait laisse n'importe qui poster {"role": "superadmin"}. `SignupRequest` n'herite pas de `UserBase`, donc ce champ n'existe pas, et `extra="forbid"` fait de son envoi un 422 plutot qu'un silence — la tentative laisse une trace. LE TROU DE CONFIDENTIALITE EST FERME, et c'etait la condition pour promettre quoi que ce soit. GET et DELETE /rag/conversations/{sid} n'avaient aucune authentification ni controle d'apparten…d21ea1e -
feat(ia): expliquer un article et comparer deux regimes, sur la page, sources a l'appui
Deux usages du modele qui ne passent pas par le chat, parce qu'il n'y a rien a converser. POST /laws/{id}/explain-article explique un article en langage courant a partir de son texte, de ses deux voisins et des metadonnees du document. La page envoie aussi l'extrait affiche : il sert de repli quand la base n'a pas de ligne pour ce numero (193 articles reconnus par la page contre 230 en base), mais seulement apres verification qu'il provient bien du document — sans ce controle, une route publique et non limitee serait un proxy de prompt gratuit sur un quota partage avec le chat. POST /compare compare deux regimes sur des criteres imposes. UNE recherche par sujet, l'une apres l'autre : mesure sur le corpus, la requete unique du chat rendait six articles d'un regime contre trois de l'autre et ratait le bloc 49-54 qui definit le second. Un schema JSON force chaque cellule a citer ses articles ; un numero sans article correspondant part dans unmatched_citations plutot que d'etre avale ; l…88e3dbc
-
fix(qualite): ruff devient une vraie barriere, et trouve un schema mort et casse
Ruff tournait sans jeu de regles declare : 1332 signalements, dont 98 faux positifs sur le motif `Depends()` de FastAPI et 615 modernisations de style. Un controle que personne ne peut passer n'est pas un controle. Le jeu retenu ne garde que ce qui signale un defaut reel (F, E4, E7, E9, I, RUF100), avec des exceptions nommees la ou l'ordre des imports EST la protection : env.py doit poser sys.path avant d'importer les modeles, conftest doit surcharger DATABASE_URL avant d'importer l'application. Ce que le controle a alors trouve : - UN SCHEMA MORT ET CASSE. `LawDetailResponse` existait deja, exposant `List[ArticleResponse]`. Il n'etait branche sur aucune route, et il etait doublement inutilisable : ArticleResponse porte le CONTENU integral de chaque article — pour le Code Minier, 166 Ko servis deux fois — et son `number` etait plafonne a 20 caracteres alors que la colonne en fait 64 depuis qu'elle accueille « QUATRE-VINGT-DIX-SEPTIEME ». Le plafond du schema est corrige, et…7435134 -
fix(analytics): mesurer au lieu d'inventer, et fermer la porte
Le tableau de bord admin affichait des chiffres fabriques comme des mesures. `GET /analytics/search` et `/analytics/usage` renvoyaient des constantes codees en dur — 350 recherches, 150 ms, 1150 appels, 25 utilisateurs actifs, une repartition 100/50/200 entre les modes — accompagnees d'un champ `note: "Mock data"` que le front ne lisait pas. S'y ajoutaient deux litteraux `change="+12%"` et `"+5.4%"` ecrits directement dans le composant, rendus sous « depuis le mois dernier ». Hors `total_laws` et `recent_laws`, tout le tableau de bord etait fictif. Rien en base ne permettait de faire mieux pour la recherche : `query_cache` ne garde qu'un hachage, avec cinq minutes de duree de vie, et une requete servie depuis le cache n'y cree aucune ligne. Ce n'est pas un journal. La migration f1a2b3c4d5e6 en ajoute un — `search_events`, une ligne par recherche, ecrite sans jamais pouvoir faire echouer la recherche elle-meme. Les metriques d'usage, elles, etaient deja mesurables : `conversations` e…9066a18 -
feat(recherche): le titre passe devant le corps du texte, et les fautes de frappe passent
Trois demandes utilisateur, quatre causes. 1. Chercher "nomination" remontait des documents sans ce mot dans leur titre, et le moins pertinent sortait premier. `articles.search_vector` ne contenait ni le titre de la loi ni celui de l'article ; `laws.search_vector` collait titre et contenu sans un seul `setweight` ; et la seule branche qui voyait `laws.title` etait gardee derriere `if not chunks:`, donc jamais executee. Mesure avant : le decret SOCADEL (titre sans le mot) sortait a 0,4000 devant cinq documents dont le titre le porte, a 0,2000. Apres ponderation : 2,10 contre 0,20. 2. "article 35 du code minier" ne pouvait pas aboutir : le decoupeur attendait `Article 35` la ou le markdown ecrit `**ARTICLE 1er.**-` et `**Article1er.-**`. 8 lois sur 27 avaient un numero reconnu, contre 27 sur 27 apres correction ; 127 numeros reels contre 599. Le Code Minier passe de 41 a 193 articles. 3. Aucune tolerance aux fautes : `word_similarity` sur titre deplie, s…893a679
-
feat(pipeline): chunk_refiner enfin cable, avec kind, embed et embed_text
599 lignes ecrites, testees, appelees par personne. Le raffinage tourne desormais entre le decoupage et la persistance. Trois colonnes sur articles (migration b7c8d9e0f1a2) : - kind : article, legal_basis, preamble, boilerplate, roster, table, fragment, continuation ; - embed : faut-il vectoriser ce chunk ; - embed_text : le texte REELLEMENT envoye au modele. Ce que cela change, mesure sur le corpus reel de 849 articles : 62 % font moins de 120 caracteres, et l'echantillon montre ce qu'ils sont — des lignes de liste nominative ("100. DOURLAI STANISLAS 0769502K"), des fragments de tableau, un separateur "---". Ils partaient tous au vectoriel, consommant du quota et saturant les resultats semantiques de textes qui ne repondent a aucune question. Ils restent en base, affichables et trouvables en plein texte ; ils ne sont plus vectorises. embed_text prefixe le contenu de l'en-tete du document — reference, titre, date, categorie, section, page. Sans lui, "Article 3.- La depense…8da83c7 -
fix(laws): le telechargement rend le document, et un seul chemin le resout
Le bouton "Telecharger" du front appelait window.print() : ce n'est pas un telechargement mais l'impression de la page telle qu'affichee. Comme la page ne monte qu'UN article a la fois et qu'aucune feuille @media print n'existe, le PDF obtenu contenait le fil d'Ariane, le sommaire, l'en-tete et l'unique article lu. Le document d'origine n'etait jamais en jeu. GET /laws/{id}/download servait pourtant le bon fichier depuis toujours, sans qu'aucun code du front ne l'appelle. Cote API, deux corrections. content_disposition_type passe de "inline" a "attachment" : un bouton nomme "Telecharger" doit enregistrer le fichier, pas l'afficher dans l'onglet. Et les CINQ endpoints qui servent un PDF (/download, /pdf-data, /pdf-stream, /pdf-info, /page/{n}) partagent desormais _law_file_path, qui delegue a resolve_upload_path. Chacun reconstruisait le chemin a la main, avec un repli joignant law.file_id au repertoire d'upload sans motif, sans resolve() ni confinement — cinq copies exactes de la faib…56362b9
-
test: suppression des tests morts et resserrage des assertions permissives
tests/test_integration/test_rag_integration.py supprime (5 tests). Toutes ses fonctions dependaient de la fixture skip_if_ollama_unavailable, qui interroge settings.OLLAMA_URL — attribut disparu avec Ollama, donc AttributeError avalee par le except et skip systematique. Cinq tests verts qui n'executaient rien depuis le passage a Gemini. Le chemin RAG est couvert par test_rag_service.py (15 tests) et test_rag_routes.py (9), l'enchainement complet par test_full_pipeline.py. test_get_conversation_success supprime : le corps etait `pass`. Assertions qui validaient la panne autant que le fonctionnement : - rag_routes, conversation absente : "404 ou 500" devient 404. Les deux tests concernes utilisaient une fixture client locale sans redirection de get_db, donc frappaient la vraie base sur le port 5432 et recevaient un 500 de connexion refusee. Ils passent par le client de conftest ; la fixture locale, renommee sync_client, reste pour les tests dont le service est double. - categor…e3f1ef7 -
fix(chunker,personas): perte de contenu sur faux en-tete, engagement en 500
text_chunker : le motif de section etait compile avec re.IGNORECASE. Un article dont le titre commence par "Titre 1" ou "Chapitre 2" etait donc pris pour un en-tete de section — et le decoupeur coupe tout ce qui suit un en-tete trouve dans un article. L'article perdait la totalite de son texte, sans erreur, juste un "trop court (0 chars), ignore" en journal. Detection desormais sensible a la casse : les en-tetes du corpus sont en capitales. Le compromis est asymetrique, rater un en-tete en minuscules coute une metadonnee, le confondre avec un titre coute l'article. persona_service.get_engagement_metrics : la sortie anticipee "aucune conversation" omettait conversations_per_active_day, champ obligatoire du schema. GET /personas/{p}/engagement renvoyait donc 500 pour tout persona sans conversation sur la periode — soit tous, sur une base neuve. Tests remis en phase avec le code : - extraction : la regle "minimum 3 articles" a ete retiree du chunker et ne doit pas revenir, la majorite…ef3c54d -
fix(classifier): assertions de validation qui court-circuitaient les erreurs
classify() ouvrait sur deux assert qui s'executaient AVANT _validate_input : ils levaient AssertionError (donc HTTP 500 au lieu de 422), disparaissaient sous python -O en supprimant toute validation, et contredisaient _validate_input qui autorise top_k jusqu'a 12 la ou l'assert plafonnait a 5. Tests remis a niveau : - test_search_routes : la fixture mock_search_service patchait app.api.routes.search.get_search_service, sans effet — FastAPI resout Depends() a la definition de la route. Les tests s'executaient donc contre le vrai service. Reecrite via dependency_overrides, en AsyncMock, sans les references a meilisearch_client et redis_client qui n'existent plus. Une fixture `client` locale masquait par ailleurs celle de conftest sans poser de surcharge get_db : /stats tombait en 500. Retiree. Les tests /health verifiaient les cles meilisearch et redis ; ils verifient desormais l'absence de ces cles et la presence des quatre sondes reelles. reindex et stats bascules su…10b5949 -
fix(search): parametre lie au lieu de chaine castee pour le vecteur
BUG DE PRODUCTION : semantic_search construisait le vecteur de requete comme une chaine "[0.1,0.2,...]" puis la castait vers Vector(3072). Le convertisseur de pgvector attend une liste ou un ndarray et rejette une chaine ("expected list or ndarray") : la recherche semantique echouait a chaque appel. Le vecteur est desormais passe en parametre lie. Verifie sur une vraie base : 1 resultat, similarite 1.0000 sur vecteur identique. Egalement : - schemas/law.py : le validateur de statut n'autorisait que draft/published/ archived, alors que le pipeline utilise pending, processing et refused. LawResponse rejetait donc sa propre reponse (500) des qu'un document etait en cours de traitement ou en echec — precisement l'etat ou l'admin doit pouvoir le consulter. Les trois statuts du cycle d'ingestion sont ajoutes. Tests remis a niveau : - test_laws_api : fixture `db` inexistante, routes /api/v1/admin/laws au lieu de /api/v1/laws/admin (elles renvoyaient 404 depuis toujours), publicat…9697198 -
Etape 14 : tests du code ajoute (54 nouveaux tests)
Le code ecrit pendant ce chantier n'avait aucun test. tests/test_api/test_auth_routes.py (16) — connexion JSON et formulaire, mot de passe faux, email inconnu avec message IDENTIQUE (verifie explicitement : les distinguer permettrait d'enumerer les comptes), last_login_at, /auth/me avec et sans jeton, jeton malforme, matrice d'autorisation 401/403/200. Un test parcourt les routes montees et echoue si un chemin sous /admin/ ou /ocr/ n'a aucune dependance d'authentification : c'est lui qui rattrapera le prochain endpoint ajoute sans garde. tests/test_services/test_llama_parse_service.py (18) — respx intercepte httpx, donc aucun appel reseau et aucun credit consomme. Couvre le nettoyage des cachets (bloc multiligne, cachet condense sur une ligne, en-tete legitime CONSERVE, filigrane, tableaux preserves, <sup> retire), le cache sha256 (verifie qu'un second appel ne repart pas sur le reseau), l'echec de job, l'absence de retry sur 401, et les quatre formes de reponse de l'API v2. tests/t…0e1604f -
Etape 11 : authentification reelle branchee (demande n°2)
app/core/auth.py (188 lignes, JWT + bcrypt + roles) etait complet et importe par PERSONNE. Les 9 endpoints d'administration passaient par deux stubs renvoyant {"id": 1, "role": "admin"} : ils etaient ouverts a tous. BUG BLOQUANT decouvert en chemin : passlib 1.7.4 (derniere version, 2020) est incompatible avec bcrypt >= 4.1 — il lit bcrypt.__about__.__version__, supprime depuis. Avec bcrypt 5.x installe, hash_password echouait avec "password cannot be longer than 72 bytes" MEME pour 13 caracteres : l'authentification aurait ete inutilisable telle que declaree. -> bcrypt utilise directement, passlib retire des manifestes. Pre-hachage SHA-256 + base64 avant bcrypt : entree de longueur fixe, donc support des phrases de passe longues sans la troncature silencieuse a 72 octets de bcrypt (deux mots de passe partageant leurs 72 premiers octets etaient equivalents). Ajouts : - app/api/routes/auth.py : POST /login (formulaire OAuth2, l'URL exacte attendue par oauth2_scheme, donc le…da2ad52 -
Etape 6 : isolation des tests corrigee — 300 tests passent
Trois defauts d'isolation, dont un qui masquait tous les autres. 1. Boucles d'evenements desalignees (cause de ~40 HTTP 500) asyncio_default_fixture_loop_scope = session placait les fixtures dans une boucle differente de celle des tests. Les connexions asyncpg creees par les fixtures y etaient "attached to a different loop" ; les routes attrapaient l'exception et renvoyaient 500, ce qui donnait l'illusion de bugs applicatifs. -> portee de boucle alignee (function), moteur async cree par test, migration Alembic isolee dans une fixture SYNCHRONE de session (psycopg2, donc independante des boucles) pour ne pas la rejouer a chaque test. 2. Sequence PostgreSQL non realignee apres semis Inserer les 12 categories avec des ids explicites ne fait pas avancer categories_id_seq : le prochain id auto-genere repartait a 1 et heurtait la premiere categorie. -> setval apres le semis. 3. Purge seulement en sortie de test Une interruption laissait la base sale …9729782
-
Etape 4 : harnais de tests sur PostgreSQL reel
Le conftest utilisait SQLite en memoire. C'etait intenable : le coeur du produit est du SQL PostgreSQL brut (websearch_to_tsquery, ts_rank_cd, DISTINCT ON, = ANY(), similarity(), extension vector) et les objets dont il depend (search_vector, index GIN, triggers, query_cache, embedding_cache) n'existent QUE dans les migrations, jamais dans Base.metadata. create_all ne pouvait pas produire un schema utilisable — une suite verte sur SQLite aurait certifie du code qui renvoie 500 en production. - conftest reecrit : moteur de session construit par `alembic upgrade head` sur une vraie base, isolation par TRUNCATE ... RESTART IDENTITY CASCADE. L'isolation transactionnelle a ete essayee et documentee comme ecartee : les SAVEPOINT du dialecte asyncpg refusent de s'executer dans une transaction ouverte manuellement, et rollback_only casse les tests qui relisent leurs propres ecritures. - Marquage `integration` automatique selon les fixtures demandees, plutot que declare dans 21 fich…0fdac35
-
chore: initial standalone backend for JuriX API (Render)
9cf9a2a