Skip to content

test(m9): Blue-trigger reactive live-repaint probe + fixtures (#53)#55

Open
ClodoCapeo wants to merge 5 commits into
forge/pulsar-44-m8-canvas-livefrom
forge/pulsar-53-m9-probe
Open

test(m9): Blue-trigger reactive live-repaint probe + fixtures (#53)#55
ClodoCapeo wants to merge 5 commits into
forge/pulsar-44-m8-canvas-livefrom
forge/pulsar-53-m9-probe

Conversation

@ClodoCapeo

Copy link
Copy Markdown
Contributor

M9 — prove a Blue trigger repaints a live scene on air

Closes the last M9 brick (Pulsar #53): the probe that proves
POST /api/v1/blueprints/{id}/trigger repaints the live scene — the
end-to-end proof of Blue ADR 001 §6 criterion 4. Built on the M8 authoring

What this proves

The chain trigger → Blue interprets → maps outputs → pushes the scoped leaf → Orion recompute → delta → Solar repaint, no reload (ADR Blue 001 §3.2.5),
demonstrated on captured pixels:

  1. SETUP (m9_setup.py, derived from m8_setup.py) authors a scene whose
    frame background is bound to __inputs.blue.pulsar-m9-bg.colour,
    declared as an operator_input with default colour A (#1A9E57). Orion
    seeds A on boot and accepts Blue's later in-scope write (sceneAcceptsPath).
  2. capture-A — pre-flight (reused M6/M8 non-blank core): the live Solar
    frame is non-blank AND its modal ≈ Abuild/m9-before.png.
  3. firePOST /blue/api/v1/blueprints/{id}/trigger with the operator
    Bearer header
    (never a query string — ADR Blue 001 R6) and
    inputs={colour: B} (#C81E5A).
  4. capture-B — poll the same browser_source until modal ≈ B within
    Orion's input→delta budget AND Manhattan(B,A) > 80build/m9-after.png.

SetCaptureSource is issued once, so the change is a live LSDP repaint, not
a reload. A 200 from /trigger alone does not pass — the pixels must move
(delivery is best-effort, ADR Blue 001 §3.2.6). A=(26,158,87) vs B=(200,30,90)
are 305 apart in RGB Manhattan, far past the modal tolerance (24) and the
repaint floor (80), so the assertion is noise-proof.

Fixture (checked-in, deterministic)

  • scripts/fixtures/m9-blueprint-bg.json — Blue passthrough blueprint
    (event→output, single core.input colour); stdlib-only nodes → publishes
    on any deployed Blue; never compiled by Orion (runs in Blue's interpreter).
  • scripts/fixtures/m9-scene.lsml.json — LSML 1.1 bundle, frame bind.background
    → the leaf, operator_inputs[].default = A. No Canvas schema change (the
    existing per-node bind; Solar's resolveProps applies any binding key).

Sensitive surface (→ Bastion clearance)

This PR exercises a new auth/network surface (ADR Blue 001 R1–R6): it fires
the operator-gated /trigger and the Blue→Orion service-token push reacts. The
operator JWT now also authorises /trigger and is added to the grep-assert
needle set. run-m9.ps1 fails the run if the operator JWT, the Twitch key, or a
JWT-shaped show-token leaks to stdout / the before+after PNGs / VOD. No secret is
committed; all credentials come from étage-1.

Validation done (not the real run)

  • py -3 -m py_compile on both modules — green.
  • Plumbing harness: bundle hash determinism; the scene's bound leaf == the
    leaf Blue's real leaf_mapper produces (__inputs.blue.pulsar-m9-bg.colour);
    A/B distance 305; /trigger issues POST with Authorization: Bearer
    header (asserted no token= in the URL), correct path + body.
  • Orion validateBindingKeys: blueprint-free scene → the __inputs.* binding
    passes compile.
  • run-m9.ps1 parses clean, no BOM.

The real run (spawn Pulsar CEF, broadcast/capture) is the separate final
validation step — left to Keeper/Probe.

Stacking

Stacked on #52 (M8): M9 reuses m8_setup.py + probe-m8-canvas-live.py +
probe-m6-live.py, none of which are on main yet. Base is set to
forge/pulsar-44-m8-canvas-live so this diff is M9-only. Merge #52 first,
then rebase/retarget this PR onto main.

Keeper run notes

  • Provide M8_OPERATOR_TOKEN (étage-1 .env.m8, short-TTL admin JWT — drives
    SETUP and /trigger; must not equal ORION_OPERATOR_TOKEN), and a
    deployed Blue with the bridge on (BLUE_OPERATOR_TOKEN set so
    pushed.delivered=true; otherwise the frame stays at A and the probe fails by
    design). Run: pwsh scripts/run-m9.ps1 (add -Broadcast + TWITCH_STREAM_KEY
    for the optional 30s go-live).

Refs #53

🤖 Generated with Claude Code

ClodoCapeo and others added 5 commits June 8, 2026 15:43
Deterministic, checked-in fixtures for the M9 proof (Blue ADR 001 §4):

- m9-blueprint-bg.json: a Blue passthrough blueprint (event→output, single
  core.input `colour`) whose /trigger maps outputs to the scoped leaf
  __inputs.blue.pulsar-m9-bg.colour. Stdlib-only nodes → publishes on any
  deployed Blue; never compiled by Orion (runs in Blue's interpreter).
- m9-scene.lsml.json: an LSML 1.1 bundle whose frame BACKGROUND binds that
  leaf, declared as an operator_input with default colour A (#1A9E57) so
  Orion seeds A on boot and accepts Blue's later in-scope write
  (sceneAcceptsPath). No Canvas schema change — the existing per-node bind.

Colour A (#1A9E57) vs trigger colour B (#C81E5A) are 305 apart in RGB
Manhattan, far past the modal tolerance, so the A→B repaint assertion is
unambiguous. Verified against the real Blue leaf_mapper (produces the exact
bound leaf) and Orion validateBindingKeys (blueprint-free scene → binding
passes compile).

Refs #53

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Derived from m8_setup.py, retargeted at ADR Blue 001's reactive contract.
Reuses the M8 toolkit wholesale (gateway HTTP client, LSML hash port,
redaction ports, Solar URL composer) by importing m8_setup; only the SETUP
orchestration + the M9 result shape are new.

Differences vs M8: one blueprint (not N≥2); the scene definition carries
blueprints=[] (the visible binds an operator-input leaf, not a blueprint-key
leaf); the provenance target A is read from the scene's operator_inputs
default (the exact declaration that drives the live leaf). Adds fire_trigger:
POST /blue/api/v1/blueprints/{id}/trigger with the operator JWT as an
Authorization: Bearer HEADER (never a query string — ADR Blue 001 R6),
inputs={colour: B}, asserting 200 + the colour passed through to the leaf.

Refs #53

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
probe-m9-canvas-live.py proves Blue ADR 001 §4: a /trigger repaints a live
scene with no reload. Reuses the M6 CEF core (spawn/reap, stdlib PNG decode,
analyse_frame, broadcast) and the M8 non-blank provenance pre-flight.

The proof is capture-A / fire / capture-B on the SAME browser_source:
  1. pre-flight → non-blank AND modal ≈ seeded default A (build/m9-before.png);
  2. fire the trigger (operator Bearer header) with inputs={colour: B};
  3. poll the same source until modal ≈ B within Orion's input→delta budget
     (REPAINT_DEADLINE_S) AND Manhattan(B,A) > REPAINT_MIN_DELTA
     (build/m9-after.png).
SetCaptureSource is issued once, so a change is a live LSDP repaint, not a
reload. A 200 from /trigger alone does NOT pass — the pixels must move
(best-effort delivery, ADR Blue 001 §3.2.6). Broadcast is opt-in
(--broadcast), so the proof needs no Twitch key.

run-m9.ps1 tees stdout + grep-asserts no credential (the operator JWT — now
also the /trigger credential — the Twitch key, and JWT-shaped show-tokens)
leaks to stdout / the before+after PNGs / VOD. A leak fails the run
regardless of probe exit. UTF-8 no BOM.

Refs #53

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The M9 probe proved the A->B repaint BEFORE going live, so the broadcast
only ever showed a static magenta field -- the green->magenta transition
never appeared on the stream or in the VOD. Reorder to M8's scene-switch
pattern: go live on the green A frame, then fire /trigger at duration/2
WHILE broadcasting so the swap happens in direct.

New live leg broadcast_with_live_trigger(): CreateDestination ->
StartDestination (M6/M8 anti-boot-race retry) on GREEN A -> capture-A on
the live wire -> poll ~duration/2 (green) -> /trigger {colour:B} at mid ->
capture-B on the live wire (assert modal~B, Manhattan(A->B) > floor) ->
poll the magenta half -> StopRecord/StopDestination. The VOD now records
the on-air green->magenta cut.

Live is now the DEFAULT (porteur: a l'antenne par defaut); --no-broadcast
keeps the prove-only mode (pre-flight -> trigger -> capture-B, no Twitch
key) for CI. run-m9.ps1 inverts to -NoBroadcast accordingly. Secret
redaction unchanged: operator JWT / Twitch key / show-token scrubbed,
grep-assert intact. py_compile clean.

Refs #53
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carry the M9 wave debt (BOM prefix) on its owning branch so it survives
the ADR-003 land. Unrelated to ADR 003; belongs to the M9 lot.

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