Skip to content

docs(runbook): M10 mount-play blocked — Orion drops animate_initial#98

Open
ClodoCapeo wants to merge 11 commits into
mainfrom
keeper/m10-animate-initial-contract-hole
Open

docs(runbook): M10 mount-play blocked — Orion drops animate_initial#98
ClodoCapeo wants to merge 11 commits into
mainfrom
keeper/m10-animate-initial-contract-hole

Conversation

@ClodoCapeo

Copy link
Copy Markdown
Contributor

Runbook for the M10 antenna run blocked at the author leg.

After the full mount-play unblock chain shipped green (Lumencast v0.3.0 on npm → Solar v0.2.3 vendored+served → Pulsar #97 animated fixture), the real Orion serves the zab-transition render-bundle without a flat animate_initial field — it nests the from-state as transitions.from, but @lumencast/runtime@0.3.0 reads the mount-play initial state off t.animate_initial only. The logo snaps; no ramp. No broadcast (guard respected).

Root cause isolated to Orion's Go render-bundle lowering (the LayoutNode round-trip Forge flagged). Fix owner: Conduit (render-bundle ↔ runtime contract) + Forge (Orion). Documents diagnostic, resolution criteria, fix owner, and the completed rollback to baseline 18fecbd4.

Descriptive doc only (docs/runbooks/). Not the run fix.

🤖 Generated with Claude Code

ClodoCapeo and others added 11 commits June 9, 2026 11:44
…endpoint (like Prism)

The runtime opens the Orion WS on the bare `orion=` URL it is handed and
sends the token in-band (LSDP §8). But ZabGate gates /show/stream on the
`?token=` query-string and 403s the WS upgrade before any frame, so the
CEF Solar host looped on 403. Prism's getSolarSceneUrl (the reference that
works) nests the show-token INSIDE the `orion=` WS URL's own query on the
`.lsdp` endpoint, so the token is present at upgrade time.

Match that contract exactly (Prism/src/main/broadcast-url.ts:84-97):
compose `orion=` on /orion/api/v1/show/stream.lsdp?token=<enc show>, drop
the useless+misleading top-level `&token=`, and align the loopback stand-in
on the SAME nested form so the gating bug cannot re-slip in. Scheme follows
the gateway (https->wss / http->ws). Unit tests mirror broadcast-url.test.ts.

Refs #79

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The real Blue-VPS pushes the scene_control leaf as a JSON STRING — the LSDP
codec forbids objects as leaf values (#31). The stand-in cut consumer ran
validate_scene_control directly on the wire value and rejected it
("value must be an object, got str"); the loopback / C-INJ path injects the
object form, which masked the gap.

When the leaf value is a str (live-wire / real-orion off /show/stream.lsdp),
decode_scene_control_leaf (the contract decoder — JSON-parse then validate);
when it is already an object (loopback injection + the in-process C-INJ
corpus), validate it directly. Both run the SAME frozen validate_scene_control
on the decoded object — only the LSDP envelope is peeled. A malicious /
undecodable string is rejected => 0 obs-ws (C-INJ preserved).

Recovers Keeper's fix (keeper/m10-probe-decode-leaf-string e3061ce) and adds
the round-trip + injection unit tests.

Refs #79

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ab logo) + A→transition→B playout

The M10 transition as a reusable STATIC Canvas/LSML scene (data), not
transition code: scripts/fixtures/zab-transition.lsml.json is a franc
full-screen white frame with the centred Zab logo embedded as a base64
data-URI in image.src. No keyframes, no wipe-cover element, no
scene_control leaf — the dormant lower_wipe_cover path is unused; the
scene round-trips as plain LSML 1.1 (props spread at the node top level,
the Orion lsmlNode form the @lumencast/runtime frame/stack/image
primitives read). No asset hosting, no Solar rebuild (the fast-track).

probe-m10-canvas-live.py gains --transition-scene: a third OBS program
scene (scene-transition) renders the active Orion zab-transition scene in
a browser_source, and the playout cuts A (screen-1) → transition →
B (screen-2) in two BARE SetCurrentProgramScene cuts with a hold between
them — franc passage, no fade, NO OBS-native transition (C-MECH). The MID
frame is checked near-white+logo (not magenta, not black). --no-broadcast
(dry) / --broadcast and C-SEC token redaction are preserved.

Verified offline (no VPS / no desktop): the fixture is valid LSML 1.1,
round-trips, the logo data-URI decodes to the exact complete image; the
playout parses, the 3 scenes + 2 franc cuts fire, C-MECH holds (zero
native-transition requests), C-SEC clean. The real CEF-of-Solar render of
the white+logo scene is Keeper's antenna run.

Refs #79

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… for M10 dry run

Combines forge/pulsar-m10-real-orion-url-fix (#94) into the zab-transition
scene + playout branch (#95) to run the SANS-Twitch dry of the M10 franc-cut
transition. Working/proof branch only — NOT for merge to main.

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

Prior M10 antenna runs declared "LIVE on Twitch" on
StartDestination.started=true. That flag only means obs_output_start was
ACCEPTED; the RTMP connect to Twitch is async on the output worker thread, so
it is NOT proof a single byte reached the channel — the porteur saw no stream.

This adds the real proof + a long on-air hold to --transition-scene:

- hold_on_air_with_ingest_proof: after the A->transition->B playout, STAY LIVE
  for --on-air-secs (Keeper antenna run uses ~80) so Twitch has time to display
  the stream, looping the franc playout to keep motion on air.
- Every 5s, read the REAL counters off the Twitch destination's rtmp_output via
  obs-ws GetOutputStatus{outputName="PulsarDest_<id>"}: outputBytes
  (obs_output_get_total_bytes) MUST grow, outputActive MUST stay true, plus
  outputDuration / congestion / skipped frames.
- Scrape pulsar.exe stdout for the verbatim RTMP lines (rtmp-stream.c):
  "Connecting to RTMP URL", "Connection ... successful", and any post-connect
  "Disconnected"/"failed" (a refused/dropped key). The Connecting line embeds
  the stream key in the URL -> always redacted (C-SEC).
- Verdict gates on measured ingest: bytes grew AND active stayed true AND no
  post-connect RTMP drop. Otherwise it FAILS honestly ("request a fresh key"),
  never a 'LIVE' false positive.

Measured antenna run: 59.9 MB pushed to rtmp://live.twitch.tv over 83s at
~6 Mbps, outputActive true throughout, RTMP "successful", no disconnect.

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

Document the incident (no stream visible / false-positive "LIVE"), the root
cause (StartDestination.started is async-accept only + ~10s too short), the
real proof (GetOutputStatus outputBytes growth + RTMP log), the antenna
procedure, the 2026-06-09 measured result (59.9 MB / 83s), the dead-key
failure mode, and the Orion-scene scope decision + rollback.

Refs keeper/m10-real-ingest-proof
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y the Blue rule leaf

The --transition-scene playout no longer hard-cuts between scenes: it arms the
OBS-native Fade transition (SetCurrentSceneTransition=Fade, resolved by
fade_transition kind off GetSceneTransitionList, + SetCurrentSceneTransitionDuration
~450ms) and verifies it via GetCurrentSceneTransition, so each SetCurrentProgramScene
CROSSFADES (A dissolves into the white+logo zab-transition passage, the passage
dissolves into B) — the scene fondu the porteur asked for.

Target scene B is now driven by the REAL Blue blueprint rule: in --live-wire/
--real-orion the playout fires the Blue /trigger, reads the scene_control leaf off
the gateway /show/stream, decodes the LSDP string-JSON envelope and validates it
through the frozen contract, then fades to ctrl.target_scene — not a probe constant.
The dry path (--loopback-leaf / no VPS) falls back to the demo leaf target.

The franc C-MECH "no native transition" assertion was the OVERLAY-CUT pivot's
invariant; it is scoped narrowly to that path and replaced for this playout by
C-FADE, which asserts the Fade IS applied (the transition used = Fade, not the
native stinger / media / a capture blend). A best-effort mid-fade frame is
captured to show the A<->white blend.

Tests: resolve_fade_transition_name (by kind, name fallback, none on a LIGHT
build), resolve_transition_target (demo target on the dry path, the validated
Blue-rule target in live-wire, reject of an invalid leaf). 94 offline tests green.

The Fade actually compositing headless and the leaf actually read off the wire
are the CTest build leg + Keeper's antenna run.

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

Adds a --transition-scene proof leg to run-probes.ps1 (the pulsar-offline-probes
CTest target) so the OBS Fade arming (GetSceneTransitionList ->
SetCurrentSceneTransition=Fade + duration -> GetCurrentSceneTransition verify)
and the two crossfade program switches A~>zab-transition~>B run against the real
CI pulsar.exe, VPS-less (--loopback-leaf --allow-blank). This is the CI hedge
against the fork's hard-cut history: if the build cannot arm a Fade at all,
C-FADE degrades + logs it here instead of only surfacing on Keeper's antenna run.
A light build / headless non-composite is tolerated (exit 3 / --allow-blank); a
hard exit is a real regression.

Refs #79
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te) + leaf declaration; playout reloads browser to replay

The M10 zab-transition scene now (1) ANIMATES on mount and (2) DECLARES the
Blue scene_control leaf, so the white+logo passage both moves visibly and
delivers the Blue rule itself — no more magenta wipe-cover workaround.

scripts/fixtures/zab-transition.lsml.json
- Add an `animate` directive on the logo (LSMLAnimateDirective: a single
  transition — transition{duration:550ms,easing:ease-out} + opacity +
  transform.scale). Authored mount fade/scale-in, no keyframes-compiler.
  Verified it compiles strict through @lumencast/compiler to per-prop
  `transitions` (opacity/scale tween, duration_ms=550 → runtime 0.55s).
- Add operator_inputs declaring __inputs.blue.m10-scene-control.scene_control
  (calque on m10-orion-scene.lsml.json) so Orion's sceneAcceptsPath (F2) fans
  the Blue delta out while THIS scene is active — no silent-drop, no magenta.
- Render root STAYS white+logo: no keyframes block, no wipe-cover render
  element (the leaf is declared for fan-out, not lowered to a cover).

scripts/probe-m10-canvas-live.py
- Local render page now plays the authored mount animation as CSS @Keyframes
  keyed to page load (opacity 0→1, scale 0.85→1, the directive's
  duration/easing) — a fresh CEF load replays it.
- Playout RE-MOUNTS the transition browser_source before each cut
  (re-SetCaptureSource with a cache-busted _replay= URL → fresh CEF load →
  the mount animation replays; obs-browser's same-URL Update() early-return
  would otherwise skip the reload).
- Capture a SEQUENCE of frames (~8 @ ~80ms) across the mount window and
  prove a RAMP (prove_mount_ramp): an intermediate frame neither blank nor
  already-settled. If every frame is identical/settled → NOT VISIBLE, said
  franchement (anti-faux-positif); --allow-blank degrades to INCONCLUSIVE.
- load_transition_bundle now asserts the animate directive + the leaf
  declaration (and still that the render root stays white+logo).
- Keeps C-SEC, --no-broadcast/--broadcast, the white settled MID + A + B +
  the Blue-rule target.

scripts/test_zab_transition.py — invert the static invariants (animate + leaf
declared), add coverage for animate_mount_params, prove_mount_ramp (ramp /
settled / blank / too-few), _bust_url. 24 tests, all green (103 across the CI
offline set).

KNOWN LIMITATION (authoring info, not a defect): in vanilla Solar today the
`animate` directive lowers to per-prop transition TIMING only and the runtime
primitives (Image/Frame) set framer `animate` WITHOUT `initial`, so a
static-prop `animate` does NOT itself mount-play. The VISIBLE mount animation
is produced by the browser_source REMOUNT (fresh page load replays the page's
mount animation). The real-Solar render at the antenna replays on reload too.

Carries the base-branch (#96 final run) WIP: the runbook addendum documenting
the real antenna run + the topology tension this resolves, and the env-driven
Blue rule `target` trigger input.

Refs #79

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt-play initial state)

Author the mount-play INITIAL STATE on the fixture itself: the logo's
`animate` directive now carries `from: {opacity:0, transform:{scale:0.85}}`
(LSML 1.1 / @Lumencast 0.3.0, lumencast-js PR #23 / Solar v0.2.3). With
the patched runtime, this directive IS a native mount-play in Solar
(compiler lowers from -> animate_initial; primitives pass framer-motion
initial={from}) — the browser_source remount re-triggers the mount and
the runtime plays the fade+scale itself, no CSS-page trick needed for
the real bundle.

animate_mount_params now reads the authored `from` (single source of
truth for the local dry-render page) and falls back to the prior 0/0.85
hidden-start defaults when `from` is absent (older fixtures keep their
ramp — covered by a regression test).

Tests: from-state authored + strictly below settled targets; fallback
without from; full offline suite 105 passed.

Refs #79, refs #97.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the root-cause-isolated contract hole found on the M10 antenna
run: after shipping Lumencast v0.3.0 + Solar v0.2.3 + Pulsar #97, the
real Orion serves the zab-transition render-bundle WITHOUT a flat
`animate_initial` field — it nests the from-state as `transitions.from`,
but @lumencast/runtime@0.3.0 reads the mount-play initial state off the
flat `t.animate_initial` only. Result: the logo snaps, no ramp; no
broadcast (guard respected). Fix owner = Orion (Go render-bundle
lowering) + Conduit (contract realignment). Rollback to baseline done.

Refs #79 #97

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