Skip to content

Post-audit reliability hardening — wave 1 (critical + high) - #108

Merged
LeadcodeDev merged 6 commits into
mainfrom
chantier/audit-fiabilisation
Aug 1, 2026
Merged

Post-audit reliability hardening — wave 1 (critical + high)#108
LeadcodeDev merged 6 commits into
mainfrom
chantier/audit-fiabilisation

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

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

Before After
--fix on an unwrappable overflow wrote style.wrap, a field CssStyle rejects → component dropped from the render, validation then reported clean removes style.white-space, falls back to wrapping
#fff / white / rgb(255,255,255) resolved to black, silently, on dark backgrounds render correctly; unresolvable colours warn and render magenta
grid with three 1fr tracks three stacked full-width rows; tracks never reached taffy three columns, pixel-identical to the flex-row equivalent
rustmotion schema Scene.children typed "items": true — no component described $ref to a 57-variant oneOf; 38 → 178 definitions
Content overflowing a fixed-height card never reported (both boxes stay inside the frame) new content_overflows_box violation
Frame-bleeding typography under overflow: hidden rejected as a viewport overflow validates clean — clipped bleed is a technique, not a defect
--strict-anim discarded t=0, the frame of maximum displacement samples proportionally to duration, no opacity short-circuit
--fix with a malformed sibling patched an unrelated component raw JSON index preserved
Validation text measurement used the fallback face while the render used the declared one fonts loaded before measuring

Verification

cargo test --workspace → 534 passed, 0 failed. cargo build --release clean.
All 6 examples/*.json still 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:

baseline (pre-fix binary):   3/11 green
after wave 1:               11/11 green

examples/mega-showcase.json changes visually: its 2×2 feature card now renders as a
real grid instead of four stacked rows. Verified by eye — this is the H1 fix landing,
not a regression.

Deliberately deferred to wave 2

  • A validation-time error for unresolvable colours. This PR makes the failure loud
    at render (magenta + stderr) but does not yet fail validate.
  • schema::GridTrack, the last stale shadow type. Deleting it means repointing a
    re-export in traits/container.rs, outside this wave's file partition.
  • schema::style::Spacing and GridPlacement, dead once LayerStyle is gone.

Merge-order note

crates/rustmotion-core/src/engine/renderer/fonts.rs has a deeper bug than the one
fixed here: register_font_file discards the Typeface returned by
FontMgr::new_from_data, so declared families are never resolvable by name. It was
rediscovered independently during this work — and it is already fixed by 208e8d1
on docs/1600-brutalist-style, which is not yet on main. No follow-up issue filed
for 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.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant