Product changelog
Every Simmetric Chat release, from the newest version to the oldest.
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) + plafondusers.widgetQuotaLimitpar propriétaire ; NULL/fail-closed 0 — les installations intactes ne voient aucun changement de comportement (l'idiome du treillismaxSponsoredUsersde la Phase-206). - Le CRUD des widgets ré-armé pour les PROPRIÉTAIRES non-admin :POST /api/widgetspasse par la nouvelle permissionwidget: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 : avecWORKSPACE_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étaireWorkspaceAccessDialoget le panneau SettingsSettingsWorkspaceAccess) 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 surrag_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 retirerag_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 endpointGET /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 viaattachmentPending) jusqu'à « completed », et laisse tomber l'attachement avec un toast surfailed/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'objetuserembarqué (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 champuserjamais 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 estdisabledtant qu'un projet n'est pas choisi, filtrée parprojectId === selectedProjectId— tandis que la page Workspaces énumérait l'espace de travail partagé (le OR-filter deGET /api/workspacesinclut le volet de concessionaccessGrants.some(userId)) mais ses lignes étaient gestion-seule, sans moyen d'y entrer.WorkspaceRowobtient 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("/") — persistelastWorkspaceId, 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 seulcurrentWorkspaceId. 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éworkspaceSelectPlaceholderKeydanssidebar/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 ;workspaceSelectDisabledbarre les monts deAppNavOverlay— l'inlineAppSidebarNavne se rend que sur les éléments hôtes). Les clés i18nworkspace.open+sidebar.selectSharedWorkspace+sidebar.sharedProjectsajoutées aux 8 locales — la parité est verte. Nouvelles épingles : les lignes lecteur montrent Open et jamais delete/edit, unonOpenabsent ne rend aucun contrôle, l'épingle à cheval de page vérifie l'écriturelastWorkspaceIdau 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
DialogContentdu kit d'UI est ungridSANSgrid-template-columnsexplicite, si bien que les enfants se posaient sur UNE seule piste impliciteautoqui 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 ajoutebreak-wordsau titre interpolé +flex-wrapaux 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 nouveauWorkspaceAccessDialog.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 optionnelusermort — 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 ;WorkspaceAccessGrantobtient le champusernameque 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 marquedata-active).e2e/settings-left-menu-nav.spec.tsest de nouveau vert. - Vérifié aussi comme pas-un-bogue :
POST/DELETE /api/system/settings/branding/iconsont 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-12whiteLabelExtractionGuard) ; 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 permissionadmin: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 deSettingsPageallait chercherGET /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/settingsest désormais permission-aware. Les administrateurs (porteurs deadmin:settings) gardent la charge utile console complète identique au bit près ; tout autre utilisateur authentifié ne reçoit que le sous-ensembleNON_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_UPLOADpour la page des téléversements) — chaque entréereadOnly: 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 deSettingsPageet le volet d'erreur deuseMed'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.parseFileflaire 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 timbreroutedAsPdfet 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.
Added
- Connecteurs : les connecteurs existants peuvent désormais être MODIFIÉS depuis l'UI admin (Q1-quick) — le volet
PUT /api/connectors/:idpré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).isEnabledreste sur le Switch de la carte, TelegrampollModesur les boutons webhook, et plateforme/botToken/workspace restent immuables par schéma. Le volet PUT gagne aussi l'événement d'auditchat_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'environnementLICENSE_KEY→ Community. Le PUT de masse/api/system/settingsrefuse 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.canModifyadditif (se retranche du repliSETTINGS_SECTION_MODIFY_PERMISSIONS— n'accorde jamais,admin:settingsterminale),settingsModifydansGET /api/roles/me/menu-sections, une barrière serveur sur les mutations de section renvoyant403 { 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/visibilityacceptecanModifypar 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-80dont le bouton du kit d'UI estwhitespace-nowrap shrink-0PAR 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ésormaisw-[22rem] max-w-[calc(100vw_-_2rem)](plus larges + bornées au viewport), les rangées de boutonsflex-wrap, les deux étiquettes d'action longues passent outre le nowrap (whitespace-normal text-left), et les titres/textes interpolés portentbreak-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 bascalc(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:3bquand 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 deChat.modeletWorkspaceAgentConfig.modelchangé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 écriventexplicit: 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 additifchatportant 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 founden provenance d'une table LanceDB qui légitimement n'existe pas jusqu'à la PREMIÈRE écriture de mémoire (l'ensureTablede 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_FLAGSest restée en arrière par le passage de connecteurs à l'EE (quick 260930-l4q) — l'épingle portée de 12 → 13 pourconnectors_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_KEYprécédente) renvoie désormais une erreur guidée nommant la cause, les leviers de rétablissement (LEGACY_PREVIOUS_ENCRYPTION_KEYS/ uneENCRYPTION_KEYprécédente, puis le redémarrage), le nombre de clés essayées, et le carnet de routedocs/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-testscripts/repo-boundary-check.test.cjs+ le poste CIrepo-boundary-checkcâblé dansbuild.needs
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-motionpour 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/saasse 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 surMODULE_NOT_FOUNDen 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
adminrescapé à chaque démarrage (consigné dans les éléments reportés de STATE.md). - Locataires :
POST /api/auth/registercommite 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 leroleInOrgde l'appelant pour que la suppression dégrade les privilèges (WR-02). - Builds enterprise : la section Settings → Advanced → Backup (et les routes
/ssoet/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
.envracine est déployé avec un placeholder du gabarit.env.examplenon rempli (<sostituire-con-valore-generato>) comme valeur deAPI_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 avecNODE_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êteX-Collector-Secreten temps constant (crypto.timingSafeEqualavec 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/loginvalide désormais son corps au niveau de la route avec le schéma partagéloginSchema(safeParse) — un corps invalide ou vide renvoie400 { error, details }cohérent avec chaque autre route d'auth, au lieu d'un401portant un message Zod brut. Des identifiants valables-mais-faux renvoient toujours401 "Invalid credentials". - Authentification :
loginexécute désormais unbcrypt.comparefactice 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ême401 "Invalid credentials". - Authentification :
POST /api/auth/admin-reset-passwordvalide désormais son corps avec le nouveau schéma partagéadminResetPasswordSchema(userIdUUID +newPasswordde 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/registerne duplique plus la logique d'auth en ligne — elle utiliseverifyToken/isTokenRevoked/isAdmin/getCachedUserWithRolesimportés statiquement (pas deawait importdynamique, 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êteX-Api-Keyen temps constant (crypto.timingSafeEqualavec 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-openexpected &&— 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-qemuest retiré, chaque entrée de matrice publielinux/amd64, et la matrice de build est désérialisée enmax-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 dedocs/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 additiveadd_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.
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), eteu_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 deployavec la sûreté de rejeuON 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-forgetdlp.bypassedconsignant 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.*portesource: "chat" | "widget"dans ses métadonnées — le noyau partagé du flux SSE tire la marque de l'en-têteX-Widget-Iddu 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) viaPUT /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.exampledocumente 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 badgeenvOverriddendans 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.envpar 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 (leenv_filedu compose pointe le fichier racine, aucune image ne cuit des secrets). - Config : les trois fichiers
.env.examplede 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 deprocess.envtenues 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 parPUT /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'imageollama/ollaman'embarque pas curl/wget/node — le conteneur rapportait « unhealthy » pour toujours) par une sondeollama ls; le healthcheck qdrant a remplacé la sondewget(l'image n'embarque pas wget/busybox) par un contrôle de socket bash/dev/tcp— les deux services rapportent désormaishealthy. L'docker/entrypoint.shdu conteneur unique fait désormais le sourcing deprovision-encryption-key.sh(à parité avec l'image séparée) : les démarrages autonomes de production auto-provisionnentENCRYPTION_KEY+API_KEY_HMAC_SECRETau 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_matchde 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 tableaumatchTypesvide 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_matchnon-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//assignse 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/:idtraite 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_KEYpersistante (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.exampleetdocs/DEPLOYMENT.mdmis à jour pour marquer la variable comme Requise-en-production. Voirdocs/ENCRYPTION_KEY_ROTATION.mdpour 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
doneaprès une transcription complète (observé avec le moteur personnalisée glm-ocr) :ocrPagesauve désormais la sortie non vide accumulée comme le résultat de la page avec un drapeautruncatedet 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 commefailedPagesdans 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/sharedgagne de NOUVEAUX fichiers source, un instantané périmé de la dépendancefile:enterprise peut planter le serveur au démarrage avec « Cannot find module './env.schema' » — pnpm prend l'instantané du paquetfile:dansnode_modules/.pnpmet les fichiers créés après le dernierpnpm installn'atteignent jamais cet instantané (les liens durs ne mettent à jour que les fichiers déjà existants), si bien que leschemas/index.jsde 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é avecpnpm install, puispnpm build), et le carnet de route d'installation air-gap dansdocs/ENTERPRISE_PLUGIN.mdgagne 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.
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).LICENSEremplacé 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 parscripts/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.jsonportée0.17.0→1.4.0(synchronisée avec le dernier tag gitv1.4). Les versions par paquet restent à0.22.0/0.23.0(paquets privés, publiés sur aucun registre). - Le champ
licensedepackage.json+ tous lespackages/*/package.jsonmis àAGPL-3.0-or-later. - Le badge de licence du
README.mdmis à 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.mdmis à jour avec les instructions de signature du CLA.
É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+withDistributedLockretirés des 8 planificateurs migrés. - SCALE-02 : le repli
scryptSyncde 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 brisanteDROP COLUMN hashedKey+ADD COLUMN key_hash; la variable d'environnementAPI_KEY_HMAC_SECRET; le carnet de route de migration opérateurdocs/API_KEY_MIGRATION.md. - SCALE-04 : 8 planificateurs
setIntervalmigré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) restentsetInterval; 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
.envde production (base64, 32 octets) ou le serveur refuse de démarrer. Voirdocs/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. Voirdocs/API_KEY_MIGRATION.md.
Infrastructure
- Une nouvelle dépendance :
pg-bossv12.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.
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).
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,getMCPToolsForWorkspacecâ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).
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/EnterprisePlugindans shared ; la couture de chargeurloadEnterprisePlugin(app)/shutdownEnterprisePlugin()avecrequire.resolve→requireen 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_logimmuable + 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
overrideFeatureLimitavec révocation réactive ; le manifesteGET /api/enterprise/modules; le chargement conditionnelReact.lazyde 4 panneaux enterprise. - EPA-08/09/10 :
priority_supportretiré ;custom_agents= une limite numérique (community 3, enterprise l'Infinité) ;licensePayloadSchemaidentique 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).
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.
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é.
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).
La stabilisation
Shipped : 4 phases (116–119).
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.