Skip to content

fix(validate): make the geometry pass call the engine instead of reimplementing it - #160

Merged
LeadcodeDev merged 1 commit into
chantier/audit-remediationfrom
fix/geometry-validator-calls-the-engine
Aug 9, 2026
Merged

fix(validate): make the geometry pass call the engine instead of reimplementing it#160
LeadcodeDev merged 1 commit into
chantier/audit-remediationfrom
fix/geometry-validator-calls-the-engine

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Round 4, vague B — lot validation géométrique, 9 constats vérifiés.

Presque tous sont la même erreur structurelle : le validateur reforke un morceau du moteur — une constante de padding, un style racine, un pivot de transform — au lieu d'appeler la fonction que le moteur utilise. Puis les deux dérivent, et le validateur se met à raisonner sur une géométrie qui n'est pas celle qui sera rendue. La correction consiste donc surtout à supprimer le doublon, pas à corriger sa valeur.

Le validateur est le seul garde-fou entre une génération et une vidéo cassée, et ses deux modes de défaillance ont un coût asymétrique — d'où le classement.

Faux négatifs — une vidéo cassée qui sort sans alerte

Constat Symptôme
États timeline et audio-réactif Invisibles même en --strict-anim : l'arbre était construit une seule fois avec anim: None, et ces deux mécanismes ne s'appliquent qu'en présence d'un vrai contexte d'animation. Reconstruit désormais par échantillon via build_scene_from_refs + BuildAnimationCtx — l'appel exact que fait render_with_new_pipeline_iter par frame rendue.
Rotation animée Pas modélisée du tout : transform_bbox ne lisait que translate et scale. Un spin sortait du cadre sans être détecté à aucun échantillon. Replié désormais par apply_static_node_transform, l'AABB à 4 coins déjà correct côté statique.
Plafond de 40 échantillons Sur une scène de 60 s, un échantillon toutes les 1,5 s : toute excursion plus brève passait entre les mailles. Relevé à 480, ce qui tient la cadence promise de 8/s jusqu'à 60 s.

Coût mesuré du relèvement, sur une scène de 60 s à 15 composants animés : 309 ms en release, 543 ms en debug. --strict-anim reste opt-in.

Faux positifs — un scénario correct bloqué, et --fix qui l'abîme ensuite

Constat Symptôme
unwrappable_text_overflow Seul check « contenu vs sa propre boîte » hors du garde d'ancêtre clippant. Un texte légitimement clippé par un overflow: hidden était rejeté — et --fix lui retirait alors un white-space correct.
transform-origin Ignoré par le pliage statique, qui pivotait toujours au centre de la boîte.
content_overflows_card Retiré, pas rapiécé. Voir ci-dessous.

Pourquoi content_overflows_card est retiré plutôt que corrigé

Son garde ne le rendait atteignable que si ni la carte ni rien entre elle et le nœud ne clippe — c'est-à-dire exactement le cas overflow: visible que CLAUDE.md et geometry-safety.md déclarent tous deux légal (« a badge sticking out of a card is legal »). Et quand la carte clippe, le garde supprime déjà tout le bloc.

Il n'existe donc aucune configuration où déclencher soit à la fois atteignable et cohérent avec le contrat documenté. Le rapiécer avec une échappatoire aurait produit un check qui ne se déclenche jamais.

Ce qui reste couvert : du contenu qui sort du device n'est jamais légal, et c'est le travail de check_viewport, pas du check retiré. J'ai ajouté un test qui épingle cette garantie — même fixture, descendue dans le cadre pour que le débordement sorte du viewport — afin que la suppression ne puisse pas devenir silencieusement un angle mort.

Le variant ViolationKind::ContentOverflowsCard est conservé sans être construit : le contrat des noms de violation est gelé (--fix, --report).

Si vous voulez un filet de sécurité contre les cartes sous-dimensionnées, il faudra un mécanisme neuf — un marqueur d'intention explicite, par exemple — pas une résurrection de celui-ci.

Géométrie rapportée fausse

  • Les vues world étaient validées contre le layout racine des vues slide. Le layout centré que synthétise le rendu world est désormais un world_default_scene_layout partagé, utilisé par les deux.
  • check_auto_scroll supposait 16+16 px de padding et, pour terminal, la mauvaise taille de police par défaut plus un line-height CSS que le peintre ignore. Il appelle maintenant CodeblockIntrinsic et TerminalIntrinsic, les mesureurs que le layout utilise lui-même.

Les refus --fix de la PR #145 : vérifiés, pas réécrits

Deux tests bout-en-bout pilotent désormais cmd_validate en entier — un scénario templaté avec config + $var, un scénario include à deux fichiers réels — chacun avec une vraie violation géométrique, et affirment que les fichiers source reviennent octet pour octet identiques.

Verdict sur « peut-on faire mieux que refuser » : non. Pour include, un chemin de violation porte l'index de scène résolu, qui n'a structurellement aucune correspondance dans le JSON brut du parent. Pour les templates, variables::substitute peut remplacer un scalaire par un objet ou un tableau : la substitution restructure l'arbre, donc un chemin calculé sur l'arbre résolu peut désigner une position qui n'existe pas dans la source. Le refus est la bonne réponse, pas seulement la prudente.

Dette de fond restante

L'agent a cartographié les endroits où le validateur duplique encore le moteur, même non touchés : resolve_transform_origin_2d (introduit ici — resolve_origin est privé à paint_pass.rs, hors périmètre ; les primitives d'unité sont partagées, seule l'orchestration est dupliquée), fold_static_camera, apply_transform_chain, container_clips, check_legibility/text_sizes, deserialize_children_indexed. Chacun porte désormais un commentaire nommant la fonction moteur dont il doit rester solidaire.

Vérification

cargo test --workspace sur cette branche seule : 20 cibles, 0 échec. 7 des 8 exemples valident, avec et sans --strict-anim ; le huitième est l'issue #157, préexistante à main.

…plementing it

Nine confirmed audit findings on the geometry pass. Almost all of them are
the same structural mistake: the validator forked a piece of the engine — a
padding constant, a root style, a transform pivot — instead of calling the
function the engine uses. Then the two drifted, and the validator started
reasoning about a geometry that is not the one being rendered. The fix is
mostly deletion of the duplicate, not correction of its value.

False negatives — a broken video shipped with no warning:

- `timeline` style states and audio-reactive transforms were invisible even
  under `--strict-anim`: the box tree was built once with `anim: None`, and
  both features only apply when a real animation context is present. It is
  now rebuilt per sample through `build_scene_from_refs` with a real
  `BuildAnimationCtx` — the same call `render_with_new_pipeline_iter` makes
  per rendered frame.
- Animated rotation was not modelled at all: `transform_bbox` read only
  translate and scale, so a `spin` left the frame undetected at every
  sample. It now folds through `apply_static_node_transform`, the same
  4-corner AABB the static path already used.
- The 40-sample cap dropped `--strict-anim` to one sample every 1.5s on a
  60s scene, so any excursion shorter than the step slipped through. Raised
  to 480, which holds the promised 8/s up to 60s. Measured cost on a 60s
  scene with 15 animated components: 309ms release, 543ms debug.

False positives — a correct scenario blocked, and `--fix` then damaging it:

- `unwrappable_text_overflow` was the only content-vs-own-box check outside
  the clipping-ancestor guard, so text legitimately clipped by an
  `overflow: hidden` ancestor was rejected and `--fix` stripped a correct
  `white-space`.
- `transform-origin` was ignored by the static transform fold, which always
  pivoted at the box centre.
- `content_overflows_card` is retired rather than patched. Its guard made it
  reachable only when neither the card nor anything between it and the node
  clips — which is exactly the `overflow: visible` case both CLAUDE.md and
  geometry-safety.md document as legal ("a badge sticking out of a card is
  legal"). When the card does clip, the guard already suppresses the whole
  block. There is no configuration where firing is both reachable and
  consistent with the documented contract. Content leaving the *device* is
  still caught — that is `check_viewport`'s job, and a test now pins that
  guarantee so the removal cannot silently become a blind spot.

Wrong geometry reported:

- `world` views were validated against the slide root layout. The centred
  default the world renderer synthesises is now a single shared
  `world_default_scene_layout`, used by both.
- `check_auto_scroll` assumed 16+16px of padding and, for terminal, the
  wrong default font size and a CSS line-height the painter ignores. It now
  calls `CodeblockIntrinsic` and `TerminalIntrinsic`, the measurers layout
  itself uses.

The `--fix` refusals from PR #145 were verified rather than rewritten: two
end-to-end tests now drive `cmd_validate` over a templated scenario and a
two-file `include` scenario, both with a real violation, and assert the
source files come back byte-identical. Doing better than refusing is not
possible for `include`, and not safe for templates: variable substitution
can replace a scalar with an object, so a path computed on the resolved tree
need not exist in the source.

Tests: full workspace green on this branch alone. 7/8 examples validate; the
eighth is issue #157, pre-existing on main.
@LeadcodeDev LeadcodeDev added the bug Something isn't working label Aug 9, 2026
@LeadcodeDev LeadcodeDev self-assigned this Aug 9, 2026
@LeadcodeDev
LeadcodeDev merged commit 4b49e83 into chantier/audit-remediation Aug 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant