Skip to content

fix(runtime): composite keyframe wrapper geometry + transform (ADR 011 I7)#24

Merged
ClodoCapeo merged 1 commit into
mainfrom
forge/adr011-keyframe-compositing-box
Jun 13, 2026
Merged

fix(runtime): composite keyframe wrapper geometry + transform (ADR 011 I7)#24
ClodoCapeo merged 1 commit into
mainfrom
forge/adr011-keyframe-compositing-box

Conversation

@ClodoCapeo

Copy link
Copy Markdown
Contributor

What

Fixes the keyframe-animation runtime so core.animation.play@1 actually moves and fades at the antenna — the 3rd and last link of ADR 011 I7 (after the trigger fix Orion #163 and the lowering fix Orion #167 + oracle Solar #23).

The render-bundle Orion serves was already correct (verified live): a keyframed frame wrapper carrying the target's static geometry (x:80,y:360, 160×160) + animated transform/opacity keyframes, with the resolved target nested beneath. Yet the box rendered 100×100 pinned at (0,0), immobile, no translateX, no fade (live frame-diff, tir I7 #2). The leaf incremented and the bundle was sound — the pixel was dead. Root cause was entirely in the runtime renderer (@lumencast/runtime).

Cause (runtime, fichier:ligne)

Two distinct bugs in @lumencast/runtime@0.6.0's keyframe path, both silently dropping the animated geometry:

  1. src/render/keyframe-player.tsx:67 (bundled in dist/tree-…js) — the KeyframePlayer wrapped the played subtree in <motion.div style={{display:"contents"}}>. A display:contents element generates no box, so the browser never composites the transform/opacity/filter framer-motion writes onto it. The wrapper's geometry never painted; the nested target rendered at its default origin.
  2. src/animate/keyframes.ts pullTransformcompileForFramer emitted the authored translateX/translateY keys verbatim, but framer-motion animates transform via its shorthand motion keys x/y. So translateX was ignored even once the box composited (only the opacity fade survived — the second half of the live symptom).

Fix

  1. The player is now a real compositing box: style={{position:"absolute", inset:0}}. It overlays the parent without disturbing sibling layout and, being positioned, becomes the containing block for the absolutely-positioned target nested beneath (Frame is position:absolute; left:0; top:0), so the target's authored x/y are preserved while the animated channels composite onto live pixels.
  2. pullTransform maps translateX→x, translateY→y for framer's animate object.

Both are repairs to the existing keyframe player (no new runtime primitive — ADR 011 §6 criterion #6). Shipped as a committed patch-package patch over @lumencast/runtime@0.6.0 (the buggy code is in the bundled dependency; Solar re-bundles the patched runtime into dist/). The patch is applied via postinstall + pretest + prebuild so it lands deterministically (npm 11 may gate postinstall; pretest/prebuild cover the CI check job).

Wipe-cover (degenerate cover) preserved: full-screen self-painting cover with no nested target still fills the screen and fades through the same keyframe path; its byte-pinned authored shape is unchanged.

Test added

tests/unit/animation-compositing.test.tsx — mounts Solar end-to-end over a fake LSDP/1.1 transport against a bundle shaped exactly like the live render-bundle, and proves on the actual DOM that the keyframe wrapper:

  • is a real positioned box, not display:contents (regression guard);
  • carries the composited keyframe translateX(400px) + opacity:1 (both halves of the bug);
  • nests the 160×160 target at (80,360) — not (0,0)/default.

The wipe-cover test's opacity helper reads off the new compositing box.

How the new build reaches the golive (étape pour Keeper)

The golive (_keeper_ops/golive-solar.mjs) points Pulsar CEF at …/orion/static/solar/v{SOLAR_VERSION}/host/index.html, currently pinned to v0.2.8. Orion serves Solar from ORION_SOLAR_ROOT (/var/lib/orion/solar/v{N.N.N}/). This PR bumps Solar to v0.2.9 + CHANGELOG so:

  1. On merge + tag v0.2.9, Solar's release.yml runs the full CI + E2E, builds dist/ (with the prebuild patch applied), and publishes solar-v0.2.9.tgz (contents of dist/, incl. host/ + the patched tree-…js chunk) to GitHub Releases. It also verifies package.json version == tag.
  2. Keeper rolls the antenna forward by adding v0.2.9 to SOLAR_VERSIONS in Orion's .github/workflows/ci.yml (Install Solar bundles step) — Orion's deploy extracts the tgz into $APP_PATH/solar/v0.2.9/ (idempotent .installed marker). Multiple versions are supported for canarying.
  3. Bump SOLAR_VERSION = "0.2.9" in _keeper_ops/golive-solar.mjs so the next golive serves the patched runtime.

Note: Orion ci.yml's SOLAR_VERSIONS is currently "v0.1.1" (stale vs the v0.2.8 the golive uses — the manual-tgz gap). Keeper should reconcile when adding v0.2.9.

CI

Local gate green and constated (not asserted): lint, typecheck, vitest (37/37 incl. the new proof), npm run build, check:bundle (size budget + zero bare specifiers), prod npm audit --omit=dev --audit-level=high (only 1 pre-existing moderate ws, below threshold), lockfile npm ci --ignore-scripts. E2E (host-bundle.spec.ts) not runnable locally (Chromium unavailable in this env) — it is a bundle-loadability smoke test unaffected by this change (module structure/import map unchanged); it runs green in CI via npx playwright install --with-deps chromium.

Surface sensible / écarts vs ADR

  • New dev dependency patch-package (+ transitive dev deps), not named in ADR 011. Justified: the buggy code is in the bundled @lumencast/runtime dependency with no Solar-side render seam; patch-package is the minimal, reversible mechanism to land the fix in the served bundle without forking the runtime. No runtime (prod) dependency added; no prod-high audit regression.
  • Bundle contract unchanged — the render-bundle shape Orion serves is correct as-is; no Conduit change needed.

Refs ADR 011 I7

🤖 Generated with Claude Code

…1 I7)

`core.animation.play@1` rendered dead at the antenna — the box sat 100×100
pinned at (0,0), immobile, with no translateX and no fade (live frame-diff,
tir I7 #2) — despite a correct render-bundle (keyframed `frame` wrapper with
the target's geometry, animated transform/opacity, target nested beneath).

Two distinct bugs in `@lumencast/runtime`'s keyframe path, both dropping the
animated geometry:

- The `KeyframePlayer` wrapped the subtree in a `display:contents` motion.div.
  `display:contents` generates no box, so the browser silently dropped the
  transform/opacity/filter framer-motion wrote — the wrapper geometry never
  composited and the nested target rendered at its default origin. The player
  is now a real compositing box (`position:absolute; inset:0`) and the
  containing block for the absolutely-positioned target, preserving its x/y.
- `compileForFramer` emitted the authored `translateX`/`translateY` channels
  verbatim, but framer-motion animates transform via `x`/`y`; the translate
  was ignored even on a real box (only opacity survived). Mapped to framer
  keys.

Both repair the EXISTING keyframe player (no new runtime primitive, ADR 011 §6
#6); shipped as a committed patch-package patch over @lumencast/runtime@0.6.0
since the code lives in the bundled dependency. Solar re-bundles the patched
runtime into dist/. The wipe-cover degenerate cover is preserved (byte-pinned
shape unchanged). New runtime test proves the wrapper composites
translateX(400px)+opacity:1 with the nested 160×160 target at (80,360).

Bumped to v0.2.9 so the release tgz carries the fix to Orion's static serve.

Refs ADR 011 I7
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ClodoCapeo
ClodoCapeo merged commit 8652764 into main Jun 13, 2026
5 checks passed
@ClodoCapeo
ClodoCapeo deleted the forge/adr011-keyframe-compositing-box branch June 13, 2026 12:34
ClodoCapeo added a commit that referenced this pull request Jun 19, 2026
The prod dependency-audit CI job (npm audit --omit=dev --audit-level=high)
fails on ws 8.0.0-8.20.1 (GHSA-58qx-3vcg-4xpx uninitialized memory
disclosure + GHSA-96hv-2xvq-fx4p memory-exhaustion DoS), pulled
transitively on the prod path via @lumencast/protocol/@lumencast/runtime
(both pin ws ^8.18.0). The advisory was published after main's last green
run, so it is environmental, not introduced by the runtime 0.7.0 bump
(ws resolved identically under 0.6.0).

Patched ws 8.21.0 is within the existing ^8.18.0 range; resolution-only
bump (constraint -> ^8.21.0), no other deps drift. Prod audit now reports
0 vulnerabilities; lint/typecheck/test/build/npm ci all green.

Refs #24
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ClodoCapeo added a commit that referenced this pull request Jun 19, 2026
* build(deps): bump @lumencast/runtime to 0.7.0

Resolve @lumencast/runtime ^0.6.0 -> ^0.7.0 (lockfile 0.7.0, transitive
@lumencast/protocol 0.6.0 -> 0.7.0). Solar's adapter surface (mount,
MountOptions, SolarError/SolarStatus) maps onto unchanged runtime types,
so no adapter code change was needed.

Re-target the ADR 011 I7 keyframe-compositing patch-package patch to
0.7.0: the fix is still not upstreamed (display:contents dead box +
translateX/Y -> framer x/y mapping both absent in published 0.7.0), so
the patch stays necessary. Regenerated against the 0.7.0 files (5 file
diffs, same edits); obsolete 0.6.0 patch removed. npm ci re-applies it.

Refs #24
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(test): adapt render test to runtime 0.7.0 asset host gate

@lumencast/runtime 0.7.0 (LSML 1.2, ADR 002 #F - Bastion T1/T2) gates
every image src through gateSrc() before the DOM, deny-by-default: T2
admits only https: schemes, T1 requires the URL host in the bundle's
assets.allowedHosts. A bundle with no allowlist has its <img> omitted.

The render-vocab test served http://x/logo.svg with no allowlist, so
under 0.7 no <img> rendered and waitFor() timed out. Declare
assets.allowedHosts: ["cdn.example"] and serve https://cdn.example/...;
width/height/src assertions preserved. Legitimate runtime hardening,
not a Solar regression.

Document the bump in CHANGELOG (Unreleased).

Refs #24
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(deps): bump ws to 8.21.0 to clear high advisory

The prod dependency-audit CI job (npm audit --omit=dev --audit-level=high)
fails on ws 8.0.0-8.20.1 (GHSA-58qx-3vcg-4xpx uninitialized memory
disclosure + GHSA-96hv-2xvq-fx4p memory-exhaustion DoS), pulled
transitively on the prod path via @lumencast/protocol/@lumencast/runtime
(both pin ws ^8.18.0). The advisory was published after main's last green
run, so it is environmental, not introduced by the runtime 0.7.0 bump
(ws resolved identically under 0.6.0).

Patched ws 8.21.0 is within the existing ^8.18.0 range; resolution-only
bump (constraint -> ^8.21.0), no other deps drift. Prod audit now reports
0 vulnerabilities; lint/typecheck/test/build/npm ci all green.

Refs #24
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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