init docs #179

Merged
Nora merged 50 commits from feature/halloween into develop 2026-09-09 19:03:40 +00:00
Owner
No description provided.
init docs
Some checks failed
CI / Renderer - couverture JS (pull_request) Has been cancelled
CI / Renderer - audit des dépendances JS (pull_request) Has been cancelled
CI / Renderer - tests + lint JS (pull_request) Has been cancelled
CI / PHP - tests + static + lint (pull_request) Has been cancelled
8800291095
Nora added 15 commits 2026-09-08 12:00:32 +00:00
Lib pure `lib/spatial-hash.js` : insert/near en temps constant, ordre de
sortie fixé par le balayage du carré englobant (cy croissant, puis cx
croissant), puis l'ordre d'insertion dans la cellule — jamais l'ordre de
remplissage d'une Map. C'est cet ordre qui fait partie de l'identité des
œuvres : glass-crack arrête une fissure sur le premier voisin rencontré,
vein-colony croît vers le nœud le plus proche.

Bord du rayon fermé (`<=`), coordonnées négatives indexées au plancher,
retour toujours tableau. Ni palette, ni op de dessin, ni rng.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNZ5grqNECq3g4YKERBYWV
Douze entrées `halloween_*` trichromes dans la bibliothèque partagée du
catalogue, hex recopiés du plan §4. Gardées par un test d'intégration qui
lit le JSON de référence : codes présents, exactement trois couleurs au
format hex, et `colors[0]` (le fond, convention du dépôt) sous 0,12 de
luminance relative WCAG.

Aucune entrée d'algorithme ici : les palettes ne sont attribuées à personne
tant que les tâches de naissance ne sont pas jouées.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Oubli de la tâche : `readCatalog` a une garde de dérive sur le nombre de
palettes du catalogue PHP, dans l'outil dev `renderer/gpu/editor/`. Les
douze palettes Halloween la font passer de 194 à 206.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Descente régulière : N sources sur le bord haut, chacune laissant une coulée
faite de pastilles. Le stick-slip qui donnera l'organique arrive avec le
procédé (T04) — tant qu'aucun tirage n'est fait, `compute` ne prend pas de
`rng`, comme les autres modules déterministes du moteur.

Les trois écritures vont ensemble, sinon `catalog-contract.test.js` reste
rouge : la ligne de `REGISTRY`, l'entrée catalogue (`active: false`, les 12
palettes Halloween, le `paramSpec` du plan §5.1) et les six traductions.

La garde de dérive de l'éditeur GPU passe à 71 algorithmes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
La tête absorbe `flowRate` à chaque pas et n'avance que quand sa masse
dépasse `stickThreshold` ; sous le seuil elle stagne et gonfle de ce qu'elle
retient. Deux tests tiennent la loi : à seuil inatteignable les `y` d'une
coulée sont tous égaux (elle n'avance pas d'un pixel), à seuil nul ils sont
strictement croissants.

`SLIP_GAIN` reste une constante de module, pas un knob : le plan §5.1 ne le
tire pas, et `flowRate` / `stickThreshold` règlent déjà la dynamique.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Six knobs rejoignent `DEFAULTS` et le `paramSpec` : la dérive latérale suit un
champ de bruit (deux coulées voisines penchent du même côté, comme sur un mur),
les bords du ruban vivent avec `widthNoise`, et une coulée mère peut engendrer
un ruisselet qui hérite d'une masse réduite.

Ordre des tirages, gelé par le golden de T06 : la table de bruit d'abord, puis
un tirage de dérive et un tirage de ruisselet par tête vivante, dans l'ordre
d'index. Les têtes sont simulées pas à pas et non coulée par coulée — c'est ce
qui rend cet ordre lisible.

Seule la coulée mère engendre (`MAX_CHILD_GENERATION`) : des petits-enfants
feraient exploser le nombre de pastilles sans rien ajouter de lisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Trois plafonds refusés loud (RangeError → 422) : le nombre de coulées, le
nombre de pas, et un majorant du nombre de pastilles qui tient compte des
ruisselets. Écrits avant leur test, sinon le RED tourne des minutes au lieu
d'échouer.

La motion se déduit du flux (plus de 200 ops, aucun `pixels`) : `draw-on`,
sans `meta.motion` à déclarer — une coulée qui descend est exactement ce que
le rejeu op par op sait montrer.

Le golden est un filet de régression, pas un test-first : relevé une fois puis
gelé. Il garde l'ordre des tirages — table de bruit, puis dérive et ruisselet
par tête vivante dans l'ordre d'index.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Une heightmap 1D, des mèches fixes, et toutes les `layerEvery` gouttes le
profil courant peint en strate fermée sur le bas du cadre. La relaxation vers
l'angle de repos — celle qui étale les bourrelets et dévie les gouttes
suivantes — arrive avec le procédé (T08).

Les trois écritures vont ensemble, sinon `catalog-contract.test.js` reste
rouge. La garde de dérive de l'éditeur GPU passe à 72 algorithmes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Après chaque goutte, `relaxIters` passes déterministes (gauche→droite puis
droite→gauche) font glisser `spread × excès` vers le voisin bas. Une mèche
unique ne fait plus une aiguille : le test reconstruit le profil du dernier
polygone et vérifie qu'aucune paire de colonnes ne dépasse `reposeSlope`.

La relaxation partielle seule n'y suffit pas — elle laisse une queue de
convergence au-dessus du seuil. Une strate est donc peinte AU REPOS : avant de
figer la couche, la cire finit de glisser (`settleToRepose`, bornée). Les deux
knobs restent vivants, ils décident où la matière se trouve quand la couche
fige.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Trois knobs de couleur rejoignent `DEFAULTS` et le `paramSpec` : l'opacité
d'une strate est un champ `alpha` à part (jamais une couleur rgba pré-cuite,
sinon le knob n'est plus testable), et la teinte dérive d'une couche à la
suivante — c'est cette dérive lente qui fait lire l'empilement comme des
couches et non comme un aplat.

Les bornes de `reposeSlope` et `spread` s'alignent sur le plan.

Le knob `relaxIters` se prouve à 1 passe et non à 6 : au-delà de trois, la
différence retombe sous le pixel sur la base réduite de la suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Trois plafonds, un test chacun (gouttes, strates, colonnes) : un body forgé
qui les franchit tous ne dirait pas lequel a mordu. Écrits avant leurs tests,
sinon le RED tourne des minutes au lieu d'échouer.

La motion demandait une décision : vingt strates ne font que vingt ops, très
en dessous du seuil du draw-on — un clignotement, pas une animation. Chaque
goutte laisse donc la perle que le plan §5.2 annonçait dans ses ops, ce qui
fait le grain de la cire ET la longueur de flux nécessaire. La détection reste
générique, aucun `meta.motion` déclaré.

Le golden gèle des octets qui ne dépendent d'aucun tirage : les mèches sont
fixes et l'ordre de balayage de la relaxation l'est aussi.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Des filaments tendus du haut au bas du cadre, chacun décrit par un rayon r(s)
peint en chaîne de pastilles : c'est le rayon qui porte l'information, jamais
un trait. Le profil est perturbé par `modes` sinusoïdes d'amplitudes tirées —
une seule série de tirages, par filament puis par mode.

La croissance sélective de ces modes (Rayleigh–Plateau) arrive avec le
procédé (T12) : ici le profil reste figé.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Un mode croît si et seulement si sa longueur d'onde dépasse la circonférence
`2πr`, il s'amortit sinon. Le test oppose deux filaments : le gros, pour lequel
aucune longueur d'onde disponible ne dépasse la circonférence, garde toutes ses
pastilles ; le fin se rompt — un rayon passé sous zéro n'a plus de matière à
peindre.

`MAX_AMPLITUDE` borne la croissance exponentielle : au-delà le filament est
déjà rompu, la borne empêche seulement de peindre des pastilles absurdes.
`growthRate` et `steps` étaient déjà au catalogue depuis la naissance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Quatre knobs rejoignent `DEFAULTS` et le `paramSpec`. Après une rupture, les
deux bouts reculent d'une fraction du vide et la matière qu'ils ramènent
grossit la pastille de bord — rien ne se perd, d'où les têtes rondes de part
et d'autre d'une cassure. Un tirage par rupture, dans l'ordre des ruptures,
décide de la gouttelette satellite laissée au milieu du vide.

Le filament s'affaisse en parabole entre ses deux ancres : le test de
naissance, qui compte une colonne par filament, lit désormais ce cas sans
affaissement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
T14 — strand-break : plafonds, motion, golden
All checks were successful
CI / Renderer - couverture JS (pull_request) Has been skipped
CI / Renderer - audit des dépendances JS (pull_request) Successful in 1m2s
CI / Renderer - tests + lint JS (pull_request) Successful in 6m44s
CI / PHP - tests + static + lint (pull_request) Successful in 7m7s
fc2af75ac7
Trois plafonds refusés loud (RangeError → 422) : filaments, pas de simulation,
et un majorant des pastilles à deux par échantillon — une du filament, une
satellite. Écrits avant leur test, sinon le RED tourne des minutes.

La motion se déduit du flux : `draw-on`, aucun `meta.motion` déclaré. Le
golden gèle l'ordre des tirages : les amplitudes par filament, puis un tirage
par rupture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Nora added 10 commits 2026-09-08 12:42:58 +00:00
Un moyeu décentré de `hubOffset × min(w, h)` dans une direction tirée, un
pourtour irrégulier d'ancres sur une ellipse du cadre, et un rayon tendu du
moyeu vers chacune. C'est le décentrage qui fait lire « toile » plutôt que
« rosace ».

Ordre d'aléa figé : la direction du moyeu d'abord, puis un rayon de pourtour
par ancre. La spirale de capture affaissée en caténaire arrive avec le procédé
(T16) ; ici les rayons sont droits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Entre deux rayons voisins, à chaque anneau, un brin de capture qui ne rejoint
pas ses deux nœuds en ligne droite : il pend sous sa propre corde, d'une flèche
valant `sag` fois sa longueur, dispersée de `sagNoise`. C'est cet affaissement
qui fait lire « toile » plutôt que « diagramme radial ».

Le test compare le milieu du brin au milieu de sa corde : droit à `sag: 0`,
strictement plus bas d'au moins un dixième de corde à `sag: 0.25`.

Un tirage d'affaissement par brin, dans l'ordre des ancres puis des anneaux.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
La toile est abandonnée : un anneau sur `tearProb` perd un arc de `tearSpan`
brins, et les deux brins qui bordent le trou pendent d'une demi-maille du côté
qui a perdu son appui. Un nœud sur `dewProb` porte une goutte.

Un SEUL tirage par anneau décide de la déchirure ET de son ancre de départ : la
position du tirage dans l'intervalle `[0, tearProb)` dit où. Réutiliser
l'uniforme conditionnelle garde le flux d'aléa indépendant du nombre de
déchirures — sinon un anneau intact décalerait toute la suite.

`colorMode` : `ring` (bandes concentriques), `radial` (secteurs), `depth`
(distance au moyeu). Onze knobs prouvés un à un, plus l'homothétie 64 → 128.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
`assertCap` sur les ancres (64), les anneaux (200) et le majorant du flux
(60 000) : un body forgé est refusé loud, jamais rendu tronqué.

Défauts relevés à `anchors: 9`, `rings: 24` pour que le flux passe les 200 ops
de `DRAW_ON_MIN_OPS` — sinon la détection retombait sur `ken-burns` et la toile
se serait filmée comme un raster mort. Mesuré sur cinq seeds : 220–230 ops, là
où `rings: 28` seul donnait 202, une marge de deux ops qu'un tirage de
déchirure suffisait à emporter. Les deux valeurs restent dans leur paramSpec.

Golden gelé après le vert : il garde l'ordre des tirages, dont celui de la
déchirure qui sert deux fois.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Un point d'impact tiré dans le cadre, et `radials` fissures qui en repartent en
marche : à chaque pas l'angle dérive d'un bruit borné, jamais la direction
d'ensemble. C'est ce qui distingue une fêlure d'une craquelure — la fissure
garde la mémoire de la direction que lui a donnée le choc.

Le tirage de dérive a lieu à chaque pas MÊME après la sortie du cadre : la
longueur du flux d'aléa ne doit pas dépendre de la géométrie, sinon une fissure
qui sort tôt décale toutes les suivantes.

L'arrêt en T sur une fissure existante arrive avec le procédé (T20) ; ici
`snapDist` est tiré mais pas encore lu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Chaque point tracé entre dans une grille de hachage spatiale ; à chaque pas la
fissure interroge son voisinage et, si elle y trouve le point d'une AUTRE
fissure, elle le rejoint exactement et s'arrête. C'est la jonction en T : la
contrainte a déjà été relâchée de l'autre côté, il n'y a plus rien à libérer.
C'est elle, et pas la géométrie, qui découpe le verre en éclats fermés.

Exception : le disque pulvérisé de l'impact, où les radiales partent toutes du
même point et sont donc légitimement voisines. Son rayon est une fraction du
CADRE et non un multiple de `snapDist` — indexé sur `snapDist`, il grandissait
avec lui et annulait exactement ce qu'il mesurait : le knob n'avait alors plus
aucun effet (ratio plat à 0,55 quelle que soit la valeur). Avec un disque fixe
le knob redevient monotone : 0,72 à `snapDist: 3`, 0,16 à un tiers du cadre.

La valeur forgée du test est dite en unités de sketch, comme toute longueur du
module : un tiers de la référence 800 px reste un tiers du cadre à n'importe
quelle taille de rendu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Un tirage par pas et par fissure mère engendre une bifurcation, et il sert deux
fois : sous `branchProb` il bifurque, sa position dans l'intervalle donne le
côté. Les branches marchent après toutes les radiales de leur impact, donc elles
meurent sur le réseau déjà posé.

Les anneaux relient deux rayons voisins en interpolant l'angle ET le rayon : ils
rejoignent exactement les deux fissures même quand elles ne sont pas à la même
distance, et un rayon mort trop tôt les accroche à sa pointe plutôt que de les
laisser en l'air. Chaque anneau ferme un éclat, peint en `polygon` d'opacité
`shardTint × f(aire)` — les petits éclats accrochent la lumière.

`OUT_OF_FRAME` passe de 8 px bruts à une fraction du cadre : en pixels il valait
un huitième du cadre à 64 px et un seizième à 128, et les fissures mouraient à
des endroits différents selon la taille de rendu. C'est ce que l'homothétie
64 → 128 a attrapé.

Le test des anneaux compare TOUTES les fissures touchées, pas la première : une
fissure morte en T a sa pointe exactement sur celle qu'elle a rejointe, donc un
point appartient légitimement à deux fissures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
`assertCap` sur les impacts (16), les radiales (128) et le majorant du flux
(300 000). Le majorant est calculable AVANT de marcher parce que les
bifurcations sont plafonnées par génération : chaque radiale vaut au plus
`steps × (1 + 5/3 + 10/9)` pas. Le pire tirage légitime (3 impacts, 24 radiales,
900 pas) donne 245 000 segments, sous le plafond.

Une branche bifurque désormais à son tour, sur deux générations — un verre brisé
a des fissures secondaires ET tertiaires. C'est ce qui porte le flux au-delà des
200 ops de `DRAW_ON_MIN_OPS` : avec une seule génération il plafonnait à ~150 et
la détection retombait sur `ken-burns`. Défauts portés à `impacts: 2`,
`radials: 16`, `branchProb: 0.06` : 229 à 287 ops mesurées sur cinq seeds, tous
dans leur paramSpec.

Golden gelé après le vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
La profondeur se rend SANS flou : une goutte lointaine est simplement plus
petite et plus pâle qu'une goutte proche. Rayon et opacité ne dépendent que du
plan, jamais de la goutte — deux gouttes d'un même plan sont interchangeables,
et c'est ce qui fait lire une profondeur plutôt qu'un désordre.

La buée est traitée comme le plan le plus lointain : un semis de micro-gouttes
qu'on ne distingue plus une à une, posé avant tout le reste. Elle partage donc
le rayon et l'opacité de ce plan, ce qui garde exactement `planes` groupes de
rayons dans le flux — c'est ce que le test vérifie.

Le glissement et la coalescence arrivent avec le procédé (T24) : ici les gouttes
sont statiques et indépendantes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
T24 — rain-glass : la coalescence, prouvée et non plus dessinée
All checks were successful
CI / Renderer - couverture JS (pull_request) Has been skipped
CI / Renderer - audit des dépendances JS (pull_request) Successful in 59s
CI / Renderer - tests + lint JS (pull_request) Successful in 6m34s
CI / PHP - tests + static + lint (pull_request) Successful in 6m43s
f10b1531ed
Une goutte condense jusqu'à passer `slipMass`, dévale alors sous son propre
poids, et avale toute goutte que son disque touche en récupérant sa masse. Rien
n'est créé, tout est regroupé : le test compte moins de têtes qu'au départ ET
une tête plus grosse que dans le cas gelé.

Le rayon vaut `√masse` : c'est l'aire qui s'additionne à la fusion, pas le
rayon. Additionner les rayons ferait grossir la matière à chaque rencontre.

Le balayage de contact suit l'index du tableau, jamais l'ordre d'un `Set`.
Une goutte sortie du cadre reste vivante : seule l'absorption retire une tête,
sinon le compte ne dirait plus rien de la coalescence.

La buée condense terme à terme comme le reste au lieu d'une forme close :
`base + growth × steps` donne un flottant voisin mais différent, et la buée
n'aurait alors plus exactement le rayon de son plan — le test de T23 l'a vu.
Elle ne cherche jamais de voisine, ce qui lui évite de coûter le carré de son
nombre.

Le test de T23 gèle désormais la vitre (`slipMass: 1e6`) : c'est la seule façon
d'isoler la loi de profondeur de celle de coalescence, une goutte fusionnée
n'ayant plus le rayon de son plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
La trace d'une goutte qui dévale essuie enfin la buée : des `dot` de la
couleur de fond posés en `composite: 'destination-out'`, que `painter.js`
lit op par op — équivalent pur du destination-out du doc source, sans un
seul accès canvas depuis le module. Le trou percé traverse aussi le fond ;
un unique `background` en `destination-over` le rebouche DERRIÈRE, sans
toucher un pixel déjà peint, et la vitre propre réapparaît sous la buée.
Il n'est émis que s'il y a quelque chose à reboucher.

Quatre knobs entrent dans `DEFAULTS` et dans le `paramSpec` du catalogue :
`wander` (dérive latérale, exprimée en fraction de la chute, donc déjà à
l'échelle du rendu), `residueProb`, `residueScale`, et `colorMode`
(`plane` | `mass` | `depth`). Deux tirages par pas de glissement, dans cet
ordre : la dérive, puis le sort du résidu. Le second a lieu même à
`residueProb` nul — sans quoi la longueur du flux d'aléa dépendrait du
knob, et deux rendus qui ne diffèrent que par lui ne seraient plus
comparables ; c'est ce qu'assert le test des résidus, qui compare trace à
trace avant de compter les gouttelettes.

`plane` et `depth` ne se confondent que si la palette a exactement autant
de couleurs de premier plan que de plans de profondeur ; la palette
d'exemple en a deux pour trois plans, les deux modes divergent donc.

Le cadre de la coalescence (T24) coupe désormais les résidus et filtre les
ops à `composite` : il compte les têtes, et rien qu'elles.

23 tests verts (rain-glass + catalog-contract), suite complète sans
régression hors les deux échecs pré-existants de `pipeline-slide`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Trois plafonds en tête de module, refusés loud par `assertCap` (RangeError
→ 422) : `DROPS_CAP = 2000`, `SLIP_STEPS_CAP = 600`, `TRAIL_CAP = 300 000`.
Le troisième majore le flux AVANT de simuler — dans le pire des cas chaque
goutte dévale à chaque pas et laisse une marque, donc `drops × slipSteps`.
Un body tiré ne l'atteint jamais (900 × 260 = 234 000). Ce ne sont pas des
knobs : le paramSpec borne le tirage, ces plafonds bornent une requête
forgée envoyée droit au renderer, ils ne sont donc ni dans `DEFAULTS` ni au
catalogue. Écrits avant leur test, sinon le forgé tourne des minutes au
lieu d'échouer.

Motion `draw-on`, par la détection générique : plus de 200 ops, aucun
`pixels`. La vitre se couvre sous les yeux, la buée puis trace après trace.

Golden gelé à `a28b896c…` — relevé une fois puis figé, ce n'est pas un
test-first. Le commentaire au-dessus dit ce qu'il garde : la buée, les
gouttes, puis par pas de glissement la dérive suivie du sort du résidu ; la
coalescence, elle, ne tire rien et balaie dans l'ordre des index.

29 tests verts sur `rain-glass` + `catalog-contract` ; suite complète
2638 verts, hors les deux échecs pré-existants de `pipeline-slide`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
T27 — vein-colony naît : module, REGISTRY, catalogue, six noms
All checks were successful
CI / Renderer - couverture JS (pull_request) Has been skipped
CI / Renderer - audit des dépendances JS (pull_request) Successful in 1m15s
CI / Renderer - tests + lint JS (pull_request) Successful in 8m25s
CI / PHP - tests + static + lint (pull_request) Successful in 8m42s
2b2404a811
Space colonization d'après Runions : chaque attracteur cherche le nœud le
plus proche dans son rayon d'influence et vote pour lui ; chaque nœud élu
avance de `stepLen` dans la moyenne NORMALISÉE des directions de ses
votants ; tout attracteur passé sous `killRadius` est consommé et se tait.
La moyenne est prise sur des directions normalisées — sans quoi un
attracteur lointain pèserait plus qu'un proche et la veine irait vers le
vide plutôt que vers l'amas. Un nœud qui garde des votants après avoir
poussé pousse encore, ailleurs : c'est exactement la bifurcation.

La recherche du plus proche passe par la grille de hachage de T01 et son
ordre de balayage figé, et le scrutin est dépouillé par index de nœud
croissant, jamais dans l'ordre de remplissage de la Map. C'est là que se
joue la reproductibilité : un « plus proche » à égalité tranché autrement
d'un rendu à l'autre ferait sauter le golden au hasard.

Le test dit la continuité, pas le pixel : hors les deux racines posées sur
le bord, toute origine de brin est l'extrémité d'un autre. Un brin qui
commence dans le vide serait un trait décoratif, pas une veine.

Les trois écritures dans le même commit, sinon `catalog-contract` reste
rouge : import + ligne REGISTRY, entrée catalogue (`active: false`, 12
palettes halloween, paramSpec §5.9), et les six noms — « Réseau veineux »,
« Vein Network », « Adergeflecht », « Red venosa », « Adernetwerk »,
« Rete venosa ». Les deux gardes de dérive de l'éditeur `rendu-json`
passent de 76 à 77 algos.

Épaisseur constante à ce stade : la règle de da Vinci est le sujet de T29.

Renderer 2639 verts hors les deux échecs pré-existants de `pipeline-slide` ;
`AlgorithmNamesTest` vert (388 assertions, six locales sans trou).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Nora added 20 commits 2026-09-08 21:52:16 +00:00
Le rayon d'influence est à la fois ce qui fait pousser et ce qui arrête :
rayon nul, personne ne vote, rien ne sort de la racine — et ce n'est pas un
compteur de pas qui l'arrête, c'est le silence des attracteurs. À l'autre
bout, un `killRadius` énorme dévore le semis en un tour et l'arbre reste un
moignon, alors même que le rayon d'influence n'a pas bougé : les deux
rayons ne font pas le même travail, le test le dit.

TDD rétroactif assumé : la loi était déjà là, T27 ne pouvait pas prouver la
continuité du réseau sans elle. Le RED a donc été obtenu par mutation —
`nearest(..., Infinity)` au lieu du rayon : la croissance devient
inconditionnelle et le rendu ne termine plus, ce qui est précisément ce que
ce test interdit.

REFACTOR : la loi est nommée en tête de module (colonisation, vote, arrêt
au silence) plutôt que paraphrasée, pour qu'un futur refacto ne la
« simplifie » pas en la cassant.

9 verts sur `vein-colony` + `catalog-contract`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Seconde loi de l'algorithme, et elle remonte des feuilles vers la racine :
`r_parent^n = Σ r_enfant^n`. Une branche est aussi épaisse que la somme de
ce qu'elle porte — c'est ce qui donne de vraies artères se divisant en
capillaires plutôt qu'un buisson de traits égaux. Elle se calcule APRÈS la
croissance, ce qui interdit d'émettre un brin dès qu'il pousse : le module
fait pousser tout l'arbre, puis peint. Un enfant ayant toujours un index
plus grand que son parent, un seul balayage à rebours suffit.

Les rayons restent en unités de sketch jusqu'à l'émission. Les mettre à
l'échelle avant la puissance ferait passer le facteur par un exposant
fractionnaire, et 64 px puis 128 px ne donneraient plus exactement la même
composition — c'est ce que garde le test d'invariance.

Ce qui est prouvé est la CONSÉQUENCE observable : le tronc plus épais que
toute pointe, l'épaisseur jamais croissante en descendant, et deux
exposants qui ne donnent pas le même tronc. Reconstruire l'arbre entier
depuis les ops pour vérifier l'égalité terme à terme coûterait plus que ça
ne rapporte — choix assumé, pas un oubli.

Cinq autres knobs : le masque de semis (`disc` | `band` | `edge`, frontière
déformée par `maskNoise`), `tipRadius`, `haloAlpha` et `colorMode`
(`root` | `depth` | `thickness`). Le champ de bruit est construit MÊME à
`maskNoise` nul, et une graine refusée par le masque n'est jamais re-tirée :
deux tirages par graine quoi qu'il arrive, le flux d'aléa ne dépend donc ni
de la forme du masque ni de sa déformation.

Deux pièges relevés en jouant : sur un cadre de test de 64 px un rayon de
sketch vaut douze fois moins, la base de `knobSuite` a donc besoin du haut
de la borne catalogue pour que la colonie pousse du tout ; et les halos
s'intercalent maintenant avant les veines, ce que le test de naissance dit
désormais explicitement.

Renderer 2653 verts hors les deux échecs pré-existants de `pipeline-slide`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Trois plafonds, et l'un d'eux a révélé une vraie pathologie. `ATTRACTORS_CAP`
et `NODES_CAP` refusent loud un body forgé (RangeError → 422) ;
`ITERATIONS_CAP` remplace la laisse muette de 400 pas : on REFUSE plutôt que
de rendre un arbre coupé au milieu.

C'est en le posant qu'on a vu que le chemin PAR DÉFAUT ne terminait pas — il
était simplement tronqué en silence depuis T27. La cause est l'orphan
attractor, pathologie connue du procédé : un votant très écarté de la moyenne
se retrouve plus loin de l'enfant que du parent, revote donc pour le parent à
l'identique, et le fait pousser à chaque pas indéfiniment. Deux corrections,
toutes deux physiques :

- une veine BIFURQUE, elle ne s'ouvre pas en éventail : un nœud pousse au
  plus deux enfants et devient ensuite invisible au scrutin, l'attracteur va
  voter pour un voisin capable de pousser ou se tait ;
- une pousse qui retomberait exactement sur la précédente est refusée — une
  veine peinte deux fois au même endroit ne se voit pas, elle ne fait que
  coûter (1,7 % des brins mesurés avant correction).

La colonie termine désormais d'elle-même : 289 ms au format print, quelques
centaines d'itérations là où le plafond en autorise 4000.

Motion `draw-on` par la détection générique. Golden gelé à `4731cef5…`,
relevé une fois puis figé — pas un test-first ; le commentaire au-dessus dit
ce qu'il garde, à commencer par les 255 tirages du champ de bruit, consommés
même à `maskNoise` nul.

Réserve mesurée et portée au backlog, pas corrigée ici : au bas de la plage
d'`influence`, la colonie rend souvent un cadre vide (racines sur le bord,
masque centré) — 56 % à `influence: 12`, plancher ~14 %. Sans objet tant que
l'algorithme reste P2 et inactif ; les deux correctifs possibles sont
décrits dans `docs/backlog.md`.

28 verts sur `vein-colony` + `catalog-contract` ; renderer 2659 verts hors
les deux échecs pré-existants de `pipeline-slide`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Le champ d'un potentiel, `F(p) = Σ qᵢ / (‖p − pᵢ‖² + ε)`, et ses isolignes
emboîtées. Deux charges voisines ne se juxtaposent pas, elles FUSIONNENT :
leurs isolignes se rejoignent en un col puis en une seule courbe, et c'est
de là que vient la forme molle et continue d'un ectoplasme. Seuil haut pour
le corps, plein ; seuils bas pour les halos. Un seul contour ne serait
qu'une flaque — c'est l'emboîtement qui fait la lueur.

Le marching squares vit dans le module et pas en `lib/` : un seul client, et
la règle du dépôt veut un second cas d'usage réel avant d'extraire.

Deux choses portent le déterminisme du chaînage. Les segments naissent
cellule par cellule, ligne puis colonne. Et ils sont chaînés par
IDENTIFIANT D'ARÊTE — un entier — jamais par coordonnée arrondie : deux
cellules voisines calculent le point de leur arête commune à partir des deux
mêmes valeurs de coins, dans le même ordre, il est donc bit-à-bit identique
des deux côtés et le chaînage n'a aucune tolérance à choisir. Les deux cas
ambigus du marching squares (les cols, 5 et 10) sont tranchés d'une seule
façon, toujours la même.

Deux longueurs sont exprimées en unités de sketch là où on les écrirait
volontiers en constantes nues : l'adoucissement ε du dénominateur et le
rayon qui fixe le seuil du corps. Sans cela le champ ne serait pas
homothétique et deux tailles de rendu ne donneraient pas la même forme.

Les trois écritures dans le même commit : import + ligne REGISTRY, entrée
catalogue (`active: false`, 12 palettes, paramSpec §5.3), six noms. Les
gardes de dérive de l'éditeur `rendu-json` passent de 77 à 78 algos.

Renderer 2660 verts hors les deux échecs pré-existants de `pipeline-slide` ;
`AlgorithmNamesTest` vert (393 assertions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Un metaball seul donne un champ à symétrie radiale exacte : son isoligne EST
un cercle, et c'est le seul cas où l'on puisse juger l'extraction sans rien
savoir du reste. Le test y prouve deux choses d'un coup — le contour se
referme (le chaînage revient à son point de départ et ne s'arrête pas en
chemin) et il est rond à la résolution de la grille. Le centre n'a pas à
être connu : celui d'un cercle est le barycentre de ses propres points.

TDD rétroactif assumé, comme pour T28 : l'extraction était déjà là, T31 ne
pouvait pas rendre un corps sans elle. Le RED a donc été obtenu par mutation,
et il est documenté dans le test parce qu'il dit ce que le seuil de 5 %
attrape VRAIMENT : un chaînage coupé (55 % de dispersion), une extraction
collée aux coins de cellule (10 % sur une grille grossière). Il ne juge pas
la finesse de l'interpolation — à 220 cellules pour 64 px, un point pris au
milieu de l'arête passe encore à 1 %. Mieux vaut l'écrire que le laisser
croire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Un ectoplasme ne flotte pas en boule, il DÉGOULINE : `dropCount` charges
secondaires posées sous leur hôte étirent les isolignes vers le bas en
pendants, à `dropPull` de distance. C'est l'attraction qui allonge la forme,
jamais un tracé qui dessinerait une goutte — et c'est ce que le test mesure,
le point le plus bas du corps, pas la présence d'un ornement. Une goutte ne
pend jamais à une goutte : le tirage de l'hôte ne voit que les metaballs
d'origine.

Le halo s'éteint en s'éloignant, au même rythme que les seuils
(`glowAlpha × levelFalloff^k`) : des anneaux à opacité égale ne feraient pas
une lueur. Et `colorMode` lit le BARYCENTRE d'un contour pour ses deux modes
géométriques — la seule position qu'une courbe possède en propre.

Un knob mort découvert en écrivant sa preuve : `charge` ne changeait
strictement rien. Le seuil du corps valait `charge / rayon²`, donc la charge
se simplifiait de part et d'autre de l'inégalité et chaque valeur rendait
exactement la même forme. Le seuil est désormais celui d'une charge
UNITAIRE : `charge` fait enfin la taille du corps, en √charge, comme le veut
un champ en 1/r².

Le cadre du contour circulaire (T32) précise maintenant `dropCount: 0` :
c'est la goutte pendante qui rompt la symétrie radiale, et ce test ne juge
que l'extraction.

Renderer 2674 verts hors les deux échecs pré-existants de `pipeline-slide`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Trois plafonds pour un coût qui est un produit — le champ vaut
`blobs × grid²` — donc `BLOBS_CAP`, `GRID_CAP` et `LEVELS_CAP` se gardent
ensemble, et chacun refuse loud avant d'allouer la grille.

Motion DÉCLARÉE et non détectée : le flux ne fait que quelques contours, la
détection générique le classerait `ken-burns`, un cadre promené sur une
image morte. `phase` recalcule le champ à chaque frame en avançant
`phaseShift` : chaque metaball dérive sur son orbite, à SA phase, et la
masse respire. Le déplacement est écrit comme un écart à la position de
départ — exactement nul à t = 0 et à t = 1 : la boucle vidéo est sans
couture ET l'œuvre imprimée reste intacte à l'octet près. Un test le dit
autrement : `spacing` ne mord qu'une fois la phase engagée, jamais sur
l'œuvre.

⚠ Décision conditionnée au banc T44 : si le recalcul dépasse 315 ms/frame,
on retire `motion` et `phaseCycles` et le repli `ken-burns` reprend la main.
C'est ce qui est arrivé à `kaleido-fold`. Le module et le test le disent
tous les deux, pour que le retrait soit un geste et non une découverte.

Le knob `levels` demandait 6 dans la suite : sur la base réduite, les seuils
au-delà du troisième tombent déjà hors cadre et n'ajoutent aucun contour. La
valeur est passée à 1 — retirer les halos, ça se voit toujours.

Golden gelé à `04ec097c…`, relevé une fois puis figé. 30 verts sur
`ectoplasm` + `catalog-contract` ; renderer 2682 verts hors les deux échecs
pré-existants de `pipeline-slide`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
L'épaisseur d'un film d'huile est un bruit fractal, et tout le reste en
découlera : la couleur d'une nappe ne vient pas d'un pigment mais de
l'interférence en film mince (T36). Pour l'instant, une rampe de palette sur
l'épaisseur — le champ existe, la physique arrive.

Une seule op `pixels`, plein cadre : un champ continu n'est pas un
assemblage de formes. Et surtout aucun `background` avant elle — `painter`
fait un `putImageData(0, 0)` qui écrase le canvas sans composite, un fond
posé avant serait effacé. Le test vérifie aussi que chaque pixel est opaque :
un pixel translucide laisserait passer la feuille blanche au tirage.

Le champ est échantillonné en coordonnées normalisées avec correction de
ratio sur `ny` — sans elle il serait étiré en portrait et les franges
n'auraient pas la même forme qu'en carré.

Coût mesuré : 3,6 s au format print (4,7 s à 7 octaves), 543 ms pour une
frame 1080p — au-dessus du budget vidéo de 315 ms, ce que le banc T44 aura à
trancher.

Les trois écritures dans le même commit ; les gardes de dérive de l'éditeur
passent de 78 à 79 algos. Renderer 2683 verts hors les deux échecs
pré-existants de `pipeline-slide` ; `AlgorithmNamesTest` vert (398
assertions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
La réflectance d'une composante vaut `sin²(π · phase · épaisseur)` : elle est
PÉRIODIQUE en épaisseur, et c'est tout le sujet. Une frange revient, un
dégradé jamais — c'est ce qui distingue une nappe d'huile d'une rampe de
bruit. Une phase par couleur de palette, puisqu'on a trois hex et pas un
spectre, et c'est leur ÉCART qui fait l'irisation : trois phases égales ne
donneraient qu'un clignotement gris.

Le test lit la loi sans texture pour la masquer : à bruit nul l'épaisseur est
constante sur tout le cadre (le bruit de Perlin vaut exactement 0,5 sur un
point du réseau, donc l'épaisseur vaut exactement `thickness`), l'image est
unie, et un pas d'une période entière la rend octet pour octet identique.
Trois phases entières 1, 2, 4 : période commune exactement 1.

Un défaut trouvé en écrivant ce test : les trois composantes étaient
ADDITIONNÉES, et saturaient à blanc presque partout — deux épaisseurs très
différentes rendaient le même pixel écrêté, ce que le test a refusé. Elles se
partagent maintenant la lumière incidente (moyenne), et la nappe retrouve ses
noirs.

Les quatre knobs du procédé (`phase0/1/2`, `darkFloor`) entrent au
`paramSpec` dès cette tâche et non en T37 : lus par le module sans y figurer,
ils feraient rougir `catalog-contract` (knob mort), et le dépôt ne doit pas
être rouge entre deux commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Des bosses gaussiennes creusent et gonflent le film, donc DÉPLACENT les
franges : c'est ce qui donne à la nappe des yeux et des tourbillons là où le
bruit seul ne fait qu'un grain régulier. Une bosse s'éteint doucement — un
bord net ferait une découpe, pas un relief.

`colorMode` offre un témoin : le mode `thickness` est la même nappe SANS sa
physique, une simple rampe de palette sur l'épaisseur. L'écart entre les deux
se voit à l'œil nu, et c'est exactement l'argument de l'algorithme.

L'invariance d'échelle a été écrite autrement que ne le proposait la fiche,
et plus fortement. La fiche voulait comparer la position du pixel le plus
lumineux à 2 % près : mesuré, ce point ne dit rien — après quantification sur
8 bits le maximum est atteint par des dizaines de pixels à la fois (15 à
64 px, 22 à 256) et lequel arrive premier est arbitraire ; le test échouait
alors que le champ, lui, était parfaitement invariant. Le champ ne dépendant
que de coordonnées normalisées, le pixel (x, y) d'un rendu 64 tombe sur la
même position que le pixel (2x, 2y) d'un rendu 128 : sous-échantillonner le
grand redonne le petit À L'OCTET PRÈS. C'est vérifié sur les 12 288 canaux, et
c'est une preuve, pas une tolérance.

Renderer 2697 verts hors les deux échecs pré-existants de `pipeline-slide`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Trois plafonds pour un coût qui est un produit — `pixels × (octaves + bosses)`
— dont un sur la surface elle-même : 40 mégapixels, soit près de trois fois
le format print, et refusé loud avant d'allouer la trame.

Motion DÉCLARÉE, et il le faut : un flux d'UNE SEULE op `pixels` retombe sur
`ken-burns`, un cadre promené sur une image morte. `phase` recalcule la nappe
à chaque frame — le point d'échantillonnage décrit un cercle dans le champ de
bruit et revient exactement à son départ, si bien que le champ défile sous
les franges et que l'irisation ondule. Écrit comme un écart à la position
initiale, le décalage est nul à t = 0 et à t = 1 : boucle sans couture, œuvre
imprimée intacte à l'octet près, et `spacing` ne mord qu'une fois la phase
engagée.

⚠ Décision conditionnée au banc T44, et cette fois le chiffre est déjà là :
543 ms pour une frame 1080p contre 315 de budget. Si le banc confirme, on
retire `motion` et `phaseCycles` et `ken-burns` reprend la main. Le module et
le test portent tous deux la mesure, pour que le retrait soit un geste et non
une découverte.

Golden gelé à `8f63b9fc…`. 30 verts sur `oil-sheen` + `catalog-contract` ;
renderer 2705 verts hors les deux échecs pré-existants de `pipeline-slide`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
La densité de départ, et rien d'autre : des bandes horizontales qui
s'éteignent vers le haut, plus des foyers cantonnés au bas du cadre. Une
brume qui flotterait au milieu ne serait qu'un nuage — la décroissance vers
le haut n'est pas un ornement, c'est la seule chose que le nom de
l'algorithme promet. L'advection semi-lagrangienne arrive en T40.

Une seule op `pixels`, plein cadre, et aucun `background` avant elle : le
`putImageData(0, 0)` de `painter` l'effacerait.

La grille est lue en coordonnées NORMALISÉES et interpolée bilinéairement.
Un échantillonnage en pixels donnerait deux compositions différentes selon la
taille de rendu ; là, la même image à toute résolution. Le rabattement sur
les bords est un clamp et non un repli — une brume qui ressortirait de
l'autre côté du cadre trahirait la grille.

Le champ de vitesse n'est PAS construit ici : sans transport à amorcer il ne
serait que du code mort (et un tirage d'aléa gaspillé). Il se placera avant
les foyers en T40, l'ordre d'aléa le dit.

Les trois écritures dans le même commit ; les gardes de dérive de l'éditeur
passent de 79 à 80 algos. Renderer 2706 verts hors les deux échecs
pré-existants de `pipeline-slide` ; `AlgorithmNamesTest` vert (403
assertions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
`d'(x) = d(x − v·dt)` : on ne pousse pas la matière vers l'avant, on va
CHERCHER en arrière d'où elle vient, et on interpole. C'est ce qui rend le
schéma de Stam inconditionnellement stable, et c'est aussi ce qui fait qu'il
transporte la densité sans en fabriquer — une brume s'étire et s'enroule,
elle n'apparaît pas. Trois passes, toujours dans cet ordre et sur des
tampons alternés : advection, diffusion, dissipation. En place, le résultat
dépendrait de l'ordre de parcours de la grille, ce qui n'est pas une
simulation mais un artefact.

Le champ de vitesse est un bruit lu comme un ANGLE, donc une vitesse de norme
constante. Lu comme deux composantes, il aurait des zones mortes là où il
passe par zéro, et la brume s'y figerait.

Un bug trouvé en écrivant la mesure : `sampleGradient` est cyclique, donc à
densité 1 la rampe était revenue au fond et la brume la plus dense
rebasculait dans le noir d'un seul coup. Le rendu s'arrête maintenant au
dernier stop.

Le seuil de conservation est à 2 % et non 1, et c'est mesuré, pas concédé :
la lumière traverse une rampe de palette à trois couleurs, donc une fonction
monotone mais non linéaire de la densité. Sur une rampe linéaire (deux gris),
le schéma conserve à 0,88 % sur trente pas — c'est la rampe qui ajoute le
demi-point restant, pas le transport.

`dissipation` et `diffusion` entrent au `paramSpec` dès cette tâche : lus par
le module sans y figurer, ils feraient rougir `catalog-contract`.

Coût mesuré : 965 ms au format print, 269 ms pour une frame 1080p — sous le
budget vidéo de 315 ms, contrairement à `oil-sheen`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
`sourceHeight` cantonne les foyers dans une tranche basse, et le test dit la
seule chose que le nom de l'algorithme promet : après vingt pas d'advection,
le tiers bas de l'image reste plus chargé que le tiers haut. Une brume qui
flotterait au milieu ne serait qu'un nuage.

`colorMode` ajoute deux lectures du même champ : `banded` le rend par
paliers — des nappes empilées plutôt qu'un dégradé — et `height` le pèse par
la hauteur, si bien qu'en haut du cadre, même dense, la brume reste pâle.

L'invariance d'échelle est écrite comme pour `oil-sheen`, et pour la même
raison : la fiche proposait de comparer la position du pixel le plus
lumineux à 2 % près, mais après quantification sur 8 bits ce maximum est
atteint par des dizaines de pixels et lequel arrive premier est arbitraire.
La grille étant lue en coordonnées normalisées, sous-échantillonner le rendu
128 redonne le rendu 64 À L'OCTET PRÈS : c'est une preuve, pas une tolérance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Trois plafonds, et le troisième est le seul qui compte vraiment : le coût est
`grid² × steps`, or 512 cellules et 200 pas passeraient les deux premiers
plafonds en demandant 52 millions de cellules-pas. C'est le PRODUIT qui est
gardé, en plus de ses facteurs — le test le dit avec ce cas précis.

La boucle vidéo se referme sans fondu croisé : les DEUX champs de bruit sont
combinés par `loopedValue`, si bien qu'un tour complet ramène exactement le
champ de départ. Un fondu ferait apparaître deux brumes superposées à
mi-clip. Un test rend l'égalité littérale — `phaseShift: 80` (un tour à
`spacing: 80`) donne les mêmes octets que `phaseShift: 0` —, ce qui est
possible parce que `loopedValue` replie `t` AVANT tout calcul : sans ce repli
l'égalité ne tiendrait qu'à un ULP près, et un golden byte-hash verrait cet
ULP.

À `phaseShift: 0` il ne reste que le premier bruit, cos(0) valant exactement
1 : l'œuvre imprimée est intacte à l'octet près, et `spacing` ne mord qu'une
fois la phase engagée.

Motion DÉCLARÉE, sans quoi un flux d'une seule op `pixels` retomberait sur
`ken-burns`. Et cette fois la mesure est favorable : 269 ms pour une frame
1080p contre 315 de budget — `low-mist` passe là où `oil-sheen` (543 ms) ne
passe pas. Le banc T44 tranchera sur la machine de prod.

Golden gelé à `26fb4199…`. 30 verts sur `low-mist` + `catalog-contract` ;
renderer 2728 verts hors les deux échecs pré-existants de `pipeline-slide`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Dix entrées propres dans `poetic-recipes.json`, cinq extraits chacune, en
français et en anglais. Elles parlent du PROCÉDÉ et de rien d'autre : le
seuil sous lequel une goutte ne bouge pas, l'angle de repos qui arrête une
pile de cire, l'ondulation qui doit dépasser la circonférence du fil pour le
rompre, la fissure qui meurt sur une fissure, la goutte qui avale ses
voisines, le vote qui fait pousser une veine, les lignes de niveau relevées
seuil après seuil, l'épaisseur d'où sort la couleur, la densité qu'on va
chercher en arrière.

Cinq extraits et non deux à quatre comme le demandait la fiche :
`testEveryActiveAlgorithmHasExactlyFiveExtracts` en exige cinq dès qu'un
algorithme est ACTIF. Écrire quatre extraits aurait posé une mine qui
n'aurait explosé qu'au go-live, au moment de l'activation.

Le test manquant est ajouté : le repli `generic` empêche `draw()` de rendre
null, donc le contrat existant passait même pour un algorithme sans le
moindre extrait à lui. Il ne vérifie que le `fr`, seule clé que la banque
lit — 42 des 70 entrées historiques portent un `en` vide, et l'exiger ici ne
garderait pas un contrat, ce serait réclamer une traduction rétroactive de
211 extraits. Cette dette dormante est notée au backlog.

phpunit 1579 verts, phpstan niveau 9 vert, mago lint et fmt verts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Mesure à la taille de travail réelle de la vidéo (1493 x 1920, la frame
qu'un format 1080 x 1920 letterbox), 30 frames avec `phaseShift` qui
avance, machine de dev 12 cœurs : 1327,7 ms/frame en moyenne, médiane
1315,3. Le budget est de 315 ms, et c'est un plancher optimiste face aux
2 cœurs partagés de la prod.

Aucun knob ne rattrape le facteur 4,2 : à `octaves: 1, blobs: 0` — sans
le bruit fractal ni les bosses, donc sans l'algorithme — il reste 673 ms.
Le plancher est l'arithmétique d'interférence par pixel.

Donc `meta.motion` et `phaseCycles` sautent, et le flux d'une seule op
`pixels` retombe sur `ken-burns`. Même issue que `kaleido-fold`, pour la
même raison. `phaseShift` et `spacing` restent lus, dormants, prêts à
resservir si le budget change. Aucun bump : aucune vidéo publiée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Même banc que `oil-sheen` : 30 frames à 1493 x 1920 avec `phaseShift` qui
avance, machine de dev 12 cœurs. 18,9 ms/frame en moyenne, médiane 18,9 —
six pour cent du budget de 315 ms.

Le champ est échantillonné sur une grille de 220 et rendu en quelques
contours, jamais pixel par pixel : c'est ce qui le sépare d'`oil-sheen`.
Les deux clés restent, la mesure est consignée dans le module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Même banc : 30 frames à 1493 x 1920 avec `phaseShift` qui avance, machine
de dev 12 cœurs. Moyenne 288,7 ms, médiane 268,6, p90 354,4 — sous les
315 ms de budget, mais c'est le seul des trois qui passe de peu.

La réserve est écrite dans le module : le budget est un plancher (2 cœurs
partagés en prod) et le p90 le dépasse déjà. Si un rendu de prod déborde,
baisser `grid` (le coût est `grid² x steps`) avant de retirer la motion —
en sachant que ça change les octets du chemin par défaut dès qu'une œuvre
existe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
T44 — le banc des trois phase : protocole, chiffres, décision
All checks were successful
CI / Renderer - couverture JS (pull_request) Has been skipped
CI / Renderer - audit des dépendances JS (pull_request) Successful in 1m1s
CI / PHP - tests + static + lint (pull_request) Successful in 6m9s
CI / Renderer - tests + lint JS (pull_request) Successful in 6m15s
ffd46b6b60
Les mesures sont consignées dans la fiche de tâche, pas seulement leur
conclusion : protocole (30 frames par `recomputeFrameFor` + `phasePath`),
taille de travail (1493 x 1920, pas la taille d'impression), machine
(12 cœurs, hors contention, donc plancher optimiste), et le balayage
`octaves` x `blobs` qui montre qu'`oil-sheen` ne se rattrape par aucun
knob — 673 ms même sans algorithme.

Le plan §5.3, §5.6, §5.8 et §11 portent l'issue de chaque algorithme au
lieu de « à confirmer au banc ».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Halloween — le hub collections.json manquant, qui vidait la galerie 3D
All checks were successful
CI / Renderer - couverture JS (pull_request) Has been skipped
CI / Renderer - audit des dépendances JS (pull_request) Successful in 1m15s
CI / PHP - tests + static + lint (pull_request) Successful in 6m49s
CI / Renderer - tests + lint JS (pull_request) Successful in 6m53s
aedf9d742f
Les dix algos Halloween n'appartenaient à aucune collection. En local, où
seuls ces dix-là sont actifs et publiés, toutes les collections tombaient à
zéro œuvre : `scene.json` sortait `rooms: []`, et le client refuse une scène
sans salle (`gallery.js`, RangeError) — la galerie 3D basculait sur son repli
2D à chaque visite. Même trou en prod dès la fenêtre d'octobre : pas de hub,
pas de badge de collection, pas de salle.

Le §9 du plan le donnait pour optionnel. Il ne l'est pas.

- `collections.json` : entrée `halloween`, `hub: false` (famille ciblée, donc
  éligible au badge de fiche), 6 slugs localisés, les 10 codes.
- `collections.halloween.*` dans les six locales : copy propre à la famille,
  physique plutôt que figuration (aucune citrouille dessinée), les 12 palettes
  de saison, plafond 25.
- `HalloweenCollectionTest` : la page rend sa copy, filtre ses algos, publie
  ses hreflang et son FAQPage.

Aucun rebuild, aucun re-rendu, aucune migration : données pures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLH2pFvr1RTCwQ571JQ8zh
Nora merged commit 5b44540c0c into develop 2026-09-09 19:03:40 +00:00
Nora deleted branch feature/halloween 2026-09-09 19:03:41 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Nora/ArtAutomatic!179
No description provided.