Changelog

Product changelog

Every Simmetric Chat release, from the newest version to the oldest.

Unreleased

Added

  • Modèle d'appartenance des widgets (2026-10-05 "wown", quick 261005-scn). Les widgets ont désormais des propriétaires (leur créateur) et des utilisateurs (attributions par widget) : - jointure widget_users (migration additive) + plafond users.widgetQuotaLimit par propriétaire ; NULL/fail-closed 0 — les installations intactes ne voient aucun changement de comportement (l'idiome du treillis maxSponsoredUsers de la Phase-206). - Le CRUD des widgets ré-armé pour les PROPRIÉTAIRES non-admin : POST /api/widgets passe par la nouvelle permission widget:manage + le plafond assigné par l'admin (dépassement → 409 { error, quota: "widgets" }) ; les barrières de licence à l'échelle de l'installation (widget_enabled / max_widgets) restent inchangées. GET list/detail, PUT settings + les liaisons de workspace atteignent creator | widget-user | admin (les id étrangers restent des 404 non-énumérants) ; DELETE reste creator | admin. - Nouvelle section admin dans la zone widget (/widgets?tab=quotas, l'onglet Quota) : des lignes par utilisateur — user | owned | max-widgets — avec les écritures assign / clear (null), une recherche paginée, journalisé en audit (user.widget_quota.updated). - Nouvel onglet « Utilisateurs » par widget (WidgetDetail, en mode édition) : le PROPRIÉTAIRE sélectionne les utilisateurs du widget (une PUT de remplacement d'ensemble, la sonde d'annuaire des attributions /widgets/owner-user-search — une surface de choix d'identité, pas d'emails) ; les utilisateurs attribués obtiennent le volet lecture seule. - Tests : une matrice serveur à 30 épingles (widgetOwnership.test.ts), les volets frontend (WidgetUsersTab/WidgetQuotasTab), le parcours e2e (widget-ownership.spec.ts).

Fixed

  • Partager un espace de travail met le rôle éditeur par défaut, et non lecteur (2026-10-05 "viewer-default"). Le symptôme signalé — « en tant qu'utilisateur attribué, je ne peux ni créer de nouveaux chats ni écrire dans les chats existants de l'espace de travail partagé » — a été ramené (par une nouvelle sonde d'intégration à 4 épingles, granteeChatWriteAccess.test.ts) au RÔLE de la concession, pas aux barrières : avec WORKSPACE_ROLE_ENFORCEMENT=true, une ligne WorkspaceAccess éditeur passe GET /chats (200) ET POST /chat (200) côté serveur ; une ligne VIEWER passe le volet de lecture et est évaluée 403 sur le volet d'écriture PAR CONCEPTION, et un attribué sans ligne reçoit le 404 non-énumérant (SC-4). LES DEUX surfaces de concession (le dialogue de partage du propriétaire WorkspaceAccessDialog et le panneau Settings SettingsWorkspaceAccess) pré-sélectionnaient lecteur dans les listes de rôle à choix unique ET de masse — un admin qui accordait sans changer la liste produisait des concessions en lecture seule. Le défaut de l'UI contredisait aussi l'intention au niveau du schéma : le commentaire de migration de la Phase 189 D-12 épingle le remplissage des lignes legacy sur éditeur (« NOT the spec's viewer suggestion »). Les deux surfaces prennent désormais éditeur par défaut (unique + masse) ; les épingles de câblage des rôles (un test qui choisit explicitement lecteur puis vérifie que lecteur traverse jusqu'au bout) sont intactes. Remarque : les utilisateurs attribués AVANT cette correction conservent leur rôle persisté — un re-grant (mise à jour du rôle) les ré-évalue.
  • « Résume le fichier joint » ne ment plus avec la phrase toute faite de non-résultat (fermeture du trou fichier-joint-avant-ingest, 2026-10-05). Joindre un fichier dans le chat le téléverse via la pipeline d'ingest ASYNCHRONE ; le message interroge un temps plus tard, le document a encore ZÉRO chunks, donc la résolution par route de attachedDocumentId (les deux volets chat dans chat.ts) n'injectait RIEN : l'agent retombait sur rag_search, le document frais n'était pas encore dans le store de vecteurs, et la règle 3 de buildSystemPrompt répondait avec la phrase toute faite « Non ho trovato informazioni su questo argomento nei documenti del workspace. Verifica che i file pertinenti siano stati caricati e indicizzati. » — une affirmation fausse (l'espace de travail peut être plein ; seul l'attachement frais n'est pas prêt). Les deux sites de résolution détectent désormais le volet à zéro chunks et injectent un avis véridique à la place (buildAttachmentNoticeChunk(new utils/attachmentNotice.ts)) : le volet non-empty-ragContext de l'orchestrator retire rag_search, si bien que le modèle répond depuis l'avis — « encore en traitement, réessayez dans un instant » (en attente/en cours) ou « traitement échoué/annulé, re-téléversez » — et l'avis interdit explicitement la phrase toute faite. Sensible au statut et à la progression (le progrès 0-100 du collector n'apparaît que si > 0) ; le balayage DLP du rag-context passe sur l'avis inchangé. 5 épingles unit sur l'aide-mémoire (les volets en attente/en cours/échoué/annulé + l'instruction anti-règle-3). Prolongement frontend — l'attenteur d'ingest du jeton d'attachement : le jeton interroge toutes les 1,5 s le nouvel endpoint GET /api/documents/:id/attachment-status (triple contrôle IDOR des métadonnées, la projection { name, status, progress }, sans chunks), affiche un spinner + le progrès % pendant l'ingest, RETIENT la barrière d'envoi (canSend + le chemin Entrée via attachmentPending) jusqu'à « completed », et laisse tomber l'attachement avec un toast sur failed/cancelled (404 → abandon silencieux ; les erreurs réseau transitoires continuent d'interroger — un hoquet du serveur ne doit pas détruire un ingest sain). La barrière est une politesse côté client : l'avis du serveur reste le rempart véridique pour les onglets vieillies. 4 épingles endpoint dans documentIdor.test.ts (la projection access-user + les volets admin D-04) ; 2 épingles ChatInputArea (en attente désactive l'envoi + le jeton de progression ; terminé ré-active).
  • Settings → « Accès aux workspaces » : les lignes de concession montrent de nouveau le nom de l'utilisateur (Q6-fast settings parity, 2026-10-05). Le dialogue d'accès aux espaces de travail avait corrigé ce même défaut (la chaîne nom d'utilisateur → id), mais le panneau Settings gardait la chaîne PRÉ-Q6 user ? displayName(user) : userId — et le fil D-15 n'envoie jamais d'objet user embarqué (T-189-11), si bien que chaque ligne dépliée du panneau retombait sur l'UUID brut. Le panneau reflète désormais la chaîne du dialogue (l'utilisateur embarqué forme legacy → grant.username → id) et sa suite épingle la vraie forme du fil : le mock statique a retiré le champ user jamais envoyé, un nouveau test vérifie que le nom s'affiche et l'UUID jamais (+1 test).
  • Point d'entrée de l'espace de travail partagé : les lignes de la page Workspaces s'OUVRENT désormais (2026-10-05 "open-row"). Un attribué à l'échelle de l'espace de travail (lecteur/éditeur SANS accès aux projets) ne pouvait jamais atteindre l'espace de travail partagé dans l'UI du chat : les sélecteurs de la barre latérale sont project-d'abord — la liste des projets passe par GET /api/projects (n'énumère que les projets du créateur / ProjectAccess, aucun volet de concession workspace) et la liste des espaces de travail est disabled tant qu'un projet n'est pas choisi, filtrée par projectId === selectedProjectId — tandis que la page Workspaces énumérait l'espace de travail partagé (le OR-filter de GET /api/workspaces inclut le volet de concession accessGrants.some(userId)) mais ses lignes étaient gestion-seule, sans moyen d'y entrer. WorkspaceRow obtient une action « Open » (en tête de la colonne d'actions, rendue pour TOUTE ligne énumérée — l'appartenance à la liste implique au moins lecteur pour le OR-filter du serveur ; la barrière client n'est que UX, les routes chat re-barrent côté serveur par requête) qui monte l'espace de travail via ChatContext (setWorkspaceId(id) + navigate("/") — persiste lastWorkspaceId, si bien que la restauration au démarrage rentre dans le contexte au rechargement sans les sélecteurs). Un effet miroir dans App garde les listes de la barre latérale cohérentes quand l'espace de travail actif change hors des sélecteurs — SEULEMENT quand le projet parent est dans la liste de projets de l'utilisateur ; la surface de chat est pilotée par le seul currentWorkspaceId. Les sélecteurs de la barre latérale obtiennent une entrée statique « Partagés avec moi » (sentinelle __shared__ dans la liste PROJECT, rendue seulement quand l'ensemble des partages n'est pas vide) : avec un projet choisi, la liste des espaces de travail reste bornée à ce projet comme avant ; choisir la sentinelle (ou une sélection vide au démarrage) fait que la liste des espaces de travail énumère l'ensemble invisible-par-projet (tous les espaces de travail atteignables par OR-filter dont le projet parent manque à GET /api/projects) — la liste des partages de l'attribué espace-seul, jusque-là inatteignable parce que le contrôle exigeait d'abord un projet ET que Radix Select ne peut pas vider un id de projet concret une fois choisi (cul-de-sac one-way ; l'entrée nommée est le chemin toujours cliquable du retour). Le placeholder est guidé par le contenu via l'aide-mémoire partagé workspaceSelectPlaceholderKey dans sidebar/navModel.tsx (une sentinelle vide périmée d'un partage révoqué dégrade vers la formulation générique au lieu d'un périmètre partagé activé-et-vide ; workspaceSelectDisabled barre les monts de AppNavOverlay — l'inline AppSidebarNav ne se rend que sur les éléments hôtes). Les clés i18n workspace.open + sidebar.selectSharedWorkspace + sidebar.sharedProjects ajoutées aux 8 locales — la parité est verte. Nouvelles épingles : les lignes lecteur montrent Open et jamais delete/edit, un onOpen absent ne rend aucun contrôle, l'épingle à cheval de page vérifie l'écriture lastWorkspaceId au localStorage via le vrai ChatProvider (WorkspacesPage), le volet partagé se rend à partir des éléments hôtes sans projet et se cache quand il est vide (AppSidebarNav), le choix de la sentinelle montre le placeholder partagé tandis qu'une sentinelle périmée dégrade, et la matrice de gating de l'overlay épingle activé/désactivé par volet + placeholder sur bureau et mobile (AppNavOverlay).
  • Le dialogue « Accès » des espaces de travail (le bouton en haut de la barre latérale) ne déborde plus de son propre bord (2026-10-05). Chaque bloc pleine largeur échappait au-delà du bord arrondi du dialogue : le DialogContent du kit d'UI est un grid SANS grid-template-columns explicite, si bien que les enfants se posaient sur UNE seule piste implicite auto qui grandit jusqu'à la contribution min-content de l'élément le plus large — une ligne de concession dont le nom est un username UUID ininterrompu de 36 caractères comptait 503–549px contre la boîte de contenu de 488px (sm:max-w-[520px]), élargissant la piste et ÉTIRANT chaque élément pleine largeur (le titre, la rangée de sélecteurs, la liste à cases à cocher, les <li> de concession) par-delà le bord. Correction à deux étages : la base du kit d'UI épingle désormais une colonne explicite à minimum-zéro (grid-cols-[minmax(0,1fr)] — toute la famille Dialog réduit les éléments à la boîte de contenu plafonnée au lieu de pousser la piste, dans toute l'application) et le site du dialogue ajoute break-words au titre interpolé + flex-wrap aux rangées de concession/de masse et à chaque <li> de concession (les boutons nowrap se replient sur leur propre ligne sous ~360px de viewport). Vérifié avec Playwright : zéro débordement 380→1280px (les noms de concession se tronquent avec ellipsis comme prévu, rien ne franchit le bord) ; le nouveau WorkspaceAccessDialog.test.tsx épingle les deux étages — 174 suites / 1487 tests frontend verts (+1 suite, +4 tests).
  • Les lignes de concession « Accès » montrent de nouveau le NOM de l'utilisateur (le suivi rapide de Q6, 2026-10-05). La réponse serveur D-15 porte {userId, username} mais PAS d'objet utilisateur embarqué (retiré avec T-189-11 pour cesser de fuir les métadonnées emails/rôles), tandis que le dialogue rendait le nom UNIQUEMENT depuis ce champ optionnel user mort — chaque ligne d'utilisateur assigné retombait sur l'id UUID. Les lignes de concession résolvent désormais l'utilisateur embarqué (forme legacy) → username → id ; WorkspaceAccessGrant obtient le champ username que le fil envoie vraiment. Nouvelle épingle : le nom rendu / l'id jamais fuilé / les lignes legacy à null-username gardent la chaîne de recours à l'id (+1 test).
  • Le deep link des Settings restauré (2026-10-05). Un chargement complet de /settings (signet, entrée de la barre latérale, volets de navigation legacy) n'ouvrait plus le dialogue des réglages depuis la révision R-5 « settings as dialog » : au premier commit les requêtes d'auth étaient encore en vol (user = null) et l'effet close-on-logout — déclaré après l'ouvreur dans le même composant — le re-fermait dans le même lot (open-then-cancel), et le pathname d'après-chargement était déjà normalisé donc rien ne rouvrait. La navigation douce (menu utilisateur) n'était pas touchée. L'ouvreur ne part désormais que sur une session LOADED et la normalisation préserve la chaîne de requête, si bien que les deep links ?tab= survivent au montage du dialogue ; les signets ?tab= ENTRENT aussi désormais dans la section visée (le volet identique à un clic sur un volet : la page de groupe s'ouvre et l'entrée marque data-active). e2e/settings-left-menu-nav.spec.ts est de nouveau vert.
  • Vérifié aussi comme pas-un-bogue : POST/DELETE /api/system/settings/branding/icon sont les routes du plugin ENTERPRISE (simmetric-enterprise/brandingRoutes, Phase 145, mountProtected + adminLicense("white_label")) — la build community répond 404 et l'UI dégrade avec le toast documenté de mise à niveau par conception (la garde grep D-12 whiteLabelExtractionGuard) ; la batterie EE 37 suites / 737 tests verts, la garde community verte.
  • Le panneau Settings ne déconnecte plus les utilisateurs non-admin qui l'ouvrent. Le routeur des settings était barré de bout en bout avec requireAdmin (la permission admin:settings) tandis que le treillis de visibilité par rôle (SETTINGS_TAB_PERMISSIONS / RoleSectionVisibility) rendait quand même des sections — Profil, Apparence, Fournisseurs LLM, Avancé — aux utilisateurs ordinaires. Ouvrir le panneau (ou une section) faisait que l'interacteur de SettingsPage allait chercher GET /api/system/settings, recevait 403, et la page confondait 403 avec 401 : elle montrait « Session expired. Please log in again. », effaçait le token et redirigeait vers /login 3 secondes plus tard — détruisant une session entièrement valide pour un simple écart de permission. Correction en deux parties (2026-10-04 « settings-panel logout ») : - Serveur : GET /api/system/settings est désormais permission-aware. Les administrateurs (porteurs de admin:settings) gardent la charge utile console complète identique au bit près ; tout autre utilisateur authentifié ne reçoit que le sous-ensemble NON_ADMIN_SETTINGS_READ_KEYS (partagé) — les clés non sensibles consommées par les surfaces atteignables des non-admins (BRANDING_* pour l'onglet Apparence / l'en-tête de l'application, ALLOW_NON_ADMIN_UPLOAD pour la page des téléversements) — chaque entrée readOnly: true (règle anti-décalage : ne rendre jamais un formulaire que le serveur barrerait d'un 403). Les secrets (SMTP_PASS, LLM_API_KEY, JWT_SECRET, DATABASE_URL, VECTOR_DB_API_KEY, ...) ne peuvent jamais atteindre une réponse non-admin. La vue à l'échelle de l'organisation (?organizationId=) et toutes les routes d'écriture (PUT /, POST /mail-test) restent admin-uniquement (fail-closed). - Frontend : seul un 401 (échec d'authentification) détruit la session — dans le volet de redirection à 3 secondes de SettingsPage et le volet d'erreur de useMe d'App.tsx. Un 403 rend désormais une bannière distincte « Vous n'avez pas la permission de voir ces réglages. » (settings.errorNoPermission, les 8 locales) et la session survit.
  • Ingest du Collector : les fichiers téléversés avec une extension de type texte (.txt .md .csv) mais portant les octets magiques PDF ne sont plus indexés comme de l'UTF-8 brut (l'incident AI-ACT.txt du 2026-10-01). Un PDF renommé .txt lu comme du texte n'alimentait l'index de vecteurs qu'avec les entrailles PDF non compressées (dictionnaire d'Info, métadonnées XMP, dictionnaires d'objets, éléments de structure) — 3321 chunks, zéro de texte normatif — tandis que le corps dans les flux compressées FlateDecode se dégradait en charabia U+FFFD. parseFile flaire désormais l'en-tête %PDF- (les 1024 premiers octets, la fenêtre à partir des ordures ISO 32000) sur les fichiers déclarés texte et les route vers l'analyse PDF ; les métadonnées de chunks gagnent un timbre routedAsPdf et un faux positif du flairage (un fichier texte qui ne fait que mentionner « %PDF- » tôt) retombe sur l'analyse texte au lieu de faire échouer l'ingest.
v0.28.1
2026-10-02

Added

  • Connecteurs : les connecteurs existants peuvent désormais être MODIFIÉS depuis l'UI admin (Q1-quick) — le volet PUT /api/connectors/:id préexistant enfin mis à jour : le menu déborder de la carte gagne Edit (première entrée), qui ouvre un dialogue prérempli sur la surface mutable (le nom, la liaison d'archive documentaire, les messages d'accueil/repli, la locale de repli, la limite de débit + la limite de sessions journalière — vide remet à null, 0 = la sentinelle UNLIMITED du router). isEnabled reste sur le Switch de la carte, Telegram pollMode sur les boutons webhook, et plateforme/botToken/workspace restent immuables par schéma. Le volet PUT gagne aussi l'événement d'audit chat_connector.updated (à parité avec le volet DELETE, D-21).
  • Phase 221 (INSTLIC) — la fente de collage de licence d'instance (la mise à niveau enterprise sans .env) : une nouvelle surface admin GET/PUT/DELETE /api/license/instance (requireAdmin + l'intergiciel de locataire ; le placement en pile par route laisse GET /info et /diagnose intactes) plus le panneau Settings → Security → License. Le collage vérifie d'abord par sonde un JWT RS256 de niveau ENTERPRISE non réclamé (une clé rejetée ne persiste jamais : énumération fermée { error, reason } — invalid / expired / plugin_bound / community_tier), ne stocke QUE le chiffré AES-256-GCM dans SystemConfig (la clé INSTANCE_LICENSE — la discipline de couture d'écriture de SMTP_PASS), invalide le cache Redis D-07, et s'applique à l'instance EN COURS via le MÊME résolveur que l'étape de démarrage exécute (setCachedLicense — les dépassements de limites injectés par le chargeur survivent ; les drapeaux booléens prennent vie immédiatement ; le retrait du collage revient à l'état du prochain démarrage avec parité de démarrage). Préséance, démarrage et exécution identiques : la fente → la ligne de plugin vérifiée → la variables d'environnement LICENSE_KEY → Community. Le PUT de masse /api/system/settings refuse la clé (la doctrine de la route d'écriture dédiée pour la rétention de chat) et l'énumération des réglages n'expose jamais le chiffré.
  • Phase 219 (SETP-01/02) — des drapeaux voir + modifier par rôle pour chaque section Settings : RoleSectionVisibility.canModify additif (se retranche du repli SETTINGS_SECTION_MODIFY_PERMISSIONS — n'accorde jamais, admin:settings terminale), settingsModify dans GET /api/roles/me/menu-sections, une barrière serveur sur les mutations de section renvoyant 403 { error, section } (les mutations /api/providers → llm; POST /api/agency/users → team), un rendu client en lecture seule des sections visibles-mais-non-modifiables, et une colonne Modify dans l'éditeur de visibilité des rôles (PUT /api/roles/:id/visibility accepte canModify par entrée — laisser-en-l'état quand absent).

Fixed

  • Les dialogues de confirmation de suppression de la barre latérale ne débordent plus (Q5-quick) : les confirmations de suppression de chat/dossier étaient des boîtes w-80 dont le bouton du kit d'UI est whitespace-nowrap shrink-0 PAR CONCEPTION — les étiquettes d'actions de dossier localisées longues (« Mantieni N chat… » / « Elimina N chat permanentemente ») s'échappaient horizontalement de la boîte, et les noms de dossier interpolés longs déborderaient le titre. Les boîtes sont désormais w-[22rem] max-w-[calc(100vw_-_2rem)] (plus larges + bornées au viewport), les rangées de boutons flex-wrap, les deux étiquettes d'action longues passent outre le nowrap (whitespace-normal text-left), et les titres/textes interpolés portent break-words. Les trois clés i18n manquantes du dialogue delete-chat (sidebar.deleteChat / .deleteChatConfirm / .deleteChatButton) expédiées dans les 8 locales — l'UI italienne n'y montre plus les retombées inline anglais. Le suivi de Q5 : la première limite ne compilait jamais — calc(100vw-2rem) dans une valeur arbitraire est du CSS INVALIDE (Tailwind exige l'idiome du tiret bas calc(100vw_-_2rem) pour les espaces autour des opérateurs) et la règle n'était jamais émise silencieusement, laissant la boîte de 352px non plafonnée sur les viewports étroits. L'utilité corrigée expédie dans le CSS construit (max-width:calc(100vw - 2rem) vérifié dans le paquet dist).
  • Résolution de modèle : les tours de chat n'atterrissent plus sur le défaut codé en dur qwen2.5:3b quand aucun épinglage explicite n'existe (Q4-quick — la cause racine de l'incident des quatre chats : les chats frais servaient un petit modèle local à la place du défaut du workspace, 34-98s par tour). Deux corrections : (1) les défauts de schéma de Chat.model et WorkspaceAgentConfig.model changés de "qwen2.5:3b" à la sentinelle "" (migration additive — ALTER COLUMN … SET DEFAULT, les lignes existantes intactes) : la chaîne falsy (model || chat.model || undefined, orchestrator:388/1059) tombe désormais dans la chaîne agent-config du workspace exactement comme la sentinelle surnommée "default" ; (2) la préférence de modèle par espace de travail au localStorage est désormais EXPLICITE-SEULEMENT — les choix de la palette écrivent explicit: true, le re-persist automatique du réensemencement est parti (un défaut résolu par chaîne n'est pas un choix utilisateur et ne doit pas devenir collant), et les valeurs legacy sans marqueur sont nettoyées une fois et jamais reprises comme candidates. Une préférence périmée (p. ex. qwen2.5:3b, toujours valable-mais-lente dans le registre) ne peut plus ombre le défaut du workspace à chaque nouveau chat.
  • Chat : « un message ouvre N chats » — le client n'adoptait le chatId du serveur que sur l'événement SSE done (jusqu'à ~90s plus tard sur les tours riches en outils), si bien qu'une relance de l'utilisateur dans cette fenêtre renvoyait sans chatId et le serveur créait un NOUVEAU chat par relance, tandis que le tour en vol se terminait en arrière-plan comme une ligne orpheline (Q3-quick). La route du flux émet désormais un événement additif chat portant le chatId au DÉBUT du tour (avant tout token) ; le client l'adopte immédiatement (protégé + idempotent — un envoi qui porte déjà un chatId ne ré-adopte jamais) et rafraîchit la barre latérale à l'instant. Les relances atterrissent désormais dans le MÊME chat. Identique au bit près pour les clients pré-Q3 (les événements SSE inconnus sont ignorés).
  • Rappel de mémoire : une paire (utilisateur, espace de travail) avec ZÉRO souvenirs emmagasinés saute désormais tout le crochet de rappel pré-LLM (Q2-quick) — un comptage Postgres indexé (@@index([userId, workspaceId])) barre le crochet avant tout travail : aucune incorporation de requête, aucun aller-retour du collector, et plus aucun avertissement par tour [vector-store] Table "ws_user_memory_…" not found en provenance d'une table LanceDB qui légitimement n'existe pas jusqu'à la PREMIÈRE écriture de mémoire (l'ensureTable de vectorStore crée à la première ingest ; le volet de recherche échoue ouvert sur []). Un hoquet de la base dans le comptage échoue ouvert au rappel pré-barrière (le contrat meilleur-effort du crochet — ne jamais bloquer le chat).
  • shared : la garde de régression FEATURE_FLAGS est restée en arrière par le passage de connecteurs à l'EE (quick 260930-l4q) — l'épingle portée de 12 → 13 pour connectors_enabled.

Fixed

  • Sauvegarde/restauration : un essai à vide ou une restauration échouant avec « Unable to decrypt (no key in chain matched) » (des identifiants de destination écrits sous une ENCRYPTION_KEY précédente) renvoie désormais une erreur guidée nommant la cause, les leviers de rétablissement (LEGACY_PREVIOUS_ENCRYPTION_KEYS / une ENCRYPTION_KEY précédente, puis le redémarrage), le nombre de clés essayées, et le carnet de route docs/ENCRYPTION_KEY_ROTATION.md — qui a gagné une section de rétablissement Docker « Backup restore dry-run fails ». Seule la longueur de la chaîne de clés est divulguée, jamais le matériel des clés.
  • Phase 220 (SPLIT-01/02) — la cohérence de la scission du dépôt : docs/REPO_BOUNDARIES.md (la matrice d'audit module→dépôt pour 4 dépôts avec la doctrine de classification à 7 critères et les exceptions par-conception) + la barrière des frontières de dépôt : scripts/repo-boundary-check.cjs (une barrière de grep à liste d'autorisation — le noyau ne doit pas référencer les paquets de plugin privés hors des deux coutures de chargeur et des sites de contrat documentés) + l'auto-test scripts/repo-boundary-check.test.cjs + le poste CI repo-boundary-check câblé dans build.needs
v0.25.0
2026-09-09

Added

  • UI : révision complète de la coque (R-2 → R-5). L'état vide du chat montre un monogramme monochrome élégant (l'animation glitch cyan/magenta reste maintenant confinée au thème hacker) ; la barre latérale devient un rail minimal à la Claude (nouveau chat + liste de chats + menu de bas de page) avec la navigation complète dans un overlay cmdk à hauteur de clavier (Cmd/Ctrl+/) qui garde le filtrage RBAC exact ; le menu utilisateur déménage de la barre du haut vers un dialogue dédié (compte, langue, thème, réglages, déconnexion) avec un tiroir à gauche sur mobile ; Settings s'ouvre comme un grand dialogue maître-détail (menu à gauche + détail pleine zone avec bascule de rail et fil d'arianne) au lieu d'une page routée — la zone principale est désormais le chat seul sur chaque page. Les thèmes (clair « Pearl », sombre, hacker, système) ont reçu une passe de cohérence avec des corrections de contraste WCAG-AA pour le texte atténué et les accents de chat, des gardes prefers-reduced-motion pour tous les recouvrements, et un branding conscient du livre blanc sur le monogramme.
  • Docker : le service de méta-recherche searXNG optionnel ajouté à la pile compose (infrastructure pure — rien n'en dépend encore ; l'étiquette de release épinglée, un healthcheck à base de wget, des réglages auto-provisionnés par le point d'entrée de l'image).
  • Docker : le plugin privé @simmetric-chat/saas se monte désormais dans le serveur compose comme le plugin enterprise (un mont-lien du dépôt frère entier ; le saasLoader no-ops sur MODULE_NOT_FOUND en mode community).

Fixed

  • Seed : un plantage de démarrage pendant le semis des utilisateurs pouvait rayer les rôles et barrer l'admin ; les semis se guérissent désormais eux-mêmes — la concession idempotente ré-applique le rôle admin à l'utilisateur admin rescapé à chaque démarrage (consigné dans les éléments reportés de STATE.md).
  • Locataires : POST /api/auth/register commite désormais la création de l'utilisateur, la concession de rôle par défaut et l'adhésion à l'org par défaut dans une seule transaction, et chaque voie de semis/démarrage ré-exécute l'aide-mémoire d'adhésion idempotente sur sa branche « l'utilisateur existe déjà » — un plantage à mi-démarrage/mi-requête ne peut plus laisser un utilisateur sans adhésion (la revue Phase 182 WR-01) ; la résurrection-pierre-tombale applique désormais le roleInOrg de l'appelant pour que la suppression dégrade les privilèges (WR-02).
  • Builds enterprise : la section Settings → Advanced → Backup (et les routes /sso et /logs) ne reste plus barrée derrière la carte « requires Enterprise » après une connexion en application ou une connexion SSO. La requête du manifeste des modules enterprise (GET /api/enterprise/modules) est désormais barrée par l'auth et réactive à la session : auparavant elle était allée chercher une fois, non authentifiée, au démarrage de l'application et gardée en cache pour toute la session (retry: false + staleTime: Infinity), si bien que sur l'écran de connexion elle prenait un 401 et les fonctionnalités Enterprise restaient cachées jusqu'à un rechargement complet AVEC le token emmagasiné. Le crochet s'abonne désormais à useMe() et re-cherche le manifeste quand une session apparaît (connection par mot de passe, le transfert SSO ?token=), et ne sonde jamais l'endpoint sans session du tout.
  • Docker : l'aide provision-encryption-key.sh de production ne met plus le serveur en boucle de plantage quand le .env racine est déployé avec un placeholder du gabarit .env.example non rempli (<sostituire-con-valore-generato>) comme valeur de API_KEY_HMAC_SECRET/ENCRYPTION_KEY — un placeholder est désormais reconnu comme « non réglé » (avertissement sonore) et le provisionnement retombe sur les volets restore/génération. Toute autre valeur non-vide invalide échoue quand même sonore à la barrière de démarrage.
  • Sécurité : le routeur d'aide aux tests E2E (/api/__tests__, un serveur MCP echo start/stop sans authentification) n'est plus monté quand le serveur tourne avec NODE_ENV=production — les démarrages de production répondent désormais 404 là, appliquant le contrat propre documenté dev/test-only du routeur (aucun endpoint de lancement de processus sans authentification en production).
  • Sécurité : le contrôle du rappel du collector d'import d'archive (PUT /api/archives/import/:jobId/callback) compare désormais l'en-tête X-Collector-Secret en temps constant (crypto.timingSafeEqual avec une garde de longueur), alignant la discipline à temps sûr déjà utilisée par le rappel de statut des documents et la barrière de secret du collector — fermant la dernière comparaison de secret en clair sur le serveur.
  • Authentification : POST /api/auth/login valide désormais son corps au niveau de la route avec le schéma partagé loginSchema (safeParse) — un corps invalide ou vide renvoie 400 { error, details } cohérent avec chaque autre route d'auth, au lieu d'un 401 portant un message Zod brut. Des identifiants valables-mais-faux renvoient toujours 401 "Invalid credentials".
  • Authentification : login exécute désormais un bcrypt.compare factice contre un hash jetable fixe quand l'utilisateur n'est pas trouvé, si bien que le temps de réponse ne distingue plus les noms d'utilisateur existants des inexistants (l'irrobustissement de l'énumération). Les deux volets renvoient toujours le même 401 "Invalid credentials".
  • Authentification : POST /api/auth/admin-reset-password valide désormais son corps avec le nouveau schéma partagé adminResetPasswordSchema (userId UUID + newPassword de 8-128 caractères, safeParse → 400 { error, details }) — la dernière route d'ath quitte la validation de corps déstructurée ad-hoc.
  • Authentification : la branche d'inscription fermée de POST /api/auth/register ne duplique plus la logique d'auth en ligne — elle utilise verifyToken / isTokenRevoked / isAdmin / getCachedUserWithRoles importés statiquement (pas de await import dynamique, pas de re-contrôle d'en-tête mort, la recherche d'utilisateur en cache) en préservant chaque forme de réponse et l'ordre révocation-avant-recherche.
  • Sécurité : l'endpoint cache-bust du widget (POST /api/config/:widgetId/cache-bust) compare désormais l'en-tête X-Api-Key en temps constant (crypto.timingSafeEqual avec une garde de longueur) et échoue fermé si la clé attendue manque, au lieu de la comparaison en clair précédente avec la forme fail-open expected && — le contrôle de clé du widget correspond désormais à la discipline à temps sûr utilisée par les barrières de secret du serveur et du collector.
  • Pipeline de release : les cinq images Docker sont désormais construites amd64-seulement. Les builds linux/arm64 émulés par QEMU se suspendaient jusqu'à ~5h (le widget d'abord, puis la compilation source pgvector tout-en-un) et épuisaient le disque du coureur sur les minutes payées d'un dépôt privé ; setup-qemu est retiré, chaque entrée de matrice publie linux/amd64, et la matrice de build est désérialisée en max-parallel: 2. Les utilisateurs arm64 peuvent s'auto-compilater localement (les Dockerfile sont surs pour la compilation croisée) ou attacher un coureur arm64 auto-hébergé — voir la note arm64 de docs/DEPLOYMENT.md.

Changed

  • Outillage : une barrière de code-mort tourne désormais en CI (knip) et échoue sur les fichiers, dépendances ou exports inutilisés hors des listes d'autorisation documentées ; une balayée du dépôt entier a retiré ~3400 lignes de code mort, la génération de la clé API du widget vit désormais derrière le flux admin standard, et les scripts i18n rapportent comme un indice de nettoyage non bloquant les clés de traduction définies-mais-jamais-utilisées.
  • Widgets : l'épingle du modèle de réponse par widget — la configuration admin de chaque widget peut désormais choisir le fournisseur LLM + le modèle qui sert les réponses de chat de ce widget (responseProviderId/responseModel, migration additive add_widget_response_model). L'épingle se résout côté serveur depuis la ligne DB du widget à chaque requête de chat (jamais fournie côté client — le proxy widget et l'UI visiteur sont intacts), a priorité sur le défaut du workspace, et les widgets non réglés gardent la chaîne workspace/globale existante. Les chats de widget enregistrent désormais le modèle qui les sert vraiment.
v0.22.0
2026-08-30

Added

  • RAG : le filtrage des métadonnées pour rag_search — l'agent peut désormais borner le rappel par types de document (documentTypes : pdf/md/txt/csv/docx/xlsx) et dates d'ingest (dateFrom/dateTo, ISO ; une borne supérieure à date seule inclut toute la journée), corrigeant le mode de défaillance « les documents de 2023 pour les questions de 2025 ». Les filtres traversent la recherche hybride en entrant dans les deux jambes de rappel : la jambe de vecteurs atteint le collector comme de vrais pré-filtres sur les métadonnées d'ingest estampillées (les prédicats JSONB de pgvector + les clauses must de la charge Qdrant ; LanceDB et Chroma ignorent les nouvelles clés avec un avertissement journalisé), et la jambe FTS gagne des prédicats SQL paramétrés sur la table des documents. Un rempart de rappost-re-rappel côté serveur contre la table documents autoritaire garantit la correction filtrée sur chaque fournisseur. Les documents ingérés à partir de maintenant sont toujours filtrables ; les vecteurs legacy (pré-estampillage) sont exclus tant qu'un filtre est actif et regagnent leur filtrage par re-embed admin — aucun changement de schéma, aucun ré-ensemencement des documents existants, et le comportement sans filtres est inchangé.
  • DLP : quatre nouveaux patterns intégrés pour les identifiants européens (surtout italiens) les plus communs, semés idempotemment par une migration de données seulement et reflétés dans le jeu de repli sans base — it_vat_iva (partita IVA ; ancré à l'étiquette, pour qu'un nombre nu de 11 chiffres ne corresponde jamais), it_codice_fiscale (le codice fiscal à 16 caractères ; majuscules-seulement, les mots de prose comme « foschi » ne peuvent jamais correspondre), iban (préfixe pays + longueur ≥ 15 ; les formes compacte et groupée-espaces, temps-linéaire/à l'abri de ReDoS), et eu_phone (téléphone IT/EU ; semé DÉSACTIVÉ pour une revue admin, puisque tout regex téléphonique structuré marque des séquences de chiffres d'ordre/référence). Les intégrés font désormais 10 au total ; les déploiements existants reçoivent les nouvelles lignes à migrate deploy avec la sûreté de rejeu ON CONFLICT DO NOTHING.
  • Chat : les admins peuvent désormais révéler ou re-cacher les textes rédigés par le DLP sur toute la conversation à la fois, par un nouvel élément « Show DLP texts » / « Hide DLP texts » dans le menu des actions supplémentaires (+) de l'entrée du chat (les icônes Eye/EyeOff, montrées seulement aux admins puisque seuls eux peuvent déplier un avis DLP). La préférence est bornée à la session, cachée par défaut et s'applique à chaque avis, y compris les messages nouvellement arrivés ; le bouton œil par-avis fonctionne toujours pour des dérogations manuelles par-message entre les bascules globales.
  • La configuration des patterns DLP : les six patterns DLP sont désormais soutenus par la base (la table dlp_patterns, semée idempotemment par la migration ainsi les déploiements existants reçoivent les intégrés) au lieu d'être codés en dur. Les admins les gèrent dans Settings → Advanced → DLP Patterns : activer/désactiver tout pattern par ligne, renommer les intégrés (leur regex est gelée — chaque tentative de modification renvoie 400), et ajouter jusqu'à 50 patterns personnalisés (nom snake_case, source regex avec validation de compilation à la sauvegarde, drapeaux, remplacement) avec un aperçu de test en direct montrant les segments appariés et le texte rédigé. Les balayages qui peuvent charger le jeu de la base — l'entrée/sortie du plugin de filtre et la vidange finale de fin de réponse — utilisent les patterns configurés à travers un cache TTL de 5 minutes avec la mise en cache des regex compilés par pattern, en retombant sur le jeu intégré quand la base est inatteignable ; la vidange progressive token par token du flux garde les patterns intégrés (la limitation v1 documentée). Le panneau entier est localisé dans les 8 locales.
  • Dérivation de rôle DLP : un nouveau réglage système DLP_BYPASS_ROLES (un tableau JSON de noms de rôles, défaut []) laisse les admins exempter les rôles choisis de TOUT balayage et rédaction DLP — l'entrée/sortie du plugin de filtre et le noyau de vidange progressive du flux (y compris le balayage du contexte RAG et la rédaction du thinking, tant pour le chat JWT que pour le compte de service du widget) honorent la même barrière. Chaque course dérivée déclenche un événement d'audit fire-and-forget dlp.bypassed consignant QUI a dérivé (les noms de rôles croisés) et la surface d'origine — jamais le contenu balayé. UI admin : une sélection multiple de rôles avec un avertissement d'exposition dans Settings → Advanced → DLP, et une étiquette d'action « Dérivé » dans le panneau DLP Match History, localisés dans les 8 locales.
  • Le marquage et le filtrage de la source d'audit DLP : chaque entrée du journal d'événements dlp.* porte source: "chat" | "widget" dans ses métadonnées — le noyau partagé du flux SSE tire la marque de l'en-tête X-Widget-Id du widget si bien que les conversations de widget sont rédigées comme celles du chat mais désormais discernables dans le panneau DLP Match History (Settings → Advanced). Le panneau gagne des filtres côté client pour la source (Tout/Chat/Widget), l'ID de chat et l'utilisateur.
  • L'interface admin pour le faucheur de brouillons de téléversement : une nouvelle section dans Settings → Maintenance expose la bascule activée (upload_draft_reaper_enabled) et l'expression cron de cadence (upload_draft_reaper_cron) via PUT /api/system/settings, avec un pré-contrôle client de la forme cron à cinq champs (le serveur garde son comportement avertir-et-retomber pour les valeurs invalides), des toasts de succès/erreur localisés et des descriptions dans les 8 locales. .env.example documente les clés du faucheur comme dépassements de variables d'environnement (upload_draft_reaper_enabled, upload_draft_reaper_cron, upload_draft_retention_days).
  • Config : la configuration d'environnement est désormais définie une fois dans un schéma Zod partagé (@simmetric-chat/shared), la préséance effective est documentée et épinglée par test comme DB > ENV > défaut (les modifications de l'interface des réglages gagnent immédiatement ; une matrice à 8 cas), et les clés qui seraient ignorées parce qu'une variable d'environnement les recouvre sont montrées avec un badge envOverridden dans l'interface des réglages.
  • Config : le .env à la racine du dépôt est désormais la seule configuration d'exécution pour le serveur, le collector et le widget — un nouveau chargeur à zéro dépendance (loadRootEnv()) le fusionne sous les dépassements .env par paquet (legacy), avec un avertissement de dépréciation une-fois-par-démarrage énumérant les noms de clés touchés (jamais les valeurs) ; Docker et Tauri alignés (le env_file du compose pointe le fichier racine, aucune image ne cuit des secrets).
  • Config : les trois fichiers .env.example de paquet sont régénérés depuis leurs schémas Zod (83/15/6 clés, groupées, défauts synchronisés) avec des pièges Jest qui échouent quand une future clé de schéma reste non documentée ; les lectures raw de process.env tenues hors du schéma (HF/openai-quirks, les clés de chiffrement/HMAC, LOG_LEVEL, les barrières de test) sont épinglées par 44 sondes de garde comportementales.
  • UX : supprimer un espace de traitement demande désormais un dialogue de confirmation standard (c'était un mini-confirm en ligne), les espaces de traitement supportent la suppression en masse multi-sélection avec un dialogue de confirmation et un toast de résultats comptant les lignes sautées (les lignes sans permission sont sautées, jamais un échec silencieux), l'aperçu OCR côte à côte garde le texte extrait lié au même segment de page que le panneau d'image, et le détail du run de synthèse montre le bouton d'approbation désactivé avec la raison explicite sur les runs non encore approbables.
  • UX : les cases de sélection des espaces de travail et la suppression en masse sont localisées dans les 8 langues.

Changed

  • Le faucheur de brouillons de téléversement est pleinement configurable via les réglages système : une bascule activé/désactivé (upload_draft_reaper_enabled, défaut « true ») et un dépassement de cadence (upload_draft_reaper_cron, défaut quotidien 03:00 UTC), tous deux gérés par PUT /api/system/settings. Les valeurs cron invalides journalisent un avertissement et retombent sur la cadence par défaut au lieu de faire échouer le démarrage ; le comportement par défaut est inchangé.

Fixed

  • Corrigé un plantage-cyclique de démarrage en production Docker où le lancement du serveur mourait d'un échec de contrainte-unique sur api_keys.prefix : le semeur de clés API de widget dérive désormais le préfixe d'affichage du digest HMAC et tolère les collisions de préfixe (re-vérification par hash de clé, un avertissement au lieu d'un plantage), et la génération de clé API re-saie avec une clé fraîche en cas de collision de préfixe (3 essais max).
  • Les citations de chat reflètent désormais les sources sur lesquelles une réponse se fonde réellement au lieu de tout ce que les outils de recherche ont prélevé : après la déduplication des citations, un filtre d'appui borne les citations par document au top-2 par score de pertinence et écarte les citations de chunk dont le texte ne partage aucun recouvrement lexical significatif avec la réponse finale (une barrière conservateur — les pages wiki/archive, les résultats web/mémoire, les citations sans texte de chunk et les réponses très courtes sont toujours gardées). Les réponses « pas d'autres infos » ne montrent plus « Fonti (5) » d'un seul document à ~1,5% de pertinence ; l'avertissement « Aucune évidence à l'appui » est jugé de même sur l'ensemble filtré (les volets streaming et non-streaming). Seulement la pipeline de citation — aucun changement d'UI, d'i18n ou de schéma.
  • Docker : le healthcheck du service ollama a remplacé la sonde curl (l'image ollama/ollama n'embarque pas curl/wget/node — le conteneur rapportait « unhealthy » pour toujours) par une sonde ollama ls ; le healthcheck qdrant a remplacé la sonde wget (l'image n'embarque pas wget/busybox) par un contrôle de socket bash /dev/tcp — les deux services rapportent désormais healthy. L'docker/entrypoint.sh du conteneur unique fait désormais le sourcing de provision-encryption-key.sh (à parité avec l'image séparée) : les démarrages autonomes de production auto-provisionnent ENCRYPTION_KEY + API_KEY_HMAC_SECRET au lieu d'échouer la barrière. Les cinq Dockerfile lindent propres sous hadolint (la politique de deps non épinglées documentée + les waivers de ENV placeholder).
  • Les événements de chronologie de l'audit DLP ne montrent plus « aucun détail de correspondance » quand le contenu apparié est arrivé dans un chunk de début de flux : l'événement dlp.output_match de flux dérive désormais ses types d'appariement ET sa liste de textes appariés des correspondances accumulées sur toute la course (toutes les vidanges progressives plus le balayage final de la queue) au lieu de la seule passe du dernier chunk — des lignes historiques pouvaient garder un tableau matchTypes vide tandis que la ligne montrait quand même un badge de type d'appariement. Le texte apparié est emmagasiné selon le précédent du plugin de filtre DLP (dlp.input_match / dlp.output_match non-streaming le portent déjà) ; le panneau d'audit est admin-seulement et barré Enterprise. La vue dépliée du panneau rend désormais la liste des types détectés avec un indice « type-seulement » pour les événements qui consignent des types de correspondance sans correspondances (localisé dans les 8 langues), gardant la retombée « no details » seulement pour les métadonnées véritablement vides.
  • Les fichiers téléversés restent disponibles pour une nouvelle tentative : les rappels de statut terminal du collector ne suppriment plus les fichiers de brouillon rangés de storage/uploads/drafts/ (le rappel de tout document frère pouvait effacer le fichier dont chaque nouvelle tentative dépend), et /retry//assign se guérissent en restaurant le fichier rangé depuis la copie OCR persistante (storage/ocr-sources/) quand elle existe au lieu d'échouer avec « n'existe plus sur le disque ».
  • Supprimer un brouillon de téléversement échoué et périmé ne renvoie plus barré (409) — DELETE /api/uploads/:id traite désormais les états de jambe terminale (terminée ou échouée) comme supprimables et ne refuse que les brouillons avec une jambe vraiment en vol (activée non-terminale), et la suppression en masse du frontend utilise le même prédicat en-vol que les jetons de filtre, si bien que ce que l'UI propose de supprimer est exactement ce que le serveur accepte.
  • La nouvelle tentative RAG/KB d'un téléversement dont le fichier de brouillon rangé manque renvoie désormais une erreur claire de re-téléversement (400) au lieu d'un toast de succès faux, les jambes de collector échouées ne suppriment plus le fichier de brouillon rangé de storage/uploads/drafts/ (les tentatives futures restent possibles — le faucheur 24h et la route DELETE restent les seuls supprimeurs de fichiers de brouillon), et les toasts de nouvelle tentative reflètent désormais l'issue par-jambe : une jambe rejetée sur HTTP 200 montre un toast d'erreur localisé et le décompte de relance en masse la compte comme échouée.
  • Les déploiements Docker de production auto-provisionnent une ENCRYPTION_KEY persistante (un volume de stockage serveur, /app/storage/.encryption-key) au lieu de la boucle de plantage sur le défaut dur. Le point d'entrée du serveur génère la clé une fois avant toute étape Prisma, la restaure à chaque redémarrage/reconstruction, préfère une valeur fournie par l'opérateur et échoue sonore sur une clé persistée corrompue. .env.example et docs/DEPLOYMENT.md mis à jour pour marquer la variable comme Requise-en-production. Voir docs/ENCRYPTION_KEY_ROTATION.md pour les notes Docker.
  • Les pages OCR de la base de connaissances n'échouent plus quand le flux de vision Ollama est fermé par le moteur de modèle sans le marqueur final done après une transcription complète (observé avec le moteur personnalisée glm-ocr) : ocrPage sauve désormais la sortie non vide accumulée comme le résultat de la page avec un drapeau truncated et un journal d'avertissement, au lieu de dédaigner une transcription réelle derrière « Did not receive done or success response in stream. » Un flux sans done et sans contenu fait quand même échouer la page.
  • Les résumés de travaux OCR sont véridiques sur la santé des pages : les pages qui se terminent par un marqueur [FAILED: sont comptées et emmagasinées comme failedPages dans le JSON de résultat du travail, et les journaux de complétion émettent un avertissement [ocr] Job completed with failed pages, si bien qu'un travail COMPLÉTÉ avec des pages échouées n'est plus indiscernable d'un succès propre (zéro token + un score de qualité au plancher seuls étaient ambigus).
  • Documenté et corrigé le mode de défaillance pour les déploiements Enterprise : quand @simmetric-chat/shared gagne de NOUVEAUX fichiers source, un instantané périmé de la dépendance file: enterprise peut planter le serveur au démarrage avec « Cannot find module './env.schema' » — pnpm prend l'instantané du paquet file: dans node_modules/.pnpm et les fichiers créés après le dernier pnpm install n'atteignent jamais cet instantané (les liens durs ne mettent à jour que les fichiers déjà existants), si bien que le schemas/index.js de l'instantané exige un module qui n'existe pas du côté enterprise, et la politique fail-loud du chargeur transforme l'échec d'enregistrement du plugin en une boucle de plantage de démarrage. La correction voyage comme de la documentation : le carnet de route de reconstruction du dépôt enterprise couvre désormais la distinction réinstalle-ne-pas-recompile (rafraîchit l'instantané avec pnpm install, puis pnpm build), et le carnet de route d'installation air-gap dans docs/ENTERPRISE_PLUGIN.md gagne la sous-section de débogage correspondante (le symptôme, la cause, la correction, le contrôle de bon sens) pour que les opérateurs sans le dépôt privé trouvent quand même le remède.
v0.21.0
2026-08-28

Balayée du dettes & hygiène des releases (était : v1.5.0)

Added

  • docs/LICENSE_DECISION.md — l'analyse du modèle de licence pour la distribution double (community + enterprise). Décision : AGPL-3.0 (community) + commercial propriétaire (enterprise).
  • LICENSE remplacé Apache-2.0 → GNU AGPL-3.0 (le dépôt community).
  • LICENSE_EE.md — la licence commerciale propriétaire du plugin enterprise (@simmetric-chat/enterprise).
  • NOTICE — l'explication de la double licence à la racine du dépôt.
  • CLA.md — l'Accord de Licence de Contributeur (v1.0) — copyright + concession de brevet, conscient de la double licence. Requis pour toute contribution externe.
  • Les en-têtes de copyright AGPL-3.0 ajoutés aux 981 fichiers source sur 5 paquets (.ts/.tsx). Appliqués par scripts/add-headers.cjs (idempotent).
  • docs/ENTERPRISE_LICENSE_TERMS.md — les termes commerciaux/clause de niveau de service à hauteur d'opérateur pour les clients enterprise.
  • .github/workflows/release.yml — GitHub Release + la publication d'images GHCR à la poussée du tag.
  • CHANGELOG.md (ce fichier) — le format Keep-a-Changelog, semé de l'histoire des jalons.
  • .planning/milestones/v1.5-debt-sweep-ROADMAP.md — le périmètre du jalon d'évaluation des dettes (le portage E2E, les sessions de débogage, le triage des tâches rapides, la discipline de synchronisation des estampilles de version).

Changed

  • La version de package.json portée 0.17.0 → 1.4.0 (synchronisée avec le dernier tag git v1.4). Les versions par paquet restent à 0.22.0/0.23.0 (paquets privés, publiés sur aucun registre).
  • Le champ license de package.json + tous les packages/*/package.json mis à AGPL-3.0-or-later.
  • Le badge de licence du README.md mis à jour Apache-2.0 → AGPL-3.0 ; la section licence mise à jour pour décrire le modèle de double licence ; la section Contributing ajoutée avec l'exigence du CLA.
  • CONTRIBUTING.md mis à jour avec les instructions de signature du CLA.
v0.20.1
2026-08-27

Échelle horizontale (l'achèvement de la couche Redis) (était : v1.4)

Goal : fermer les 4 éléments reportés SCALE (CSW-19 + SCALE-02/03/04) et prouver la mise à l'échelle horizontale avec un vrai test d'intégration multi-instances — activant un déploiement serveur multi-instances sûr derrière un équilibreur de charge.

Shipped : 7 phases (161–167), 13 plans, 29 tâches. Les 22/22 exigences satisfaites. Tag git : v1.4.

Added

  • SCALE-01 (CSW-19) : les 3 planificateurs restants enveloppés de verrou → puis les 8 migrés aux travaux cron pg-boss ; setInterval + isRunning + withDistributedLock retirés des 8 planificateurs migrés.
  • SCALE-02 : le repli scryptSync de ENCRYPTION_KEY retiré en production — un démarrage fail-loud (logger.error + process.exit(1)) + un lancer de service defense-in-depth ; dev/test garde scrypt ; le carnet de route de rotation mis à jour.
  • SCALE-03 : la vérification des clés API de boucle bcrypt → une recherche de digest findUnique({key_hash}) O(1) à HMAC clef ; une migration brisante DROP COLUMN hashedKey + ADD COLUMN key_hash ; la variable d'environnement API_KEY_HMAC_SECRET ; le carnet de route de migration opérateur docs/API_KEY_MIGRATION.md.
  • SCALE-04 : 8 planificateurs setInterval migrés à pg-boss (une file de travaux portée par Postgres, amie de l'air-gap) ; 2 sondes à 10s sensibles à la latence (OCR + synthèse) restent setInterval ; le singleton pg-boss (jobQueue.ts) avec dégradation gracieuse.
  • La vérification du fan-out SSE : la mise en attente transversale aux instances fermée — 4 tests transversaux aux instances sur mocks prouvant le relais SSE Redis pub/sub (A publie → B relaie, le saut d'origine, le détricotage, la dégradation).
  • La doc opérateur : docs/SCALING.md (377 lignes) — la topologie du déploiement multi-instances, l'exigence REDIS_URL, la dépendance Postgres de pg-boss, les renvois croisés SCALE-02/03, les sondes de 10s, la dégradation gracieuse.

Modifications brisantes — l'action opérateur requise

  • `ENCRYPTION_KEY` doit être réglée au .env de production (base64, 32 octets) ou le serveur refuse de démarrer. Voir docs/ENCRYPTION_KEY_ROTATION.md.
  • Les clés API passées de bcrypt (apiKeyHash) → HMAC-SHA256 (key_hash). Les opérateurs doivent ré-émettre les clés API widget/collector. Voir docs/API_KEY_MIGRATION.md.

Infrastructure

  • Une nouvelle dépendance : pg-boss v12.28.0 (serveur). Une nouvelle variable d'environnement : API_KEY_HMAC_SECRET. L'ordonnancement de démarrage change — l'initialisation pg-boss est désormais la 7e étape d'init async avec un détricotage d'arrêt.
v0.20.0
2026-08-26

La balayée des CONCERNS (était : v1.3)

Goal : balayer l'audit CONCERNS.md du 2026-08-26 — corriger toutes les victoires faciles directement, affronter les éléments d'effort moyen en phases focalisées, et évaluer (reporter ou documenter) les points bloquants/à grand effort.

Shipped : 7 phases (154–160), 14 plans. Un jalon d'irrobustissement brownfield ; aucune nouvelle fonctionnalité visible à l'utilisateur.

Added

  • le correctif du gitignore de la doc, la carte de fichiers de chat.ts, les bascules de règle de lint.
  • les victoires faciles d'irrobustissement du code (le cap take:10 de apiKeyMiddleware, etc.).
  • la réduction du det de lint du frontend (3 plans, les bascules warn→error).
  • l'avertissement de démarrage de production de ENCRYPTION_KEY (CSW-11) + la génération groupée de titre+tags+followUps.
  • les scissions de modules (l'extraction du parseur de fournisseurs, les registres de capacités).
  • la couverture de tests (chatImportService, chatExportService, apiKeyService, 5 services de plus ; collector + widget câblés dans le config root de jest).
  • évalué et documenté (les droits JWT de licence du .env, le carnet de route de rotation de la clé de signature de licence, le correctif de l'affirmation périmée d'AGENTS.md).
v0.19.1
2026-08-25

Les raffinements (était : v1.2)

Goal : le branding, la dénonciation IA, la reimplementación TypeScript de l'algorithme de graphe du wiki, les correctifs de recherche RAG, l'irrobustissement MCP, le premier lancement de l'Assistant de Configuration.

Shipped : 6 phases (149–153 + 153.1).

Added

  • BRAND-01/02 : le favicon SVG du monogramme S, le composant Monogramme réutilisable, la dénonciation IA localisée et atténuée sous chaque message de l'assistant (7 locales).
  • MCP-01/02/03 : l'irrobustissement MCP — la Map de transport SSE par session, la barrière d'auth Bearer MCP_API_KEY, la fermeture de l'IDOR de list_workspaces, getMCPToolsForWorkspace câblé dans resolveSkillsForChat.
  • RAG-01/02/03 : le correctif de localité FTS (la tokenisation multi-locale pour it/ru/de/es/fr/zh), la déduplication des résultats de repli rag_search↔wiki_query, le prompt d'affordance LLM pour rag_search vs wiki_query.
  • WIKI-01 : la reimplementación TypeScript de l'algorithme Graphify du wiki (la détection de communauté + la génération markdown du wiki, la reimplementación propre d'Apache-2.0, ~280 LOC Python → TS).
  • WIZ-01/02 : l'UI de premier lancement de l'Assistant de Configuration (multi-étape : le compte admin + la configuration du fournisseur + la base de vecteurs, la consommation de /api/system/initialize) ; SEED_BOOTSTRAP_ADMIN désactivé quand l'assistant est actif.
  • .1 : le nettoyage du det technique de v1.2 (la bascule warn→error de React Compiler).
v0.19.0
2026-08-19

L'architecture du plugin Enterprise (était : v1.1)

Goal : refactorer le modèle de licence des « drapeaux de capacité à l'exécution dans la source publique » à un « plugin enterprise privé séparé » : les capacités enterprise (SSO, journal d'audit, livre blanc, sauvegarde) vivent dans un paquet privé livré seulement aux clients payants ; le dépôt community ne contient aucun code de capacité enterprise. Les capacités marchandise deviennent toujours-AC TIVES dans la communauté.

Shipped : 9 phases (140–148), 19 plans, 26 tâches.

Added

  • EPA-01 : les types PluginContext/EnterprisePlugin dans shared ; la couture de chargeur loadEnterprisePlugin(app) / shutdownEnterprisePlugin() avec require.resolve→require en deux pas (MODULE_NOT_FOUND = no-op de la communauté, les autres erreurs = process.exit(1) fail-loud).
  • EPA-02 : 9 drapeaux marchandise retirés de FEATURE_FLAGS (20→10) : web_search, webhooks, push_notifications, memory, lead_export, widget_analytics, auto_title, synthesis_rate_limit. Les routes marchandise toujours-AC TIVES.
  • EPA-03 : SSO (SAML + OIDC + SCIM 2.0) extrait au plugin enterprise.
  • EPA-04 : le journal d'audit (la table event_log immuable + un rôle de base INSERT-seulement) extrait au plugin enterprise.
  • EPA-05 : le branding livre blanc (le crochet IoC du validateur de clé de config) extrait au plugin enterprise.
  • EPA-06 : la Sauvegarde (~1400 LOC : backupService, le registre de fournisseurs, le planificateur Bree, la restauration, les routes, l'UI) extraite au plugin enterprise.
  • EPA-07 : le résolveur overrideFeatureLimit avec révocation réactive ; le manifeste GET /api/enterprise/modules ; le chargement conditionnel React.lazy de 4 panneaux enterprise.
  • EPA-08/09/10 : priority_support retiré ; custom_agents = une limite numérique (community 3, enterprise l'Infinité) ; licensePayloadSchema identique au bit près (RS256).
  • EPA-12 : l'alignement docs/env (docs/ENTERPRISE_PLUGIN.md, le mont de volume docker-compose, la scission de .env.example), le profil CI air-gap (une barrière de grep sur le service de licence pour les primitives HTTP).
v0.18.0
2026-08-12

Le candidat de release publique (simmetric-chat) (était : v1.0)

Goal : préparer et publier le projet comme un dépôt public propre, renommé simmetric-chat, sans secrets/données personnelles/chemins locaux, avec la vérification de licence des dépendances, un générateur de clés enterprise (séparé, non publié), et une documentation complète.

Shipped : 8 phases (132–139), 12 plans, 16 tâches. Le dépôt public publié, le renommage complet, l'audit de licences, le générateur de clés enterprise (HS256 → migré à RS256 après la v1.0), les docos canoniques, le CI GitHub Actions, un dépôt neuf.

v0.20
2026-08-11

Le widget UX, l'i18n & la fiabilité

Shipped : 7 phases (125–131), 32 plans, 75 tâches, 16/16 exigences satisfaites.

Added

  • WID-ADMIN : la sous-page à onglets de création/édition du widget (/widgets/:id + /widgets/new), le panneau d'aperçu en direct, la garde de souillure, l'onglet des leads.
  • WID-I18N : le transport par blocs JSON (tue le bogue %20), la chaîne de repli visiteur→défaut-du-widget→en, la localisation de coque + contenu à 7 locales, le filetage de la localisation widget→proxy→serveur→orchestrator.
  • WID-QUEST : les questions suggérées à trois états (Défaut/Aucune/Personnalisées), des groupes à 7 locales, le correctif de débordement line-clamp.
  • WID-CREDITS : les crédits de bas de page + le badge IA, le retrait barré par le livre blanc, le relais de popup real-embed vérifié.
  • WID-BUGS : les boutons d'entrée de 44px, le aller-retour de l'hôte-FAB, le contrat d'icônes SVG-seulement, la bannière de contact, la boucle du wiki, la persistance des leads, l'i18n du RAG dégradé.
v0.19
2026-08-08

Les diagnostics & les dettes

Shipped : 5 phases (120–124). Les diagnostics de licence (les journaux de démarrage, un endpoint diagnose admin-seulement, le script pnpm license:check), le det de test (la dérive ESM, LoginPage QueryClientProvider, la spé de forme de fixtures), l'évaluation du det technique (le pic de la couche d'échelle Redis — reporté à la v1.4).

v0.18
2026-08-04

La stabilisation

Shipped : 4 phases (116–119).

v0.17
2026-08-02

Enterprise & les finitions

Shipped : 8 phases (108–115).

Les jalons antérieurs (v0.1–v0.16) sont documentés dans .planning/PROJECT.md (la section Milestone History). L'histoire entière au niveau des phases vit dans .planning/MILESTONES.md.