diff --git a/.github/workflows/creator-kit-candidate.yml b/.github/workflows/creator-kit-candidate.yml index 0d1529f..aefcd32 100644 --- a/.github/workflows/creator-kit-candidate.yml +++ b/.github/workflows/creator-kit-candidate.yml @@ -8,7 +8,7 @@ on: - "creator-kit-templates/**" - "scripts/creator-kit/**" - "creator-kit/**" - - "data/v2/examples/player-show-complete-cue-tracks/**" + - "data/v2/examples/player-show-global-sparse-cues/**" - "package.json" - ".github/workflows/creator-kit-*.yml" @@ -34,6 +34,6 @@ jobs: set -euo pipefail node scripts/creator-kit/build-release-candidate.mjs --release-dir .creator-kit-tmp/first --source-commit "$GITHUB_SHA" node scripts/creator-kit/build-release-candidate.mjs --release-dir .creator-kit-tmp/second --source-commit "$GITHUB_SHA" - cmp .creator-kit-tmp/first/spectoda-creator-kit-0.1.0-rc.3.tar .creator-kit-tmp/second/spectoda-creator-kit-0.1.0-rc.3.tar + cmp .creator-kit-tmp/first/spectoda-creator-kit-0.1.0-rc.4.tar .creator-kit-tmp/second/spectoda-creator-kit-0.1.0-rc.4.tar cmp .creator-kit-tmp/first/provenance.json .creator-kit-tmp/second/provenance.json git diff --exit-code -- creator-kit diff --git a/.github/workflows/creator-kit-release.yml b/.github/workflows/creator-kit-release.yml index c6280e6..c559e64 100644 --- a/.github/workflows/creator-kit-release.yml +++ b/.github/workflows/creator-kit-release.yml @@ -47,7 +47,7 @@ jobs: CREATOR_KIT_VERSION: ${{ inputs.version }} run: | set -euo pipefail - test "$CREATOR_KIT_VERSION" = "0.1.0-rc.3" + test "$CREATOR_KIT_VERSION" = "0.1.0-rc.4" test "$(jq -r .version creator-kit/bundle.json)" = "$CREATOR_KIT_VERSION" - run: bun run creator-kit:check - name: Require the approved prerelease license and channel posture diff --git a/AGENTS.md b/AGENTS.md index 1f1e780..bc9681d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,10 +36,11 @@ scriptů, projektových patternů a integračních snippetů. - `creator-kit/` is a generated, read-only public snapshot, not an authoring surface. Documentation remains authoritative; the public copy is immutable and flows in one direction only. -- The reviewed `0.1.0-rc.3` snapshot contains seven CC BY 4.0 Documentation +- The `0.1.0-rc.4` review candidate contains seven CC BY 4.0 Documentation derivatives, the complete hash-locked FW 0.12.11 config contract/schema and - one MIT-licensed Event Player example. Spectoda names, logos and trademarks - are outside those license grants. + one MIT-licensed sparse global Event Player example. The published rc.3 + remains immutable until a separate protected rc.4 release is explicitly + approved. Spectoda names, logos and trademarks are outside those grants. - Keep exact source commits, the Documentation bundle digest, firmware provenance, example hashes, checksums and the `unpublished` stable-channel descriptor intact. Updates require partner approval and a normal review PR. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 14e1f96..b9992cc 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -63,8 +63,10 @@ private `documentation` module remains authoritative for Markdown/MDX and the repository only as an immutable, checksum-locked, one-way derivative; Examples does not become an authoring backchannel for Documentation. -The `0.1.0-rc.3` snapshot includes seven CC BY 4.0 documents, two hash-locked -FW 0.12.11 config assets and one MIT example. It carries the exact +The `0.1.0-rc.4` review candidate includes seven CC BY 4.0 documents, two +hash-locked FW 0.12.11 config assets and the MIT sparse global Event Player. +The published rc.3 remains immutable until a protected rc.4 release is +explicitly approved. The candidate carries the exact Documentation snapshot digest, Documentation/Firmware/Examples source commits, license posture, compatibility contract, indexes and checksums. `scripts/creator-kit/validate.mjs` checks public safety, links, hashes, diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d7f92..8ec71a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ Date-based versions use `YYYYMMDD`. +## 20260810 + +### Changed + +- Simplified the Event Player to one global mixed-ID stream. Tracks remain + per-ID authoring lanes while sparse Cues contain only the Event values + selected for that moment. +- Replaced the unreleased per-ID and content-addressed Player prototypes with + the compact SPM v3 manifest and fixed `player.000` through `player.999` + slots. Every manifest entry pins the exact 48-bit NetworkStorage version and + absolute segment start. +- Retired the parallel segmented single-ID Player example. The global sparse + Player is the single canonical Project Berry plugin; causal scene recall + remains a separate timeline-independent example. +- The directly injectable Berry script now reconciles exactly one Cue Group at + or immediately before a timeline discontinuity. It does not select future + Cues or replay older history, and Events omitted from the sparse Cue retain + their EventStates. +- Prepared Creator Kit `0.1.0-rc.4` with the final + `DEBUG_UNIVERSAL_0.12.11_20260810` compatibility pin. No release or stable + channel promotion is implied by the candidate. + ## 20260809 ### Added diff --git a/README.md b/README.md index ddb9018..f53dd8b 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,7 @@ data/v2/examples/ ├── gledopto-gl-rc-001wl-espnow-remote/ # Berry espnow.rx examples for a GLEDOPTO GL-RC-001WL remote ├── network-nested-element-groups/ # App elementsJson + controlPages group → groups → devices pattern ├── seb-causal-scene-recall/ # recall a mixed-ID SEB scene at the causal event time -├── seb-segmented-id-track-player/ # play an ordered one-ID Track from bounded SEB segments -└── player-show-complete-cue-tracks/ # static Show Index Player with complete per-ID Cues +└── player-show-global-sparse-cues/ # global mixed-ID Player with sparse Cues ``` ## Example Rules @@ -57,8 +56,8 @@ customer-specific data that is not meant to be public. ## Spectoda Creator Kit public prerelease -`creator-kit/` is the immutable source snapshot for the public English -Spectoda Creator Kit `0.1.0-rc.3` prerelease. It contains the complete selected +`creator-kit/` is the review candidate for the public English Spectoda Creator +Kit `0.1.0-rc.4` prerelease. It contains the complete selected FW 0.12.11 Controller Config documentation and machine-readable config contract/schema under CC BY 4.0, plus one MIT-licensed Event Player example. Documentation remains the private authoring source of truth; this repository @@ -78,6 +77,6 @@ bun run creator-kit:check `creator-kit:build` creates only a local deterministic archive under the ignored `.creator-kit-tmp/`; it does not call GitHub Releases. The public -release is available at the matching +latest published prerelease remains [`creator-kit-v0.1.0-rc.3`](https://github.com/Spectoda/examples/releases/tag/creator-kit-v0.1.0-rc.3) -prerelease after the protected publication workflow completes. +until the protected publication workflow is explicitly approved for rc.4. diff --git a/creator-kit-evaluations/questions.json b/creator-kit-evaluations/questions.json index 3f8b71a..f57fc57 100644 --- a/creator-kit-evaluations/questions.json +++ b/creator-kit-evaluations/questions.json @@ -4,7 +4,7 @@ "prompt": "Where is the Berry plugin required by the Studio Event Player?", "expected": { "abstain": false, - "citation": "examples/player-show-complete-cue-tracks/README.md" + "citation": "examples/player-show-global-sparse-cues/README.md" } }, { diff --git a/creator-kit-evaluations/responses/claude.json b/creator-kit-evaluations/responses/claude.json index 5ec6be7..898501d 100644 --- a/creator-kit-evaluations/responses/claude.json +++ b/creator-kit-evaluations/responses/claude.json @@ -4,7 +4,7 @@ { "id": "event-player-plugin", "answer": "The required Project Berry source is player.be in the locked Event Player example; it must be added to each Controller Project.", - "citations": ["examples/player-show-complete-cue-tracks/README.md"], + "citations": ["examples/player-show-global-sparse-cues/README.md"], "abstained": false }, { diff --git a/creator-kit-evaluations/responses/codex.json b/creator-kit-evaluations/responses/codex.json index 9bc045e..97a8f74 100644 --- a/creator-kit-evaluations/responses/codex.json +++ b/creator-kit-evaluations/responses/codex.json @@ -4,7 +4,7 @@ { "id": "event-player-plugin", "answer": "Add the locked player.be Project Berry source to every Controller Project; Studio Event Player upload does not install it.", - "citations": ["examples/player-show-complete-cue-tracks/README.md"], + "citations": ["examples/player-show-global-sparse-cues/README.md"], "abstained": false }, { diff --git a/creator-kit-templates/AGENTS.md b/creator-kit-templates/AGENTS.md index 5d51814..af48d2c 100644 --- a/creator-kit-templates/AGENTS.md +++ b/creator-kit-templates/AGENTS.md @@ -23,7 +23,7 @@ You are reading a pinned, read-only public knowledge bundle. digest. Do not follow a floating URL or silently refresh the bundle. The copy-ready FW 0.12.11 Event Player integration is available at -`examples/player-show-complete-cue-tracks/` under MIT. Documentation and config +`examples/player-show-global-sparse-cues/` under MIT. Documentation and config data are CC BY 4.0. Spectoda names, logos and trademarks are not licensed by those grants. diff --git a/creator-kit-templates/README.md b/creator-kit-templates/README.md index 78a277a..24434d4 100644 --- a/creator-kit-templates/README.md +++ b/creator-kit-templates/README.md @@ -10,13 +10,13 @@ The bundle contains: - the complete FW 0.12.11 Controller Config contract and JSON Schema under `assets/docs/controller-config/0.12.11/`; - one copy-ready Event Player example under - `examples/player-show-complete-cue-tracks/`. + `examples/player-show-global-sparse-cues/`. Read `bundle.json`, `manifest.json`, `source-lock.json`, `licenses.json` and `checksums.sha256` before use. Documentation content is locked to `Spectoda/documentation@6f6051686fe556a85318c7dd529ac061ab48c38d`; the example is locked to -`Spectoda/examples@667b238349cc635792f0e42130a08a4bde701fdf`. The firmware +`Spectoda/examples@{{EXAMPLES_COMMIT}}`. The firmware config assets identify their own exact `Spectoda/firmware` provenance. ## Licensing diff --git a/creator-kit-templates/RELEASE_NOTES.md b/creator-kit-templates/RELEASE_NOTES.md index f6f4082..b2c04c9 100644 --- a/creator-kit-templates/RELEASE_NOTES.md +++ b/creator-kit-templates/RELEASE_NOTES.md @@ -1,6 +1,7 @@ -# Spectoda Creator Kit 0.1.0-rc.3 public prerelease +# Spectoda Creator Kit 0.1.0-rc.4 public prerelease -This is the first public, downloadable Spectoda Creator Kit prerelease. +This candidate updates the public Creator Kit with the final FW 0.12.11 sparse +global Event Player and its final WASM compatibility pin. It includes the complete documented FW 0.12.11 Controller Config surface, machine-readable contract and JSON Schema, plus the MIT-licensed Event Player diff --git a/creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json b/creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json index b1ba10d..169683b 100644 --- a/creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json +++ b/creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json @@ -13,7 +13,7 @@ "transport": { "const": "github-release-v1" }, "updatePolicy": { "const": "exact-version-partner-approved" }, "firmware": { "const": "0.12.11" }, - "wasm": { "const": "DEBUG_UNIVERSAL_0.12.11_20260808" } + "wasm": { "const": "DEBUG_UNIVERSAL_0.12.11_20260810" } }, "additionalProperties": false } diff --git a/creator-kit-templates/schemas/creator-kit-manifest.v1.schema.json b/creator-kit-templates/schemas/creator-kit-manifest.v1.schema.json index 5c5d7a4..77de6ab 100644 --- a/creator-kit-templates/schemas/creator-kit-manifest.v1.schema.json +++ b/creator-kit-templates/schemas/creator-kit-manifest.v1.schema.json @@ -66,7 +66,7 @@ "required": ["firmware", "wasm"], "properties": { "firmware": { "const": "0.12.11" }, - "wasm": { "const": "DEBUG_UNIVERSAL_0.12.11_20260808" } + "wasm": { "const": "DEBUG_UNIVERSAL_0.12.11_20260810" } }, "additionalProperties": false }, diff --git a/creator-kit/AGENTS.md b/creator-kit/AGENTS.md index 5d51814..af48d2c 100644 --- a/creator-kit/AGENTS.md +++ b/creator-kit/AGENTS.md @@ -23,7 +23,7 @@ You are reading a pinned, read-only public knowledge bundle. digest. Do not follow a floating URL or silently refresh the bundle. The copy-ready FW 0.12.11 Event Player integration is available at -`examples/player-show-complete-cue-tracks/` under MIT. Documentation and config +`examples/player-show-global-sparse-cues/` under MIT. Documentation and config data are CC BY 4.0. Spectoda names, logos and trademarks are not licensed by those grants. diff --git a/creator-kit/README.md b/creator-kit/README.md index 45c127e..e8ac6d1 100644 --- a/creator-kit/README.md +++ b/creator-kit/README.md @@ -1,8 +1,8 @@ -# Spectoda Creator Kit 0.1.0-rc.3 +# Spectoda Creator Kit 0.1.0-rc.4 This directory is the immutable source snapshot of the public Spectoda Creator Kit prerelease. Download the packaged artifact from the matching -`creator-kit-v0.1.0-rc.3` GitHub prerelease in `Spectoda/examples`. +`creator-kit-v0.1.0-rc.4` GitHub prerelease in `Spectoda/examples`. The bundle contains: @@ -10,13 +10,13 @@ The bundle contains: - the complete FW 0.12.11 Controller Config contract and JSON Schema under `assets/docs/controller-config/0.12.11/`; - one copy-ready Event Player example under - `examples/player-show-complete-cue-tracks/`. + `examples/player-show-global-sparse-cues/`. Read `bundle.json`, `manifest.json`, `source-lock.json`, `licenses.json` and `checksums.sha256` before use. Documentation content is locked to `Spectoda/documentation@6f6051686fe556a85318c7dd529ac061ab48c38d`; the example is locked to -`Spectoda/examples@667b238349cc635792f0e42130a08a4bde701fdf`. The firmware +`Spectoda/examples@e4be6dd07814c1dc8816ffc0e4ea518532e411ec`. The firmware config assets identify their own exact `Spectoda/firmware` provenance. ## Licensing diff --git a/creator-kit/RELEASE_NOTES.md b/creator-kit/RELEASE_NOTES.md index f6f4082..b2c04c9 100644 --- a/creator-kit/RELEASE_NOTES.md +++ b/creator-kit/RELEASE_NOTES.md @@ -1,6 +1,7 @@ -# Spectoda Creator Kit 0.1.0-rc.3 public prerelease +# Spectoda Creator Kit 0.1.0-rc.4 public prerelease -This is the first public, downloadable Spectoda Creator Kit prerelease. +This candidate updates the public Creator Kit with the final FW 0.12.11 sparse +global Event Player and its final WASM compatibility pin. It includes the complete documented FW 0.12.11 Controller Config surface, machine-readable contract and JSON Schema, plus the MIT-licensed Event Player diff --git a/creator-kit/bundle.json b/creator-kit/bundle.json index 0ae5994..85b914c 100644 --- a/creator-kit/bundle.json +++ b/creator-kit/bundle.json @@ -1,7 +1,7 @@ { "schemaVersion": "creator-kit-bundle.v1", "name": "Spectoda Creator Kit", - "version": "0.1.0-rc.3", + "version": "0.1.0-rc.4", "status": "candidate", "locale": "en", "contentScope": "licensed-documentation-with-public-examples", diff --git a/creator-kit/checksums.sha256 b/creator-kit/checksums.sha256 index 28eecfb..a54356a 100644 --- a/creator-kit/checksums.sha256 +++ b/creator-kit/checksums.sha256 @@ -1,36 +1,36 @@ 0545b1ad0f346229618fe5222d15f6652096d590b4c301b1fbea7b80f833bd2f schemas/creator-kit-term-index.v1.schema.json 097131ae899e8d8227b7a89411b6ac28ba95e0332be84f4828ad729d50c0c4ea LICENSES/MIT.txt +106abd031b6705585394908b114d2920d5343994e8e66d499eb72c5b9afcdaa6 schemas/creator-kit-selection.v1.schema.json 1455dc5a74614c6f5feef5572f38f894b5cc69e6a77ef47de495cde8063d7430 indexes/documents.json -14c0f344148247ca5e72ee7715895fa1dd866f2ef1c0a3bb4a5f66d251d85021 AGENTS.md -1cdb7983ba3d2f517c2a1fe72feac8174fdf9a51635b53f587a511178bdf582b README.md -2336e0232be1969b38c1d8f7f37366e9ef149c5d6e26ce0132c98bb1a214d6af schemas/creator-kit-manifest.v1.schema.json -2747b556ecdabb68e328771c33b6720cf78e97e36d9015ae864366b96e800fc7 bundle.json +2c2ffb32334ff0cd45d5fc711fce6318f0ea6ee9a681b035691c914d2987fa4c RELEASE_NOTES.md +2d144a4e77d7fc6f4e6253fcd22cb9a6ec89bf3d7c9485269d0de42ce8ef3024 examples/player-show-global-sparse-cues/player.be 2f3e88fee1e1ae55fad6642e4041574aad5565c028509e2e0a4731d8bbaa2335 schemas/creator-kit-stable-channel.v1.schema.json +3297ca221e2057b9e3d94d46eea55ec6971ff8587246b21a33373b43fe4650e9 examples/player-show-global-sparse-cues/example.yaml +3431864275b863fb9aaa89fb17617e4a489676df999abb3f9379949de21ffb31 AGENTS.md +35fa413fe0f59efc4a0f5689986d981bd0f739048d8183dd88dda7f3336a4803 examples/player-show-global-sparse-cues/README.md 3655bdaac1de00eed7d21b28a00a452aa7163abfe620c477ff021dfdd738d48c documents/en/pro-vyrobce-a-tvurce/controller-config/index.md 41f576a534c5277269c3c969eb30d6f3e9b8f8112f04eb2ea28c0875a3a0aeb2 stable-channel.json -458b574c6fd6e8cd1e515382f35a6bf20d180541a9c2e69d83c3b084d85e9c8d examples/player-show-complete-cue-tracks/example.yaml 45fab551c3e40f895602d7210f078a890e45bd27e952a6eb7cc92bdeaf187eb6 indexes/terms.json -51842804d9802dc1726c396d25af1308243e0ce047c07010ae9e688c85769fac schemas/creator-kit-selection.v1.schema.json -62e84670ded3bdd035539b29cd1a0cfc3ee2b10db6670ce21184489e14adf178 selection.json +51c3fe50f72c6cb1b4155ef69bc3999a7aec8d783696ece89a2f56a1bd76fb3b manifest.json 69a88ae4b09d1a54c30e58e1850c1d353a26ef4bbc03f54e6482353d3c6f6db3 licenses.json 6fb146ca9d11d6b34b483514a0524f1287fe3038c73592c006e380e06fa1f533 schemas/creator-kit-licenses.v1.schema.json -7202aa8d223cd69384aebe039cc6944d4f45ebb78895da277b7f392437d1ab36 source-lock.json +73fc82fd4476abb8026dccccb39bd4766ec3a4f26849c0a3458ce4b4db231d22 schemas/creator-kit-manifest.v1.schema.json 760cbcde5038f98bae60aea96f6f36f2b4b01aae04c33571b26cfbe2fcd1b38f assets/docs/controller-config/0.12.11/controller-config.contract.json 7b97193f4ce1f58c46e2e1eb6e5d9eb929747e2326844c8e19b9a8d5c79390b3 documents/en/pro-vyrobce-a-tvurce/controller-config/wasm-schema.md 7c4531571bd515b29242e2026bcfb93c0a2452e5ba27c74dea490d2ba22f779d schemas/creator-kit-compatibility.v1.schema.json 8ec2c51cb8999c5ec46cfb90a7d19f400cc0f199523f60fba3184cdaf753ae71 LICENSES/CC-BY-4.0.md -9c4d744525a911de48ca5cd96037d5d20acc1df6e39589a84e6475cffd68f23c RELEASE_NOTES.md 9f14dd2893688637f440a16432f947609d68267549288f6c96be354ce4c7a7fd documents/en/pro-vyrobce-a-tvurce/spectoda-creator-kit.md 9f9e115705c24bece3a9af6f1ae9473cb67a45284a4f2e79dae45c995401e324 compatibility.json -a537ea02342dc335f4d5b7b478a1217b6d35ca72ad6aeff638e46aa105e44948 examples/player-show-complete-cue-tracks/player.be +9fe16ebb56415591d0804f4b7ec8d38cc05412160a31bee16c41713a2f1a2448 source-lock.json acc885a529f405ac2047fadbb0de9b431f5ab841677f3b47f83db5c354d4ba0e documents/en/pro-vyrobce-a-tvurce/controller-config/fw-01211-keywords.md -af580f8f045e1133773f57a8d24d17a2b4072eca8c3d9cf2d1c515146d0321de examples/player-show-complete-cue-tracks/README.md c392c1c2289eeaad6b0f3e0c77ea821ea455ee9c688b893adb8204e4523c61a0 documents/en/pro-vyrobce-a-tvurce/controller-config/fw-01211-io-types.md d3c02cb1a56016712615c98502e34fb8ab7895f383f2a9e65a8160057de334d4 schemas/creator-kit-policy.v1.schema.json d6441fd0b663b8f77cf9c6acee73dd4d475cbcb1abf1f56118f67ff5294b1e1b documents/en/pro-vyrobce-a-tvurce/controller-config/ethernet.md d9ee689ad41bb0b845070d77dfe6eb061976f9e077c293a730e5f520ea8f037c assets/docs/controller-config/0.12.11/controller-config.schema.json dac54ab88acfab5fd13c6b5b5d01a68bf0880e590f653bd856a00e1aa8c3ce3d schemas/creator-kit-source-lock.v1.schema.json +e123ba30631843ec145156298739758b259b4214f78e6dff11a02c97aa876795 selection.json e29e767380b857be6295bef82fc4878f005a025b3ec9d537de427e1b06c98ec3 documents/en/pro-vyrobce-a-tvurce/controller-config/io-type-dali.md e2b4a6840f3145bcc23611cd64f7adc8082186411ff9b8d1cb0146ed28b9d177 schemas/creator-kit-bundle.v1.schema.json e3b49e1798f3892b6b92035efd46c85328f89e072beee43da57b501850c0165a schemas/creator-kit-document-index.v1.schema.json -ef6d35b2cad0e1462008a3261c387d29233dba72c07c20f3046c8fedd28f585c manifest.json +ed1d40be162badd9e0f546b35612c315e06732ae783e8277156e2584bc10c808 bundle.json +fa9c7b83930fa3a2ed2963d32c7956ab4afdf6e648f9422e32767bd46dae6498 README.md diff --git a/creator-kit/examples/player-show-complete-cue-tracks/README.md b/creator-kit/examples/player-show-complete-cue-tracks/README.md deleted file mode 100644 index 6615f1f..0000000 --- a/creator-kit/examples/player-show-complete-cue-tracks/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Complete-Cue Track Player from `player.show` - -This is the canonical copy-ready Project Berry Player for firmware **0.12.11**. -Studio compiles one static `.show` index and deterministic per-ID SEB -chains. The plugin follows the shared timeline; it owns no start, seek or loop -clock of its own. - -```berry -Player({"base": "player", "ids": [1], "debug": false}) -``` - -Configure `ids` with only the local Spectoda IDs 0–254 owned by this Controller. -ID255 is selected automatically when the Show Index contains it. NetworkStorage -still synchronizes every file to every Controller; the plugin opens only ID255 -and its configured local IDs. - -## Authoring and upload - -One Track owns one Spectoda ID. Its Event definitions are stable across the -Track, and every Cue supplies one complete type-correct value for every enabled -Event. SEB files contain those encoded Event values/records; landing them -produces normal EventStore updates. - -Use Studio's Event Player to Capture and edit Cues, then follow this workflow: - -1. pause the shared timeline; -2. upload the changed SEB files and `.show`; -3. wait for ordinary NetworkStorage propagation — App Controller readback does - not prove the whole mesh is ready; -4. rewind the shared timeline; -5. play. - -Hot update and simultaneous writers for the same base are unsupported in v1. -Old deterministic files no longer referenced by `.show` are ignored. - -## Runtime behavior and limits - -The plugin reads only the bounded Show Index into Berry. Each SEB payload is -read and atomically landed by the native `SEB.land(filename, -{"source":"networkStorage", ...})` path, without a Berry payload buffer. - -- one complete Cue contains 1–340 Event values; -- one SEB file is at most 4,092 bytes and spans at most 65,535 ms; -- `.show` is exact-EOF validated and at most 4,096 bytes; -- the base is 1–11 safe ASCII characters and generated names are at most 23 - bytes; -- one plugin turn performs at most one SEB contribution; -- pause performs no landing; resume derives a fresh timeline projection; -- seek, rewind, loop and the 24-hour timeline wrap reconcile the last complete - Cue at or before the target. - -The canonical compact source compiles to a 3,999-byte TNGL BERRY payload -(3,995 source bytes plus framing). Including the copy-ready `return Player(...)` -invocation produces 4,056 bytes, 40 bytes below the 4 KiB hard limit. The 1 KiB -target remains a stretch measurement, not a correctness promise. - -The short local names and positional runtime arrays in `player.be` are -intentional consequences of that hard limit. `R` holds loaded state, selected -Tracks, reconciliation state, the next Track, seek target, last timeline -position, and epoch. Each selected Track stores ID, records per complete Cue, -ordered segments, next segment, and next Cue. The parser verifies those tuple -shapes before playback, rejects chronological segment overlap, and stops a -Track after an already-committed native result disagrees with the Show Index; -it never retries that committed contribution. - -`player-show-artifacts.json` is a public synthetic three-Track corpus used by -the Berry smoke. It is illustrative compiled output; Project data remains the -authoring source of truth. - -For causal scene recall, use the separate `seb-causal-scene-recall` example. A -scene is an ordinary SEB file landed at its trigger Event time and is independent -of the timeline Player. diff --git a/creator-kit/examples/player-show-complete-cue-tracks/example.yaml b/creator-kit/examples/player-show-complete-cue-tracks/example.yaml deleted file mode 100644 index f61e32a..0000000 --- a/creator-kit/examples/player-show-complete-cue-tracks/example.yaml +++ /dev/null @@ -1,35 +0,0 @@ -schemaVersion: example.v2 -slug: player-show-complete-cue-tracks -title: Complete-Cue Event Player from player.show -category: controller -summary: >- - Canonical firmware 0.12.11 Project Berry Player for one static player.show - index and deterministic per-ID SEB chains. It lands global ID255 plus explicit - local-ID Tracks from the shared timeline with complete Cue atomicity. -tags: - - berry - - event-player - - network-storage - - player-show - - seb - - timeline -hardware: >- - Requires firmware 0.12.11 with SEB.land NetworkStorage source support. - Configure only local IDs 0-254; ID255 is automatic. Pause before upload, wait - for ordinary propagation, rewind, and then play. -files: - - path: README.md - role: Setup, workflow, runtime behavior, and limits - language: markdown - - path: creator-kit.json - role: Fail-closed Spectoda Creator Kit selection - language: json - - path: player.be - role: Canonical copy-ready Project Berry Player - language: berry - - path: player-show-artifacts.json - role: Synthetic player.show and SEB golden corpus - language: json -related: - - seb-causal-scene-recall - - seb-segmented-id-track-player diff --git a/creator-kit/examples/player-show-complete-cue-tracks/player.be b/creator-kit/examples/player-show-complete-cue-tracks/player.be deleted file mode 100644 index 4157ba0..0000000 --- a/creator-kit/examples/player-show-complete-cue-tracks/player.be +++ /dev/null @@ -1,186 +0,0 @@ - -# Compact by design: compiled Project Player must fit one 4 KiB TNGL block. -def Player(s) -var b=s.find("base","player") -var ids=s.find("ids",[]) -var d=s.find("debug",false) -if type(b)!="string" || size(b)<1 || size(b)>11 || -type(ids)!="instance" || classname(ids)!="list" -raise "value_error","invalid Player config" -end -for j:0..size(b)-1 -var c=b[j] -if !((c>="A"&&c<="Z")||(c>="a"&&c<="z")|| -(c>="0"&&c<="9")||c=="_"||c=="-") -raise "value_error","invalid Player base" -end -end -var w={} -for i:ids -if type(i)!="int" || i<0 || i>254 || w.find(i,false) -raise "value_error","local IDs must be unique 0..254" -end -w[i]=true -end -var R=[false,[],false,0,0,0,-1] -def wanted(i)return i==255 || w.find(i,false)end -def n3(n) -if n<10 return "00"+str(n)end -if n<100 return "0"+str(n)end -return str(n) -end -def file(i,n)return b+"."+n3(i)+"."+n3(n)+".seb" end - -def load(x) -var z=spectoda.getNetworkStorageData(b+".show") -if z==nil || z.size()<16 || z.size()>4096 || -z.get(0)!=80 || z.get(1)!=83 || z.get(2)!=72 || -z.get(3)!=1 || z.get(4)!=16 || z.get(5)!=0 || -z.get(12,4)!=0 -return false -end -var c=z.get(6,2) -var duration=z.get(8,4) -if c<1 || c>256 || duration<1 || duration>86400000 -return false -end -var tracks=[] -var p=16 -var previous_id=-1 -for ti:0..c - 1 -if p+8>z.size()return false end -var id=z.get(p) -var rpc=z.get(p+2,2) -var sc=z.get(p+4,2) -if z.get(p+1)!=0 || z.get(p+6,2)!=0 || -id<=previous_id || rpc<1 || rpc>340 || sc<1 -return false -end -previous_id=id -p+=8 -var segments=[] -var previous_base=-1 -var previous_end=-1 -for si:0..sc - 1 -if p+8>z.size()return false end -var base=z.get(p,4) -var cc=z.get(p+4,2) -if z.get(p+6,2)!=0 || cc<1 || rpc*cc>340 || -base<=previous_base || base>=duration || -(si>0 && base<=previous_end)|| -(si==0 && base !=0)|| p+8+cc*2>z.size() -return false -end -p+=8 -previous_base=base -var offsets=[] -var previous_offset=-1 -for ci:0..cc - 1 -var offset=z.get(p,2) -p+=2 -if(ci==0 && offset !=0)|| -(ci>0 && offset<=previous_offset)|| -base+offset>=duration -return false -end -previous_offset=offset -offsets.push(offset) -end -previous_end=base+previous_offset -segments.push([base,offsets]) -end -if wanted(id)tracks.push([id,rpc,segments,0,0])end -end -if p !=z.size()return false end -R[0]=true R[1]=tracks R[2]=false R[3]=0 R[4]=x -return true -end - -def advance(t,si,ci) -ci+=1 -if ci>=t[2][si][1].size()si+=1 ci=0 end -t[3]=si -t[4]=ci -end - -def reconcile() -if R[3]>=R[1].size()R[2]=true return true end -var t=R[1][R[3]] -var bs=0 -var bc=0 -var found=false -for si:0..t[2].size()- 1 -var seg=t[2][si] -for ci:0..seg[1].size()- 1 -if seg[0]+seg[1][ci]<=R[4] -bs=si bc=ci found=true -end -end -end -if !found return false end -var o=t[2][bs][1][bc] -var at=timeline.at(R[4]- o) -if at==nil return false end -var cur=bc*t[1] -var q=SEB.land(file(t[0],bs),{"source":"networkStorage", -"at":at,"cursor":cur,"until":o}) -if q["ok"] -if q["cursor"]==cur+t[1]advance(t,bs,bc) -else t[3]=t[2].size()end -R[3]+=1 return true -end -if d print("Player reconcile",t[0],q["error"])end -return false -end - -def forward(x) -var pick=nil -var due=nil -for t:R[1] -if t[3]65535 until=65535 end -var cur=pick[4]*pick[1] -var q=SEB.land(file(pick[0],si),{"source":"networkStorage", -"at":at,"cursor":cur,"until":until}) -if q["ok"] -if q["cursor"]<=cur || q["cursor"]%pick[1]!=0 -pick[3]=pick[2].size() pick[4]=0 -else -var n=int(q["cursor"]/ pick[1]) -if n>=seg[1].size()pick[3]=si+1 pick[4]=0 -else pick[4]=n end -end -return true -end -if d print("Player forward",pick[0],q["error"])end -return false -end - -return Plugin(def() -var s=timeline.getState() -var x=s["time"] -var e=s["epoch"] -if !R[0]|| e !=R[6]|| x- + Canonical firmware 0.12.11 Project Berry Player for one static player.show + SPM v3 manifest and deterministic mixed-ID player.NNN SEB slots. Every + Controller lands every record while Tracks remain per-ID authoring lanes and + Cues remain sparse. +tags: + - berry + - event-player + - network-storage + - player-show + - seb + - sparse-cues + - timeline +hardware: >- + Requires firmware 0.12.11 with timeline.at and SEB.land NetworkStorage source + support plus exact NetworkStorage version strings in Berry. Upload does not + pause or rewind, and no local ID list is required. +files: + - path: README.md + role: Setup, upload semantics, runtime behavior, and limits + language: markdown + - path: creator-kit.json + role: Fail-closed Spectoda Creator Kit selection + language: json + - path: player.be + role: Canonical copy-ready Project Berry Player + language: berry + - path: player-show-artifacts.json + role: Synthetic player.show SPM v3 and SEB golden corpus + language: json +related: + - seb-causal-scene-recall diff --git a/creator-kit/examples/player-show-global-sparse-cues/player.be b/creator-kit/examples/player-show-global-sparse-cues/player.be new file mode 100644 index 0000000..a20c319 --- /dev/null +++ b/creator-kit/examples/player-show-global-sparse-cues/player.be @@ -0,0 +1,171 @@ +(def() +# R: version, manifest, seg, cursor, time, epoch, poll, count, next. +var R=[nil,nil,0,0,-1,-1,0,0,0] +def name(i) +if i<10 return "player.00"+str(i) end +if i<100 return "player.0"+str(i) end +return "player."+str(i) +end +def same(z,p,v) +if type(v)!="string"||size(v)!=12 return false end +var b=bytes(v,-6) +for i:0..5 +if z.get(p+i)!=b[5-i] return false end +end +return true +end +def head(i,st,dur) +var n=name(i) var h=spectoda.getNetworkStorageData(n,0,12) +if h==nil||h.size()!=12||h.get(0)!=83||h.get(1)!=69|| +h.get(2)!=66||h.get(3)!=1||h.get(4)!=12||h.get(5)!=0|| +h.get(10)!=12||h.get(11)!=0 return nil end +var c=h.get(6,2) var t=h.get(8,2) +if c<1||c>340||st+t>=dur return nil end +var e=spectoda.getNetworkStorageData(n,11+c*12,2) +if e==nil||e.size()!=1 return nil end +return (c<<16)|t +end +def ready(L,z) +var n=z.get(6,2) var c=0 +for m:L +var k=m.find("name",nil) var v=m.find("version",nil) +if type(k)=="string"&&size(k)==10&&k[0..6]=="player." +var i=int(k[7..9]) +if ix return true end +var h=head(k,st,z.get(8,4)) +if h==nil return false end +var count=h>>16 var c=0 var u=x-st var next=0 var g=0 var o=-1 +while c32 q=32 end +var b=spectoda.getNetworkStorageData(name(k),12+c*12,q*12) +if b==nil||b.size()!=q*12 return false end +var j=0 +while ju next=v break end +if v!=o g=c+j o=v end +j+=1 +end +c+=j +if j=count R[2]=k+1 +else R[2]=k R[3]=c R[7]=count R[8]=next +end +return true +end +def load(L,v) +var z=spectoda.getNetworkStorageData("player.show") +if z==nil||z.size()<12||z.get(0)!=83||z.get(1)!=80|| +z.get(2)!=77||z.get(3)!=3||z.get(4)!=12||z.get(5)!=0 return false end +var n=z.get(6,2) var dur=z.get(8,4) +if n>1000||dur<1||dur>86400000||z.size()!=12+n*10||!ready(L,z) +return false end +var last=-1 var lastend=-1 +for i:0..n-1 +var st=z.get(18+i*10,4) +var h=head(i,st,dur) +if st<=last||st<=lastend||st>=dur||h==nil return false end +last=st lastend=st+(h&65535) +end +R[0]=v R[1]=z R[5]=-1 +return true +end +def forward(x,L) +var z=R[1] var n=z.get(6,2) +if R[2]>=n return true end +var st=z.get(18+R[2]*10,4) +if st+R[8]>x return true end +if R[7]==0 +var h=head(R[2],st,z.get(8,4)) +if h==nil return false end +R[7]=h>>16 +end +if !valid(R[2],z,L)return false end +var fn=name(R[2]) +var at=timeline.at(st) +if at==nil return true end +var u=x-st +if u>65535 u=65535 end +var q=SEB.land(fn,{"source":"networkStorage","at":at, +"cursor":R[3],"until":u}) +if !q["ok"]return false end +R[3]=q["cursor"] +if R[3]>=R[7] +R[2]+=1 R[3]=0 R[7]=0 R[8]=0 +else +var o=spectoda.getNetworkStorageData(fn,20+R[3]*12,2) +if o==nil||o.size()!=2 return false end +R[8]=o.get(0,2) +end +return true +end +Plugin(def() +var s=timeline.getState() var x=s["time"] var e=s["epoch"] +var L=nil +R[6]-=1 +if R[6]<=0 +R[6]=50 +L=spectoda.listNetworkStorageData() +var v=nil +for m:L +if m.find("name",nil)=="player.show" v=m.find("version",nil) break end +end +if v==nil R[1]=nil +elif R[1]==nil||v!=R[0] +if !load(L,v)R[1]=nil end +elif !ready(L,R[1])R[1]=nil +end +end +if R[1]==nil return end +if e!=R[5]||x.show` index and deterministic per-ID SEB -chains. The plugin follows the shared timeline; it owns no start, seek or loop -clock of its own. - -```berry -Player({"base": "player", "ids": [1], "debug": false}) -``` - -Configure `ids` with only the local Spectoda IDs 0–254 owned by this Controller. -ID255 is selected automatically when the Show Index contains it. NetworkStorage -still synchronizes every file to every Controller; the plugin opens only ID255 -and its configured local IDs. - -## Authoring and upload - -One Track owns one Spectoda ID. Its Event definitions are stable across the -Track, and every Cue supplies one complete type-correct value for every enabled -Event. SEB files contain those encoded Event values/records; landing them -produces normal EventStore updates. - -Use Studio's Event Player to Capture and edit Cues, then follow this workflow: - -1. pause the shared timeline; -2. upload the changed SEB files and `.show`; -3. wait for ordinary NetworkStorage propagation — App Controller readback does - not prove the whole mesh is ready; -4. rewind the shared timeline; -5. play. - -Hot update and simultaneous writers for the same base are unsupported in v1. -Old deterministic files no longer referenced by `.show` are ignored. - -## Runtime behavior and limits - -The plugin reads only the bounded Show Index into Berry. Each SEB payload is -read and atomically landed by the native `SEB.land(filename, -{"source":"networkStorage", ...})` path, without a Berry payload buffer. - -- one complete Cue contains 1–340 Event values; -- one SEB file is at most 4,092 bytes and spans at most 65,535 ms; -- `.show` is exact-EOF validated and at most 4,096 bytes; -- the base is 1–11 safe ASCII characters and generated names are at most 23 - bytes; -- one plugin turn performs at most one SEB contribution; -- pause performs no landing; resume derives a fresh timeline projection; -- seek, rewind, loop and the 24-hour timeline wrap reconcile the last complete - Cue at or before the target. - -The canonical compact source compiles to a 3,999-byte TNGL BERRY payload -(3,995 source bytes plus framing). Including the copy-ready `return Player(...)` -invocation produces 4,056 bytes, 40 bytes below the 4 KiB hard limit. The 1 KiB -target remains a stretch measurement, not a correctness promise. - -The short local names and positional runtime arrays in `player.be` are -intentional consequences of that hard limit. `R` holds loaded state, selected -Tracks, reconciliation state, the next Track, seek target, last timeline -position, and epoch. Each selected Track stores ID, records per complete Cue, -ordered segments, next segment, and next Cue. The parser verifies those tuple -shapes before playback, rejects chronological segment overlap, and stops a -Track after an already-committed native result disagrees with the Show Index; -it never retries that committed contribution. - -`player-show-artifacts.json` is a public synthetic three-Track corpus used by -the Berry smoke. It is illustrative compiled output; Project data remains the -authoring source of truth. - -For causal scene recall, use the separate `seb-causal-scene-recall` example. A -scene is an ordinary SEB file landed at its trigger Event time and is independent -of the timeline Player. diff --git a/data/v2/examples/player-show-complete-cue-tracks/example.yaml b/data/v2/examples/player-show-complete-cue-tracks/example.yaml deleted file mode 100644 index f61e32a..0000000 --- a/data/v2/examples/player-show-complete-cue-tracks/example.yaml +++ /dev/null @@ -1,35 +0,0 @@ -schemaVersion: example.v2 -slug: player-show-complete-cue-tracks -title: Complete-Cue Event Player from player.show -category: controller -summary: >- - Canonical firmware 0.12.11 Project Berry Player for one static player.show - index and deterministic per-ID SEB chains. It lands global ID255 plus explicit - local-ID Tracks from the shared timeline with complete Cue atomicity. -tags: - - berry - - event-player - - network-storage - - player-show - - seb - - timeline -hardware: >- - Requires firmware 0.12.11 with SEB.land NetworkStorage source support. - Configure only local IDs 0-254; ID255 is automatic. Pause before upload, wait - for ordinary propagation, rewind, and then play. -files: - - path: README.md - role: Setup, workflow, runtime behavior, and limits - language: markdown - - path: creator-kit.json - role: Fail-closed Spectoda Creator Kit selection - language: json - - path: player.be - role: Canonical copy-ready Project Berry Player - language: berry - - path: player-show-artifacts.json - role: Synthetic player.show and SEB golden corpus - language: json -related: - - seb-causal-scene-recall - - seb-segmented-id-track-player diff --git a/data/v2/examples/player-show-complete-cue-tracks/player-show-artifacts.json b/data/v2/examples/player-show-complete-cue-tracks/player-show-artifacts.json deleted file mode 100644 index b446ca0..0000000 --- a/data/v2/examples/player-show-complete-cue-tracks/player-show-artifacts.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "schemaVersion": "spectoda.player-show-fixture.v1", - "base": "demo", - "description": "Synthetic complete-Cue Tracks for ID1, ID2 and global ID255.", - "files": [ - { - "name": "demo.001.000.seb", - "hex": "534542010c00040088130c003f0000ffd2c33c03000001000b84d71700d02404000001013fff0000d2c33c03881301000b8c864700d0240488130101" - }, - { - "name": "demo.002.000.seb", - "hex": "534542010c00040088130c003f0000ffd2c33c03000002000b84d71700d02404000002013fff0000d2c33c03881302000b8c864700d0240488130201" - }, - { - "name": "demo.255.000.seb", - "hex": "534542010c00020088130c000b00000000d024040000ff000b105e5f00d024048813ff00" - }, - { - "name": "demo.show", - "hex": "5053480110000300102700000000000001000200010000000000000002000000000088130200020001000000000000000200000000008813ff00010001000000000000000200000000008813" - } - ] -} diff --git a/data/v2/examples/player-show-complete-cue-tracks/player.be b/data/v2/examples/player-show-complete-cue-tracks/player.be deleted file mode 100644 index 4157ba0..0000000 --- a/data/v2/examples/player-show-complete-cue-tracks/player.be +++ /dev/null @@ -1,186 +0,0 @@ - -# Compact by design: compiled Project Player must fit one 4 KiB TNGL block. -def Player(s) -var b=s.find("base","player") -var ids=s.find("ids",[]) -var d=s.find("debug",false) -if type(b)!="string" || size(b)<1 || size(b)>11 || -type(ids)!="instance" || classname(ids)!="list" -raise "value_error","invalid Player config" -end -for j:0..size(b)-1 -var c=b[j] -if !((c>="A"&&c<="Z")||(c>="a"&&c<="z")|| -(c>="0"&&c<="9")||c=="_"||c=="-") -raise "value_error","invalid Player base" -end -end -var w={} -for i:ids -if type(i)!="int" || i<0 || i>254 || w.find(i,false) -raise "value_error","local IDs must be unique 0..254" -end -w[i]=true -end -var R=[false,[],false,0,0,0,-1] -def wanted(i)return i==255 || w.find(i,false)end -def n3(n) -if n<10 return "00"+str(n)end -if n<100 return "0"+str(n)end -return str(n) -end -def file(i,n)return b+"."+n3(i)+"."+n3(n)+".seb" end - -def load(x) -var z=spectoda.getNetworkStorageData(b+".show") -if z==nil || z.size()<16 || z.size()>4096 || -z.get(0)!=80 || z.get(1)!=83 || z.get(2)!=72 || -z.get(3)!=1 || z.get(4)!=16 || z.get(5)!=0 || -z.get(12,4)!=0 -return false -end -var c=z.get(6,2) -var duration=z.get(8,4) -if c<1 || c>256 || duration<1 || duration>86400000 -return false -end -var tracks=[] -var p=16 -var previous_id=-1 -for ti:0..c - 1 -if p+8>z.size()return false end -var id=z.get(p) -var rpc=z.get(p+2,2) -var sc=z.get(p+4,2) -if z.get(p+1)!=0 || z.get(p+6,2)!=0 || -id<=previous_id || rpc<1 || rpc>340 || sc<1 -return false -end -previous_id=id -p+=8 -var segments=[] -var previous_base=-1 -var previous_end=-1 -for si:0..sc - 1 -if p+8>z.size()return false end -var base=z.get(p,4) -var cc=z.get(p+4,2) -if z.get(p+6,2)!=0 || cc<1 || rpc*cc>340 || -base<=previous_base || base>=duration || -(si>0 && base<=previous_end)|| -(si==0 && base !=0)|| p+8+cc*2>z.size() -return false -end -p+=8 -previous_base=base -var offsets=[] -var previous_offset=-1 -for ci:0..cc - 1 -var offset=z.get(p,2) -p+=2 -if(ci==0 && offset !=0)|| -(ci>0 && offset<=previous_offset)|| -base+offset>=duration -return false -end -previous_offset=offset -offsets.push(offset) -end -previous_end=base+previous_offset -segments.push([base,offsets]) -end -if wanted(id)tracks.push([id,rpc,segments,0,0])end -end -if p !=z.size()return false end -R[0]=true R[1]=tracks R[2]=false R[3]=0 R[4]=x -return true -end - -def advance(t,si,ci) -ci+=1 -if ci>=t[2][si][1].size()si+=1 ci=0 end -t[3]=si -t[4]=ci -end - -def reconcile() -if R[3]>=R[1].size()R[2]=true return true end -var t=R[1][R[3]] -var bs=0 -var bc=0 -var found=false -for si:0..t[2].size()- 1 -var seg=t[2][si] -for ci:0..seg[1].size()- 1 -if seg[0]+seg[1][ci]<=R[4] -bs=si bc=ci found=true -end -end -end -if !found return false end -var o=t[2][bs][1][bc] -var at=timeline.at(R[4]- o) -if at==nil return false end -var cur=bc*t[1] -var q=SEB.land(file(t[0],bs),{"source":"networkStorage", -"at":at,"cursor":cur,"until":o}) -if q["ok"] -if q["cursor"]==cur+t[1]advance(t,bs,bc) -else t[3]=t[2].size()end -R[3]+=1 return true -end -if d print("Player reconcile",t[0],q["error"])end -return false -end - -def forward(x) -var pick=nil -var due=nil -for t:R[1] -if t[3]65535 until=65535 end -var cur=pick[4]*pick[1] -var q=SEB.land(file(pick[0],si),{"source":"networkStorage", -"at":at,"cursor":cur,"until":until}) -if q["ok"] -if q["cursor"]<=cur || q["cursor"]%pick[1]!=0 -pick[3]=pick[2].size() pick[4]=0 -else -var n=int(q["cursor"]/ pick[1]) -if n>=seg[1].size()pick[3]=si+1 pick[4]=0 -else pick[4]=n end -end -return true -end -if d print("Player forward",pick[0],q["error"])end -return false -end - -return Plugin(def() -var s=timeline.getState() -var x=s["time"] -var e=s["epoch"] -if !R[0]|| e !=R[6]|| x- + Canonical firmware 0.12.11 Project Berry Player for one static player.show + SPM v3 manifest and deterministic mixed-ID player.NNN SEB slots. Every + Controller lands every record while Tracks remain per-ID authoring lanes and + Cues remain sparse. +tags: + - berry + - event-player + - network-storage + - player-show + - seb + - sparse-cues + - timeline +hardware: >- + Requires firmware 0.12.11 with timeline.at and SEB.land NetworkStorage source + support plus exact NetworkStorage version strings in Berry. Upload does not + pause or rewind, and no local ID list is required. +files: + - path: README.md + role: Setup, upload semantics, runtime behavior, and limits + language: markdown + - path: creator-kit.json + role: Fail-closed Spectoda Creator Kit selection + language: json + - path: player.be + role: Canonical copy-ready Project Berry Player + language: berry + - path: player-show-artifacts.json + role: Synthetic player.show SPM v3 and SEB golden corpus + language: json +related: + - seb-causal-scene-recall diff --git a/data/v2/examples/player-show-global-sparse-cues/player-show-artifacts.json b/data/v2/examples/player-show-global-sparse-cues/player-show-artifacts.json new file mode 100644 index 0000000..582c779 --- /dev/null +++ b/data/v2/examples/player-show-global-sparse-cues/player-show-artifacts.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": "spectoda.player-show-fixture.v3", + "description": "Synthetic global sparse Cue Groups across ID1, ID2 and global ID255.", + "files": [ + { + "name": "player.show", + "version": "0198a1b2d100", + "hex": "53504d030c000200a0860100d4c3b2a198010000000001d0b2a1980100000100" + }, + { + "name": "player.000", + "version": "0198a1b2c3d4", + "hex": "534542010c00070088130c003f221100d2c33c03000001000b000000cc713c14000001003f0000ffd2c33c030000ff000b105e5fcc713c14e803010014000000c8914802e80302003fff0000d2c33c03881301003f00ff00d2c33c038813ff00" + }, + { + "name": "player.001", + "version": "0198a1b2d001", + "hex": "534542010c00020070110c000b000000cc713c147011010028000000c891480270110200" + } + ] +} diff --git a/data/v2/examples/player-show-global-sparse-cues/player.be b/data/v2/examples/player-show-global-sparse-cues/player.be new file mode 100644 index 0000000..a20c319 --- /dev/null +++ b/data/v2/examples/player-show-global-sparse-cues/player.be @@ -0,0 +1,171 @@ +(def() +# R: version, manifest, seg, cursor, time, epoch, poll, count, next. +var R=[nil,nil,0,0,-1,-1,0,0,0] +def name(i) +if i<10 return "player.00"+str(i) end +if i<100 return "player.0"+str(i) end +return "player."+str(i) +end +def same(z,p,v) +if type(v)!="string"||size(v)!=12 return false end +var b=bytes(v,-6) +for i:0..5 +if z.get(p+i)!=b[5-i] return false end +end +return true +end +def head(i,st,dur) +var n=name(i) var h=spectoda.getNetworkStorageData(n,0,12) +if h==nil||h.size()!=12||h.get(0)!=83||h.get(1)!=69|| +h.get(2)!=66||h.get(3)!=1||h.get(4)!=12||h.get(5)!=0|| +h.get(10)!=12||h.get(11)!=0 return nil end +var c=h.get(6,2) var t=h.get(8,2) +if c<1||c>340||st+t>=dur return nil end +var e=spectoda.getNetworkStorageData(n,11+c*12,2) +if e==nil||e.size()!=1 return nil end +return (c<<16)|t +end +def ready(L,z) +var n=z.get(6,2) var c=0 +for m:L +var k=m.find("name",nil) var v=m.find("version",nil) +if type(k)=="string"&&size(k)==10&&k[0..6]=="player." +var i=int(k[7..9]) +if ix return true end +var h=head(k,st,z.get(8,4)) +if h==nil return false end +var count=h>>16 var c=0 var u=x-st var next=0 var g=0 var o=-1 +while c32 q=32 end +var b=spectoda.getNetworkStorageData(name(k),12+c*12,q*12) +if b==nil||b.size()!=q*12 return false end +var j=0 +while ju next=v break end +if v!=o g=c+j o=v end +j+=1 +end +c+=j +if j=count R[2]=k+1 +else R[2]=k R[3]=c R[7]=count R[8]=next +end +return true +end +def load(L,v) +var z=spectoda.getNetworkStorageData("player.show") +if z==nil||z.size()<12||z.get(0)!=83||z.get(1)!=80|| +z.get(2)!=77||z.get(3)!=3||z.get(4)!=12||z.get(5)!=0 return false end +var n=z.get(6,2) var dur=z.get(8,4) +if n>1000||dur<1||dur>86400000||z.size()!=12+n*10||!ready(L,z) +return false end +var last=-1 var lastend=-1 +for i:0..n-1 +var st=z.get(18+i*10,4) +var h=head(i,st,dur) +if st<=last||st<=lastend||st>=dur||h==nil return false end +last=st lastend=st+(h&65535) +end +R[0]=v R[1]=z R[5]=-1 +return true +end +def forward(x,L) +var z=R[1] var n=z.get(6,2) +if R[2]>=n return true end +var st=z.get(18+R[2]*10,4) +if st+R[8]>x return true end +if R[7]==0 +var h=head(R[2],st,z.get(8,4)) +if h==nil return false end +R[7]=h>>16 +end +if !valid(R[2],z,L)return false end +var fn=name(R[2]) +var at=timeline.at(st) +if at==nil return true end +var u=x-st +if u>65535 u=65535 end +var q=SEB.land(fn,{"source":"networkStorage","at":at, +"cursor":R[3],"until":u}) +if !q["ok"]return false end +R[3]=q["cursor"] +if R[3]>=R[7] +R[2]+=1 R[3]=0 R[7]=0 R[8]=0 +else +var o=spectoda.getNetworkStorageData(fn,20+R[3]*12,2) +if o==nil||o.size()!=2 return false end +R[8]=o.get(0,2) +end +return true +end +Plugin(def() +var s=timeline.getState() var x=s["time"] var e=s["epoch"] +var L=nil +R[6]-=1 +if R[6]<=0 +R[6]=50 +L=spectoda.listNetworkStorageData() +var v=nil +for m:L +if m.find("name",nil)=="player.show" v=m.find("version",nil) break end +end +if v==nil R[1]=nil +elif R[1]==nil||v!=R[0] +if !load(L,v)R[1]=nil end +elif !ready(L,R[1])R[1]=nil +end +end +if R[1]==nil return end +if e!=R[5]||x- - Drive one logical ID1 Track with the tracked event brigh from the shared - timeline using bounded compiled SEB segments. Project Berry owns segment - selection, local-millis projections, - pause behavior, cursors and explicit checkpoint reconstruction after seek. -tags: - - berry-plugin - - event-player - - eventstore - - networkstorage - - seb - - timeline - - checkpoint -hardware: >- - Requires exactly firmware 0.12.11 with timeline.getState(), timeline.at() - and final SEB v1. Install the same Berry source and binary NetworkStorage - artifacts on each Controller participating in distributed playback. The - example targets only synthetic brigh[ID1] and supports explicit timeline - entry/seek positions 0 ms and 500 ms. -files: - - path: README.md - role: readme - language: markdown - - path: segmented-track-player.be - role: copy-ready Project Berry player - language: berry - - path: track-artifacts.json - role: exact NetworkStorage segment and checkpoint bytes - language: json -related: - - seb-causal-scene-recall diff --git a/data/v2/examples/seb-segmented-id-track-player/segmented-track-player.be b/data/v2/examples/seb-segmented-id-track-player/segmented-track-player.be deleted file mode 100644 index ffffdac..0000000 --- a/data/v2/examples/seb-segmented-id-track-player/segmented-track-player.be +++ /dev/null @@ -1,158 +0,0 @@ -# Project-level timeline Player for firmware 0.12.11. -# Segment: [NetworkStorage file, timeline start ms, duration ms]. -# Checkpoint: [exact seek time, scene file, next segment, next source cursor]. -def TimelinePlayer(S) - var segments = S.find("segments", []) - var checkpoints = S.find("checkpoints", []) - var debug = S.find("debug", false) - var R = { - "segment": 0, - "cursor": 0, - "bytes": nil, - "at": nil, - "last_time": 0, - "last_epoch": 0, - "last_paused": true, - "ready": false - } - - def reset_segment(segment_index, cursor) - R["segment"] = segment_index - R["cursor"] = cursor - R["bytes"] = nil - R["at"] = nil - end - - def open_segment() - if R["segment"] < 0 || R["segment"] >= segments.size() - return false - end - - var segment = segments[R["segment"]] - var bytes_value = spectoda.getNetworkStorageData(segment[0]) - var at = timeline.at(segment[1]) - if bytes_value == nil || at == nil - if debug - print("TimelinePlayer cannot open", segment[0]) - end - return false - end - - R["bytes"] = bytes_value - R["at"] = at - return true - end - - def process_forward(target) - while R["segment"] < segments.size() - var segment = segments[R["segment"]] - if target < segment[1] - return true - end - if R["bytes"] == nil && !open_segment() - return false - end - - var until = target - segment[1] - if until > segment[2] - until = segment[2] - end - - var result = SEB.land(R["bytes"], { - "at": R["at"], - "cursor": R["cursor"], - "until": until - }) - if !result["ok"] - if debug - print("TimelinePlayer SEB error", result["error"]) - end - return false - end - R["cursor"] = result["cursor"] - - if !result["done"] - return true - end - reset_segment(R["segment"] + 1, 0) - end - return true - end - - def reconcile(target) - var index = 0 - while index < checkpoints.size() - var checkpoint = checkpoints[index] - if checkpoint[0] == target - var bytes_value = spectoda.getNetworkStorageData(checkpoint[1]) - var at = timeline.at(target) - if bytes_value == nil || at == nil - return false - end - - var result = SEB.land(bytes_value, {"at": at}) - if !result["ok"] || !result["done"] - if debug - print("TimelinePlayer checkpoint error", result["error"]) - end - return false - end - - reset_segment(checkpoint[2], checkpoint[3]) - return true - end - index += 1 - end - - if debug - print("TimelinePlayer missing checkpoint", target) - end - return false - end - - return Plugin(def() - var state = timeline.getState() - var time = state["time"] - var epoch = state["epoch"] - - if !R["ready"] - if !reconcile(time) - return - end - R["ready"] = true - elif epoch != R["last_epoch"] || time < R["last_time"] - if !reconcile(time) - return - end - # Resume keeps the epoch but establishes a new causal projection. - # Reopen timing for the remaining source suffix so its event clocks - # include the wall-clock interval spent paused. - elif R["last_paused"] && !state["paused"] - R["bytes"] = nil - R["at"] = nil - if !process_forward(time) - return - end - elif !state["paused"] - if !process_forward(time) - return - end - end - - R["last_time"] = time - R["last_epoch"] = epoch - R["last_paused"] = state["paused"] - end) -end - -TimelinePlayer({ - "segments": [ - ["brigh-000.seb", 0, 500], - ["brigh-001.seb", 500, 500] - ], - "checkpoints": [ - [0, "brigh-cp-000.seb", 0, 1], - [500, "brigh-cp-500.seb", 1, 1] - ], - "debug": false -}) diff --git a/data/v2/examples/seb-segmented-id-track-player/track-artifacts.json b/data/v2/examples/seb-segmented-id-track-player/track-artifacts.json deleted file mode 100644 index ed1c4d3..0000000 --- a/data/v2/examples/seb-segmented-id-track-player/track-artifacts.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "schema": "spectoda.seb.artifact-set.v1", - "firmware": "0.12.11", - "encoding": "hex", - "track": { - "identifier": "brigh", - "id": 1 - }, - "files": [ - { - "filename": "brigh-000.seb", - "timelineStartMs": 0, - "durationMs": 500, - "hex": "534542010c000300f4010c000b000000c8914802000001000b8c8647c8914802640001020b105e5fc8914802f4010100", - "values": [ - { "offsetMs": 0, "percentage": 0, "priority": 0 }, - { "offsetMs": 100, "percentage": 75, "priority": 2 }, - { "offsetMs": 500, "percentage": 100, "priority": 0 } - ] - }, - { - "filename": "brigh-001.seb", - "timelineStartMs": 500, - "durationMs": 500, - "hex": "534542010c000300f4010c000b84d717c8914802000001000b08af2fc8914802fa0001000b8c8647c8914802f4010100", - "values": [ - { "offsetMs": 0, "percentage": 25, "priority": 0 }, - { "offsetMs": 250, "percentage": 50, "priority": 0 }, - { "offsetMs": 500, "percentage": 75, "priority": 0 } - ] - }, - { - "filename": "brigh-cp-000.seb", - "seekTimelineMs": 0, - "nextSegment": 0, - "nextCursor": 1, - "hex": "534542010c00010000000c000b000000c891480200000100", - "state": { "percentage": 0 } - }, - { - "filename": "brigh-cp-500.seb", - "seekTimelineMs": 500, - "nextSegment": 1, - "nextCursor": 1, - "hex": "534542010c00010000000c000b84d717c891480200000100", - "state": { "percentage": 25 } - } - ] -} diff --git a/scripts/creator-kit/build.mjs b/scripts/creator-kit/build.mjs index 6403371..3dc4300 100644 --- a/scripts/creator-kit/build.mjs +++ b/scripts/creator-kit/build.mjs @@ -3,7 +3,7 @@ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; import path from "node:path"; export const BUNDLE_NAME = "Spectoda Creator Kit"; -export const BUNDLE_VERSION = "0.1.0-rc.3"; +export const BUNDLE_VERSION = "0.1.0-rc.4"; export const EXPORTER_VERSION = "examples-creator-kit-packager/1.1.0"; export const sha256 = (value) => createHash("sha256").update(value).digest("hex"); diff --git a/scripts/creator-kit/creator-kit.test.mjs b/scripts/creator-kit/creator-kit.test.mjs index 7610a11..3a5da73 100644 --- a/scripts/creator-kit/creator-kit.test.mjs +++ b/scripts/creator-kit/creator-kit.test.mjs @@ -37,7 +37,7 @@ async function replaceDocumentAndRehash(bundle, content) { test("validates the committed licensed Creator Kit snapshot", async () => { const result = await validateBundle(BUNDLE_ROOT); - assert.equal(result.bundleVersion, "0.1.0-rc.3"); + assert.equal(result.bundleVersion, "0.1.0-rc.4"); assert.equal(result.documentCount, 7); assert.equal(result.assetCount, 2); assert.equal(result.exampleCount, 1); @@ -145,9 +145,9 @@ test("stable-channel promotion remains separately human-gated", async () => { await assert.rejects( promoteStableChannel({ bundleRoot: BUNDLE_ROOT, - version: "0.1.0-rc.3", + version: "0.1.0-rc.4", digest: validation.checksumDigest, - releaseUrl: "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/Spectoda/examples/releases/tag/creator-kit-v0.1.0-rc.3", + releaseUrl: "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/Spectoda/examples/releases/tag/creator-kit-v0.1.0-rc.4", outputPath: path.join(root, "stable-channel.json"), confirmation: "", }), diff --git a/scripts/creator-kit/import-documentation-snapshot.mjs b/scripts/creator-kit/import-documentation-snapshot.mjs index 2e2bbda..518f51d 100644 --- a/scripts/creator-kit/import-documentation-snapshot.mjs +++ b/scripts/creator-kit/import-documentation-snapshot.mjs @@ -9,11 +9,13 @@ import { BUNDLE_VERSION, bundleFiles, createChecksums, sha256, verifyChecksums } const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const DEFAULT_OUTPUT = path.join(ROOT, "creator-kit"); const TEMPLATE_ROOT = path.join(ROOT, "creator-kit-templates"); -const EXAMPLE_ROOT = path.join(ROOT, "data/v2/examples/player-show-complete-cue-tracks"); -const EXAMPLE_DESTINATION = "examples/player-show-complete-cue-tracks"; +const EXAMPLE_ID = "player-show-global-sparse-cues"; +const EXAMPLE_ROOT = path.join(ROOT, `data/v2/examples/${EXAMPLE_ID}`); +const EXAMPLE_DESTINATION = `examples/${EXAMPLE_ID}`; +const DOCUMENTATION_BUNDLE_VERSION = "0.1.0-rc.3"; const DOCUMENTATION_COMMIT = "6f6051686fe556a85318c7dd529ac061ab48c38d"; const DOCUMENTATION_CHECKSUM_DIGEST = "d4e2958303e3fd34dcb5e5c50a8a5c6a0266068ec2f15d88406a2a33bcb1f054"; -const EXAMPLES_COMMIT = "667b238349cc635792f0e42130a08a4bde701fdf"; +const EXAMPLES_COMMIT = "e4be6dd07814c1dc8816ffc0e4ea518532e411ec"; const FIRMWARE_COMMIT = "51a8d6337d968b47f563bf2decb8f7404d93c27a"; const EXAMPLE_FILES = ["README.md", "example.yaml", "player.be"]; @@ -88,7 +90,7 @@ async function extendSchemas(staging) { type: "object", required: ["id", "path", "title", "topic", "audience", "license", "stability", "compatibility", "source", "files"], properties: { - id: { const: "player-show-complete-cue-tracks" }, + id: { const: EXAMPLE_ID }, path: { const: EXAMPLE_DESTINATION }, title: { type: "string", minLength: 1 }, topic: { const: "studio-event-player" }, @@ -161,7 +163,7 @@ async function extendSchemas(staging) { type: "object", required: ["id", "selectionSourcePath", "source", "files"], properties: { - id: { const: "player-show-complete-cue-tracks" }, + id: { const: EXAMPLE_ID }, selectionSourcePath: { type: "string", minLength: 1 }, source: exampleSourceSchema, files: { type: "array", minItems: 3, maxItems: 3, items: { type: "string", minLength: 1 } }, @@ -191,7 +193,7 @@ export async function importDocumentationSnapshot({ documentationBundle, outputD const licenses = await readJson(path.join(input, "licenses.json")); const stable = await readJson(path.join(input, "stable-channel.json")); if ( - bundle.version !== BUNDLE_VERSION || + bundle.version !== DOCUMENTATION_BUNDLE_VERSION || bundle.contentScope !== "licensed-documentation" || sourceLock.repository !== "Spectoda/documentation" || sourceLock.commit !== DOCUMENTATION_COMMIT || @@ -209,7 +211,7 @@ export async function importDocumentationSnapshot({ documentationBundle, outputD throw new Error("Controller Config assets do not match the authorized firmware provenance"); } - const selectionPath = "data/v2/examples/player-show-complete-cue-tracks/creator-kit.json"; + const selectionPath = `data/v2/examples/${EXAMPLE_ID}/creator-kit.json`; const selectionBytes = await readFile(path.join(ROOT, selectionPath)); if (!selectionBytes.equals(gitBytes(EXAMPLES_COMMIT, selectionPath))) throw new Error("Event Player selection differs from the locked Examples commit"); const exampleSelection = JSON.parse(selectionBytes.toString("utf8")); @@ -230,7 +232,7 @@ export async function importDocumentationSnapshot({ documentationBundle, outputD await rm(path.join(staging, "checksums.sha256"), { force: true }); const exampleFiles = []; for (const name of EXAMPLE_FILES) { - const sourcePath = `data/v2/examples/player-show-complete-cue-tracks/${name}`; + const sourcePath = `data/v2/examples/${EXAMPLE_ID}/${name}`; const bytes = await readFile(path.join(ROOT, sourcePath)); if (!bytes.equals(gitBytes(EXAMPLES_COMMIT, sourcePath))) throw new Error(`${sourcePath} differs from the locked Examples commit`); const bundlePath = `${EXAMPLE_DESTINATION}/${name}`; @@ -249,10 +251,12 @@ export async function importDocumentationSnapshot({ documentationBundle, outputD : null; if (!title) throw new Error("Event Player README has no title"); + bundle.version = BUNDLE_VERSION; bundle.contentScope = "licensed-documentation-with-public-examples"; + manifest.bundleVersion = BUNDLE_VERSION; manifest.contentScope = bundle.contentScope; manifest.examples = [{ - id: "player-show-complete-cue-tracks", + id: EXAMPLE_ID, path: EXAMPLE_DESTINATION, title, topic: exampleSelection.topic, @@ -291,7 +295,7 @@ export async function importDocumentationSnapshot({ documentationBundle, outputD ]; const selection = await readJson(path.join(staging, "selection.json")); selection.selectedExamples = [{ - id: "player-show-complete-cue-tracks", + id: EXAMPLE_ID, selectionSourcePath: selectionPath, source: { repository: "Spectoda/examples", commit: EXAMPLES_COMMIT }, files: exampleFiles.map((file) => file.sourcePath), @@ -304,7 +308,12 @@ export async function importDocumentationSnapshot({ documentationBundle, outputD await writeJson(path.join(staging, "source-lock.json"), sourceLock); await writeJson(path.join(staging, "selection.json"), selection); await writeJson(path.join(staging, "licenses.json"), licenses); - await writeFile(path.join(staging, "README.md"), (await readFile(path.join(TEMPLATE_ROOT, "README.md"), "utf8")).replaceAll("{{BUNDLE_VERSION}}", BUNDLE_VERSION)); + await writeFile( + path.join(staging, "README.md"), + (await readFile(path.join(TEMPLATE_ROOT, "README.md"), "utf8")) + .replaceAll("{{BUNDLE_VERSION}}", BUNDLE_VERSION) + .replaceAll("{{EXAMPLES_COMMIT}}", EXAMPLES_COMMIT), + ); await writeFile(path.join(staging, "AGENTS.md"), await readFile(path.join(TEMPLATE_ROOT, "AGENTS.md"))); await writeFile(path.join(staging, "RELEASE_NOTES.md"), await readFile(path.join(TEMPLATE_ROOT, "RELEASE_NOTES.md"))); await mkdir(path.join(staging, "LICENSES"), { recursive: true }); diff --git a/scripts/creator-kit/validate.mjs b/scripts/creator-kit/validate.mjs index 266ea65..f587bec 100644 --- a/scripts/creator-kit/validate.mjs +++ b/scripts/creator-kit/validate.mjs @@ -8,9 +8,10 @@ import { BUNDLE_VERSION, bundleFiles, relativePosix, sha256, verifyChecksums } f const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const DEFAULT_BUNDLE = path.join(ROOT, "creator-kit"); +const EXAMPLE_ID = "player-show-global-sparse-cues"; const DOCUMENTATION_COMMIT = "6f6051686fe556a85318c7dd529ac061ab48c38d"; const DOCUMENTATION_CHECKSUM_DIGEST = "d4e2958303e3fd34dcb5e5c50a8a5c6a0266068ec2f15d88406a2a33bcb1f054"; -const EXAMPLES_COMMIT = "667b238349cc635792f0e42130a08a4bde701fdf"; +const EXAMPLES_COMMIT = "e4be6dd07814c1dc8816ffc0e4ea518532e411ec"; const FIRMWARE_COMMIT = "51a8d6337d968b47f563bf2decb8f7404d93c27a"; const EXPECTED_DOCUMENTS = [ "en/pro-vyrobce-a-tvurce/controller-config/ethernet.md", @@ -43,9 +44,9 @@ const EXPECTED_ASSETS = [ }, ]; const EXPECTED_EXAMPLE_FILES = [ - "examples/player-show-complete-cue-tracks/README.md", - "examples/player-show-complete-cue-tracks/example.yaml", - "examples/player-show-complete-cue-tracks/player.be", + `examples/${EXAMPLE_ID}/README.md`, + `examples/${EXAMPLE_ID}/example.yaml`, + `examples/${EXAMPLE_ID}/player.be`, ]; const EXPECTED_SCHEMAS = [ "creator-kit-bundle.v1.schema.json", @@ -322,7 +323,7 @@ export async function validateBundle(bundleRoot = DEFAULT_BUNDLE) { check(manifest.examples?.length === 1, "Creator Kit must contain exactly one reviewed public example"); const example = manifest.examples[0]; - check(example.id === "player-show-complete-cue-tracks" && example.license === "MIT", "Event Player example identity/license is invalid"); + check(example.id === EXAMPLE_ID && example.license === "MIT", "Event Player example identity/license is invalid"); check(example.source?.repository === "Spectoda/examples" && example.source.commit === EXAMPLES_COMMIT, "Event Player source commit is invalid"); check(example.compatibility?.firmware === "0.12.11", "Event Player firmware compatibility is invalid"); check(JSON.stringify(example.files.map((file) => file.path).sort()) === JSON.stringify([...EXPECTED_EXAMPLE_FILES].sort()), "Event Player file selection is invalid"); @@ -337,14 +338,15 @@ export async function validateBundle(bundleRoot = DEFAULT_BUNDLE) { const lock = exampleLocks.get(file.sourcePath); check(lock?.role === "file" && lock.bundlePath === file.path && lock.bundleSha256 === file.sha256 && lock.license === "MIT", `${file.sourcePath} source lock is invalid`); } - const selectionPath = "data/v2/examples/player-show-complete-cue-tracks/creator-kit.json"; + const selectionPath = `data/v2/examples/${EXAMPLE_ID}/creator-kit.json`; const selectionLock = exampleLocks.get(selectionPath); check(selectionLock?.role === "selection" && selectionLock.bundlePath === null && selectionLock.license === "MIT", "Event Player selection lock is invalid"); check(selectionLock.sourceSha256 === sha256(gitBytes(EXAMPLES_COMMIT, selectionPath)), "Event Player selection hash is invalid"); check(selection.selectedExamples?.[0]?.source?.commit === EXAMPLES_COMMIT, "Event Player selection provenance is invalid"); - const plugin = await readFile(path.join(root, "examples/player-show-complete-cue-tracks/player.be"), "utf8"); + const plugin = await readFile(path.join(root, `examples/${EXAMPLE_ID}/player.be`), "utf8"); check(plugin.includes("timeline.at") && !plugin.includes("timeline.toMillis"), "Event Player plugin does not use the reviewed timeline.at API"); + check(plugin.includes('"source":"networkStorage"') && !plugin.includes('find("ids"'), "Event Player plugin must land the global stream without local ID selection"); check((await readFile(path.join(root, "LICENSES/MIT.txt"))).equals(await readFile(path.join(ROOT, "LICENSE"))), "Bundled MIT license differs from the repository license"); const ccLicense = await readFile(path.join(root, "LICENSES/CC-BY-4.0.md"), "utf8"); check(ccLicense.includes("https://creativecommons.org/licenses/by/4.0/") && /trademarks are\s+not licensed/iu.test(ccLicense), "CC BY 4.0 attribution/trademark notice is incomplete"); diff --git a/tests/player-show-size.mjs b/tests/player-show-size.mjs index 525eb22..acb8620 100644 --- a/tests/player-show-size.mjs +++ b/tests/player-show-size.mjs @@ -2,21 +2,17 @@ import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' const sourcePath = - 'data/v2/examples/player-show-complete-cue-tracks/player.be' + 'data/v2/examples/player-show-global-sparse-cues/player.be' const sourceBytes = Buffer.byteLength(readFileSync(sourcePath, 'utf8')) -const invocation = - 'return Player({"base":"player","ids":[1],"debug":false})' // TnglCompiler encodes BERRY_SCRIPT flag + uint16 length + source bytes and // the surrounding TNGL stream adds END_OF_TNGL_BYTES. const tnglBytes = 1 + 2 + sourceBytes + 1 -const projectTnglBytes = tnglBytes + 1 + Buffer.byteLength(invocation) -assert.ok(tnglBytes <= 4096, `Player TNGL is ${tnglBytes} bytes (>4096)`) assert.ok( - projectTnglBytes <= 4096, - `Player plus canonical invocation is ${projectTnglBytes} bytes (>4096)`, + tnglBytes <= 4096, + `Directly injectable Player TNGL is ${tnglBytes} bytes (>4096)`, ) console.log( - `player.show plugin size: ${sourceBytes} source bytes, ${tnglBytes} TNGL bytes; canonical Project is ${projectTnglBytes} bytes, ${4096 - projectTnglBytes} bytes below the hard limit and ${projectTnglBytes - 1024} above the stretch target`, + `player.show plugin size: ${sourceBytes} source bytes, ${tnglBytes} TNGL bytes; ${4096 - tnglBytes} bytes below the hard limit and ${tnglBytes - 1024} above the stretch target`, ) diff --git a/tests/player-show-stub.be b/tests/player-show-stub.be index b886b45..e7e1833 100644 --- a/tests/player-show-stub.be +++ b/tests/player-show-stub.be @@ -1,318 +1,348 @@ -# Host-Berry lifecycle smoke for the public static Player plugin. It uses the -# same synthetic player.show + SEB corpus shipped with the example and small -# firmware API stubs; no Controller or customer data is involved. +# Host-Berry lifecycle smoke for player.show SPM v3. import json import string -# Declare firmware globals before class/function compilation in host Berry. -spectoda = nil -timeline = nil -SEB = nil -stub_state = nil -Player = nil +spectoda=nil +timeline=nil +SEB=nil +stub_state=nil +Player=nil class SpectodaStub - var files, reads - + var files,reads,metadata_reads,max_range def init(fixture) - self.files = {} - self.reads = [] - for item : fixture["files"] - self.files[item["name"]] = bytes(item["hex"]) + self.files={} + self.reads=[] + self.metadata_reads=0 + self.max_range=0 + for item:fixture["files"] + self.files[item["name"]]={"bytes":bytes(item["hex"]), + "version":item["version"]} end end - - def getNetworkStorageData(name) - self.reads.push(name) - return self.files.find(name, nil) + def listNetworkStorageData() + self.metadata_reads+=1 + var result=[] + for name:self.files.keys() + var file=self.files[name] + if file!=nil + result.push({"name":name,"version":file["version"], + "fingerprint":file["bytes"].asstring()}) + end + end + return result + end + def getNetworkStorageData(name,*args) + if self.reads!=nil self.reads.push([name,args]) end + var file=self.files.find(name,nil) + if file==nil return nil end + var data=file["bytes"] + if args.size()==0 return data end + assert(args.size()==2) + var offset=args[0] + var length=args[1] + if length>self.max_range self.max_range=length end + if offset>data.size() return nil end + if offset==data.size() return bytes() end + var available=data.size()-offset + if length>available length=available end + return data[offset..offset+length-1] end end class TimelineStub - var state, local_now, projection_failures - + var state,local_now,projection_failures def init() - self.state = {"time": 0, "paused": true, "epoch": 1} - self.local_now = 1000 - self.projection_failures = 0 + self.state={"time":0,"paused":true,"epoch":1} + self.local_now=1000 + self.projection_failures=0 end - - def getState() - return self.state - end - + def getState() return self.state end def at(position) - if self.projection_failures > 0 - self.projection_failures -= 1 + if self.projection_failures>0 + self.projection_failures-=1 return nil end - return self.local_now + position - self.state["time"] + return self.local_now+position-self.state["time"] end end class SebStub - var applied, calls, cursor_delta - + var applied,calls def init() - self.applied = [] - self.calls = [] - self.cursor_delta = 0 + self.applied=[] + self.calls=[] end - - def land(name, args) - var original_cursor = args.find("cursor", 0) - self.calls.push({"name": name, "args": args}) - if args.find("source", nil) != "networkStorage" - return {"ok": false, "cursor": original_cursor, "consumed": 0, - "done": false, "error": "source"} + def land(name,args) + var original=args.find("cursor",0) + self.calls.push({"name":name,"args":args}) + if args.find("source",nil)!="networkStorage" + return {"ok":false,"cursor":original,"consumed":0,"error":"source"} + end + var file=spectoda.files.find(name,nil) + if file==nil return {"ok":false,"cursor":original,"consumed":0,"error":"file"} end + var data=file["bytes"] + var records=data.get(6,2) + if data.size()!=12+records*12 + return {"ok":false,"cursor":original,"consumed":0,"error":"length"} end - var data = spectoda.files.find(name, nil) - if data == nil || data.size() < 12 || data.get(0) != 83 || - data.get(1) != 69 || data.get(2) != 66 - return {"ok": false, "cursor": original_cursor, "consumed": 0, - "done": false, "error": "file"} + var previous=-1 + for i:0..records-1 + var offset=data.get(20+i*12,2) + if offsetdata.get(8,2) + return {"ok":false,"cursor":original,"consumed":0,"error":"offset"} + end + previous=offset end - var records = data.get(6, 2) - var cursor = original_cursor - var until = args.find("until", 0) - var consumed = 0 - while cursor < records && data.get(12 + cursor * 12 + 8, 2) <= until - var p = 12 + cursor * 12 - self.applied.push({ - "value": data.get(p, 4), - "offset": data.get(p + 8, 2), - "id": data.get(p + 10), - "at": args["at"] - }) - cursor += 1 - consumed += 1 + var cursor=original + var consumed=0 + var until=args.find("until",0) + while cursor= 0) -assert(string.find(player_source, "timeline.toMillis") < 0) -assert(string.find(player_source, '"source":"networkStorage"') >= 0) -var duplicate_rejected = false -try - Player({"base": "demo", "ids": [5, 5]}) -except .. - duplicate_rejected = true -end -assert(duplicate_rejected) -var unsafe_base_rejected = false -try - Player({"base": "bad.name", "ids": [1]}) -except .. - unsafe_base_rejected = true -end -assert(unsafe_base_rejected) -var non_ascii_base_rejected = false -try - Player({"base": "přehrávač", "ids": [1]}) -except .. - non_ascii_base_rejected = true -end -assert(non_ascii_base_rejected) -print("player.show static complete-Cue lifecycle smoke: PASS") +# A nil causal projection leaves cursor and EventStore untouched and retries. +boot(fixture()) +timeline.projection_failures=1 +stub_state["callback"]() +assert(SEB.calls.size()==0) +stub_state["callback"]() +assert(SEB.calls.size()==1) + +# A realistic 50-segment show retains only its compact manifest. Berry never +# requests an entire SEB while activating or seeking; the largest range is the +# 32-record (384-byte) seek window. +boot(many_segments(50)) +spectoda.reads=nil +stub_state["callback"]() +assert(spectoda.max_range<=384) + +# Public API shape and one-block budget. +var f=open("data/v2/examples/player-show-global-sparse-cues/player.be","r") +var source=f.read() +f.close() +assert(size(source)<=4093) +assert(string.find(source,"timeline.at")>=0) +assert(string.find(source,"timeline.toMillis")<0) +assert(string.find(source,'"source":"networkStorage"')>=0) +assert(string.find(source,'find("ids"')<0) +assert(string.find(source,'find("base"')<0) +assert(string.find(source,"def Player")<0) +print("player.show SPM v3 mixed-ID sparse-Cue lifecycle smoke: PASS")