Skip to content

Release 1.624 — hotfix upload image PROD + plugin Pour/Contre (éteint) - #626

Merged
cslucki merged 57 commits into
mainfrom
develop
Sep 23, 2026
Merged

cslucki merged 57 commits into
mainfrom
develop

Conversation

@cslucki

@cslucki cslucki commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Release PROD 1.613 → 1.624

Synchronisation de main sur develop après 11 merges de PR accumulés depuis le 20/09. Motif prioritaire : TASK-1623 corrige un bug d'upload d'image incompatible avec le stockage temporaire distant de PROD.

Le correctif qui motive cette release

En PROD, filesystems.default = public, public.driver = s3, et Livewire n'a pas de disque temporaire dédié — il retombe donc sur S3. TemporaryUploadedFile::getPathname() y rend un chemin relatif (livewire-tmp/xxx.png), qu'Intervention Image ne peut pas résoudre : DirectoryNotFoundException.

Quatre corrections d'une ligne, Image::decode($file)Image::decode($file->get())le contenu, jamais le chemin — sur ChatLoop, MessageThread, CreateFeedPost et EditFeedPost. Cette configuration a été mesurée sur PROD, pas supposée.

TASKS_INCLUDED

T1614, T1615, T1616, T1617, T1618, T1619, T1620, T1621, T1622, T1623, T1624.

Migrations : 5, toutes SAFE

Migration TASK Type
create_organization_loop_plugins_table T1614 CREATE TABLE
create_loop_plugins_table T1616 CREATE TABLE
create_loop_ai_assistants_table T1616 CREATE TABLE
create_loop_plugin_ai_models_table T1617 CREATE TABLE
add_paid_approval_to_loop_plugin_ai_models_table T1622 ALTER sur une table créée dans le même déploiement, donc vide

Liste établie par différencemigrate:status sur PROD ne voit que les fichiers du code déployé. 281 migrations dans develop moins 276 réellement jouées en PROD = 5. Contrôle inverse fait, et vérification table par table sur la base : les 4 tables n'existent pas. Les deux méthodes concordent.

Aucune colonne NOT NULL sur une table peuplée, aucun index sur une table volumineuse, aucune suppression. Volumes réels : 2 organisations, 47 utilisateurs, 4 Boucles. Downtime attendu nul.

Un rollback code après ces migrations est sûr : elles n'ajoutent que des structures que le code 1.613 ignore totalement.

Ce qui reste éteint

Tout le bloc T1614→T1622 est fermé par défaut : la chaîne catalogue → disponibilité Organization → activation Boucle lit « absence de ligne = non disponible ». Les tables arrivant vides, le plugin Pour/Contre est invisible et inerte. Aucune dépense IA possible : sans ligne dans loop_plugin_ai_models, un assistant est refusé avant tout appel provider.

7 variables d'environnement nouvelles, toutes avec un défaut — aucune bloquante.

Aucun changement de dépendance : composer.json, composer.lock, package.json ne bougent pas.

Protection de la base

PITR Neon 7 jours confirmé par l'API Laravel Cloud (db-cluster:get, config.retention_days = 7), recoupé par db-cluster:list et corroboré par db-restore:create --point-in-time.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3

Cyril and others added 30 commits September 21, 2026 12:47
…que des lignes (TASK-1615)

`loop_messages.created_at` est un `timestamp` de precision 0 — la seconde
pleine, sans sous-seconde. Des messages ecrits dans la meme requete portent
donc EXACTEMENT la meme valeur, et la base locale compte deja 8 groupes
(loop_id, created_at) reellement a egalite.

`LoopClaimCompiler::sourceMessages()` ordonnait par `created_at` SEUL. A
egalite, PostgreSQL rend un ordre indefini, libre de varier entre deux
requetes identiques selon l'etat du tas. Or `empreinteSource()` hache
l'ORDRE : deux lectures d'une source INCHANGEE rendaient deux empreintes.

La garde de cout de TASK-1541 cedait alors — `hash_equals` echouait, le
compilateur resolvait un provider et appelait le modele. Le balayeur
automatique passe toutes les dix minutes : une conversation ENDORMIE pouvait
etre refacturee a chaque passage. Defaut de production, pas flake de test.

`(created_at, id)` est l'ordre canonique deja documente par
ClaimResurrectionGuard et applique par GuestConversation. sourceMessages()
etait le lecteur qui ne l'appliquait pas.

MESURE — le test echoue AVANT la correction (PostgreSQL) :

  ✓ premisse : les 3 messages portent bien le meme created_at
  ⨯ la requete de source porte un ordre TOTAL
  ⨯ l empreinte ne bouge pas quand l ordre physique des lignes change
  ✓ l ordre rendu est stable d un appel a l autre

Apres : 4/4 verts ; TASK1541ClaimRetrievalTest 9/9 ; Knowledge + Architecture
573 verts ; et le shard Feature 4/6 EXACT qui avait rougi deux fois en CI
rejoue OK (1355 tests, 6424 assertions).

Decouvert par la CI de la PR #615 : TASK-1614 n'ajoute qu'un fichier de tests,
mais celui-ci redistribue les six shards et remue la table autrement. La
branche a EXPOSE le defaut, elle ne l'a pas cause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…615)

Second cas de la MEME famille, expose par la redistribution des shards.

CI de la PR #616 : shard 4/6 VERT (le defaut vise est ferme), mais shard 1/6
rouge sur TASK1096SeriesNumberingTest::test_numbering_is_never_persisted :

  -    0 => 'La racine'        +    0 => 'Une annexe'
  -    1 => 'Une annexe'       +    1 => 'La racine'

`blog_posts.created_at` est AUSSI un timestamp de precision 0. Les deux
articles sont ecrits dans la meme requete, donc a la meme seconde : a egalite
`created_at` seul ne departage rien et PostgreSQL rend un ordre indefini. Le
test asserait donc un ordre que la requete ne garantissait pas.

Le defaut est dans le TEST, pas dans le code metier. Ajouter le departage ne
l'affaiblit pas : il lui fait asserter quelque chose de DEFINI.

Perimetre STRICTEMENT borne par arbitrage : cette ligne seule. Les 66 autres
occurrences de orderBy('created_at') dans tests/ ne sont ni auditees ni
corrigees ici.

Rejeu PostgreSQL local : shard 1/6 OK (1299 tests), shard 3/6 OK (1462 tests,
ou le fichier a migre apres l'edition), shard 4/6 OK (1331 tests).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…ssage-pour-l-empreinte-source-de-loopclaimcompiler

TASK-1615 — Ordre TOTAL des LoopMessage pour l'empreinte source de LoopClaimCompiler
…zation (TASK-1614)

SLICE A de la Product Spec V0 « ChatLoop — 3 assistants IA », validee
repo-grounded par OPUS et passee a VALIDATED sur cette branche.

Ce que cette TASK rend possible, et rien de plus :

    « ce plugin existe au catalogue plateforme, et il est autorise ou
     non pour telle Organization. »

Le CATALOGUE est un fichier (config/loop_plugins.php), pas une table :
un deuxieme plugin doit rester une entree de configuration, comme un
cinquieme type de Boucle l'est dans config/loop_types.php.

La DISPONIBILITE est une table (organization_loop_plugins), parce que
c'est une decision, qu'elle change sans deploiement et qu'elle est
toujours celle d'UNE Organization. organization_id y est NOT NULL :
loop_type_settings l'accepte nullable parce qu'un reglage de type existe
a deux portees et que null y veut dire « la Plateforme » ; ici un null
voudrait dire « disponible partout », soit le contraire de ce qu'une
capacite experimentale doit faire quand personne n'a rien decide.

Ferme par defaut. Eteindre n'efface pas la ligne : available=false
conserve qui a coupe et quand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…tre du cycle de vie (TASK-1614)

CI rouge, shard Feature 3/6 :
UserDataLifecycleRegistryTest::test_every_user_foreign_key_is_declared_in_lifecycle_registry
-> 'Every FK to users.id must be classified in UserDataLifecycleRegistry.'

La migration ajoute une cle etrangere vers users.id (updated_by, qui porte
l'auteur de la decision de disponibilite). Le depot tient un registre
DECLARATIF de toutes ces cles : une cle non classee est une donnee
utilisateur dont personne n'a dit ce qu'elle devient a la suppression du
compte. La garde a fait exactement son travail.

Politique DETACH, org_scope direct, sur le modele de
custom_loop_types.created_by et ai_credit_setting_changes.changed_by : savoir
si un plugin est autorise dans une Organization est une decision
d'administration, pas une donnee personnelle. Elle doit survivre a son auteur,
qui est simplement detache — la cle est deja nullOnDelete.

Mesure : 30 verts en PostgreSQL (UserDataLifecycle + TASK1614).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…istants-ia-disponibilite-organization

TASK-1614 — Catalogue plugin « 3 assistants IA » + disponibilité Organization
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…tants se reglent (TASK-1616)

SLICE B de la Product Spec V0 « ChatLoop — 3 assistants IA ». Les deux
maillons suivants de la chaine posee par SLICE A :

    catalogue plateforme -> disponibilite Organization
      -> ACTIVATION BOUCLE -> CONFIGURATION DES 3 ASSISTANTS

LE PLUGIN N'EST PAS UNE LOOPCARD, et c'est la decision centrale.

Le bloc « Actions de ChatLoop » est pilote par les Cards de placement
chat_action (« Resume IA »). Modeliser le plugin ainsi aurait semble naturel
et aurait ete faux : LoopPresetConfigurator::canConfigure() est
proprietaire-only — « un animateur anime, il ne recompose pas » — et la
matrice mesuree donne loops.manage_cards = NON pour owner, facilitator ET
member, TASK-1083 l'ayant retiree au motif qu'elle annoncait un droit que
l'interface ne donnait pas.

Passer par la Card imposait donc de choisir entre exclure le facilitator
(contre le CDC §5) et percer une doctrine posee expres. Le plugin garde son
PROPRE etat, rendu A COTE de « Resume IA », et son PROPRE droit :
loop_plugins.configure — owner + facilitator, membre refuse, SuperAdmin et
Admin d'Organization par les etapes 3 et 4 du resolveur.

LE HARD GATE VIT DANS LE SERVICE, pas dans l'ecran. Cacher un bouton n'a
jamais empeche un POST : chaque lecture et chaque ecriture reconfronte la
disponibilite de l'Organization de la BOUCLE. Retirer l'autorisation eteint
toutes ses Boucles sans reecrire une ligne.

ECART, PAS COPIE. Une posture identique au defaut n'est pas stockee, et vider
un champ SUPPRIME l'ecart — sinon la Boucle figerait le texte du jour. Les
postures SURVIVENT a l'extinction, dans la Boucle comme dans l'Organization.

ZERO APPEL PROVIDER : ni capability, ni AiEconomicGuard, ni ai_pricing, ni
OpenRouter, ni AiTurnLock, ni Evidence Build, ni follow-up.

MESURES
  27/27 verts. Sabotage 1 (gate Organization neutralise) -> 6 rouges ;
  sabotage 2 (droit retire au facilitator) -> 3 rouges. Voisins : 238 verts.
  Recette navigateur : les deux surfaces, l'ecran des trois postures, la
  persistance relue, et 403 en URL directe sur une Organization non autorisee.

UX_DEBT_SLICE_E : le facilitator detient le droit mais n'atteint pas /outils,
garde par canConfigure() des Cards, anterieur a cette TASK. Arbitrage de
Cyril : ne pas ouvrir cette page, reporter la decouvrabilite a la SLICE E sur
une surface accessible depuis ChatLoop. Consigne comme dette, pas comme bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…-assistants-ia-dans-une-boucle-et-configuration-aperio-traverse-limen

TASK-1616 — Activation du plugin « 3 assistants IA » dans une Boucle + configuration Aperio/Traverse/Limen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…nomie (TASK-1617)

SLICE C de la Product Spec V0. Le SuperAdmin choisit un modele OpenRouter
VERIFIE GRATUIT par assistant, la capability loop_multi_ai existe, et un
modele prouve gratuit vaut un cout CONNU de 0. Zero generation.

LE SERVICEPROVIDER DE FUSION A ETE ABANDONNE, et c'est la decision centrale.

Fusionner les slugs gratuits dans config('ai_pricing.models.openrouter') au
boot semblait le chemin court. Deux raisons l'ont tue, la seconde dirimante :
lire la base au boot rend `artisan migrate` dependant d'une table metier ; et
surtout `php artisan config:cache` SERIALISE la configuration dans un fichier
— la fusion y serait FIGEE, donc une preuve de gratuite perimee servie
indefiniment, exactement l'inverse de la fraicheur de 15 minutes exigee.

A la place : DynamicPricingSource, resolue A L'APPEL, consultee seulement la
ou la configuration versionnee n'a rien dit. Elle ne calcule aucun cout : elle
propose une entree qu'AiPricingCatalog valide comme les autres. UNE autorite
de prix, UN ledger. `bind` et non `singleton` : rien n'est lu au boot.

LE CHIFFRAGE NE SORT JAMAIS SUR LE RESEAU. entryFor() lit exclusivement la
preuve persistee. Rafraichir appartient a LoopPluginModelGuard, sur le chemin
de la generation — la ou un echec est un refus, pas une trace perdue.

LA GARDE EST INDEPENDANTE DU CATALOGUE DE PRIX. Savoir COMBIEN ca coute
n'autorise pas a APPELER : un tarif statique historique ne doit pas laisser
generer un modele dont la preuve FREE a expire.

La gratuite se PROUVE sur les tarifs : prompt, completion ET request a 0,
sortie texte exclusivement, tout poste applicable non explicitement ignore
doit valoir 0. Le suffixe « :free » n'a aucune autorite.

DEUX DEFAUTS TROUVES PAR LA RECETTE, pas par les tests :

1. « openrouter/free » etait selectionnable. Il est bien prouve gratuit, et
   refuse quand meme : ce routeur choisit lui-meme son modele, donc la trace
   nommerait le routeur plutot que le modele employe (CDC section 3).

2. timestampTz rendait TOUTE preuve perimee, en PostgreSQL seulement. La
   valeur revenait dans le fuseau de la session (+02:00 en CEST) face a un
   Carbon::now() en UTC : une preuve de 29 secondes se lisait 2 h dans le
   passe. La garde aurait refuse toute generation pendant l'heure d'ete.
   SQLite ignore les fuseaux — les 34 tests passaient.

ECONOMIE : loop_multi_ai entre dans LEDGER_AUTHORITY_SINCE_BY_PROCESS (garde,
16 process) et PAS dans CREDITABLE_PROCESSES (credit, 14, inchange). Deux
semantiques, jamais fusionnees. Les deux recensements voisins (T1286, T1261)
ont rougi et ont ete mis a jour avec leur raison — c'est leur role.

ProviderResolver INCHANGE. Aucun BYOK. Contrat credential pour SLICE D
consigne dans le TASK file.

MESURES : 35 verts en SQLite ET en PostgreSQL ; 3 sabotages (TTL, reseau dans
le chiffrage, retour a timestampTz) ; 269 voisins verts ; recette sur le
catalogue REEL (446 modeles, 21 selectionnables, trois modeles differents
affectes et Operationnels).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…s (TASK-1617)

CI rouge au premier passage : six echecs, deux familles, aucune regression
fonctionnelle — ce sont toutes des gardes de DECLARATION.

1. UserDataLifecycleRegistryTest : loop_plugin_ai_models.updated_by n'etait
   pas au registre du cycle de vie. Politique DETACH, org_scope none (la
   table est PLATEFORME, sans organization_id).

   TROISIEME fois que cette garde parle dans la campagne, DEUXIEME oubli :
   TASK-1614 l'a enseignee, TASK-1616 l'a appliquee dans la meme salve,
   TASK-1617 l'a re-oubliee.

2. Cinq recensements de capabilities (T1227, T1253 x2, T1284, T1285).
   Ajouter une capability change trois choses que le depot fige exprès :

   - son LIBELLE HUMAIN doit exister dans les deux locales. loop_multi_ai
     n'en avait aucun — vrai manque : sans lui, la capability serait partie
     en production affichee comme une cle technique ;
   - la liste ORDONNEE des canoniques ;
   - les comptes : couvertes 15 -> 16, total 19 -> 20.

   Mis a jour avec leur raison. Ces gardes ont eu raison.

Mesure : 150 verts en PostgreSQL sur toute la famille touchee, 53 en SQLite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…uter-free-capability-loop-multi-ai-et-economie-ia

TASK-1617 — SLICE C : modèles OpenRouter free, capability loop_multi_ai, économie IA
Evidence Build unique via ContextBuilder, modele effectif par assistant,
ledger feature loop_multi_ai:<assistant>, traces evidence_shared.
39 tests verts. Point de restauration AVANT la salve de sabotage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…la partagent (TASK-1618)

SLICE D de la campagne ChatLoop « 3 assistants IA ». Une question, des preuves
construites UNE FOIS par ContextBuilder, trois generations sequentielles
Aperio -> Traverse -> Limen qui les partagent, et ce qu'il en reste ecrit.

Aucune UX : ce lot rend une structure, SLICE E decidera de l'affichage.

Trois invariants tenus, et mesures :

1. l'empreinte de SharedEvidence decide SI les preuves peuvent etre partagees.
   Elle ne remplace aucun controle : appartenance active, meme Organization,
   disponibilite du plugin et refus de source restent AVANT elle ;
2. un refus avant le provider n'ecrit aucune AiProviderInvocation mais laisse
   une trace metier nommee (assistant_key, turn_id, correlation_id, raison) ;
3. un assistant en ERROR ou REFUSED ne bloque pas les suivants.

Le moteur documentaire n'est JAMAIS appele : answer() GENERE, et l'appeler pour
ses preuves aurait paye une quatrieme generation hors ledger du plugin, sur le
modele de l'Organization.

Le modele effectif garde le provider, l'instance SDK et donc le credential du
tenant ; seul le slug bascule sur le modele prouve gratuit (TASK-1617). Le
ledger porte feature = loop_multi_ai:<assistant>, capability canonique.

40 tests (SQLite + PostgreSQL), 7 sabotages mesures. L'un d'eux est reste vert
et a revele un trou de couverture reel : le filet de securite externe n'etait
teste par personne. Un test a ete ajoute, le sabotage rougit desormais.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…ge-orchestration-sequentielle-3-assistants-et-traces

TASK-1618 — SLICE D : l'Evidence se construit une fois et les 3 assistants la partagent
… seul, publieur, synthese (TASK-1619)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…pChat (TASK-1619)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…ulle (TASK-1619)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…elles produit (TASK-1619)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…te responsive reel (TASK-1619)

tests/e2e/helpers/auth.js n'a JAMAIS ete commite : git log --all ne rend rien.
Le shim TASK-1317 dans ai/ (gitignore) pointait donc dans le vide, et TOUT le
banc e2e etait inexecutable — « Cannot find module » sur n'importe quel spec.

Le gate RESPONSIVE_HARD_GATE_SLICE_E utilise un VRAI viewport Playwright
(test.use), pas un redimensionnement MCP : 1614, 1616 et 1617 avaient annonce
« responsive verifie » sur une mesure qui restait a 1920.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
Quatre boutons (Aperio / Traverse / Limen / Demander aux 3), etats individuels,
traduction produit du 429, reponses partielles conservees, reessai HUMAIN,
follow-ups du meme appel, synthese Limen declenchee par l'humain, et la
decouvrabilite du plugin pour le facilitator.

Deux arbitrages MASTER tenus, et mesures :

- un assistant en echec n'ecrit RIEN dans le fil. Les reussites sont des bulles
  permanentes ; l'indisponibilite vit dans le composeur, chez le seul
  demandeur. Le fil est lu par tout le cercle et pour toujours : un incident de
  trente secondes n'y a pas sa place ;
- « Demander aux 3 » lance les trois EN PAIRS. Aucun ne lit les autres. La
  synthese est un GESTE humain, la seule lecture d'une IA par une IA du plugin.

Aucun code technique n'atteint l'ecran : PROVIDER_CALL_FAILED, RATE_LIMITED,
HTTP 429 et upstream_provider_shared_pool restent dans les traces SuperAdmin.

`loop_chat.multi_ai` est declare comme 19e execution_path — etiqueter le plugin
`loop_chat.ia` aurait refait la regression que V0-G avait corrigee.

La recette reelle a trouve un defaut que le banc ne pouvait pas voir : un
modele qui place la section « Pour approfondir » en tete vidait le corps et
faisait jeter une reponse reellement obtenue et payee. Corrige et teste.

29 tests Livewire + 6 tests Playwright sur un VRAI viewport (mobile + desktop),
fermant RESPONSIVE_HARD_GATE_SLICE_E.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…assistants-etats-partiels-429-produit-follow-ups-et-responsive

TASK-1619 — SLICE E : les 3 assistants dans ChatLoop, états partiels et libellés produit
Defaut constate par Cyril sur test.laravel : cliquer « Demander a Aperio »
lisait le composeur et GENERAIT aussitot. Le message humain n'avait pas ete
soumis, et « reflechit… » pouvait apparaitre sur un texte que personne n'avait
envoye — trois generations payees pour un brouillon.

La cause etait une erreur de conception de TASK-1619 : j'avais fait des
assistants des ACTIONS, la ou le composeur n'a jamais eu qu'un MODE et un
declencheur, le submit.

Le composeur n'a plus qu'une action : « Demander aux 3 IA ». Elle arme un mode
EXCLUSIF (elle eteint IA et Dossiers : trois assistants en plus d'un moteur
documentaire seraient quatre generations pour un envoi). Elle ne declenche
rien. Le tour part de `sendMessage()`, au meme rang que les autres moteurs,
apres publication du message humain — Entree comprise, puisqu'elle emprunte le
meme handler.

Le mode est ONE-SHOT, contrairement a IA et Dossiers : un membre qui a demande
trois regards une fois n'a pas demande a en payer trois a chaque phrase. Le
desarmement a lieu AVANT le tour, donc meme un echec ne laisse pas le mode arme.

Deux consequences assumees :

- le message humain reste dans le fil meme si les trois echouent. Il a ete
  ENVOYE : il appartient au membre. Seul l'echec reste hors du fil ;
- « demander a une autre IA » est retire : dans le flux « aux 3 », les deux
  autres ont deja repondu, et c'etait un declencheur direct de plus.

La configuration quitte le composeur pour « Gerer la Boucle » : configurer
n'est pas un geste de conversation. Le droit et la route ne bougent pas.

Badge admin : « Operationnel » devient « Gratuit verifie ». Il mesure modele
choisi + encore au catalogue gratuit + preuve fraiche, jamais la disponibilite
du provider — les recettes 1618 et 1619 ont montre le badge vert pendant que
chaque appel rendait 429. Aucun health-check ajoute.

20 tests neufs dont celui qui aurait rougi (armer sans soumettre = zero
generation, sabotage verifie), 27 tests SLICE E migres, 7 tests Playwright sur
vrai viewport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…eur-ne-genere-qu-au-submit-et-badge-admin-factuel

TASK-1620 — le mode 3 IA s'arme, et seul le submit génère
…ups (TASK-1621)

Les bancs TASK-1619 et TASK-1620 sont RETIRES : ils pilotaient des actions et
une Evidence qui n'existent plus. Leurs garanties encore valides sont reprises
par TASK1621PourContreEngineTest (moteur) et le banc de flux a venir.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…ent de preuve (TASK-1621)

Le message humain est publie et RENDU avant toute generation : le submit
remplit une file, une requete differee (wire:init, precedent du depot) lance
un role a la fois. POUR s'affiche sans attendre CONTRE.

Deux retours de recette de Cyril, corriges et testes :

- une pastille de mode VIDE doublait le badge. Le composeur ne connait pas le
  mode `multi_ai` et rendait une puce sans libelle, reduite a son bouton de
  fermeture. Un seul badge desormais, celui du panneau d'etat ;
- le badge d'attente portait un sablier sans identite : il porte l'icone du
  selecteur a gauche, et dit « Preparation des arguments "pour" ».

Admin : « Actualiser les modeles » renouvelle enfin les preuves, mais SEULEMENT
si le releve qui vient d'avoir lieu montre le slug encore present et encore
verifie gratuit. Disparu, redevenu payant ou releve en erreur : aucune
fraicheur fabriquee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
Cyril and others added 27 commits September 22, 2026 16:02
…pe silencieuse (TASK-1621)

Deux corrections mesurees, et l'UX qui les accompagne.

1. La connaissance du modele d'abord, la Boucle ensuite

Le module ne lisait plus rien, et le modele repondait « the provided Loop
material says nothing about... » : il commentait l'absence d'une matiere
qu'on ne lui avait jamais promise. La conversation revient en CONTEXTE —
dix derniers messages, bornes au declencheur — jamais en source.

ContextBuilder est INCHANGE : les deux bornes voyagent par ContexteIa, le
DTO qui porte deja les donnees par operation. Les sept autres capabilities
declarant loop.messages rendent la requete d'avant, a la virgule pres.

La borne est haute et EXCLUSIVE, pas une exclusion par identifiant : POUR
et CONTRE tournent dans deux requetes differees, et une exclusion n'aurait
ferme que le premier des deux. Ordre total sur (created_at, id) : created_at
est a la seconde.

2. Une reponse coupee ne se publie plus en silence

A 900 jetons, 13 des 14 tours « vides » s'arretaient EXACTEMENT au plafond,
et 12 reponses publiees comme completes l'atteignaient aussi. Le modele
n'etait pas muet : il brulait son budget en raisonnement.

- max_tokens 900 -> 2400 ;
- finishReason est lu, et ABSENT n'est pas Length : sans mesure, le
  comportement d'avant tient ;
- Length + texte -> PARTIAL, publie et marque, jamais un SUCCESS ;
- Length + vide -> OUTPUT_BUDGET_EXHAUSTED, plus EMPTY_MODEL_ANSWER ;
- notre propre plafond de caracteres marque la coupe lui aussi.

3. L'ecran

Modale supprimee, le bouton EST l'interrupteur. Bulles vert (POUR) et rouge
(CONTRE), modele affiche discretement. Rangee des modes visible sur mobile,
a defilement horizontal ; contraste du bouton + corrige. Le panneau
« Pourquoi cette reponse ? » ne dit plus « MULTI_AI ».

Defaut trouve en chemin : aiModeOf() ne connaissait pas multi_ai et
retombait sur llm — tout ce qui en dependait restait eteint a l'ecran alors
que le code etait la.

Tests : 1293 verts sur la salve des voisins, 3 sabotages qui mordent, gate
responsive REEL a 390 px. Le seul rouge est pre-existant, mesure sur la
branche nue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
« Windows ou Linux que choisir ? » rendait DEUX reponses en faveur de Linux.
Les deux modeles avaient obei : la consigne disait « defends LA proposition
exprimee par la question », et une question « A ou B ? » n'en exprime aucune.
La clause de repli des personas laissait alors chaque role choisir son sujet,
et les deux roles tournent dans deux appels separes : rien ne pouvait les
recoordonner.

Le referent est fixe dans le socle administrable (v3), au seul rang partage et
identique pour les deux appels. Aucun parser PHP : ce n'est pas une analyse
syntaxique, c'est une convention de lecture.

Et tout se dit en termes POSITIFS de camp DEFENDU. Un premier jet fixait le
referent mais laissait au rang du dessous un verbe — « conteste », « 3 a 5
objections » — qui permettait de le contourner : en recette, CONTRE attaquait
son propre camp, donc renforcait celui de POUR. Interdire une forme ne ferme
pas le comportement.

- socle : « TON CAMP EST UNE POSITION QUE TU DEFENDS, jamais une cible que tu
  attaques », et le piege nomme : « tu ne dois PAS attaquer B, B est TON camp » ;
- questions ouvertes : la regle prime sur toute consigne de nombre d'arguments,
  aucun camp n'est invente, les deux roles repondent la meme chose ;
- personas FR + EN reformulees, plus aucune formulation negative.

Ni Constitution ni doctrine touchees.

Recette reelle, MEME modele pour les deux roles afin d'isoler la semantique :
5 cas sur 5 conformes, 0 echec provider. L'inversion de l'ordre inverse bien
les camps.

Tests : 63 verts sur le banc moteur, 1298 sur la salve des voisins. Trois
sabotages mordent. Le seul rouge est pre-existant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…et les reponses sont deux fois plus courtes (TASK-1621)

1. NOT_APPLICABLE

« Quel CMS choisir ? » publiait DEUX bulles disant la meme chose : deux appels
payes pour un seul message utile. Le modele annonce desormais le verdict par un
marqueur exact, cherche dans le texte BRUT (le sanitiseur pourrait manger les
crochets). Aucun parser : une reponse qui PARLE de reformulation sans le
marqueur reste une reponse ordinaire, et un test le pin.

Quatrieme etat, ni success ni error ni refused — les trois lectures sont
fausses. Au ledger, `completed` sans `failure_reason` : l'appel a abouti, le
compter comme panne fausserait les sommes de fiabilite. La file s'annule des
que le premier role s'abstient. La notice est neutre, sans nom d'assistant et
sans « Reessayer » : ce n'est pas un role qui a echoue.

2. Des reponses deux fois plus courtes

Mesure : 833-1221 caracteres, 5 puces dans 10 cas sur 12. « 3 a 5 arguments »
-> le modele prend toujours le maximum. Et un sous-titre en gras par puce, qui
contredisait la regle « une seule phrase en gras » deja presente.

Raccourci par la CONSIGNE, jamais par les plafonds : depuis le correctif de
troncature, toute coupe par plafond affiche « Reponse ecourtee ». Baisser
max_tokens aurait signale comme incompletes des reponses qui ne le sont pas.

Resultat mesure en recette reelle : 497-672 caracteres, 3 puces partout, zero
gras dans les puces, finishReason Stop partout, aucun badge.

3. Les ecrans

Limen retire des deux ecrans de reglage — `catalogueVivant()`, distinct du
catalogue complet que `label()` et les bulles anciennes ont encore besoin de
lire. On retire un role des ecrans, pas des donnees. Le plugin s'appelle
« Pour / Contre » partout ou un humain le lit. La mire d'attente nomme le
modele qui prepare.

Tests : 90 verts sur les deux bancs, 1394 sur la salve des voisins. Cinq
sabotages mordent. Le seul rouge est pre-existant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…progressive (TASK-1621)

Point de retour demande par Cyril avant la passe d'affichage progressif :
carte 2 colonnes groupee par reply_to_id, mire par role, En attente…,
bulle a largeur de colonne. Non recette — la passe suivante corrige le
cycle d'affichage, le mobile sans tableau et la barre d'actions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…, deux gestes uniques (TASK-1621)

ROOT_CAUSE du constat Cockpit : observation anterieure a la logique
$debatEnCours — l'enchainement progressif marchait deja (capture Cyril).
Cette passe : carte hidden md:block / bulles md:hidden (pas de tableau
sur smartphone, decision Cyril) ; mire et echecs du bandeau en md:hidden
(une surface par viewport) ; Repondre retire ; Ajouter au Dossier unique
capitalisant le debat entier sous garde anti demi-debat, camp ecourte
annonce ; Copier unique repare (x-data remonte a la racine) ;
:show-copy-button lie. 36 verts flow + 138 voisins + 4 sabotages rouges.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…ve etendu, recette mesuree (TASK-1621)

Le format demande (titre gras + 3 puces) sortait DEJA du modele : le rendu
le perdait — whitespace-pre-wrap transformait les sauts de ligne inter-<li>
en lignes vides et le preflight retirait list-style. bp-bulle-markdown +
regles app.css. Recette Claude for Chrome : T_CARD-T0 = 0 ms (meme
mutation), POUR lisible pendant la mire CONTRE, cardGone=0, brouillon de
debat verifie, NOT_APPLICABLE sans carte ni bulle. Gate Playwright 5/5
(390/1440). Nettoyage recette : 4 supprimes, 0 restant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…SK-1621)

Le helper posait owner_id ET loop_id ; la contrainte CHECK n'existe que
sous PostgreSQL — verts SQLite, 3 rouges au shard 4/6. Un Dossier de
Boucle n'a pas de proprietaire personnel : owner_id null. Rejoue en PG
(36/36, 144 assertions) et en SQLite (36/36).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…-contre-deux-assistants-sans-rag-et-message-humain-publie-avant-generation

TASK-1621 — le plugin devient Pour / Contre : deux assistants sans RAG, message humain publié avant génération
…fie (TASK-1622)

model_type + approved_at/approved_by (FK declaree au lifecycle registry) ;
assignPaid a 4 marches fail-closed (shortlist config, tarif statique exact
non-free via AiPricingCatalog::staticRateFor, slug connu du catalogue) ;
garde d'execution branche payante sans aucun repli ; entryFor ignore les
lignes payantes (jamais chiffrees 0, meme forgees) ; UI admin par role
(type, tarifs par option, badges, audit) ; statut ledger 'completed' ->
STATUS_SUCCESS (les lignes du moteur entrent enfin dans les releves —
requis avant toute depense payante). 19 verts neufs + 208 voisins +
122 en PG + 4 sabotages rouges (dont 1 test corrige d'un vert pour la
mauvaise raison).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
… semantique (TASK-1622)

Audit MASTER du changement completed -> success : success est bien le
domaine canonique du provider (migration creatrice T1220, 17 writers sur
18) ; completed etait un hapax de ce moteur, 132 lignes d'un seul jour,
lu par aucun consommateur. Pas de lecture backward-compatible : admettre
les 29 lignes a cout inconnu injecterait 3 operations au quota du mois.

Retire le changement COLLATERAL sur ai_interactions (autre table, autre
vocabulaire) : un seul des trois chemins de sortie avait ete change.
Commentaire reecrit sur la mesure (103 cout connu 0 + 29 cout INCONNU,
dont 10 appels payants de T1621 qui n'ont jamais consomme le quota).

4 tests neufs : domaine canonique, non-contamination des traces (les 3
sorties), hors-sujet paye et compte comme tel, et garde TASK-1624 sur
CREDITABLE_PROCESSES. 23 verts + 193 voisins + 105 PG + 6 sabotages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
… ledger (TASK-1622)

Pin de semantique demande par MASTER : les TROIS sorties du moteur qui
ecrivent une ligne (succes, abstention, reponse vide) sont exercees, et
l'ensemble des statuts ecrits doit etre exactement [failed, success].
La colonne est un varchar(10) sans enum : ce test est la seule barriere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…ssigner le camp (TASK-1622)

Decision MASTER du 23/09. La v3 posait l'assignation du camp en TETE :
un modele applique alors une etiquette POUR/CONTRE puis cherche ce
qu'elle peut vouloir dire. v4 impose un ORDRE en 5 etapes — comprendre,
decider s'il y a deux camps, s'abstenir si non, recevoir son camp,
VERIFIER chaque argument avant d'ecrire.

CHANGEMENT DU SOCLE COMMUN, identique pour tous les modeles : aucune
logique par modele ni par provider (doctrine « un contrat commun,
plusieurs candidats »). Les bornes de forme sont conservees a
l'identique. AiPromptSeeder n'ecrase jamais : v4 est une version NEUVE,
v3 desactivee par la regle du plus haut actif.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
Les trois tests qui epinglaient le TEXTE v3 suivent la reecriture : les
invariants de camp survivent, seule leur formulation a bouge. Pin NEUF et
plus solide sur le point meme de la v4 — l'ORDRE des etapes se mesure par
une POSITION (comprendre < s'abstenir < assigner < verifier), pas par une
phrase. Sabotage verifie : etape de comprehension deplacee -> rouge.

Banc v4 rejoue (66 appels, 0,0145 USD cumule sur les deux bancs, aucun
retry) : aucun candidat n'atteint 6/6, la configuration de reference ne
change pas. Messages de recette nettoyes (0 restant).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
Socle commun v5 : COMPRENDRE -> CADRER -> ASSIGNER -> VERIFIER ->
DEFENDRE. Hypothese MASTER : une sequence POSITIVE conduit mieux qu'un
empilement de defenses. Les v3/v4 avaient grossi par sedimentation —
chaque defaut de recette ajoutait son garde-fou, si bien que le modele
lisait une liste d'interdits avant de savoir ce qu'on attendait de lui.
v5 dit la meme chose en cinq gestes affirmatifs. Aucun contenu neuf :
une reformulation. Plus la contrainte de langue (meme langue que la
question).

Socle COMMUN, identique pour tous les modeles. v3 et v4 CONSERVEES en
base (desactivees) et dans le seeder : revenir en arriere est un
changement de is_active, jamais une reecriture.

Pins de tests migres sur les invariants v5 — le camp n'est pas choisi,
referent A/B, defendre et pas seulement attaquer — et l'ORDRE des cinq
etapes reste mesure par POSITION, pas par une phrase.

Baseline avant ce commit : 1bfd609

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…res mesure (TASK-1622)

ARBITRAGE MASTER 23/09 05h30. v5 degrade franchement NOT_APPLICABLE :
4/6 modeles s'abstenaient correctement en v3, 3/6 en v4, 1/6 en v5. La
sequence positive est plus elegante, mais en faisant de l'abstention un
sous-cas du cadrage, elle lui retire le poids qu'elle avait comme regle
autonome.

v4 retenue : ling sans regression demontree, nova 2/6 -> 5/6, qwen
4/6 -> 5/6, NOT_APPLICABLE nettement meilleur qu'en v5.

Le seeder pose is_active = false sur l'entree v5 — sans quoi une base
neuve (CI, nouvel environnement) aurait reactive le socle rejete, la
regle du seeder etant « la version ACTIVE la plus haute gagne ».
Idempotence verifiee par rejeu. Le texte de v5 est conserve avec son
motif de rejet : une mesure negative est un resultat. Pins de tests
restaures sur les invariants v4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
DEFAUT REEL constate en recette (Cyril, 23/09) : « PC ou Mac que
choisir ? » rend NOT_APPLICABLE alors que le membre nomme deux
alternatives. Mesure : 3/3 essais, POUR s'abstient ; la file etant
annulee des la premiere abstention, CONTRE n'est jamais lance et le
membre ne lit que la notice neutre. Trace : 6 abstentions recentes,
toutes du role aperio.

CAUSE mesuree : v3 traitait la question 3/3. Ce n'est pas le modele qui
a change, c'est le socle. Piste de la virgule ECARTEE par la mesure
(« PC ou Mac, que choisir ? » echoue aussi). Reste : le modele juge que
PC et Mac ne sont pas nettement opposables, et l'etape de cadrage de v4
lui donne la permission de s'arreter la.

CORRECTION : la reconnaissance d'une comparaison A/B explicite PRECEDE
desormais l'abstention, qui devient le DERNIER recours ; le socle dit
de respecter le cadre pose par le membre meme si les alternatives se
chevauchent techniquement ou sont nommees familierement. Aucun cas
special PC/Mac : la regle ne nomme aucun produit. v4 conservee.

Pins migres sur les invariants v6, dont l'ORDRE des etapes mesure par
position : comparaison A/B AVANT abstention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…ABLE (TASK-1622)

Checkpoint avant la passe « aider a reformuler ». Baseline de cette
passe : socle v6 (A/B avant abstention), HEAD v6 =
35db2a6.

TASK repasse IN_PROGRESS / LOCKED — le TASK file (gitignore) porte le
detail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…TASK-1622)

Quand « Pour / Contre » conclut qu'une question n'ouvre aucun debat, le
MEME appel provider propose desormais une reformulation exploitable. Le
membre clique, le texte arrive dans le composeur, il le modifie s'il veut
et l'envoie LUI-MEME : l'IA propose, l'humain garde le dernier geste.

Audit avant d'inventer. Le jeton [[SUGGESTION]] etend le seul idiome de
contrat de prompt du depot et reste RETRO-COMPATIBLE (str_contains sur le
marqueur d'abstention inchange : les socles v3-v6 s'abstiennent
exactement comme avant). Canal DEDIE sur AssistantOutcome plutot que
followUps : un approfondissement prolonge une reponse, une reformulation
la remplace. useSuggestion() ne prend AUCUN parametre — le serveur relit
sa propre suggestion, rien ne voyage depuis le client.

Piege evite grace a l'audit : le composeur est en wire:model differe et
son hasText pilote le bouton d'envoi ; sans l'evenement composer-filled,
le membre aurait lu sa question sans pouvoir l'envoyer.

Socle v7 derive de v6 (v6 conservee). Il interdit d'inventer un produit
que le membre n'a pas nomme : pas de suggestion fidele possible = pas de
suggestion, et l'ecran demande une precision.

41 verts (+5), 169 avec les voisins, 65 en PG, 3 sabotages rouges.
Micro-recette reelle Ling : 5 cas sur 6 conformes ; le cas « Quelle est
la difference entre X et Y ? » declenche encore un debat — rapporte a
MASTER, non corrige seul (le corriger risque de ramener le defaut
« PC ou Mac » que cette passe vient de fermer).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…622)

« Quelle est la difference entre PC et Mac ? » declenchait un debat.
L'etape 2 de v6/v7 ne posait qu'une question — deux alternatives sont-elles
nommees ? — et deux noms suffisaient a fabriquer deux camps. Ce n'etait pas
un accident : cette detection agressive est ce qui avait repare « PC ou Mac
que choisir ? ». Le defaut etait le prix de la reparation.

v8 (derivee de v7, v7 conservee) pose DEUX conditions conjointes a l'etape
2 : deux alternatives nommees ET une intention de les departager. Les formes
informatives sont nommees explicitement, sans quoi la regle serait restee
abstraite.

Le cas ecarte ne tombe pas dans le vide : l'etape 4 reformule une question
de COMPARAISON en question de CHOIX entre les deux memes noms, dans l'ordre
ou le membre les a donnes — rien n'est invente. Garde soeur indispensable :
une question qui cherche a FAIRE COEXISTER les deux ne doit pas se voir
proposer de les departager, le membre possede deja les deux ; sans elle, la
reformulation aurait casse « Comment faire communiquer un PC et un Mac ? ».

Aucun cas special PC/Mac : la regle ne nomme aucun produit, et un test
l'epingle.

Recette reelle Ling, 6 cas sur 6 conformes, « PC ou Mac » intact dans les
deux ordres. 171 verts, 136 en PG, 2 sabotages rouges.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…ee-verifies-payants-approuves

TASK-1622 — Pour / Contre : modèles FREE vérifiés / PAYANTS approuvés
…1623)

Test ROUGE avant correction. Les quatre chemins d'upload image passent
l'objet fichier a Intervention Image, qui le resout par getPathname().
TemporaryUploadedFile::getPathname() rend Storage::disk($tmp)->path($p), et
FilesystemAdapter::path() ne fait que prefixer avec la racine du disque :
absolue en local, VIDE sur s3/gcs. Le chemin rendu est alors relatif —
« livewire-tmp/xxx.png » — et Intervention leve DirectoryNotFoundException.

Le point qui a demande le plus de soin : aucun test existant ne POUVAIT
attraper ce bug. Livewire::test()->set('image', ...) passe par
FileUploadConfiguration::disk(), qui rend « tmp-for-tests » en dur des que
l'app tourne en test, et storage() en fait un Storage::fake() local. Le
harnais de test de Livewire ne peut pas produire la condition de production ;
un test ecrit par ce chemin serait reste vert quoi qu'il arrive.

Le banc construit donc un vrai TemporaryUploadedFile sur un disque dont le
FilesystemAdapter recoit root vide : path() rend un chemin relatif, comme s3,
sans reseau — tandis que le contenu est reellement servi. C'est exactement ce
que fait un stockage distant.

5 rouges, avec l'exception et le message exacts du rapport PROD. Les 2 verts
comptent autant : la premisse et le cas local passent deja, donc le banc ne
rougit pas par construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…hemin (TASK-1623)

Quatre corrections ciblees, une ligne chacune : Image::decode($file) devient
Image::decode($file->get()). C'est l'idiome deja en place dans le depot
(GenerateServiceThumbnail lit son image par $disk->get($path)), et il ne
demande ni service generique, ni abstraction, ni refactor.

Sur un stockage temporaire distant, TemporaryUploadedFile::getPathname()
rend « livewire-tmp/xxx.png » — un chemin qui n'existe localement nulle part,
parce que FilesystemAdapter::path() ne fait que prefixer avec la racine du
disque, et que cette racine est vide sur s3/gcs. Le contenu, lui, a toujours
ete lisible : c'est lui qu'il fallait passer.

Le banc de reproduction passe de 5 rouges a 7 verts, et le cas local est
verifie avec le meme objet de production sur un disque a racine absolue.
Sabotage : redonner l'objet fichier a LoopChat fait rougir la garde de
non-retour, qui balaie les quatre surfaces d'un coup.

Hors scope, verifie et non modifie : ServiceController et ProfileController
recoivent un UploadedFile d'une requete HTTP classique, dans le tmp local de
PHP — ils ne presentent pas ce defaut.

118 verts en PostgreSQL. Le seul rouge voisin, « workspace cards shell », est
porte par #[Group('ci-known-red')] et documente comme deja rouge sur develop
avant TASK-1112 : il est hors gate GitHub, et hors de cette TASK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…vewire-stockage-distant

TASK-1623 — Hotfix upload image Livewire stockage distant
TASK OPS d'audit et de synchronisation PROD. Aucune migration, aucun code
applicatif : le contrat TASK/VERSION du depot s'applique quand meme, et
c'est l'arbitrage MASTER.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…ation-prod-laravel-cloud

TASK-1624 — Preparation synchronisation PROD Laravel Cloud
@cslucki
cslucki merged commit f6db2cb into main Sep 23, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant