Trouvé en recette : GET /assets/options n'avait aucun filtre — tout
Demandeur voyait le parc complet dans le sélecteur d'équipement de
"Nouvelle demande", sur le web ET le mobile (même endpoint partagé).
Risque réel : signaler accidentellement une panne sur un ascenseur qu'on
ne gère pas.
Corrigé à la racine, sur les deux plateformes à la fois :
- Relation many-to-many User↔Location (assignedSites/assignedUsers,
migration r6_demandeur_sites, même style que Team.members) — vide =
aucune restriction, comportement historique inchangé pour tous les
rôles sauf un Demandeur affecté à un site.
- AssetsService.allowedLocationIds(user) : sites + zones filles
autorisés, ou null si aucune restriction — réutilisée par options()
ET par RequestsService.create (défense en profondeur : un assetId
soumis directement hors périmètre est rejeté, 400).
- UsersService : assertTopLevelSites (un Demandeur est affecté à un
site, jamais une zone) ; invite()/update() branchent locationIds
(remplace l'affectation, comme teamIds).
- Web (personnes.tsx) : ModaleInvitation affiche les sites à cocher pour
un rôle Demandeur ; colonne "Sites" éditable via une modale dédiée.
- Mobile (formulaire-demande.tsx) : bouton "Scanner l'étiquette" en
raccourci — résout uniquement contre les options déjà chargées (déjà
filtrées), jamais de repli sur le parc complet qui annulerait la
restriction. Aucun changement à useAssetOptions() : le filtrage
serveur profite automatiquement au formulaire mobile.
- Seed : Karim Doukkali (démo) rattaché à Tour Atlas.
Bug trouvé en vérification avant tout commit : create() comparait
allowed.includes(dto.assetId), mais allowed est une liste d'ids de
sites/zones, pas d'ids d'appareils — aurait rejeté à tort tout
signalement d'un Demandeur affecté, y compris dans son propre périmètre.
Corrigé (comparaison sur asset.locationId) ; méthode renommée
allowedAssetIds → allowedLocationIds pour que le nom dise ce qu'elle
retourne.
exploitation.e2e-spec.ts mis à jour (A1/C1 → A2/B1, dans le site de
Karim — sinon rejetés par la nouvelle règle, comportement voulu).
79/80 tests API verts, le seul échec (documents-analytics, monthCost)
est le flake calendaire déjà identifié cette session, sans rapport.
Typecheck/tests/lint verts sur les 4 paquets.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Écran Voix R5 (maquetté, jamais construit) implémenté sur décision du
référent : open-source et local, pas d'API externe.
- apps/ai : faster-whisper (CTranslate2, CPU, MIT) opt-in
(AI_TRANSCRIPTION=off|locale|deterministe, défaut off) ; endpoint
/internal/transcrire — l'audio ne survit JAMAIS à l'appel (fichier
temporaire supprimé quoi qu'il arrive) ; indexer_bilans inclut
désormais InterventionReport.note anonymisée (champ existant depuis
R2, jamais eu d'écran jusqu'ici) ; 29 pytest.
- Contrat (77 opérations) : POST /assistant/transcribe (multipart).
- API : proxy multipart vers siop2-ai (WORK_ORDERS.edit — même droit
que la saisie du bilan) ; 2 tests e2e (80 tests API au total).
- Mobile : expo-audio + expo-file-system, bouton dicter/terminer sur
l'écran de clôture, purge locale après transcription, « Joindre la
description à l'OT » (corrige un bug latent : enfilerBilan ignorait
silencieusement les mises à jour de note).
- Docker : siop2-ai embarque le modèle Whisper au build (1,54→2,19 Go),
construit et vérifié (transcription réelle en conteneur, non-root).
- Vérifié réellement : transcription fidèle (voix de synthèse
française) en direct, bout en bout via l'API, dans le conteneur
Docker construit, et chaîne corpus complète (note → clôture →
réindexation → recherche sémantique).
- Base de dev locale réinitialisée avec accord explicite du référent
(prisma migrate reset, bloqué par défaut pour un agent IA) après
pollution par les tests manuels de la recette terrain précédente.
Reste : test tactile sur iPhone physique (bouton dicter) — bloqué par
une connexion USB qui ne s'est pas rétablie malgré câble/port/
redémarrage essayés à plusieurs reprises, reporté comme la recette
Android.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recette (mode extractif, aucune clé) — elle a invalidé le modèle R5.1 :
- MiniLM-384 classait la page-réponse DERRIÈRE des passages sans rapport
(0,24 vs 0,41 sur la question type du plan) → bascule mesurée vers
paraphrase-multilingual-mpnet-base-v2 (768 d, local/CPU), ADR-004 amendé
avec le banc comparatif (e5-large écarté : 2,2 Go, scores compressés).
- Migration r5_embeddings_mpnet : pgvector 384 → 768, index vidé
(re-dérivable par « Réindexer tout »).
- Découpage affiné (~350 caractères) : la phrase-réponse ne se noie plus,
l'extrait cité est lisible ; seuils par défaut recalés 0,45/0,40/0,55.
- Rejouée après bascule : réponse sourcée p. 2 en tête, refus honnête
chiffré, suggestions étagées — 16/16 Playwright, 78 API, 23 pytest.
- Revue pixel publiée (6 écrans réels vs maquettes, 3 arbitrages).
Durcissement :
- apps/ai/Dockerfile : uv, modèle ONNX téléchargé AU BUILD (ADR-004 §1),
non-root, healthcheck ; répétition locale conteneurisée validée
(healthz, reindex via MinIO/pgvector, 401 sans jeton, refus de boot
api-sans-clé, réponse sourcée depuis le conteneur).
- Compose Dokploy : siop2-ai interne (jamais sur dokploy-network,
AI_SERVICE_TOKEN requis, génération opt-in, seuils par env) ;
siop2-api branché (AI_SERVICE_URL).
- Runbook §5-6 : service IA en production, calibrage des seuils sur le
corpus client, réindexation post-déploiement.
Le tag release/r5 attend la validation de la revue pixel par le référent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apps/ai : /internal/ask — seuil de pertinence, extraits sourcés ou
refus honnête portant la taille du corpus cherché (D2), rédaction via
le Generateur opt-in ; /internal/suggest — similarité sémantique entre
la description libre et les libellés ACTIFS des référentiels, un code
par champ, confiance FORTE/MOYENNE, « N bilans similaires sur ce
parc ». Sans LLM : déterministe, explicable. 23 pytest.
Contrat (74 opérations) : POST /assistant/ask → AssistantAnswer
(EXTRACTIVE/GENERATED/REFUSAL, extraits cités, corpus cherché) et
POST /assistant/suggest-bilan (codes existants seulement) ; clients
web/mobile régénérés.
API NestJS : module assistant — proxy vers siop2-ai (AI_SERVICE_URL/
AI_SERVICE_TOKEN, ADR-004 §4), permissions matrice (ask=view,
suggest=edit), traduction interne→contrat, 503 propre si service
éteint. 6 e2e sur stub HTTP (76 tests API).
Bug débusqué par la vraie chaîne : fastembed ne norme pas ses
vecteurs — la similarité des suggestions dépassait 1 (pgvector
normalisait dans son opérateur, masquant l'écart). Normalisation à
l'encodage + réindexation : bilans en tête (0.41), refus hors corpus,
scores cosinus ≤ 1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
La migration r5_ia exige pgvector, absent de postgis/postgis — la
bascule annoncée en R1 (« pgvector arrivera en R5 ») est faite : les
jobs api et e2e construisent infra/postgres et le démarrent par étape
(les services GitHub ne savent pas builder). La migration crée aussi
l'extension elle-même (idempotent, même patron que PostGIS en r1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ADR-004 : embeddings locaux sur CPU (fastembed ONNX,
paraphrase-multilingual-MiniLM-L12-v2, 384 dims — les textes du client
ne quittent jamais le serveur), pgvector dans le Postgres existant
(RagChunk possédé par Prisma, migration r5_ia + état de corpus sur
Document), génération opt-in (mode extractif par défaut : la recette
passe sans clé API), service siop2-ai jamais exposé — joint par l'API
NestJS seule (X-Service-Token).
apps/ai (FastAPI + uv) : pipeline PDF MinIO → texte paginé (pypdf) →
anonymisation D4 (e-mails, téléphones marocains, noms connus de la
base, insensible casse/accents — fonction pure testée) → découpage
avec chevauchement (testé) → embeddings → RagChunk localisé (« p. 42 »,
« bilan du 17/07 »). Bilans codés clôturés ingérés. Exclusion de
corpus (D3) appliquée à l'ingestion ET à la lecture.
14 pytest + ruff, embeddeur déterministe en CI (aucun téléchargement),
job CI ai (uv), deploy en dépend. Vérifié en réel avec le vrai modèle :
corpus seedé réindexé en 7 s (PDF réel → 31 extraits paginés + 3
bilans), recherche sémantique concluante, e-mails → ⟨contact⟩,
0 identité dans les chunks (contrôle SQL).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Serveur : updatedAt exposé au détail OT ; baseUpdatedAt optionnel sur
transition/coche/bilan → 409 « Conflit de version » contextualisé (qui,
quand) ; toute écriture secondaire (coche, bilan, commentaire, conso,
MO) fait avancer la version — sans version fournie, le web est
inchangé. ADR-003 : sécurité & protocole de routage mobile (qui vit où
sur l'appareil, purge complète à la déconnexion — correctif réel : la
file et le cache persisté survivaient au logout).
Mobile : file persistée AsyncStorage rejouée dans l'ordre — succès →
propagation de la version fraîche aux saisies restantes du même OT
(nos écritures ne se conflictent pas entre elles, un écart étranger
reste détecté) ; coupure → tout attend ; refus → CONFLIT, la file
s'arrête, l'humain tranche (voir l'OT / rejouer sur version à jour /
abandonner). Transitions, coches, bilan et photos (D5, compressées
~1600 px) passent par la file avec patch optimiste du cache ; écran
Synchro (badge tabbar ambre/rouge) ; préchargement parc + référentiels
(le bilan hors-ligne a ses vocabulaires).
Recette « mode avion » 13/13 en Expo web piloté : gestes hors-ligne →
3 en file → modification concurrente de Salma → conflit tranché →
serveur Terminé avec bilan. 17 tests jest-expo, 74 tests API.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apps/mobile (Expo SDK 57, TS strict, workspace pnpm) : connexion
e-mail/mdp + sélecteur démo ADR-002 (uniquement si l'API l'expose),
tabbar de la maquette validée (onglets futurs marqués R4.2/R4.3),
« Ma journée » triée priorité puis échéance (fonction pure testée),
urgence en tête, pastille de synchro.
D1 en actes (lecture) : cache TanStack persisté dans AsyncStorage
(7 jours), NetInfo → onlineManager + bandeau hors-ligne horodaté ;
jeton en SecureStore ; tokens light/dark répliqués et testés ; client
typé régénéré depuis docs/openapi.json (règle d'or).
API : CORS_ORIGINS opt-in (vide par défaut) — Expo web/debug seulement,
le web de prod reste derrière le proxy, les apps natives n'ont pas
d'Origin.
Vérifié 10/10 en Expo web piloté (connexion démo Ahmed → Ma journée
scopée → hors-ligne servie du cache → retour) ; 6 tests jest-expo ;
lint racine étendu (react-hooks) ; job CI mobile, deploy en dépend.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Arbitrage du référent sur la revue pixel : tout corriger, activer la
recherche.
- Stock : filtre fournisseur, sous-seuil en tête, « Entrée de stock »
depuis la liste ; fiche pièce : fournisseur → lien Tiers.
- Statistiques : période 3/6/12 mois (paramètre months au contrat).
- Tiers : rattachements syndic→site (migration r3_recette_fixes,
Location.partnerId gardé CLIENT), éditable sur la fiche site, seedé.
- Bibliothèque : filtre « Rattaché à » + glisser-déposer (modale
préremplie, rattachement toujours requis).
- Recherche globale : GET /search (73 opérations) — familles OT/
ascenseurs/sites filtrées par la matrice, « voir autre » respecté ;
topbar ⌘K, debounce, résultats groupés, navigation clavier.
74 tests API (8 nouveaux sur le scoping de la recherche), 14/14
Playwright dont un parcours « recette corrigée », 18/18 contrôles en
navigateur réel, zéro erreur console.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Stock : alertes sous seuil (strie ambre), « Préparer le BC » sur la ligne ;
fiche pièce où les mouvements SONT le stock (entrée / ajustement motivé)
- Bons de commande : liste + panneau (envoi, annulation, réception → entrées
de stock à PU figé) ; création préremplie depuis l'alerte de seuil
- Fiche OT : carte « Pièces & main-d'œuvre » réelle (consommation à prix figé
annoncé avant le clic, temps au taux figé) + carte Documents (upload/download) ;
même carte Documents sur la fiche ascenseur
- Statistiques : 4 KPI, pannes par organe (bilans codés), coûts par mois,
top équipements ; le tableau de bord allume « Pannes par organe »
- Tiers, bibliothèque (upload rattaché, filtre par type), taux horaire
éditable dans Personnes ; nav Ressources/Pilotage activée par la matrice
- Recette R3 Playwright (2 parcours autonomes) ; cleanup-e2e étendu aux
entités R3 ; le formulaire BC attend la fin du refetch (stock périmé en cache)
13/13 e2e verts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- FileStorage : putObject/getObjectStream/removeObject, bucket créé au
démarrage — MinIO confiné à son implémentation (règle ESLint intacte)
- documents : upload multipart (PDF/JPG/PNG, 20 Mo max, rattachement
appareil OU OT requis, permission d'édition sur la CIBLE), liste
filtrable, téléchargement STREAMÉ par l'API (MinIO jamais exposé),
suppression ; e2e : octets téléchargés identiques aux octets envoyés
- analytics : GET /analytics/summary dérivé du réel — coûts/mois
(mouvements + main-d'œuvre figés), pannes par organe (bilans codés),
taux de préventif, durée moyenne de résolution, top équipements
- générateur OpenAPI : query params, multipart, réponse binaire (71 ops)
- CI : service MinIO (bitnami) sur les jobs api et e2e
- test de régression du tri « Interventions récentes » rendu déterministe
(positions absolues instables sous 12 suites parallèles) ; 58 tests,
6 runs complets consécutifs verts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Anomalie de recette (référent) : OT assigné à Ahmed, traité, clôturé —
visible dans son tableau de bord mais pas dans celui de Salma ni de
l'admin. Cause : liste triée par statut (Terminés en queue) alors que le
tableau de bord prend les 5 premières lignes ; les listes scopées
(technicien) sont courtes, celles des rôles « voir autre » non.
- tri par dernière activité (updatedAt desc) : un OT fraîchement clôturé
remonte en tête pour tous
- urgences « personne bloquée » ACTIVES épinglées au sommet (une urgence
annulée ne squatte plus la tête — corrigé au passage)
- test de régression dans la recette e2e (position ≤ urgences actives)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- migration : Asset.underContract + WorkOrder.periodKey avec unicité
[assetId, periodKey] — l'idempotence de la génération est EN BASE
(deux appels concurrents ne doublent jamais une grille)
- génération : une grille par appareil sous contrat, périodicités ancrées
sur la mise en service (mensuelles toujours dues), premier contrôle
(toutes les tâches) pour un appareil sans historique, échéance fin de
mois, événement GENERATED ; déclenchement manuel (cron au durcissement)
- gabarits administrables (désactivé = exclu des générations suivantes) ;
GET /preventive/status (générées, terminées, en retard, premiers contrôles)
- compteurs : GET meters (2 compteurs, récents d'abord) ; relevé
strictement croissant, refus motivé sinon
- contrat +7 opérations (48) ; 50 tests verts (94,7 % / 78 %) ; smoke test
prod : juillet généré (9 grilles, 8 premiers contrôles), 2e appel à zéro
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- migration r2_exploitation (10 tables) : WorkOrder (référence séquentielle,
horodatages), WorkOrderEvent, Request (1-1, motif de rejet), ReferenceValue,
InterventionReport (6 FK), TaskTemplate/ChecklistItem, Meter/MeterReading
- contrat : 15 opérations (41 total) ; la table des transitions et les champs
requis du bilan vivent dans @siop/shared ; la fiche OT expose
allowedTransitions + closureBlockers (messages métier)
- API : machine à états stricte ; garde de clôture (bilan 3 champs requis +
checklist sans tâche en attente) ; approbation → OT lié 1-1 (409 si déjà
traitée) ; rejet à motif obligatoire ; scoping « voir autre » sur listes et
accès directs (404 sans fuite) ; validation des valeurs de bilan par champ ;
« personne bloquée » triée en tête côté API
- seed : 31 valeurs de référentiels, 8 gabarits (parachute réglementaire),
OT/demandes/compteurs de la maquette — idempotent
- 45 tests verts (95 % stmts / 79 % branches) dont la recette officielle
rejouée de bout en bout ; smoke test sur build de prod
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ci-contract : régénère docs/openapi.json + schema.d.ts et échoue au
moindre diff — la règle d'or (ADR-001) devient bloquante
- api : services PostgreSQL 18 + Redis, prisma migrate deploy, typecheck,
Jest avec coverageThreshold global 70 % (mesuré : 97,5 % stmts / 90,7 % branches)
- web : typecheck + vitest + build de production
- test unitaire PermissionsGuard (23 tests au total) ; badge CI au README
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>