Post-audit reliability hardening — wave 1 (critical + high) - #108
Merged
Conversation
7 tasks
Colours written `#fff`, `#FFF`, `white` or `rgb(255,255,255)` deserialised without error and then fell back to SColor::BLACK when painting. On the dark backgrounds this engine targets that is invisible text, and `validate` still reported "Valid scenario" — the worst possible failure mode. Only the 6-digit hex form actually worked. Add `parse_css_color` covering 3/4/6/8-digit hex, rgb/rgba, hsl/hsla and the CSS named-colour set, and route the paint pass through it. An unresolvable colour now warns on stderr and renders as opaque magenta rather than black: black is a legitimate colour choice and disappears into a dark frame, magenta never blends in. Lengths had the same shape of bug — an unparseable value silently became 0px. Add fallible `try_parse` counterparts so callers can detect it. Alpha now rounds in both notations, so `0.5` and `50%` agree on 128. The old parser truncated to 127; that was an artefact of the formula rather than a decision, and it made the two spellings of one value disagree. Refs #104
The grid component accepted `grid-template-columns`, the validator made it mandatory, and taffy was never told about it — the only grid reference in taffy_bridge was the Display::Grid mapping. Taffy therefore laid out a grid with no tracks and collapsed every child into one implicit column, so a three-column KPI row rendered as three stacked full-width rows while validation reported no problem. Translate grid-template-columns/rows, grid-column, grid-row and grid-auto-flow into their taffy equivalents. Also reorder GridTrack's untagged variants. Length's inner String arm matched any JSON string or number, so it swallowed bare numbers meant for Fr and keywords meant for Keyword, leaving both unreachable. That is why [1,1,1] used to mean three 1px tracks instead of three equal fractions. Single fr values use taffy's flex() helper (minmax(0, Nfr)) rather than fr(), so tracks stay evenly sized regardless of content — matching the flex-row equivalent byte for byte, which matters for a video renderer. Refs #105
…ating `rustmotion schema` described none of the 57 components: Scene.children is a Vec<serde_json::Value>, so schemars emitted "items": true — literally any JSON. Constrained generation was therefore impossible, which is the structural reason the authoring surface needs tens of thousands of words of prose rules to be usable. The full schema already existed internally, built by validate_attrs to populate a warning allowlist and never surfaced. Merge schema_for!(Component) into the exported document and type Scene.children as a $ref to it. Definitions go from 38 to 178, with a 57-variant oneOf. Validation also measured text through the Helvetica/Arial fallback while the render used the declared face, because load_custom_fonts only ran on render paths. Call it in run_checks, before any measurement. Delete LayerStyle and its orphaned siblings (BlendMode, Overflow, CardDisplay, Size). LayerStyle is the pre-CSS style model: dead code, but still exported and still carrying wrap/motion-path/size, which kept that vocabulary alive in the documentation long after the code stopped accepting it. Refs #106
--fix wrote style.wrap on an unwrappable_text_overflow. CssStyle is deny_unknown_fields and has no wrap field, so the component failed to deserialise and was dropped from the render — and because the geometry walker then saw nothing left to check, validation reported clean. The auto-fixer turned text that overflowed into text that was absent, and certified it. Remove style.white-space instead, which falls back to the wrapping default. Violation paths indexed the post-filter child vector while navigate() indexed raw JSON, so a single malformed sibling made --fix patch an unrelated component. Preserve the raw index. container_clips was computed and never read. Honouring it fixes both directions at once: content clipped by an ancestor is no longer a false positive, which is what made deliberate frame-bleeding typography — the signature of the reference style — impossible to author. Its old heuristic also treated a background as clipping, which would have suppressed real overflow on any card that had one. Add ContentOverflowsBox: wrapping text measured against the box it was actually assigned. A paragraph in a fixed-height card paints far outside it while both boxes stay inside the frame, so the viewport check never saw it. --strict-anim discarded t=0 through an opacity guard, and every entrance preset ramps opacity from zero — so the frame of maximum displacement was always the one skipped. It also forked the renderer's effect resolution and re-timed on start_at, which the renderer does not do. Reuse effective_effects and resolve_props_for_effects, and sample proportionally to scene duration. Static css.transform and non-keyframed scene.camera now contribute to bboxes; both were silently ignored. Camera folding is skipped when a scene uses per-plane depth, where the global formula would not apply. Refs #103
The skills corpus is the de-facto schema an LLM generates from, and it had drifted onto the pre-CSS style model. Because CssStyle is deny_unknown_fields, every stale property silently drops the whole component — so the guide was teaching people to write invisible scenarios. Its canonical Example 1 produced a dropped shape. Move fill/stroke, timeline/stagger and text-background to component root; fix box-shadow to an array with kebab keys; flex-wrap to an enum; transform to an array of functions; reduce grid-template-columns to its one valid syntax; remove motion-path and style.size, which do not exist. Rewrite geometry-safety around white-space, document the fifth violation kind and the clipping exemptions, and correct --fix now that it is safe to use. Resolve the contradictions: position inside card, end_at on counters (the checklist forbade it and two examples used it), the preset counts (31/45/39, actually 40) and the easing count (11, actually 17 plus cubic_bezier). Component count 51 to 57, qrcode to qr_code, and document qr_code, waveform and audio_spectrum, which appeared nowhere. Link the orphaned rules and reference examples/, which is more accurate than the prose was: extracting the 217 fenced JSON blocks and validating each takes failures from 26 to 14, with the remainder being illustrative paths and deliberately-invalid teaching examples. Refs #107
LeadcodeDev
force-pushed
the
chantier/audit-fiabilisation
branch
from
August 1, 2026 15:09
4db53d5 to
0610a83
Compare
This was referenced Aug 1, 2026
Closed
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.
Wave 1 of the post-audit chantier — the 2 critical and 8 high findings. Closes the
two silent-failure paths, makes the geometry validator trustworthy, and realigns the
authoring documentation with the schema the code actually accepts.
Parent: #102 · Sub-issues: #103 #104 #105 #106 #107
One commit per workstream; each is independently reviewable.
What was broken
--fixon an unwrappable overflowstyle.wrap, a fieldCssStylerejects → component dropped from the render, validation then reported cleanstyle.white-space, falls back to wrapping#fff/white/rgb(255,255,255)gridwith three1frtracksrustmotion schemaScene.childrentyped"items": true— no component described$refto a 57-variantoneOf; 38 → 178 definitionscontent_overflows_boxviolationoverflow: hidden--strict-animt=0, the frame of maximum displacement--fixwith a malformed siblingVerification
cargo test --workspace→ 534 passed, 0 failed.cargo build --releaseclean.All 6
examples/*.jsonstill validate.Beyond the per-workstream tests, an independent acceptance harness re-derived every
audit reproduction from scratch — deliberately reusing none of the fixtures the
implementing agents wrote — including pixel measurement of rendered frames:
examples/mega-showcase.jsonchanges visually: its 2×2 feature card now renders as areal grid instead of four stacked rows. Verified by eye — this is the H1 fix landing,
not a regression.
Deliberately deferred to wave 2
at render (magenta + stderr) but does not yet fail
validate.schema::GridTrack, the last stale shadow type. Deleting it means repointing are-export in
traits/container.rs, outside this wave's file partition.schema::style::SpacingandGridPlacement, dead onceLayerStyleis gone.Merge-order note
crates/rustmotion-core/src/engine/renderer/fonts.rshas a deeper bug than the onefixed here:
register_font_filediscards theTypefacereturned byFontMgr::new_from_data, so declared families are never resolvable by name. It wasrediscovered independently during this work — and it is already fixed by
208e8d1on
docs/1600-brutalist-style, which is not yet onmain. No follow-up issue filedfor that reason.
Consequence: the font fix in this PR (measuring with the declared face rather than the
fallback) only takes full effect once that branch lands. When both are in, geometry
results may legitimately shift for scenarios using custom fonts.