diff --git a/.agents/skills/terra-verified-implement/SKILL.md b/.agents/skills/terra-verified-implement/SKILL.md new file mode 100644 index 0000000..9777b3f --- /dev/null +++ b/.agents/skills/terra-verified-implement/SKILL.md @@ -0,0 +1,24 @@ +--- +name: terra-verified-implement +description: Implement a spec or ticket by coordinating gpt-5.6-terra subagents at high reasoning and independently verifying the final change. Use when the user asks to implement, build, or fix work and wants delegated implementation with a verification pass. +--- + +# Terra Verified Implement + +Invoke `$implement` and follow it as the baseline workflow. Treat this skill as its delegated, verified variant. + +## Workflow + +1. Read the spec or ticket, repository guidance, affected flow, and existing worktree changes. Preserve unrelated changes. +2. Split only genuinely independent, bounded work. If the change is small or one seam owns it, use one implementation agent; do not manufacture parallelism. +3. Spawn each implementation agent with `fork_turns: "none"`, `model: "gpt-5.6-terra"`, and `reasoning_effort: "high"`. Give each its exact owned files/seam, acceptance criteria, required checks, and task-local context. Tell agents not to overwrite or revert other agents' work. +4. Wait for agents, inspect their diffs, resolve overlaps minimally, and run the implementation checks required by `$implement`: targeted tests and typecheck during work, then the full relevant suite once. +5. Spawn a fresh, read-only verification agent with `fork_turns: "none"`, `model: "gpt-5.6-terra"`, and `reasoning_effort: "high"`. Give it the original request, current diff, and test output, but not implementation conclusions. Ask it to check correctness, regressions, specification coverage, and test evidence. Have it report findings without changing files. +6. Fix confirmed findings, rerun affected checks, and repeat verification once if fixes were needed. Run `$code-review` before completion and commit the completed work to the current branch. + +## Delegation rules + +- Keep all agents in the same workspace; assign non-overlapping ownership before they edit. +- Use at most the available concurrency slots. Reserve one slot for coordination and verification. +- Do not delegate secrets, production writes, destructive operations, or changes requiring new user authority. +- If subagents are unavailable, implement directly, run the same checks, and state that delegated verification was unavailable. diff --git a/.agents/skills/terra-verified-implement/agents/openai.yaml b/.agents/skills/terra-verified-implement/agents/openai.yaml new file mode 100644 index 0000000..301b62c --- /dev/null +++ b/.agents/skills/terra-verified-implement/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Terra Verified Implement" + short_description: "Delegate implementation and verify it" + default_prompt: "Use $terra-verified-implement to implement this task with Terra agents and verify the result." diff --git a/.vscode/settings.json b/.vscode/settings.json index bee8cb1..d0ea173 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "java.compile.nullAnalysis.mode": "automatic", "java.configuration.updateBuildConfiguration": "automatic", - "java.autobuild.enabled": false + "java.autobuild.enabled": false, + "java.server.launchMode": "LightWeight" } \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 4e406bf..73e3416 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ Use the single-context documentation layout. See [domain.md](file:///home/nessar - Extract reusable logic when it is duplicated or independently testable. - Avoid unrelated refactoring in feature or bug-fix merge requests. - Do not introduce a new dependency when the existing stack can reasonably solve the problem. -- Preserve backward compatibility unless the issue explicitly requires a breaking change. +- The application is not in production. Change development contracts and schemas directly when the domain model changes; update all callers and tests together instead of preserving backward compatibility. - Add or update tests for changed behavior. - Do not commit secrets, tokens, credentials, generated artifacts, or local environment files. - Update relevant domain documentation when behavior, architecture, or business rules change. @@ -156,7 +156,7 @@ Use the single-context documentation layout. See [domain.md](file:///home/nessar - Follow the repository's existing REST conventions. - Use nouns for resource paths and standard HTTP methods. - Return appropriate HTTP status codes. -- Keep request and response contracts stable. +- Keep request and response contracts coherent with the current model; replace obsolete development contracts rather than adding compatibility layers. - Use explicit DTOs instead of generic maps. - Validate incoming requests with Jakarta Bean Validation. - Return consistent, structured error responses. @@ -210,13 +210,12 @@ Use the single-context documentation layout. See [domain.md](file:///home/nessar - Test success, validation failure, authorization failure, and important edge cases. - Add regression tests for bug fixes. -## Database migrations +## Development data lifecycle -- Create a migration for every schema or reference-data change. -- Do not modify a migration that may already have been applied outside the local environment. -- Ensure migrations are backward-compatible with the currently deployed application when rolling deployment is used. -- Avoid destructive migrations without an explicit data-retention and rollback plan. -- Include a safe data migration strategy when changing populated columns or constraints. +- The application is not in production; development and test data are disposable and should be deleted or reseeded when the model changes. +- Do not create data migrations, compatibility adapters, dual-read or dual-write paths, or legacy schema support. +- Replace obsolete schemas, reference data, API contracts, and development documents directly, then update every caller and test in the same change. +- Do not plan for rolling upgrades or backward-compatible deployment until an explicit project decision declares production data must be preserved. ## Before finishing diff --git a/CONTEXT.md b/CONTEXT.md index 30aa54e..4caeef4 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -21,9 +21,13 @@ Personne enregistrée pour pouvoir être cosignataire d'un bail, y compris avant _Avoid_: Candidat, occupant **Appartement** (Code: `Apartment`): -Logement meublé du portefeuille d'un bailleur, pouvant être associé à des baux successifs. Sa fiche conserve ses caractéristiques courantes réutilisables dans les baux : identification fiscale, adresse, surface et composition, caractéristiques de l'immeuble, équipements et annexes, chauffage et eau chaude, performance énergétique et accès aux technologies de l'information. Chaque brouillon de bail en prend une copie contractuelle indépendante afin qu'une modification ultérieure de la fiche ne change pas l'historique. Un appartement référencé par un bail est conservé afin de ne pas rompre cet historique. +Logement meublé du portefeuille d'un bailleur, pouvant être associé à des baux successifs. Sa fiche conserve ses caractéristiques courantes réutilisables dans les baux : identification fiscale, adresse, surface, espaces privatifs structurés, caractéristiques de l'immeuble, équipements, chauffage et eau chaude, performance énergétique et accès aux technologies de l'information. Chaque brouillon de bail en prend une copie contractuelle indépendante afin qu'une modification ultérieure de la fiche ne change pas l'historique. Un appartement référencé par un bail est conservé afin de ne pas rompre cet historique. _Avoid_: Bien, logement, unité locative +**Espace privatif** (Code: `PrivateArea`): +Pièce ou annexe dont le locataire a la jouissance exclusive, identifiée durablement dans l'appartement par une famille fermée — pièce principale, pièce de service ou annexe —, un sous-type courant ou autre, et un libellé libre afin d'être référencée par l'inventaire du mobilier, les états des lieux et les photos. Le bail en fige un snapshot et le nombre réglementaire de pièces principales est calculé uniquement depuis la première famille, sans être stocké séparément. +_Avoid_: Zone photo, nombre de pièces saisi, autre surface libre + **Diagnostic immobilier** (Code: `PropertyDiagnostic`): Document typé rattaché à la fiche appartement avec sa date d'établissement et, le cas échéant, sa date d'expiration. Les diagnostics applicables sont présélectionnés pour le bail ; un document manquant ou apparemment expiré déclenche une alerte juridique contournable. Lors de la génération d'une révision, chaque fichier sélectionné est copié physiquement dans son dossier sans déduplication ; la finalisation conserve le dossier de la révision confirmée, afin que le remplacement ou la suppression ultérieure du diagnostic sur la fiche appartement ne modifie jamais l'historique. _Avoid_: Lien mutable vers un fichier courant, caractéristique DPE @@ -73,7 +77,7 @@ Correction administrative motivée qui rend sans effet un congé mal enregistré _Avoid_: Rétractation, modification directe, suppression **Clôture du bail** (Code: `LeaseClosure`): -Confirmation par le bailleur que la sortie du logement peut être administrativement achevée après la fin contractuelle du bail et la restitution des clés. Elle constitue l'unique voie vers l'archivage sans éteindre les dettes restantes ; un bail étudiant arrivé à son terme n'est donc pas automatiquement clôturé, et l'état des lieux de sortie n'est pas encore une condition de clôture. +Confirmation par le bailleur que la sortie du logement peut être administrativement achevée après la fin contractuelle du bail et la finalisation de l'état des lieux de sortie, dont la date constitue la restitution des clés sans être ressaisie. Elle constitue l'unique voie vers l'archivage sans éteindre les dettes restantes ; un bail étudiant arrivé à son terme ou un état des lieux de sortie finalisé ne clôture donc jamais automatiquement le bail. _Avoid_: Fin contractuelle, expiration automatique, suppression **Occupation sans titre** (Code: `UnauthorizedOccupancy`): @@ -81,7 +85,7 @@ Maintien dans le logement après la fin contractuelle du bail sans nouveau titre _Avoid_: Bail actif, reconduction tacite, loyer courant **Version de modèle de bail** (Code: `LeaseTemplateVersion`): -Contenu réglementaire immuable applicable pendant une période déterminée ; la date de dernière signature sélectionne la version requise. Un bail signé avec une version qui n'est plus applicable à cette date doit être régénéré et signé de nouveau avant sa finalisation. +Contenu réglementaire immuable applicable pendant une période déterminée, comprenant le modèle du bail et le catalogue du mobilier minimal prérempli ; la date de dernière signature sélectionne la version requise. Un bail signé avec une version qui n'est plus applicable à cette date doit être régénéré avec le catalogue correspondant et signé de nouveau avant sa finalisation. _Avoid_: Modèle courant, modèle de PDF **Alerte de conformité juridique** (Code: `LegalComplianceWarning`): @@ -89,15 +93,15 @@ Signalement non bloquant d'une règle juridique susceptible d'évoluer, évalué _Avoid_: Erreur de validation, conseil juridique, blocage réglementaire permanent **Révision du document de bail** (Code: `LeaseDocumentRevision`): -Numéro visible dans le pied de chaque page du PDF permettant au bailleur d'identifier la version des faits contractuels et des annexes qui a été générée pour signature. Une modification contractuelle rend la révision courante obsolète et la génération suivante incrémente son numéro, tandis qu'un nouveau téléchargement sans changement conserve le même numéro. Lors de la finalisation, le bailleur confirme explicitement que le numéro figurant sur le PDF signé téléversé correspond à la dernière révision attendue par l'application ; le serveur ne tente pas de lire lui-même ce numéro dans le fichier. Le dossier de cette révision est alors conservé définitivement, tandis que les dossiers obsolètes jamais finalisés peuvent être supprimés. +Numéro visible dans le pied de chaque page du PDF permettant au bailleur d'identifier la version des faits contractuels et des annexes qui a été générée pour signature. Une modification contractuelle rend la révision courante obsolète et la génération suivante incrémente son numéro, tandis qu'un nouveau téléchargement sans changement conserve le même numéro. Lors de la finalisation, le bailleur confirme explicitement que le numéro figurant sur le PDF signé téléversé correspond à la dernière révision attendue par l'application et déclare séparément les éventuelles corrections manuscrites d'écarts d'inventaire à l'entrée, sans jamais modifier le numéro de révision à la main ; le serveur ne tente pas de lire lui-même ces informations dans le fichier. Le dossier de cette révision est alors conservé définitivement, tandis que les dossiers obsolètes jamais finalisés peuvent être supprimés. _Avoid_: Version du modèle réglementaire, vérification automatique du PDF **Finalisation du bail** (Code: `LeaseFinalization`): -Opération irréversible réalisée depuis une page dédiée ouverte depuis le brouillon, puis soumise à une dernière confirmation explicite. Le bailleur y téléverse le PDF signé, renseigne la date de dernière signature figurant sur le bail et atteste obligatoirement que toutes les parties l'ont signé et que son numéro de révision correspond à celui attendu par l'application. La finalisation est bloquée si le fichier signé, la date ou une attestation obligatoire manque, si la révision ou le modèle réglementaire ne correspond plus, si un acte de cautionnement manque pour un couple garant-locataire déclaré, ou si la période chevauche un bail déjà finalisé pour l'appartement. Les diagnostics, pièces conditionnelles et autres règles juridiques susceptibles d'évoluer ne produisent que des alertes contournables et auditées. L'application ne vérifie pas cryptographiquement les signatures ni le numéro dans le fichier ; elle conserve le document, sa révision déclarée, la date civile de dernière signature déclarée sans conversion de fuseau horaire, l'auteur et l'horodatage UTC de la finalisation ainsi que l'empreinte SHA-256 du fichier, puis fige les faits contractuels, réserve la période de l'appartement et crée les échéances. +Opération irréversible réalisée depuis une page dédiée ouverte depuis le brouillon, puis soumise à une dernière confirmation explicite. Le bailleur y téléverse le PDF signé, renseigne la date de dernière signature figurant sur le bail, indique obligatoirement si l'état des lieux d'entrée a révélé des écarts d'inventaire et atteste que toutes les parties ont signé, que le numéro de révision correspond à celui attendu par l'application et, le cas échéant, que chaque correction manuscrite déclarée a été paraphée par elles. La finalisation est bloquée si le fichier signé, la date, la réponse sur les écarts d'inventaire ou une attestation obligatoire manque, si une correction déclarée est incomplète, si la révision ou le modèle réglementaire ne correspond plus, si un acte de cautionnement manque pour un couple garant-locataire déclaré, ou si la période chevauche un bail déjà finalisé pour l'appartement. Les diagnostics, pièces conditionnelles et autres règles juridiques susceptibles d'évoluer ne produisent que des alertes contournables et auditées. L'application ne vérifie pas cryptographiquement les signatures, le numéro ni les corrections dans le fichier ; elle conserve le document, sa révision et ses corrections manuscrites déclarées, la date civile de dernière signature déclarée sans conversion de fuseau horaire, l'auteur et l'horodatage UTC de la finalisation ainsi que l'empreinte SHA-256 du fichier, puis fige les faits contractuels, réserve la période de l'appartement et crée les échéances. _Avoid_: Activation du bail, signature électronique, validation du brouillon **Annexe postérieure du bail** (Code: `LeasePostFinalizationAttachment`): -Document reçu après la finalisation, notamment l'état des lieux d'entrée, l'inventaire du mobilier, l'attestation d'assurance ou le récépissé d'une déclaration de mise en location. Son ajout ne modifie ni les faits contractuels ni la révision du bail : l'application en conserve le fichier immuable, l'auteur, l'horodatage UTC et l'empreinte SHA-256. Une correction ajoute un nouveau fichier qui désigne le précédent comme remplacé, sans le modifier ni le supprimer. +Document reçu après la finalisation, notamment l'état des lieux d'entrée, l'attestation d'assurance ou le récépissé d'une déclaration de mise en location. Son ajout ne modifie ni les faits contractuels ni la révision du bail : l'application en conserve le fichier immuable, l'auteur, l'horodatage UTC et l'empreinte SHA-256. Une correction ajoute un nouveau fichier qui désigne le précédent comme remplacé, sans le modifier ni le supprimer. _Avoid_: Réouverture du bail, remplacement destructif d'une annexe, nouvelle révision du bail **Clause particulière de bail** (Code: `LeaseSpecialClause`): @@ -205,15 +209,43 @@ Dépense de valeur significative (seuil fiscal de 500 € HT en France) pour des _Avoid_: Investissement, charge à amortir **Inventaire du mobilier** (Code: `FurnitureInventory`): -Liste exhaustive et détaillée des meubles, appareils et équipements mis à la disposition du locataire dans un logement meublé, obligatoirement annexée au bail. Sa création et sa gestion feront l'objet d'un parcours ultérieur et restent hors du périmètre de la première implémentation de finalisation du bail. -_Avoid_: Liste des meubles +Annexe contractuelle du bail recensant de façon exhaustive et détaillée les meubles, appareils et équipements mis à la disposition du locataire, dont les mêmes éléments sont ensuite décrits dans les états des lieux d'entrée et de sortie. Chaque ligne regroupe un type d'objet homogène dans un espace privatif avec sa désignation, sa quantité et une description facultative, sans décrire son état ; l'inventaire est prérempli depuis le catalogue de la version de modèle de bail applicable, complété librement par le bailleur, exigé avant la génération du bail et figé avec ce catalogue dans chaque révision générée, chaque élément réglementaire devant être explicitement renseigné sans être présumé présent et son absence produisant une alerte juridique contournable et auditée plutôt qu'un blocage permanent. +_Avoid_: Inventaire des lieux, liste des meubles + +**Écart d'inventaire à l'entrée** (Code: `EntryInventoryDiscrepancy`): +Différence entre l'inventaire du mobilier inclus dans la révision générée du bail et les quantités ou descriptions constatées lors de l'état des lieux d'entrée. Il peut être corrigé à la main sur le bail imprimé avant signature, à condition que toutes les parties le paraphent ; lors de la finalisation, le bailleur répond obligatoirement s'il existe de tels écarts, confirme parmi les corrections préremplies depuis le constat celles réellement reportées et paraphées, peut en ajuster le texte ou en ajouter une exceptionnelle, tandis que toute autre modification contractuelle exige une nouvelle révision générée. +_Avoid_: Incohérence, modification libre du bail signé **État des lieux** (Code: `PropertyConditionReport`): -Constat écrit et contradictoire de l'état de chaque pièce du logement et de ses équipements lors de l'entrée (remise des clés) ou de la sortie (restitution des clés) du locataire, illustré de photos de preuve. Sa création et sa gestion feront l'objet d'un parcours ultérieur et restent hors du périmètre de la première implémentation de finalisation du bail. +Constat écrit et contradictoire rattaché au bail, décrivant l'état de chaque espace privatif du logement et de ses équipements lors de l'entrée (remise des clés) ou de la sortie (restitution des clés), et illustré de photos de preuve ; il fige les parties au bail effectives à sa date, présentes ou représentées, les relevés de compteurs, les clés et autres moyens d'accès ainsi que, à la sortie, une adresse de correspondance propre à chaque locataire effectif sans modifier son profil ni son adresse contractuelle, tandis que chaque sol, mur, plafond, équipement et ligne d'inventaire reçoit un état parmi neuf, bon état, état d'usage, dégradé, absent ou non applicable, assorti d'une observation libre obligatoire pour les trois derniers, et que la quantité constatée d'une ligne d'inventaire peut être répartie entre plusieurs états sans modifier sa quantité contractuelle. Sa date civile réelle n'est jamais future dans le fuseau IANA du navigateur, la sortie ne précède jamais l'entrée sans devoir coïncider avec une borne contractuelle ; son brouillon peut commencer dès la génération d'une révision du bail mais devient obsolète si celle-ci est remplacée ; le bail est finalisé en premier, puis la finalisation de ce seul parcours amiable exige le téléversement de sa version papier signée et numérisée, l'attestation de sa correspondance par le bailleur et la signature de chaque partie ou de son mandataire identifié, conserve l'auteur du téléversement, l'horodatage UTC et l'empreinte SHA-256, et rend le constat immuable ; le départ isolé d'un colocataire n'en crée pas un nouveau, toute correction ultérieure est ajoutée sans remplacer le constat finalisé et l'établissement par commissaire de justice reste hors du périmètre. _Avoid_: Constat, check-list d'entrée +**Révision de l'état des lieux** (Code: `PropertyConditionReportRevision`): +Numéro visible sur le PDF généré pour identifier les observations, photos et références soumises à la signature des parties. Toute modification ultérieure du brouillon rend cette révision obsolète ; la finalisation exige le scan signé de la dernière révision et l'attestation de sa correspondance, conserve son dossier définitivement et permet de supprimer les dossiers obsolètes jamais finalisés. +_Avoid_: Version du bail, PDF courant sans numéro + +**Dossier d'état des lieux** (Code: `PropertyConditionReportBundle`): +Archive ZIP immuable produite après finalisation, contenant le PDF signé, les photos WebP nommées par leur référence et un manifeste de leurs empreintes SHA-256. Le bailleur la télécharge et atteste l'avoir remise à chaque partie hors de l'application, qui conserve l'attestation sans prétendre certifier la transmission externe. +_Avoid_: E-mail certifié, accès locataire, dossier photo mutable + +**Relevé de compteur** (Code: `MeterReading`): +Index décimal non négatif constaté dans un état des lieux pour un compteur d'électricité, de gaz, d'eau froide, d'eau chaude ou d'un autre type explicitement libellé, avec son unité et, lorsqu'ils existent, son libellé ou numéro et une photo. Les compteurs identifiés à l'entrée sont repris à la sortie par la même référence ; un remplacement ou une impossibilité d'accès exige une observation. +_Avoid_: Consommation calculée, facture d'énergie + +**Moyen d'accès** (Code: `AccessMeans`): +Clé, badge, télécommande ou autre dispositif remis au locataire, identifié dans l'état des lieux par son type, sa destination et sa quantité. La sortie reprend la même référence et constate la quantité restituée ; tout écart exige une observation sans produire automatiquement de retenue financière. +_Avoid_: Équipement du logement, retenue sur dépôt de garantie + +**Complément d'état des lieux** (Code: `PropertyConditionReportSupplement`): +Ajout immuable rattaché à un état des lieux d'entrée finalisé, demandé par le locataire dans les dix jours suivant son établissement ou, pour les éléments de chauffage, pendant le premier mois de la période de chauffe dont le début réel est déclaré par le bailleur. Il conserve la date de la demande, les ajouts acceptés, leurs photos éventuelles et le PDF signé correspondant sans modifier le constat initial ; un dépassement du délai produit une alerte juridique versionnée, contournable et auditée plutôt qu'un blocage, et la comparaison tient compte du constat et de ses compléments acceptés. +_Avoid_: Modification de l'état des lieux, correction silencieuse + +**Comparaison des états des lieux** (Code: `PropertyConditionComparison`): +Vue dérivée qui aligne les mêmes pièces, zones et lignes d'inventaire par leurs références stables, compare leurs états structurés déclarés et présente leurs photos d'entrée et de sortie côte à côte. Elle signale les changements déclarés sans analyser les pixels, attribuer de responsabilité ni calculer de retenue sur le dépôt de garantie. +_Avoid_: Détection automatique de dégâts, estimation automatique de retenue + **Parcours de photos** (Code: `PhotoSequence`): -Séquence guidée de clichés requis (ex. climatisation, cuisine, salle de bain) à prendre obligatoirement dans l'application lors de l'établissement d'un état des lieux pour fiabiliser le constat. +Séquence guidée de clichés ajoutés dans l'application lors de l'établissement d'un état des lieux au moyen du contrôle natif préférant la caméra arrière, sans prétendre interdire le repli vers la photothèque laissé au navigateur, dont les zones sont définies à l'entrée puis reprises à l'identique à la sortie ; chaque cliché d'entrée exige alors un cliché de sortie qui le référence, tandis que des clichés de sortie supplémentaires restent possibles, la cuisine et la salle de bains sont préremplies, les équipements déclarés dans l'inventaire ajoutent leurs zones applicables, notamment la climatisation lorsqu'elle existe, et le bailleur peut ajouter librement d'autres zones et photos. Chaque zone ou cliché correspondant attendu exige une photo ou un motif d'absence confirmé et audité sans bloquer définitivement la finalisation ; chaque téléversement doit être une image unique JPEG, PNG, WebP ou HEIC/HEIF reconnue d'après son contenu, les animations, documents multipages et autres formats étant refusés ; après application de l'orientation EXIF, chaque original téléversé est remplacé sans agrandissement par un WebP à 75 % de qualité tenant proportionnellement dans 1920 × 1080, ou 1080 × 1920 en portrait, et dépourvu de métadonnées, auquel sont attribuées sa référence stable et son empreinte SHA-256, puis l'état des lieux papier signé désigne l'ensemble par son nombre et sa plage de références, le dossier correspondant est transmis aux parties et conservé sans modification ni signature électronique, l'original temporaire n'est pas conservé, et le bailleur peut contrôler l'aperçu puis reprendre le cliché avant la finalisation. _Avoid_: Album photos, galerie **Calendrier** (Code: `Calendar`): diff --git a/docs/adr/0011-rental-aggregate-boundaries.md b/docs/adr/0011-rental-aggregate-boundaries.md index 15a53cc..60f4aed 100644 --- a/docs/adr/0011-rental-aggregate-boundaries.md +++ b/docs/adr/0011-rental-aggregate-boundaries.md @@ -4,6 +4,14 @@ Within Rental Management, `Lease` owns its contractual lifecycle, dates, rent terms, and lease-party relationships while referencing independent `Apartment` and `Tenant` aggregates by ID. Activation freezes the landlord identity and address, apartment address, tenant identities and addresses, guarantors, and financial terms; later changes are effective-dated immutable `LeaseAmendment` records that preserve both contract versions and may only recalculate future unpaid dues. `RentDue` and `RentPayment` are independent aggregates; a positive payment owns allocations whose sum cannot exceed the amount received, each allocation cannot exceed its due's remaining balance, and one payment may cover multiple dues only within the same lease. Any surplus stays unallocated. `Guarantor` remains internal to `Lease` until it gains an independent lifecycle or must be shared across leases. +`Apartment` owns stable `PrivateArea` values covering rooms and annexes under the tenant's exclusive use. A lease snapshot freezes those identifiers and descriptions; furniture-inventory lines, property-condition observations, and photo zones all reference them, while the regulatory main-room count is derived rather than stored independently. + +`FurnitureInventory` is also an independent aggregate referenced by `LeaseId`, not a growing collection inside `Lease`. It remains editable with the lease draft, every generated `LeaseDocumentRevision` freezes its own inventory snapshot for the printed contract, and changing the inventory obsoletes that revision; lease finalization then freezes the aggregate while separately retaining any declared handwritten entry corrections. + +`PropertyConditionReport` is an independent aggregate identified by its own ID and uniquely constrained by `(LandlordId, LeaseId, ENTRY|EXIT)`. It owns its observations, photo references, finalization evidence, and append-only supplements; `Lease` does not embed that growing inspection history. + +Creating the `EXIT` report snapshots the effective finalized `ENTRY` report, including accepted supplements, while preserving the stable room, zone, inventory-line, and entry-photo references needed for deterministic comparison. The exit draft then evolves independently; every entry photo requires one explicitly linked exit counterpart or an audited absence reason, while additional unpaired exit photos remain allowed. + A new `Lease` is always a mutable, permanently deletable `DRAFT`; draft overlap is allowed and creates no dues. Explicit activation reserves the period in the apartment's versioned `ApartmentOccupancy` aggregate, which retains active and archived lease periods and rejects inclusive overlap, then creates one `RentDue` per landlord, lease, and occupied calendar month under a unique database constraint. Making concurrent activations contend on the same occupancy document prevents write skew that a query-only overlap check would allow. Boundary months prorate rent and charges by occupied calendar days. An amendment split combines its before and after portions before performing one monetary rounding step, while settlement and due-date timing remain independent facts on the due. Lease activation, amendment reconciliation, payment confirmation and later allocation, reversal, and refund confirmation run in Mongo transactions whenever they change more than one aggregate document. Versioned aggregate documents reject stale concurrent writes; occupancy reservations, contract snapshots, generated or reconciled dues, payments, reversals, and refunds therefore commit or roll back without a single-process lock or manual database compensation. A payment owns at most one allocation per due, preventing two stale copies of one due from being mutated inside the same command. diff --git a/docs/adr/0016-model-lease-endings-as-dated-facts.md b/docs/adr/0016-model-lease-endings-as-dated-facts.md index 862a307..b8b8081 100644 --- a/docs/adr/0016-model-lease-endings-as-dated-facts.md +++ b/docs/adr/0016-model-lease-endings-as-dated-facts.md @@ -5,3 +5,5 @@ Lease endings are represented by immutable, audited facts owned by `Lease`: a standard furnished lease has no real end until a tenant notice or mutually agreed early end is recorded, while a student lease retains its fixed nine-month end. Tenant notices freeze the legal rule and calculated notice period that applied when received; agreed early endings, accepted withdrawals, administrative invalidations, evidence versions, and closure confirmations are appended without erasing history. Operational labels such as notice in progress, awaiting closure, and unauthorized occupancy are derived from those facts and civil dates rather than persisted as additional lease statuses or advanced by a scheduler. User-entered civil dates are validated in the browser-provided IANA time zone and stored without an artificial UTC conversion, while audit timestamps are instants recorded in UTC. A known contract end immediately bounds the otherwise open-ended apartment occupancy and rent dues, including a prorated final month and removal of later unallocated dues. When that recalculation makes an existing allocation exceed its due, the due temporarily exposes a required financial correction while the confirmed payment and its allocation remain immutable; only the existing explicit reversal workflow removes the allocation before the correct payment is recorded. Closure once the contract end is reached and the actual key-return date is known is the sole transition to `ARCHIVED`; keys may have been returned earlier, and closure neither clears outstanding debts nor prevents later settlement of existing dues. A successor lease may be finalized without date overlap before the former lease is closed, but any later withdrawal or correction that would create overlap is rejected. Landlord-issued notices, tenant notices for shared leases, security-deposit settlement, exit inspections, and occupancy compensation remain outside this increment. + +Issue #7 extends that model by making the finalized exit `PropertyConditionReport` the authoritative source of the actual key-return date. Lease closure remains an explicit later command, allowed only after the contract end, and reuses that date without asking the landlord to enter the same fact again; finalizing the report never archives the lease automatically. diff --git a/docs/adr/0017-normalize-property-condition-photos.md b/docs/adr/0017-normalize-property-condition-photos.md new file mode 100644 index 0000000..bada8d2 --- /dev/null +++ b/docs/adr/0017-normalize-property-condition-photos.md @@ -0,0 +1,11 @@ +# Normalize property-condition photos before retaining them + +**Status**: accepted + +Inspection-photo uploads are auto-oriented from EXIF, fitted without upscaling inside an orientation-aware 1920 × 1080 frame while preserving aspect ratio, converted by the server to WebP at 75% quality, and stripped of metadata before receiving their stable reference and SHA-256 fingerprint. The normalized image is the immutable evidence retained and shared with the parties; the original upload is deleted after successful conversion, trading original fidelity and device-supplied metadata for bounded local storage and reduced privacy leakage while allowing the landlord to review and retake a photo before finalization. Reliable capture references and audit timestamps remain application-controlled facts rather than EXIF claims. + +The server delegates this normalization to ImageMagick installed in the runtime image and invoked directly without a shell. Its security policy denies every coder and delegate by default, enables only the input formats explicitly accepted by the product and WebP output, restricts paths to application-controlled temporary files, and bounds dimensions, sequence length, memory, mapped memory, disk, threads, and execution time. The application additionally enforces a process timeout, validates the resulting format and dimensions, and publishes the normalized file only after successful verification. This avoids depending on small Java WebP wrappers while keeping the image processor replaceable behind one adapter. + +Accepted uploads are single-image JPEG, PNG, WebP, and HEIC/HEIF files, identified from their decoded content rather than trusted extensions or client-supplied media types. Animated or multi-image inputs and every other format are rejected. HEIC/HEIF support is supplied by the runtime ImageMagick build through its system `libheif` codec; the container build verifies that the four accepted decoders and the WebP encoder are actually available. + +Upload byte counts and source pixel dimensions are operational resource controls, not domain rules. ImageMagick limits remain configurable and their defaults are calibrated with representative and adversarial images under the target container memory and expected concurrency; they are not inferred from codec capabilities. Exceeding a resource limit or failing normalization deletes the temporary source, leaves the report draft unchanged, and returns an actionable retry error. No stable photo reference is created until the normalized result has passed format and dimension verification. diff --git a/docs/adr/0018-allow-declared-handwritten-inventory-corrections.md b/docs/adr/0018-allow-declared-handwritten-inventory-corrections.md new file mode 100644 index 0000000..789438a --- /dev/null +++ b/docs/adr/0018-allow-declared-handwritten-inventory-corrections.md @@ -0,0 +1,5 @@ +# Allow declared handwritten inventory corrections at lease entry + +**Status**: accepted + +When the entry condition report reveals a difference from the furniture inventory printed in the latest lease revision, the parties may correct that inventory by hand and initial the correction before signing. The printed revision number is never changed by hand: it continues to identify the generated base document, while the declared corrections form a separate contractual layer. Lease finalization requires the landlord to answer whether such differences exist, confirm which corrections prefilled from the structured report were actually written and initialed, and attest that every party initialed them; correction text may be adjusted and exceptional inventory corrections added, while changes to any other lease term still require a regenerated revision. The signed scan becomes the contractual evidence. diff --git a/docs/adr/0019-reset-development-data-instead-of-migrating.md b/docs/adr/0019-reset-development-data-instead-of-migrating.md new file mode 100644 index 0000000..47e4c23 --- /dev/null +++ b/docs/adr/0019-reset-development-data-instead-of-migrating.md @@ -0,0 +1,5 @@ +# Reset development data instead of preserving compatibility + +**Status**: accepted + +The application is not in production, so existing development data and contracts are not compatibility constraints. Until an explicit later decision declares production data must be preserved, domain and schema changes replace obsolete Mongo documents, reference data, and API contracts directly; all callers and tests are updated together, and development data is deleted or reseeded instead of adding migrations, compatibility adapters, or transitional read/write paths. diff --git a/docs/research/issue-7-java-webp-normalization.md b/docs/research/issue-7-java-webp-normalization.md new file mode 100644 index 0000000..f7e0c65 --- /dev/null +++ b/docs/research/issue-7-java-webp-normalization.md @@ -0,0 +1,161 @@ +# Issue #7 — normalisation WebP côté backend Java + +> **Superseded:** the Java-library recommendation in this note was rejected in favor of ImageMagick. See [Issue #7 — socle pérenne de normalisation des photos](issue-7-maintainable-image-stack.md) and ADR 0017. The capability and limitation findings below remain historical research. + +Recherche arrêtée au 23 juillet 2026. Les conclusions ci-dessous reposent uniquement sur les API, dépôts et métadonnées de publication des projets concernés. + +## Conclusion + +La proposition « JPEG/PNG/WebP, 20 Mo, 50 mégapixels » ne découle d'aucune limite de bibliothèque. Les codecs ne fournissent pas une politique de sécurité prête à l'emploi et ne justifient donc ni 20 Mo ni 50 MP. + +L'option minimale qui couvre l'ADR-0017 est : + +- `net.coobird:thumbnailator:0.4.21` pour l'orientation EXIF JPEG, le calcul de taille et le redimensionnement ; +- `com.github.usefulness:webp-imageio:0.11.0` pour l'encodeur WebP lossy, explicitement épinglé car Spring Boot ne le gère pas ; +- les lecteurs ImageIO du JDK pour les formats d'entrée. + +Elle permet une implémentation courte, mais avec trois précautions indispensables : + +1. `size(1920, 1080)` de Thumbnailator **agrandit** une petite image. Pour respecter « sans upscaling », il faut calculer `scale = min(1, 1920 / largeur_orientée, 1080 / hauteur_orientée)` ou utiliser `scale(1)` quand l'image orientée tient déjà dans le cadre. +2. L'auto-orientation de Thumbnailator est centrée sur l'EXIF d'un JPEG (arbre `javax_imageio_jpeg_image_1.0` ou segment APP1). Elle n'est pas une garantie générale pour l'EXIF embarqué dans un TIFF, PNG ou WebP. +3. Ni Thumbnailator ni `webp-imageio` n'impose un plafond de pixels protecteur. Le plafond d'octets existant ne protège pas seul contre une image très compressée demandant beaucoup de mémoire une fois décodée. Le seuil opérationnel doit faire l'objet d'une décision séparée, mesurée avec la mémoire allouée au conteneur, et non être présenté comme une contrainte du codec. + +Pour les seuls uploads photo usuels JPEG/PNG, c'est le meilleur rapport simplicité/couverture. Si le ticket exige une orientation EXIF uniforme pour **tous** les conteneurs acceptés, remplacer la lecture EXIF de Thumbnailator par `metadata-extractor` et appliquer explicitement l'une des huit transformations est plus robuste, au prix de code métier supplémentaire. + +## Ce que fournit le JDK 25 seul + +L'API ImageIO du JDK 25 garantit des lecteurs et écrivains pour **BMP, GIF, JPEG, PNG, TIFF et WBMP**. WebP ne fait pas partie des plug-ins standards. Source : [documentation Java SE 25 de `javax.imageio`](https://docs.oracle.com/en/java/javase/25/docs/api/java.desktop/javax/imageio/package-summary.html). + +Conséquences : + +- entrées réellement décodables sans dépendance : BMP, GIF, JPEG, PNG, TIFF, WBMP ; +- sortie WebP : impossible sans plug-in ou outil externe ; +- redimensionnement/rotation : possible avec Java2D (`BufferedImage`, `Graphics2D`, `AffineTransform`), donc aucune dépendance n'est techniquement nécessaire pour les pixels ; +- orientation EXIF : ImageIO ne l'applique pas automatiquement. Le lecteur JPEG expose notamment les marqueurs APP dans son arbre de métadonnées, mais il faut analyser l'APP1/TIFF et appliquer la transformation soi-même ; +- métadonnées : `IIOImage` permet de les transmettre à un écrivain, mais une image réencodée avec des métadonnées `null` ne recopie pas celles de l'original ; +- animation/multipage : le plug-in GIF sait écrire des séquences et les lecteurs peuvent exposer plusieurs images, mais la commodité `ImageIO.read(...)` ne lit que l'image d'index 0. + +Le JDK seul échoue donc sur deux exigences du ticket : WebP et auto-orientation EXIF. Réimplémenter un parseur EXIF pour éviter une petite dépendance n'est pas l'option la plus simple ni la moins risquée. + +## Option recommandée : Thumbnailator + usefulness/webp-imageio + +### Capacités réelles + +| Sujet | Comportement vérifié | +|---|---| +| Formats d'entrée | Ceux des lecteurs ImageIO présents : avec le JDK et `webp-imageio`, BMP, GIF, JPEG, PNG, TIFF, WBMP et WebP. Le type MIME et l'extension ne sont pas nécessaires à la sélection du lecteur. HEIC/HEIF et AVIF ne sont pas décodés par cette combinaison. | +| WebP en sortie | Oui, lossy ou lossless. Le plug-in enregistre un `ImageWriter` ImageIO WebP. | +| Qualité 75 | Oui. Thumbnailator transmet `0.75f` à `ImageWriteParam.setCompressionQuality`; `WebPWriteParam` multiplie cette valeur par 100 pour libwebp. Il faut conserver le type de compression `Lossy`. | +| Orientation EXIF | Oui pour les huit valeurs d'orientation JPEG. Activée par défaut dans Thumbnailator et activable explicitement avec `useExifOrientation(true)`. Le code ajoute la rotation/réflexion avant les autres filtres et échange les dimensions pour les orientations 5 à 8. | +| Portée EXIF | JPEG surtout : le parseur lit l'arbre natif JPEG et son repli capture le segment APP1. Le lecteur usefulness retourne toujours `null` pour les métadonnées WebP ; un WebP portant une orientation EXIF ne sera donc pas réorienté par cette combinaison. | +| Métadonnées de sortie | Supprimées : Thumbnailator appelle l'écrivain avec `new IIOImage(img, null, null)` et l'écrivain usefulness retourne `null` pour ses métadonnées par défaut/conversion. Le WebP produit ne reprend donc ni EXIF, ni XMP, ni GPS, ni miniature. | +| Animation | Thumbnailator lit l'image 0. Son usage ordinaire aplatit donc un GIF multipage sur sa première image. Le lecteur usefulness annonce toujours une seule image et n'expose pas l'animation WebP. Il n'y a pas de rejet automatique uniforme des animations. | +| Redimensionnement | Conservation du ratio et filtres de redimensionnement fournis. Attention : `size(1920, 1080)` peut agrandir ; le non-upscaling nécessite le `min(1, ...)` explicite. | +| Dépendances natives | `webp-imageio` utilise JNI et embarque libwebp. Binaires présents pour Linux x86, x86_64, ARM/ARMv6/ARMv7/AArch64, PPC64 ; macOS x86_64/AArch64 ; Windows x86/x86_64. Aucun paquet système libwebp n'est à installer sur ces cibles. | +| Java/JDK | Thumbnailator 0.4.21 produit un JAR Java ancien et sans dépendance runtime propre. `webp-imageio` annonce Java 11, cible un bytecode ancien et teste actuellement les JDK 17, 21 et 24. Java 25 n'est pas déclaré dans sa matrice ; sa compatibilité doit donc rester couverte par un test d'intégration du projet sous le JDK 25 réel. Aucune intégration Spring spécifique n'est requise. | +| Licences | Thumbnailator : MIT. usefulness/webp-imageio : Apache-2.0 ; binaires libwebp sous la licence du projet libwebp. | +| Maintenance | Thumbnailator 0.4.21 a été publié le 1er octobre 2025 et son dépôt reste actif en 2026. usefulness/webp-imageio 0.11.0 a été publié le 15 juin 2026 avec libwebp 1.6.0. | +| Limites/protections | Aucun plafond de pixels documenté. Le lecteur usefulness met le flux WebP compressé entier dans un `byte[]`, refuse seulement une longueur connue supérieure à `Integer.MAX_VALUE`, puis alloue le raster décodé. Thumbnailator contient un mécanisme optionnel de sous-échantillonnage dépendant de la mémoire libre, mais ce n'est ni une validation d'upload ni une limite contractuelle. | + +Sources primaires : + +- [README, plateformes, usage, licence de usefulness/webp-imageio](https://github.com/usefulness/webp-imageio) +- [release 0.11.0 et contraintes glibc](https://github.com/usefulness/webp-imageio/releases/tag/v0.11.0) +- [lecteur usefulness : une image, flux en `byte[]`, aucune métadonnée](https://github.com/usefulness/webp-imageio/blob/master/webp-imageio/src/main/kotlin/com/luciad/imageio/webp/internal/WebPReader.kt) +- [écrivain usefulness : aucune métadonnée, encodage RGB/RGBA](https://github.com/usefulness/webp-imageio/blob/master/webp-imageio/src/main/kotlin/com/luciad/imageio/webp/internal/WebPWriter.kt) +- [`WebPWriteParam` : qualité ImageIO 0..1 vers libwebp 0..100](https://github.com/usefulness/webp-imageio/blob/master/webp-imageio/src/main/kotlin/com/luciad/imageio/webp/WebPWriteParam.kt) +- [binaires natifs embarqués](https://github.com/usefulness/webp-imageio/tree/master/webp-imageio/src/main/resources/native) +- [matrice de CI usefulness](https://github.com/usefulness/webp-imageio/blob/master/.github/workflows/default.yml) +- [source de l'orientation EXIF Thumbnailator](https://github.com/coobird/thumbnailator/blob/master/src/main/java/net/coobird/thumbnailator/util/exif/ExifUtils.java) +- [transformations des huit orientations](https://github.com/coobird/thumbnailator/blob/master/src/main/java/net/coobird/thumbnailator/util/exif/ExifFilterUtils.java) +- [ordre orientation puis lecture/redimensionnement](https://github.com/coobird/thumbnailator/blob/master/src/main/java/net/coobird/thumbnailator/tasks/io/InputStreamImageSource.java) +- [sortie Thumbnailator sans métadonnées et transmission de la qualité](https://github.com/coobird/thumbnailator/blob/master/src/main/java/net/coobird/thumbnailator/tasks/io/OutputStreamImageSink.java) +- [publication et licence Thumbnailator](https://github.com/coobird/thumbnailator) + +### Forme minimale de l'implémentation + +Le traitement doit rester explicite sur les étapes suivantes : + +1. ouvrir un `ImageInputStream` et sélectionner le lecteur d'après le contenu ; +2. lire largeur, hauteur, nombre d'images quand le lecteur sait le fournir, et l'orientation EXIF avant le raster complet ; +3. déterminer les dimensions après orientation ; +4. calculer `min(1, 1920 / largeur, 1080 / hauteur)` afin de ne jamais agrandir ; +5. lire, orienter et redimensionner ; +6. écrire avec le writer WebP en `Lossy`, qualité `0.75f`, et `IIOImage(..., null, null)` ; +7. rouvrir le résultat, vérifier dimensions/format, puis seulement attribuer la référence stable et supprimer l'original comme prévu par l'ADR. + +Thumbnailator raccourcit les étapes 3 à 6 ; il ne dispense pas de la validation de la source ni de la décision sur les animations et les ressources. + +## Alternative : Java2D + usefulness/webp-imageio + metadata-extractor + +`com.drewnoakes:metadata-extractor` lit l'EXIF, notamment l'orientation, dans JPEG, TIFF, WebP, PNG et plusieurs autres conteneurs. Il ne décode pas les pixels, ne redimensionne pas et n'écrit pas WebP. Sa version actuelle cible Java 8, n'emploie pas de code natif et est sous Apache-2.0. + +Cette variante conserve les mêmes entrées pixel réellement décodables que JDK + usefulness, mais rend la lecture de l'orientation indépendante du lecteur ImageIO. Il faut ensuite écrire explicitement les huit transformations `AffineTransform`, le calcul de taille et le redimensionnement Java2D. L'écriture et les contraintes natives restent celles de usefulness/webp-imageio. + +Avantage : comportement EXIF plus large et contrôlable, erreurs de métadonnées observables. Inconvénient : plus de code sensible qu'avec Thumbnailator. À choisir uniquement si l'exigence porte bien sur l'EXIF de plusieurs conteneurs, et pas seulement sur les JPEG de téléphone. + +Sources primaires : [formats et usage de metadata-extractor](https://github.com/drewnoakes/metadata-extractor), [POM, Java 8 et licence](https://github.com/drewnoakes/metadata-extractor/blob/master/pom.xml), [constante `TAG_ORIENTATION` de l'IFD0](https://github.com/drewnoakes/metadata-extractor/blob/master/Source/com/drew/metadata/exif/ExifIFD0Directory.java). + +## Alternative : Scrimage WebP + +`com.sksamuel.scrimage:scrimage-webp:4.6.6` offre une API plus intégrée : lecture ImageIO, metadata-extractor, auto-orientation activée par défaut, `bound(...)` pour réduire sans agrandir et `new WebpWriter().withQ(75)`. + +Elle n'est pas la solution minimale pour ce backend : + +- l'artefact WebP entraîne `scrimage-core`, TwelveMonkeys JPEG, metadata-extractor, commons-io, pngj, commons-lang3 et SLF4J ; +- l'écriture passe par un PNG temporaire puis exécute `cwebp` dans un processus ; +- la bibliothèque extrait des exécutables embarqués dans un fichier temporaire et doit pouvoir les exécuter ; +- les binaires embarqués couvrent Linux x86_64/AArch64, macOS x86_64/ARM64 et Windows x64, moins de cibles que usefulness ; +- les ressources du module contiennent libwebp 1.4.0, alors que usefulness 0.11.0 embarque 1.6.0 ; +- le timeout de processus est de cinq minutes, mais aucune limite de pixels ou d'octets n'est imposée ; +- le projet cible Java 11 et est activement publié en 2026 sous Apache-2.0. + +Scrimage sait aussi traiter les animations via des API dédiées, fonctionnalité inutile au ticket tant qu'aucune politique d'animation n'est décidée. + +Sources primaires : [dépôt et release actuelle Scrimage](https://github.com/sksamuel/scrimage), [dépendances de `scrimage-core`](https://github.com/sksamuel/scrimage/blob/master/scrimage-core/build.gradle.kts), [écrivain WebP et qualité](https://github.com/sksamuel/scrimage/blob/master/scrimage-webp/src/main/java/com/sksamuel/scrimage/webp/WebpWriter.java), [exécution de `cwebp`](https://github.com/sksamuel/scrimage/blob/master/scrimage-webp/src/main/java/com/sksamuel/scrimage/webp/CWebpHandler.java), [plateformes et extraction des binaires](https://github.com/sksamuel/scrimage/blob/master/scrimage-webp/src/main/java/com/sksamuel/scrimage/webp/WebpHandler.java), [auto-orientation par défaut](https://github.com/sksamuel/scrimage/blob/master/scrimage-core/src/main/java/com/sksamuel/scrimage/nio/ImmutableImageLoader.java). + +## TwelveMonkeys ImageIO + +TwelveMonkeys 3.14.x est activement maintenu, Java 8+, pur Java et BSD-3-Clause. Son plug-in WebP sait **lire**, y compris les trames d'animation, et analyser ICC/EXIF/XMP ; il ne sait pas écrire WebP. Son tableau de formats indique explicitement `R = oui`, `W = non`. + +Il peut améliorer la validation d'un WebP entrant ou apporter un lecteur JPEG plus tolérant (notamment aux variantes rencontrées dans la nature), mais ne remplace pas usefulness pour la sortie. L'associer à usefulness installe deux lecteurs WebP concurrents, ajoute plusieurs JARs et ne réalise toujours pas automatiquement l'orientation. Ce n'est donc pas justifié pour la première implémentation de l'issue #7. + +Sources primaires : [table des formats TwelveMonkeys](https://github.com/haraldk/TwelveMonkeys#file-formats-supported), [source du lecteur WebP et des animations/métadonnées](https://github.com/haraldk/TwelveMonkeys/blob/master/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java), [licence BSD-3-Clause](https://github.com/haraldk/TwelveMonkeys/blob/master/LICENSE.txt), [versions Maven Central](https://repo1.maven.org/maven2/com/twelvemonkeys/imageio/imageio-webp/). + +## Décisions qui restent au produit, pas à la bibliothèque + +### Formats acceptés + +La combinaison recommandée **peut** décoder BMP, GIF, JPEG, PNG, TIFF, WBMP et WebP. Cela ne signifie pas que l'API doit tous les accepter. Pour un parcours « photo de constat », JPEG et PNG couvrent le cas minimal ; accepter WebP entrant est simple mais son orientation EXIF n'est pas couverte par Thumbnailator. HEIC/HEIF exige un autre décodeur et n'est fourni par aucune option minimale ci-dessus. + +La validation doit utiliser le lecteur capable de décoder le flux, pas seulement l'extension ou le `Content-Type` déclaré. + +### Animations et fichiers multipages + +Les bibliothèques ne convergent pas vers une politique : GIF/TIFF peuvent contenir plusieurs images, usefulness ne représente pas l'animation WebP, Thumbnailator choisit l'image 0, TwelveMonkeys expose les trames. Il faut décider explicitement entre rejet et aplatissement sur la première image ; aucun des deux choix ne découle d'une limite technique. + +### Octets et pixels + +Aucun seuil « 20 Mo » ou « 50 MP » n'est fourni ou recommandé par ces bibliothèques. Les faits techniques sont seulement : + +- le multipart Spring possède déjà une limite globale configurable ; +- les pixels décodés consomment typiquement au moins `largeur × hauteur × 4` octets, auxquels s'ajoutent les buffers du lecteur, de l'image orientée, du redimensionnement et de l'encodeur ; +- usefulness charge en plus le WebP compressé complet en mémoire ; +- une limite d'octets compressés ne borne pas la mémoire du raster ; +- les dimensions peuvent être interrogées avant le décodage complet avec ImageIO pour prendre une décision. + +La prochaine décision devrait donc partir du budget mémoire et du parallélisme réels du conteneur, avec des images adversariales et des photos de téléphones représentatives. En l'absence de cette mesure, conserver provisoirement la limite multipart existante et ne pas inventer un plafond « métier » présenté comme une capacité de la bibliothèque ; en revanche, le décodage d'uploads non fiables devra recevoir une protection de ressources avant exposition publique. + +## Recommandation finale pour l'issue #7 + +Adopter Thumbnailator 0.4.21 + usefulness/webp-imageio 0.11.0, restreindre initialement l'entrée aux formats réellement requis par le parcours, et tester sous JDK 25 : + +- les huit orientations EXIF JPEG ; +- paysage/portrait au-dessus et au-dessous de 1920 × 1080, en vérifiant l'absence d'agrandissement ; +- sortie WebP lossy à `0.75f` ; +- absence d'EXIF/XMP/GPS dans le résultat ; +- comportement choisi pour GIF/TIFF/WebP animés ; +- échec propre d'un flux tronqué ou non-image ; +- disponibilité du binaire JNI dans l'image Docker de production. + +Ne fixer aucun nouveau seuil octets/pixels dans cette décision de bibliothèque. Le documenter séparément une fois le budget mémoire et la concurrence connus. diff --git a/docs/research/issue-7-maintainable-image-stack.md b/docs/research/issue-7-maintainable-image-stack.md new file mode 100644 index 0000000..881dfcf --- /dev/null +++ b/docs/research/issue-7-maintainable-image-stack.md @@ -0,0 +1,114 @@ +# Issue #7 — socle pérenne de normalisation des photos + +Recherche arrêtée au 23 juillet 2026. Les affirmations techniques ci-dessous reposent sur la documentation et les dépôts officiels des projets. + +## Conclusion + +Pour éviter de dépendre de petits wrappers Java et de binaires natifs embarqués dans des JAR, le choix le plus durable et le plus simple est **ImageMagick 7 en ligne de commande**, installé dans l'image Docker et invoqué par Java avec `ProcessBuilder`. + +Après une lecture contrôlée du format, du nombre d'images et des dimensions orientées, une seule commande de conversion couvre le pipeline décidé pour l'issue #7. L'adaptateur choisit `1920x1080>` pour un paysage ou un carré et `1080x1920>` pour un portrait : + +```text +magick [0] -auto-orient -resize -strip -orient Undefined -quality 75 -define webp:lossless=false webp: +``` + +Dans `ProcessBuilder`, chaque élément est un argument distinct : il ne faut pas passer cette chaîne à un shell. Les chemins d'entrée et de sortie doivent être des chemins temporaires produits par l'application, pas des noms fournis directement par l'utilisateur. + +Cette commande : + +- applique l'une des orientations EXIF avant le redimensionnement ; +- conserve le ratio dans une boîte de 1920 × 1080 en paysage ou 1080 × 1920 en portrait et, grâce au suffixe `>`, ne redimensionne que les images plus grandes ; +- supprime profils, commentaires, EXIF, XMP et chunks de métadonnées connus, puis efface explicitement toute orientation résiduelle ; +- écrit un WebP avec pertes de qualité 75 ; +- sélectionne la première image avec `[0]`. Accepter, rejeter ou aplatir les formats animés reste une décision produit, pas une conséquence d'ImageMagick. + +Sources : [auto-orientation, qualité, redimensionnement et suppression des métadonnées](https://imagemagick.org/command-line-options/), [géométrie `WxH>` et conservation du ratio](https://imagemagick.org/command-line-processing/), [options de l'encodeur WebP](https://imagemagick.org/webp/). + +## Pourquoi ce choix répond au risque de maintenance + +ImageMagick n'est pas une petite bibliothèque Java : + +- le projet est maintenu par **ImageMagick Studio LLC** ; +- son dépôt compte plus de 24 000 commits et 277 publications ; +- la version 7.1.2-27 a été publiée le 5 juillet 2026 ; +- le projet publie des versions Linux, macOS et Windows et possède un canal d'avis de sécurité actif ; +- le support WebP s'appuie sur la bibliothèque `libwebp` du système, au lieu d'enfermer une copie dans le JAR de l'application. + +Sources : [organisation ImageMagick Studio LLC](https://imagemagick.org/support/), [dépôt et historique des versions](https://github.com/ImageMagick/ImageMagick), [téléchargements officiels](https://imagemagick.org/download/), [avis de sécurité officiels](https://github.com/ImageMagick/ImageMagick/security/advisories). + +Aucun projet ne garantit qu'il sera maintenu éternellement. Ici, le risque est toutefois limité par deux propriétés : le projet est ancien, très diffusé et activement publié ; l'intégration applicative repose sur un contrat de processus minuscule, sans types ni API Java propres à ImageMagick. Un éventuel remplacement ne toucherait que l'appel de normalisation et l'image Docker. + +## Sécurité des uploads non fiables + +La contrepartie d'ImageMagick est sa grande surface fonctionnelle. **L'installation par défaut, ouverte, ne doit pas traiter directement les uploads.** Le projet fournit précisément une politique pour réduire cette surface : + +- refuser tous les modules puis autoriser seulement les formats d'entrée retenus et l'écriture WebP ; +- interdire tous les délégués externes et filtres ; +- interdire les lectures indirectes (`@...`), les flux standard et les chemins sensibles ; +- limiter largeur, hauteur et longueur des séquences ; +- borner mémoire, mémoire mappée, disque temporaire, threads et durée ; +- utiliser un répertoire temporaire privé ; +- vérifier la politique chargée avec `magick identify -list policy`. + +ImageMagick documente des limites distinctes pour la mémoire, le `mmap`, le disque, la durée, la largeur, la hauteur et le nombre d'images. La limite `area` ne rejette pas une image : elle fait basculer son cache de pixels vers le disque. Les valeurs d'exemple de la documentation (`256MiB`, `4KP`, etc.) ne sont **pas** des recommandations pour LMNP Organizer ; elles devront être choisies selon la mémoire réelle du conteneur et la concurrence mesurée. Le timeout Java et la limite mémoire du conteneur restent la dernière barrière. + +Source : [politique de sécurité ImageMagick et exemple complet](https://imagemagick.org/security-policy/), [ressources et variables de limite](https://imagemagick.org/resources/). + +## Comparaison + +| Option | Pipeline complet | Pérennité observable | Sécurité et ressources | Coût d'intégration | Verdict | +|---|---|---|---|---|---| +| **ImageMagick 7 CLI** | Oui, en une commande | ImageMagick Studio LLC, 277 versions, publication active en juillet 2026 | Politique fine : formats, chemins, délégués, mémoire, disque, dimensions, séquences, temps | Un paquet système, un appel `ProcessBuilder`, un `policy.xml` | **Recommandé** | +| **libvips / `vipsthumbnail`** | Oui, en une commande | Projet actif, 102 versions, utilisé notamment par Mastodon, Sharp, Rails et MediaWiki | Faible mémoire, blocage des opérations non fiables et dimension maximale ; garde-fous moins complets qu'ImageMagick au niveau CLI | Un paquet système et un appel `ProcessBuilder` | Excellente alternative si les mesures montrent qu'ImageMagick consomme trop | +| **`cwebp` officiel** | Non | Implémentation de référence WebP publiée par Google, API documentée en version 1.6.0 | Option `-low_memory`, mais pas de politique complète de décodage/upload | Nécessite du code ou un second outil pour lire l'orientation et calculer la taille | Ne pas retenir seul | +| **Bibliothèques Java ImageIO** | Non | TwelveMonkeys est établi et actif | Décodage dans le processus JVM ; pas de politique équivalente | WebP TwelveMonkeys est en lecture seule ; un autre encodeur reste nécessaire | Ne résout pas le problème de dépendance | + +## Détail : libvips + +`vipsthumbnail` sait réaliser la même transformation une fois le cadre orienté choisi : + +```text +vipsthumbnail --size --path .webp[Q=75,keep=none] +``` + +La documentation garantit que : + +- la sortie tient dans la boîte demandée ; +- le suffixe `>` limite le redimensionnement aux images plus grandes ; +- l'orientation des métadonnées est appliquée par défaut ; +- `keep=none` ne conserve aucune métadonnée ; +- le writer WebP est avec pertes par défaut et `Q` accepte 0 à 100 ; +- les formats disponibles dépendent des codecs présents à la compilation, notamment `libwebp` et `libheif`. + +libvips est un projet C majeur, pas un petit wrapper : son dépôt officiel le donne comme moteur de Mastodon, Sharp, imgproxy, Rails et MediaWiki ; il publie encore régulièrement des correctifs issus du fuzzing. Il expose `VIPS_BLOCK_UNTRUSTED` pour bloquer les opérations dépendant de bibliothèques non durcies et `VIPS_MAX_COORD` pour borner une dimension. Il consomme généralement moins de mémoire qu'ImageMagick. + +Il reste en second choix ici parce que la demande priorise la notoriété et la pérennité perçue, et parce qu'ImageMagick offre une politique de sécurité plus complète directement adaptée à un service d'upload. Si un benchmark réel révèle un problème de mémoire ou de débit, remplacer la commande par `vipsthumbnail` est une évolution localisée. + +Sources : [guide officiel `vipsthumbnail`](https://www.libvips.org/API/current/using-vipsthumbnail.html), [`thumbnail` : orientation, boîte et absence d'agrandissement](https://www.libvips.org/API/current/ctor.Image.thumbnail.html), [écriture WebP](https://www.libvips.org/API/current/method.Image.webpsave.html), [formats, utilisateurs et dépendances officielles](https://github.com/libvips/libvips), [blocage des opérations non fiables](https://www.libvips.org/2022/05/28/What%27s-new-in-8.13.html), [versions officielles](https://github.com/libvips/libvips/releases). + +## Détail : `cwebp` + +`cwebp`, livré avec la bibliothèque de référence `libwebp`, est l'encodeur le plus directement gouverné par le projet WebP. Il accepte PNG, JPEG, TIFF, WebP et Y'CbCr brut ; `-q 75` produit un WebP avec pertes ; `-metadata none` est le comportement par défaut ; `-resize` peut redimensionner et préserver le ratio si une dimension vaut zéro. + +Il ne fournit cependant pas l'auto-orientation EXIF et son redimensionnement reçoit une largeur et une hauteur calculées. Pour respecter simultanément l'orientation, la boîte 1920 × 1080 et l'absence d'agrandissement, l'application devrait ajouter un parseur EXIF et du code de transformation, ou lancer un deuxième outil. Ce serait plus de code et deux chaînes de traitement alors qu'ImageMagick ou libvips le font seuls. + +Sources : [documentation officielle de `cwebp`](https://developers.google.com/speed/webp/docs/cwebp), [API officielle libwebp 1.6.0](https://developers.google.com/speed/webp/docs/api), [libwebp comme implémentation de référence](https://developers.google.com/speed/webp). + +## Absence de solution Java établie et suffisante + +Le JDK 25 n'écrit pas WebP. TwelveMonkeys est l'extension ImageIO Java la plus établie examinée, mais son tableau officiel indique explicitement que son plugin WebP sait lire et ne sait pas écrire. Il offre un bon redimensionneur, mais il faudrait toujours un encodeur natif ou un autre projet. + +Une grosse abstraction Java par-dessus un outil natif n'améliore donc pas la pérennité : elle ajoute un mainteneur et une API à suivre sans retirer la dépendance au codec. Appeler directement un exécutable maintenu et versionné dans Docker est plus simple et rend le cycle de correctifs natifs indépendant des dépendances Maven. + +Sources : [formats supportés par TwelveMonkeys](https://github.com/haraldk/TwelveMonkeys#file-formats-supported), [formats garantis par ImageIO dans Java 25](https://docs.oracle.com/en/java/javase/25/docs/api/java.desktop/javax/imageio/package-summary.html). + +## Intégration minimale recommandée + +1. Installer ImageMagick 7 avec le codec WebP dans l'étage d'exécution Docker ; vérifier au build la version et `WEBP` avec `magick -version` et `magick identify -list format`. +2. Ajouter un `policy.xml` en refus par défaut et n'autoriser que les codecs décidés. Ne pas recopier des seuils arbitraires : les mesurer puis les fixer. +3. Écrire l'upload dans un fichier temporaire contrôlé par l'application. +4. Exécuter la commande via les arguments de `ProcessBuilder`, sans shell, avec délai d'expiration et destruction forcée du processus. +5. Exiger le code de sortie zéro, rouvrir le résultat, vérifier qu'il est WebP et dans la boîte 1920 × 1080, puis seulement publier la référence canonique et supprimer l'original. +6. Couvrir dans l'image Docker réelle les huit orientations EXIF, portrait/paysage, absence d'agrandissement, qualité avec pertes, suppression des métadonnées, flux invalide et arrêt sur dépassement des limites. + +Cette décision remplace la proposition Thumbnailator + usefulness/webp-imageio ; elle ne fixe ni les formats d'entrée du contrat utilisateur, ni les seuils de ressources.