fix(validate): make the geometry pass call the engine instead of reimplementing it - #160
Merged
LeadcodeDev merged 1 commit intoAug 9, 2026
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
timelineet audio-réactif--strict-anim: l'arbre était construit une seule fois avecanim: None, et ces deux mécanismes ne s'appliquent qu'en présence d'un vrai contexte d'animation. Reconstruit désormais par échantillon viabuild_scene_from_refs+BuildAnimationCtx— l'appel exact que faitrender_with_new_pipeline_iterpar frame rendue.transform_bboxne lisait que translate et scale. Unspinsortait du cadre sans être détecté à aucun échantillon. Replié désormais parapply_static_node_transform, l'AABB à 4 coins déjà correct côté statique.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-animreste opt-in.Faux positifs — un scénario correct bloqué, et
--fixqui l'abîme ensuiteunwrappable_text_overflowoverflow: hiddenétait rejeté — et--fixlui retirait alors unwhite-spacecorrect.transform-origincontent_overflows_cardPourquoi
content_overflows_cardest 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: visiblequeCLAUDE.mdetgeometry-safety.mddé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::ContentOverflowsCardest 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
worldétaient validées contre le layout racine des vuesslide. Le layout centré que synthétise le rendu world est désormais unworld_default_scene_layoutpartagé, utilisé par les deux.check_auto_scrollsupposait 16+16 px de padding et, pourterminal, la mauvaise taille de police par défaut plus unline-heightCSS que le peintre ignore. Il appelle maintenantCodeblockIntrinsicetTerminalIntrinsic, les mesureurs que le layout utilise lui-même.Les refus
--fixde la PR #145 : vérifiés, pas réécritsDeux tests bout-en-bout pilotent désormais
cmd_validateen entier — un scénario templaté avecconfig+$var, un scénarioincludeà 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::substitutepeut 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_originest 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 --workspacesur 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.