marquee-cartridge-spec

Marquee Conformance Suite

The executable definition of what a Marquee Surface puts on screen. A Surface engine conforms to the specification when, for every scenario here, the trace it produces matches expected.json exactly — and, where a scenario lists them, so do the Loader’s warnings.

Both first-party engines — Swift and TypeScript — run this suite in CI. Third-party Surfaces are welcome to run it too.

conformance/
  build.py            builds cartridges/*.db from the definitions in the script
  cartridges/         the fixture cartridges (committed; rebuild with build.py)
  scenarios/MCS-NN/   scenario.json (setup, clock, host behaviour) + expected.json (the trace)
  run.mjs             validates the suite, or runs it against an engine
python3 conformance/build.py                          # rebuild the cartridges
node conformance/run.mjs                              # validate the suite itself
node conformance/run.mjs --engine path/to/engine.js   # run every scenario
node conformance/run.mjs --engine path/to/engine.js MCS-02 MCS-10

No dependencies: python3 3.9+ and Node 22.13+ (built-in node:sqlite).

The fixture show

All cartridges describe one fictional show: SHOW26 / LOBBY3, venue timezone America/Los_Angeles, Day 1 = 2026-09-15 (a whole venue-local day; two-days adds 2026-09-16). One location, LOBBY3-A. A location carries no orientation (specification §6): each scenario gives the engine its orientation in scenario.json, as a host passes the orientation its device renders. There are no media bytes — an engine never reads them — so manifest hashes are derived from file names.

IDs are systematic so a trace reads on its own:

   
Playlist entry id = its media item id = its position (playlist B uses 11, 12; a session board entry takes its position)
Portrait file id 100 + item id
Landscape file id 200 + item id
Stills 10 s (display_duration)
Videos 12 s (intrinsic_duration), no trim
Schedule entry id its row, in order: 1 is the first
Demo branding demo-station only: item 9 is the background and item 8 the overlay; neither is in a playlist
Session board templates template and template-dangling only: item 50 is the Show’s template and 51 set 1’s, each one application/zip file (150, 151) in the portrait slot, in no playlist; in template-dangling the items are 99 and 98 and their files are not in the manifest

The base show: one schedule, playlist A from Day 1 00:00 (a surface has one schedule, whatever the orientation: specification §5.1); playlist A = entries 1–4, all standard ON at 08:00; entry 2 has a takeover from 11:30 to 12:30. Other cartridges are variations, named for what they change.

scenario.json

Field Meaning
id, title, rules Identity and the rules under test
cartridge Which cartridges/<name>.db to load
engine orientation: the orientation the device renders. demoStation: true: the host runs the DemoStation mode, so the engine resolves the demo_station slot (specification §5.11). boardVariant: the device’s board setting, now-next, schedule or both (specification §5.15; both when absent)
clock.source surface (real/synthetic show clock) or preview (Studio transport)
clock.start Wall-clock start, ISO 8601 with offset
clock.tickMs, clock.seconds Tick every tickMs; run ticks 0 … seconds − 1
clock.deviceTimeZone The process timezone while running. Engines must not depend on it.
clock.commands Preview only: { atSecond, set?, action? } with action play or pause
host How the simulated host answers render items (below)
events { atSecond, type: "wallJump", deltaMs } — a wall-clock correction; { atSecond, type: "setOrientation", orientation } — the device rotates; { atSecond, type: "setBoardVariant", variant } — the operator sets the device’s board variant

The simulated host

The runner plays the host, the same way for every scenario. At each tick, in order:

  1. Apply events and preview commands due at this second. The monotonic time is always second × tickMs; a wallJump moves only the wall clock.
  2. Deliver scheduled video completions whose monotonic time has come.
  3. Call tick(wall, mono). If it returns a render item, answer it:
    • entry in host.failEntries → onLoadFailed, then tick again at the same instant (up to 64 times), so a skip and its replacement land on the same tick;
    • entry in host.noFirstFrameEntries → no answer at all;
    • otherwise → onFirstFrame at once. A video not in host.stuckEntries then has its completion scheduled at mono + duration. Completions are delivered even if the item has since been replaced — a real player’s late callback — and the engine must ignore them.
  4. A blank render item needs no answer.

A DemoStation’s items — the picture-in-picture’s, while a demo is on — are answered the same way: the host plays the rotation wherever the engine puts it.

Session board page counts come from host.boardPages ({ "<session_set id>": pages }) through an injected board resolver. Production resolvers are checked against the same page counts separately.

The trace

expected.json is { id, trace: [event, …] }, and for a scenario about the Loader also loadWarnings (below). Events are compared field for field, in order.

Field Present on Meaning
t all Show-clock time, ISO 8601 with the venue offset (compared as Unix ms)
kind all render, cut, jump, hold, blank, skip, warning
code all The stable reason code (below)
entry render, cut, skip The playlist entry concerned (for cut, the item that was on screen)
set render standard or takeover
file render of a media item The media file for this orientation
board render of a session board The session set

A render event is recorded when the host reports the item’s first frame. Nothing is recorded for a natural end: the next render shows when it happened.

Reason codes

Codes are the stable contract; any human-readable text an engine attaches is free to change.

Kind Code Meaning
render rotation.start First entry of a set: no cursor yet, or a takeover or alert period beginning
render rotation.next The first entry after the cursor’s position
render rotation.wrap Nothing after the cursor, so the first entry again
cut alert.activate A transition to the alert set, from standard or takeover (a time hint reached)
cut takeover.activate The standard → takeover transition (a time hint reached)
cut schedule.change A schedule boundary changed the active playlist
cut orientation.change The device rotated: the rotation continues after its cursors in the new orientation
cut mode.change A DemoStation’s demo started or ended: the rotation moved into the picture-in-picture or back, and continues after its cursors
cut cartridge.commit A new cartridge was committed
jump jump.backward, jump.forward A show-clock jump (Reference §5.5); re-evaluation follows at once
hold set.empty The working set is empty; the last frame stays
hold set.all_failed Every entry of the working set failed in a row; retrying after 2 s
blank schedule.blank The schedule entry’s playlist is NULL: an authored blank
skip media.load_failed The host reported a load failure
skip media.no_first_frame No first frame within 10 s of monotonic time
skip media.watchdog A video did not complete within its duration + 10 s
skip media.not_playable The entry’s item is not decodable image or video media (a template package, say)
warning orientation.missing No orientation supplied; the orientation gate was skipped

Holds are recorded on entry, not on retry. A hold is recorded unless the previous render, hold, or blank event was a hold with the same code. Skips are always recorded.

Load warnings

A scenario about the Loader lists, in expected.json, the warnings it must give for the cartridge: loadWarnings: [{ code, table, column?, row? }, …], compared field for field and in order (the Loader’s order: the rows of one table by id). row is the row’s id. A scenario without loadWarnings does not compare them. Codes are the Loader’s, as stable as the trace’s: table.unknown, column.unknown, value.unknown, value.malformed, reference.dangling, position.duplicate, timezone.invalid, days.empty, locations.empty.

The engine module

run.mjs --engine <module> imports an ES module exporting:

Export Contract
loadCartridge(bytes) → Promise<Snapshot> The Loader. The Snapshot’s warnings are { code, table, column?, rowId? }
surfaceClock(), previewClock() Clock sources; the preview clock has set(showMs), play(), pause()
createEngine({ snapshot, orientation, demoStation, clock, boardResolver }) Returns an engine for one device

The engine has tick(wallMs, monoMs) → { renderItem?, trace }, and onFirstFrame(token), onMediaCompleted(token), onLoadFailed(token, reason), setOrientation(o), each returning the trace events they caused. A trace event carries showTime (ms), kind, code, and as relevant entryId, set, mediaFileId, sessionSetId. A render item carries token, kind (media, sessionBoard, blank), entryId, and for media media.contentType and media.duration (seconds). The Swift runner in MarqueeSurfaceEngine reads the same folders and applies the same host rules.

Scenarios

ID Scenario Cartridge Rules
MCS-01 Basic rotation base Reference §6.5, §6.6
MCS-02 A takeover starting mid-item cuts immediately base Reference §6.6, §6.7
MCS-03 A takeover ending mid-item lets it finish; standard resumes after its cursor short-takeover Reference §6.5, §6.7
MCS-04 A standard directive turning off mid-item lets it finish; the entry then drops out e3-off Reference §6.3, §6.7
MCS-05 The cursor’s entry is excluded during a takeover; standard resumes at the next position e3-off-during-takeover Reference §6.5
MCS-06 Overlapping takeovers form one set in position order two-takeovers Reference §6.4
MCS-07 Outside the event, a device in another timezone shows Day 1 at the venue’s time of day base Reference §5.3, INV-01
MCS-08 Synthetic time before the first directive holds, then plays when it turns on base Reference §5.3, §6.4
MCS-09 Show-clock jumps, backward and forward, force re-evaluation base Reference §5.5
MCS-10 A multi-page session board lasts duration × pages; a takeover cuts it mid-board board Reference §6.8, INV-08
MCS-11 Video: completion, a load failure, and the watchdog video Reference §6.6, §6.9
MCS-12 A schedule change to another playlist cuts immediately and resets cursors schedule-change Reference §6.2, §6.7
MCS-13 Directives are scoped to the current day two-days Reference §6.3
MCS-14 Studio preview: set, play, pause base Reference §5.4, §5.5
MCS-15 Orientation slots: an empty slot excludes; a slot’s file plays as authored orientation Reference §9.4, INV-14
MCS-16 Every entry fails: the engine holds and retries, never disarming base Reference §6.6, INV-10
MCS-17 No first frame within 10 s is a load failure base Surface Engine PRD §5.6
MCS-18 A late completion from a replaced item is ignored late-callback Surface Engine PRD §5.6
MCS-19 Same-playlist boundary is a no-op; a NULL playlist blanks boundaries Reference §6.2
MCS-20 One schedule on a portrait device: a portrait-only item plays, a landscape-only item is excluded, and a portrait takeover cuts one-schedule Reference §6.2, §6.3, §9.4
MCS-21 The same schedule on a landscape device: the landscape-only item plays, and the portrait takeover never interrupts one-schedule Reference §6.2, §6.3, §9.4
MCS-22 A device rotating mid-show keeps its playlist and its cursors, and continues with the other orientation’s files one-schedule Reference §6.2, §6.5, §6.7, §9.4
MCS-23 A DemoStation’s picture-in-picture plays the same playlist with the opposite orientation’s files, and the rotation continues as the demo starts and ends demo-station Reference §6.5, §6.7, §7, §9.4
MCS-24 A schedule row on a retired slot is malformed: skipped with a warning, and the one schedule plays on retired-slot Specification §4.4, §10.4
MCS-25 An alert outranks a takeover: its activation cuts the takeover item, the alert set is the whole rotation, and when it turns off a takeover period begins again alert Reference §6.3, §6.4, §6.5, §6.7
MCS-26 An alert is never day-scoped: one ON at timestamp 0 governs on Day 2 and suppresses Day 2’s standard rotation alert-standing Reference §6.3, §6.4
MCS-27 The orientation gate comes before the alert gate: an alert with a landscape file only never reaches a portrait device, and takes over when the device turns landscape alert-landscape Reference §6.3, §6.4, §9.4
MCS-28 A session board template rides the cartridge: the Show’s pointer and a set’s override load with no warning, and the board plays as before template Specification §5.15, §7.5, §7.7
MCS-29 A dangling template pointer and malformed settings are warnings, not refusals: the next template applies and the board plays template-dangling Specification §5.15, §5.13
MCS-30 The device’s board variant changes: a board on screen is cut and the rotation continues; a still on screen plays on template Specification §5.15, §5.9

Expected traces were derived by hand from the specification and are reviewed before either engine exists. When an engine disagrees with a trace, decide which is wrong against the specification — never edit a trace to match an engine.

Adding a scenario

  1. Add or reuse a cartridge definition in build.py, and run it.
  2. Add scenarios/MCS-NN/scenario.json and expected.json.
  3. node conformance/run.mjs must report every scenario well-formed.
  4. Keep everything fictional (see the repository’s CLAUDE.md), and run its leak scan.