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")