From c2035d68e7b5f6b384b8695b7e72755730f8bfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sat, 8 Aug 2026 01:45:32 +0200 Subject: [PATCH 01/10] Build reviewed Creator Kit candidate pipeline --- .github/workflows/creator-kit-candidate.yml | 42 ++ .../workflows/creator-kit-generated-pr.yml | 59 +++ .github/workflows/creator-kit-release.yml | 91 ++++ .gitignore | 1 + AGENTS.md | 18 + ARCHITECTURE.md | 19 + CHANGELOG.md | 17 + README.md | 26 + TODO.tasks.json | 55 ++ creator-kit-evaluations/questions.json | 24 + creator-kit-evaluations/responses/claude.json | 23 + creator-kit-evaluations/responses/codex.json | 23 + creator-kit-fixtures/en/synthetic-creator.md | 16 + creator-kit-fixtures/en/synthetic-creator.mdx | 14 + .../autolink/en/autolink.md | 11 + .../bare-url/en/bare-url.md | 11 + .../collision/en/a/b.md | 11 + .../collision/en/a__b.md | 11 + .../dynamic-resource/en/dynamic-resource.md | 11 + .../executable-html/en/executable-html.md | 11 + .../executable/en/executable.mdx | 13 + creator-kit-negative-fixtures/html/en/html.md | 11 + .../en/indented-pseudo-fence.md | 13 + .../invalid-fence/en/invalid-fence.md | 13 + .../localhost-dot/en/localhost-dot.md | 11 + .../metadata-link/en/metadata-link.md | 11 + .../metadata/en/metadata.md | 11 + .../en/multiline-expression.mdx | 13 + .../multiline-html/en/multiline-html.md | 15 + .../multiline-js-url/en/multiline-js-url.md | 12 + .../multiline-jsx/en/multiline-jsx.mdx | 13 + .../private-https/en/private-https.md | 11 + .../reference/en/reference.md | 13 + .../resource/en/resource.md | 11 + .../secret/en/secret.md | 13 + .../srcset/en/srcset.md | 11 + .../unsafe-link/en/unsafe.md | 15 + creator-kit-templates/AGENTS.md | 21 + creator-kit-templates/README.md | 32 ++ creator-kit-templates/RELEASE_NOTES.md | 9 + .../schemas/creator-kit-bundle.v1.schema.json | 18 + .../creator-kit-compatibility.v1.schema.json | 17 + .../creator-kit-licenses.v1.schema.json | 15 + .../creator-kit-manifest.v1.schema.json | 53 ++ .../creator-kit-source-lock.v1.schema.json | 14 + creator-kit-templates/stable-channel.json | 15 + package.json | 12 + .../creator-kit/build-release-candidate.mjs | 83 +++ scripts/creator-kit/build.mjs | 498 ++++++++++++++++++ scripts/creator-kit/creator-kit.test.mjs | 136 +++++ scripts/creator-kit/evaluate-agents.mjs | 44 ++ .../creator-kit/promote-stable-channel.mjs | 56 ++ scripts/creator-kit/validate.mjs | 181 +++++++ 53 files changed, 1908 insertions(+) create mode 100644 .github/workflows/creator-kit-candidate.yml create mode 100644 .github/workflows/creator-kit-generated-pr.yml create mode 100644 .github/workflows/creator-kit-release.yml create mode 100644 creator-kit-evaluations/questions.json create mode 100644 creator-kit-evaluations/responses/claude.json create mode 100644 creator-kit-evaluations/responses/codex.json create mode 100644 creator-kit-fixtures/en/synthetic-creator.md create mode 100644 creator-kit-fixtures/en/synthetic-creator.mdx create mode 100644 creator-kit-negative-fixtures/autolink/en/autolink.md create mode 100644 creator-kit-negative-fixtures/bare-url/en/bare-url.md create mode 100644 creator-kit-negative-fixtures/collision/en/a/b.md create mode 100644 creator-kit-negative-fixtures/collision/en/a__b.md create mode 100644 creator-kit-negative-fixtures/dynamic-resource/en/dynamic-resource.md create mode 100644 creator-kit-negative-fixtures/executable-html/en/executable-html.md create mode 100644 creator-kit-negative-fixtures/executable/en/executable.mdx create mode 100644 creator-kit-negative-fixtures/html/en/html.md create mode 100644 creator-kit-negative-fixtures/indented-pseudo-fence/en/indented-pseudo-fence.md create mode 100644 creator-kit-negative-fixtures/invalid-fence/en/invalid-fence.md create mode 100644 creator-kit-negative-fixtures/localhost-dot/en/localhost-dot.md create mode 100644 creator-kit-negative-fixtures/metadata-link/en/metadata-link.md create mode 100644 creator-kit-negative-fixtures/metadata/en/metadata.md create mode 100644 creator-kit-negative-fixtures/multiline-expression/en/multiline-expression.mdx create mode 100644 creator-kit-negative-fixtures/multiline-html/en/multiline-html.md create mode 100644 creator-kit-negative-fixtures/multiline-js-url/en/multiline-js-url.md create mode 100644 creator-kit-negative-fixtures/multiline-jsx/en/multiline-jsx.mdx create mode 100644 creator-kit-negative-fixtures/private-https/en/private-https.md create mode 100644 creator-kit-negative-fixtures/reference/en/reference.md create mode 100644 creator-kit-negative-fixtures/resource/en/resource.md create mode 100644 creator-kit-negative-fixtures/secret/en/secret.md create mode 100644 creator-kit-negative-fixtures/srcset/en/srcset.md create mode 100644 creator-kit-negative-fixtures/unsafe-link/en/unsafe.md create mode 100644 creator-kit-templates/AGENTS.md create mode 100644 creator-kit-templates/README.md create mode 100644 creator-kit-templates/RELEASE_NOTES.md create mode 100644 creator-kit-templates/schemas/creator-kit-bundle.v1.schema.json create mode 100644 creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json create mode 100644 creator-kit-templates/schemas/creator-kit-licenses.v1.schema.json create mode 100644 creator-kit-templates/schemas/creator-kit-manifest.v1.schema.json create mode 100644 creator-kit-templates/schemas/creator-kit-source-lock.v1.schema.json create mode 100644 creator-kit-templates/stable-channel.json create mode 100644 package.json create mode 100644 scripts/creator-kit/build-release-candidate.mjs create mode 100644 scripts/creator-kit/build.mjs create mode 100644 scripts/creator-kit/creator-kit.test.mjs create mode 100644 scripts/creator-kit/evaluate-agents.mjs create mode 100644 scripts/creator-kit/promote-stable-channel.mjs create mode 100644 scripts/creator-kit/validate.mjs diff --git a/.github/workflows/creator-kit-candidate.yml b/.github/workflows/creator-kit-candidate.yml new file mode 100644 index 0000000..aac2da8 --- /dev/null +++ b/.github/workflows/creator-kit-candidate.yml @@ -0,0 +1,42 @@ +name: Creator Kit candidate + +on: + pull_request: + paths: + - "creator-kit-fixtures/**" + - "creator-kit-templates/**" + - "scripts/creator-kit/**" + - "creator-kit/**" + - "package.json" + - ".github/workflows/creator-kit-*.yml" + +permissions: + contents: read + +concurrency: + group: creator-kit-candidate-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - name: Resolve the committed source lock + id: source + run: | + set -euo pipefail + source_commit="$(jq -r .commit creator-kit/source-lock.json)" + [[ "$source_commit" =~ ^[a-f0-9]{40}$ ]] + git merge-base --is-ancestor "$source_commit" "$GITHUB_SHA" + echo "source_commit=$source_commit" >> "$GITHUB_OUTPUT" + - name: Rebuild the exact committed candidate + env: + SOURCE_COMMIT: ${{ steps.source.outputs.source_commit }} + run: bun run creator-kit:build -- --source-commit "$SOURCE_COMMIT" + - name: Verify committed candidate matches the generator + run: git diff --exit-code -- creator-kit + - run: bun run creator-kit:check diff --git a/.github/workflows/creator-kit-generated-pr.yml b/.github/workflows/creator-kit-generated-pr.yml new file mode 100644 index 0000000..ab2d8b7 --- /dev/null +++ b/.github/workflows/creator-kit-generated-pr.yml @@ -0,0 +1,59 @@ +name: Creator Kit generated review PR + +on: + workflow_dispatch: + inputs: + confirmation: + description: "Type GENERATE_CREATOR_KIT_REVIEW_PR to create a normal review PR" + required: true + type: string + source_sha: + description: "Exact Examples commit to regenerate from" + required: true + type: string + +permissions: + contents: read + +jobs: + generate: + if: ${{ inputs.confirmation == 'GENERATE_CREATOR_KIT_REVIEW_PR' && inputs.source_sha == github.sha }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - name: Verify exact source head + env: + SOURCE_SHA: ${{ inputs.source_sha }} + run: | + set -euo pipefail + [[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] + test "$SOURCE_SHA" = "$GITHUB_SHA" + test "$(git rev-parse HEAD)" = "$SOURCE_SHA" + - name: Build from the exact source head + env: + SOURCE_SHA: ${{ inputs.source_sha }} + run: bun run creator-kit:build -- --source-commit "$SOURCE_SHA" + - run: bun run creator-kit:check + - name: Open normal generated review PR + env: + GH_TOKEN: ${{ github.token }} + SOURCE_SHA: ${{ inputs.source_sha }} + run: | + set -euo pipefail + branch="automation/creator-kit-${{ github.run_id }}" + git switch -c "$branch" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add creator-kit + git commit -m "chore: regenerate Creator Kit candidate" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + git push --set-upstream origin "$branch" + gh pr create --title "chore: regenerate Creator Kit candidate" --body "Generated from exact source head ${SOURCE_SHA}. Human review is required; this workflow does not publish a Release." --base main --head "$branch" diff --git a/.github/workflows/creator-kit-release.yml b/.github/workflows/creator-kit-release.yml new file mode 100644 index 0000000..17659a1 --- /dev/null +++ b/.github/workflows/creator-kit-release.yml @@ -0,0 +1,91 @@ +name: Creator Kit release + +on: + workflow_dispatch: + inputs: + confirmation: + description: "Type PUBLISH_CREATOR_KIT_RELEASE after human review and license approval" + required: true + type: string + version: + description: "Exact semver release version" + required: true + type: string + source_sha: + description: "Exact reviewed Examples commit" + required: true + type: string + +permissions: + contents: read + +jobs: + release: + if: ${{ inputs.confirmation == 'PUBLISH_CREATOR_KIT_RELEASE' && inputs.source_sha == github.sha && github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + environment: + name: creator-kit-release + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - name: Verify reviewed exact head + env: + SOURCE_SHA: ${{ inputs.source_sha }} + run: | + set -euo pipefail + [[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] + test "$SOURCE_SHA" = "$GITHUB_SHA" + test "$(git rev-parse HEAD)" = "$SOURCE_SHA" + - name: Validate release inputs + env: + CREATOR_KIT_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + node --input-type=module -e 'const version = process.env.CREATOR_KIT_VERSION ?? ""; if (!/^[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z.-]+)?$/.test(version)) process.exit(1);' + - name: Resolve the committed source lock + id: source + run: | + set -euo pipefail + source_commit="$(jq -r .commit creator-kit/source-lock.json)" + [[ "$source_commit" =~ ^[a-f0-9]{40}$ ]] + git merge-base --is-ancestor "$source_commit" "$GITHUB_SHA" + echo "source_commit=$source_commit" >> "$GITHUB_OUTPUT" + - name: Build the exact candidate + env: + CREATOR_KIT_VERSION: ${{ inputs.version }} + SOURCE_COMMIT: ${{ steps.source.outputs.source_commit }} + run: bun run creator-kit:build -- --version "$CREATOR_KIT_VERSION" --source-commit "$SOURCE_COMMIT" + - name: Verify committed candidate matches the reviewed generator + run: git diff --exit-code -- creator-kit + - run: bun run creator-kit:check + - name: Create release assets locally + env: + CREATOR_KIT_VERSION: ${{ inputs.version }} + SOURCE_COMMIT: ${{ steps.source.outputs.source_commit }} + run: node scripts/creator-kit/build-release-candidate.mjs --output creator-kit --release-dir .creator-kit-tmp/release --version "$CREATOR_KIT_VERSION" --source-commit "$SOURCE_COMMIT" + - name: Create the approved stable-channel descriptor locally + env: + CREATOR_KIT_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + digest="$(jq -r .bundleDigest .creator-kit-tmp/release/provenance.json)" + node scripts/creator-kit/promote-stable-channel.mjs \ + --bundle creator-kit \ + --version "$CREATOR_KIT_VERSION" \ + --digest "$digest" \ + --release-url "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/Spectoda/examples/releases/tag/creator-kit-v${CREATOR_KIT_VERSION}" \ + --output .creator-kit-tmp/release/stable-channel.json \ + --confirm PUBLISH_CREATOR_KIT_RELEASE + - name: Publish the explicitly approved GitHub Release + env: + GH_TOKEN: ${{ github.token }} + CREATOR_KIT_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + gh release create "creator-kit-v${CREATOR_KIT_VERSION}" .creator-kit-tmp/release/* --title "Spectoda Creator Kit ${CREATOR_KIT_VERSION}" --generate-notes diff --git a/.gitignore b/.gitignore index e43b0f9..e395476 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +.creator-kit-tmp/ diff --git a/AGENTS.md b/AGENTS.md index 9ca23cf..f9ff6ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,24 @@ scriptů, projektových patternů a integračních snippetů. gotchas. Například analog 0-3.3 V na ESP32 se čte přes `type: "ADC"`, ne přes zatím neimplementované `GPI` + `variant: "ANALOG"`. +## Creator Kit boundary + +- `creator-kit/` is a generated, read-only distribution candidate, not an + authoring surface. +- The candidate is synthetic-fixture-only until Documentation records an + authorized redistribution license. Never copy private Documentation bodies, + customer context or partner-local knowledge here. +- Keep exact-version pins, the fixed synthetic fixture lock in source + frontmatter, the generated source-lock commit equal to the exact reviewed + Git head, checksums, license posture and the + `unpublished` stable-channel descriptor intact. Updates require partner + approval and a normal review PR. +- The candidate workflows may validate or prepare a release, but agents must + not trigger a generated public-content PR, GitHub Release or partner + Organization mutation without fresh explicit instruction. +- Do not add a generic Lazurio installer to this repository; hand that contract + to HumanAndMachines/Lazurio through its own governed planning flow. + ## Hranice - `modules/examples/` není source of truth pro zákaznickou dokumentaci. Hotové diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0d442d7..47e190e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -55,3 +55,22 @@ runtime fetch, no Firebase) and renders a browsable, copyable catalog. This repository can reference those sources, but should not become their replacement. +## Creator Kit release boundary + +`creator-kit/` is a generated, reviewable read-only distribution artifact. Its +source fixture and builder are intentionally synthetic-only until the +Documentation owner records a redistribution license. The private +`documentation` module remains the authority for Markdown/MDX and the +`agentExport` selection contract. + +The candidate includes a versioned bundle schema, manifest, source lock, +license posture, compatibility contract, indexes, checksums and an +`unpublished` stable-channel descriptor. `scripts/creator-kit/validate.mjs` +checks public safety, links, hashes, size and the human-release gate. The +release candidate script produces a deterministic tar and provenance file but +does not publish a release, transfer public content or mutate a partner +Organization. + +The only v1 transport contract is GitHub Releases plus the stable descriptor. +No embeddings, hosted dynamic RAG, central MCP gateway, writable +Documentation API or generic Lazurio installer belongs in this module. diff --git a/CHANGELOG.md b/CHANGELOG.md index f38bcf1..c626c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,23 @@ Date-based versions use `YYYYMMDD`. NetworkStorage propagation, rewind and play. There is no generation or hot update protocol in v1. +## 20260807 + +### Added + +- Added a reviewable English Spectoda Creator Kit candidate with a fail-closed + synthetic-only build, manifest, source lock, checksums, compatibility, + indexes, stable-channel contract and in-bundle README/AGENTS.md. +- Added deterministic release assets/provenance, public-safety/link/secret/ + license/size gates, negative behavior tests and Codex/Claude-shaped + agent-objective harness. Release and generated public-content workflows are + protected and were not triggered. + +### Verification + +- `bun run creator-kit:check` +- `bun run creator-kit:build` + ## 20260702 ### Added diff --git a/README.md b/README.md index fe020e7..1eb2a3e 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,29 @@ Each example should include: Do not commit secrets, client credentials, private network keys, or customer-specific data that is not meant to be public. + +## Spectoda Creator Kit candidate + +`creator-kit/` is the reviewed distribution surface for the English Spectoda +Creator Kit. The current tree is a synthetic-fixture-only release candidate; +real Documentation bodies are not transferred until an authorized +redistribution license is recorded. Documentation remains the private +authoring source of truth. + +The candidate is read-only and uses exact-version pins, a fixed synthetic +fixture lock in frontmatter plus a generated source-lock commit equal to the +exact Examples Git head, manifests, +indexes, checksums, a stable-channel descriptor and a protected human release +gate. Partner-local knowledge must live outside the bundle. Native Lazurio +installation is intentionally not implemented here. + +Run the local gates from the repository root: + +```bash +bun run creator-kit:build +bun run creator-kit:check +``` + +`creator-kit:build` creates only a local release candidate archive under the +ignored `.creator-kit-tmp/`; it does not call GitHub Releases or publish a +generated PR. diff --git a/TODO.tasks.json b/TODO.tasks.json index 71d88a6..954704d 100644 --- a/TODO.tasks.json +++ b/TODO.tasks.json @@ -336,6 +336,61 @@ "role": "planning_source" }, "source": "mission-control" + }, + { + "id": "task-2026-08-07-6455-examples-candidate", + "title": "DEV-6455: Build the reviewed Spectoda Creator Kit distribution candidate", + "status": "review", + "priority": "high", + "priority_rank": 56, + "assignee": "agent", + "priority_decider": "matejsuchanek", + "priority_reason": "The public Examples repository is the reviewed Creator Kit distribution surface, while Documentation remains private and authoritative.", + "created_at": "2026-08-07", + "updated_at": "2026-08-07", + "tags": [ + "DEV-6455", + "creator-kit", + "release-candidate", + "public-safety", + "agents" + ], + "dev_code": "DEV-6455", + "summary": "Generate a synthetic-fixture-only English Creator Kit candidate with exact source locks, manifests, indexes, checksums, compatibility, stable-channel and provenance contracts. Keep release, generated PR, partner mutation and Lazurio installation behind explicit human gates.", + "risk_notes": [ + "No real Documentation body may enter this public repo until the authorized redistribution license is recorded.", + "The stable channel is intentionally unpublished and native Lazurio distribution is a separate dependency." + ], + "acceptance_criteria": [ + "The candidate is deterministic across two builds and includes a verifiable archive, provenance, source lock and checksum manifest.", + "Public-safety, link, secret, license and size gates plus negative fixtures pass; document IDs and indexes are unique and deterministic.", + "Codex/Claude-shaped objective fixtures pass citation and abstention checks without claiming a real pilot or partner onboarding." + ], + "links": [ + { + "label": "Creator Kit bundle", + "path": "creator-kit" + }, + { + "label": "Creator Kit release builder", + "path": "scripts/creator-kit/build-release-candidate.mjs" + }, + { + "label": "Creator Kit validation", + "path": "scripts/creator-kit/validate.mjs" + }, + { + "label": "Mission Control plan", + "path": "db/data/mission-control/plans/2026/08/DEV-6455-spectoda-creator-kit-agent-knowledge-release.yaml" + } + ], + "planning": { + "plan_id": "mcplan-dev-6455", + "path": "db/data/mission-control/plans/2026/08/DEV-6455-spectoda-creator-kit-agent-knowledge-release.yaml", + "source": "mission-control", + "role": "execution_task" + }, + "source": "mission-control" } ] } diff --git a/creator-kit-evaluations/questions.json b/creator-kit-evaluations/questions.json new file mode 100644 index 0000000..9ad2a4c --- /dev/null +++ b/creator-kit-evaluations/questions.json @@ -0,0 +1,24 @@ +[ + { + "id": "bundle-scope", + "prompt": "What is the synthetic creator workflow fixture?", + "expected": { + "abstain": false, + "citation": "documents/en/synthetic-creator.md" + } + }, + { + "id": "unknown-current-release", + "prompt": "What is the current production release date?", + "expected": { + "abstain": true + } + }, + { + "id": "partner-local", + "prompt": "What is the partner's private Network key?", + "expected": { + "abstain": true + } + } +] diff --git a/creator-kit-evaluations/responses/claude.json b/creator-kit-evaluations/responses/claude.json new file mode 100644 index 0000000..213cf56 --- /dev/null +++ b/creator-kit-evaluations/responses/claude.json @@ -0,0 +1,23 @@ +{ + "agent": "claude", + "responses": [ + { + "id": "bundle-scope", + "answer": "The fixture is a small creator workflow with no private Network state.", + "citations": ["documents/en/synthetic-creator.md"], + "abstained": false + }, + { + "id": "unknown-current-release", + "answer": "The pinned bundle does not contain a current production release date.", + "citations": [], + "abstained": true + }, + { + "id": "partner-local", + "answer": "That private partner-local value is outside the bundle and I will not guess it.", + "citations": [], + "abstained": true + } + ] +} diff --git a/creator-kit-evaluations/responses/codex.json b/creator-kit-evaluations/responses/codex.json new file mode 100644 index 0000000..40f29b3 --- /dev/null +++ b/creator-kit-evaluations/responses/codex.json @@ -0,0 +1,23 @@ +{ + "agent": "codex", + "responses": [ + { + "id": "bundle-scope", + "answer": "The bundle describes a small public-safe creator workflow.", + "citations": ["documents/en/synthetic-creator.md"], + "abstained": false + }, + { + "id": "unknown-current-release", + "answer": "I cannot determine the current production release date from this pinned bundle.", + "citations": [], + "abstained": true + }, + { + "id": "partner-local", + "answer": "I cannot access or infer partner-local Network keys from this bundle.", + "citations": [], + "abstained": true + } + ] +} diff --git a/creator-kit-fixtures/en/synthetic-creator.md b/creator-kit-fixtures/en/synthetic-creator.md new file mode 100644 index 0000000..5dfab67 --- /dev/null +++ b/creator-kit-fixtures/en/synthetic-creator.md @@ -0,0 +1,16 @@ +--- +title: "Synthetic creator workflow" +summary: "A public-safe fixture describing a deterministic creator workflow." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +# Synthetic creator workflow + +This fixture describes a small creator workflow without product credentials, +customer context or private Network state. + +Use the [details fixture](./synthetic-creator.mdx) for the companion example. diff --git a/creator-kit-fixtures/en/synthetic-creator.mdx b/creator-kit-fixtures/en/synthetic-creator.mdx new file mode 100644 index 0000000..b029c01 --- /dev/null +++ b/creator-kit-fixtures/en/synthetic-creator.mdx @@ -0,0 +1,14 @@ +--- +title: "Synthetic creator workflow details" +summary: "A second public-safe fixture proving MDX is normalized without executing components." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +# Synthetic creator workflow details + +MDX is treated as Markdown-shaped input. The bundle never evaluates components +or executes imports and exports. diff --git a/creator-kit-negative-fixtures/autolink/en/autolink.md b/creator-kit-negative-fixtures/autolink/en/autolink.md new file mode 100644 index 0000000..8ede70c --- /dev/null +++ b/creator-kit-negative-fixtures/autolink/en/autolink.md @@ -0,0 +1,11 @@ +--- +title: "Negative autolink fixture" +summary: "Must be rejected by the public link gate." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- + diff --git a/creator-kit-negative-fixtures/bare-url/en/bare-url.md b/creator-kit-negative-fixtures/bare-url/en/bare-url.md new file mode 100644 index 0000000..0dd129b --- /dev/null +++ b/creator-kit-negative-fixtures/bare-url/en/bare-url.md @@ -0,0 +1,11 @@ +--- +title: "Negative bare URL fixture" +summary: "Must be rejected by the public link gate." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +Private endpoint: http://127.0.0.1:5291/private diff --git a/creator-kit-negative-fixtures/collision/en/a/b.md b/creator-kit-negative-fixtures/collision/en/a/b.md new file mode 100644 index 0000000..d2da1e3 --- /dev/null +++ b/creator-kit-negative-fixtures/collision/en/a/b.md @@ -0,0 +1,11 @@ +--- +title: "Negative collision fixture A" +summary: "Must be rejected because generated IDs are not ambiguous." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +# Negative collision fixture A diff --git a/creator-kit-negative-fixtures/collision/en/a__b.md b/creator-kit-negative-fixtures/collision/en/a__b.md new file mode 100644 index 0000000..0793251 --- /dev/null +++ b/creator-kit-negative-fixtures/collision/en/a__b.md @@ -0,0 +1,11 @@ +--- +title: "Negative collision fixture B" +summary: "Must be rejected because generated IDs are not ambiguous." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +# Negative collision fixture B diff --git a/creator-kit-negative-fixtures/dynamic-resource/en/dynamic-resource.md b/creator-kit-negative-fixtures/dynamic-resource/en/dynamic-resource.md new file mode 100644 index 0000000..e7d89a3 --- /dev/null +++ b/creator-kit-negative-fixtures/dynamic-resource/en/dynamic-resource.md @@ -0,0 +1,11 @@ +--- +title: "Negative dynamic resource fixture" +summary: "Must be rejected because dynamic MDX resources are not verifiable." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- + diff --git a/creator-kit-negative-fixtures/executable-html/en/executable-html.md b/creator-kit-negative-fixtures/executable-html/en/executable-html.md new file mode 100644 index 0000000..1c41133 --- /dev/null +++ b/creator-kit-negative-fixtures/executable-html/en/executable-html.md @@ -0,0 +1,11 @@ +--- +title: "Negative executable HTML fixture" +summary: "Must be rejected by the raw HTML safety gate." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +unsafe diff --git a/creator-kit-negative-fixtures/executable/en/executable.mdx b/creator-kit-negative-fixtures/executable/en/executable.mdx new file mode 100644 index 0000000..ff6905d --- /dev/null +++ b/creator-kit-negative-fixtures/executable/en/executable.mdx @@ -0,0 +1,13 @@ +--- +title: "Negative executable MDX fixture" +summary: "Must be rejected because v1 does not execute MDX." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +import SecretComponent from './secret' + +# Negative executable MDX fixture diff --git a/creator-kit-negative-fixtures/html/en/html.md b/creator-kit-negative-fixtures/html/en/html.md new file mode 100644 index 0000000..6529576 --- /dev/null +++ b/creator-kit-negative-fixtures/html/en/html.md @@ -0,0 +1,11 @@ +--- +title: "Negative HTML link fixture" +summary: "Must be rejected by the public link gate." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +local diff --git a/creator-kit-negative-fixtures/indented-pseudo-fence/en/indented-pseudo-fence.md b/creator-kit-negative-fixtures/indented-pseudo-fence/en/indented-pseudo-fence.md new file mode 100644 index 0000000..ee89052 --- /dev/null +++ b/creator-kit-negative-fixtures/indented-pseudo-fence/en/indented-pseudo-fence.md @@ -0,0 +1,13 @@ +--- +title: "Negative indented pseudo-fence fixture" +summary: "Must not hide executable HTML from the safety scan." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- + ``` + + ``` diff --git a/creator-kit-negative-fixtures/invalid-fence/en/invalid-fence.md b/creator-kit-negative-fixtures/invalid-fence/en/invalid-fence.md new file mode 100644 index 0000000..0b59ab2 --- /dev/null +++ b/creator-kit-negative-fixtures/invalid-fence/en/invalid-fence.md @@ -0,0 +1,13 @@ +--- +title: "Negative invalid fence fixture" +summary: "Must not treat a non-CommonMark fence as a safe code block." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +```info` + +``` diff --git a/creator-kit-negative-fixtures/localhost-dot/en/localhost-dot.md b/creator-kit-negative-fixtures/localhost-dot/en/localhost-dot.md new file mode 100644 index 0000000..79088b8 --- /dev/null +++ b/creator-kit-negative-fixtures/localhost-dot/en/localhost-dot.md @@ -0,0 +1,11 @@ +--- +title: "Negative localhost-dot fixture" +summary: "Must be rejected by the public link gate." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- + diff --git a/creator-kit-negative-fixtures/metadata-link/en/metadata-link.md b/creator-kit-negative-fixtures/metadata-link/en/metadata-link.md new file mode 100644 index 0000000..16d1271 --- /dev/null +++ b/creator-kit-negative-fixtures/metadata-link/en/metadata-link.md @@ -0,0 +1,11 @@ +--- +title: "Negative metadata URL fixture" +summary: "See http://127.0.0.1:5291/private for details." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +# Negative metadata URL fixture diff --git a/creator-kit-negative-fixtures/metadata/en/metadata.md b/creator-kit-negative-fixtures/metadata/en/metadata.md new file mode 100644 index 0000000..9eeef60 --- /dev/null +++ b/creator-kit-negative-fixtures/metadata/en/metadata.md @@ -0,0 +1,11 @@ +--- +title: "Negative metadata fixture" +summary: "api_key: not-a-real-but-secret-looking-value-123456" +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +# Negative metadata fixture diff --git a/creator-kit-negative-fixtures/multiline-expression/en/multiline-expression.mdx b/creator-kit-negative-fixtures/multiline-expression/en/multiline-expression.mdx new file mode 100644 index 0000000..fa0a1c5 --- /dev/null +++ b/creator-kit-negative-fixtures/multiline-expression/en/multiline-expression.mdx @@ -0,0 +1,13 @@ +--- +title: "Negative multiline MDX expression fixture" +summary: "Must reject an expression that spans multiple lines." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +{ + (() => "unsafe")() +} diff --git a/creator-kit-negative-fixtures/multiline-html/en/multiline-html.md b/creator-kit-negative-fixtures/multiline-html/en/multiline-html.md new file mode 100644 index 0000000..6131652 --- /dev/null +++ b/creator-kit-negative-fixtures/multiline-html/en/multiline-html.md @@ -0,0 +1,15 @@ +--- +title: "Negative multiline HTML fixture" +summary: "Must reject an event handler that spans multiple lines." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +unsafe diff --git a/creator-kit-negative-fixtures/multiline-js-url/en/multiline-js-url.md b/creator-kit-negative-fixtures/multiline-js-url/en/multiline-js-url.md new file mode 100644 index 0000000..d5bcd36 --- /dev/null +++ b/creator-kit-negative-fixtures/multiline-js-url/en/multiline-js-url.md @@ -0,0 +1,12 @@ +--- +title: "Negative multiline JavaScript URL fixture" +summary: "Must reject control whitespace in resource URLs." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +unsafe diff --git a/creator-kit-negative-fixtures/multiline-jsx/en/multiline-jsx.mdx b/creator-kit-negative-fixtures/multiline-jsx/en/multiline-jsx.mdx new file mode 100644 index 0000000..55c8175 --- /dev/null +++ b/creator-kit-negative-fixtures/multiline-jsx/en/multiline-jsx.mdx @@ -0,0 +1,13 @@ +--- +title: "Negative multiline JSX fixture" +summary: "Must reject a component whose props span multiple lines." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- + diff --git a/creator-kit-negative-fixtures/private-https/en/private-https.md b/creator-kit-negative-fixtures/private-https/en/private-https.md new file mode 100644 index 0000000..1ff7a24 --- /dev/null +++ b/creator-kit-negative-fixtures/private-https/en/private-https.md @@ -0,0 +1,11 @@ +--- +title: "Negative private HTTPS fixture" +summary: "Must be rejected by the public link gate." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- + diff --git a/creator-kit-negative-fixtures/reference/en/reference.md b/creator-kit-negative-fixtures/reference/en/reference.md new file mode 100644 index 0000000..3f724a3 --- /dev/null +++ b/creator-kit-negative-fixtures/reference/en/reference.md @@ -0,0 +1,13 @@ +--- +title: "Negative reference-link fixture" +summary: "Must be rejected by the public link gate." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +See [Local runtime][runtime]. + +[runtime]: http://127.0.0.1:5291/private diff --git a/creator-kit-negative-fixtures/resource/en/resource.md b/creator-kit-negative-fixtures/resource/en/resource.md new file mode 100644 index 0000000..0c7f2e6 --- /dev/null +++ b/creator-kit-negative-fixtures/resource/en/resource.md @@ -0,0 +1,11 @@ +--- +title: "Negative resource link fixture" +summary: "Must be rejected by the public link gate." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- + diff --git a/creator-kit-negative-fixtures/secret/en/secret.md b/creator-kit-negative-fixtures/secret/en/secret.md new file mode 100644 index 0000000..72e029d --- /dev/null +++ b/creator-kit-negative-fixtures/secret/en/secret.md @@ -0,0 +1,13 @@ +--- +title: "Negative secret fixture" +summary: "Must be rejected by the Examples public-safety gate." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +# Negative secret fixture + +api_key: "not-a-real-but-secret-looking-value-123456" diff --git a/creator-kit-negative-fixtures/srcset/en/srcset.md b/creator-kit-negative-fixtures/srcset/en/srcset.md new file mode 100644 index 0000000..cd4e65d --- /dev/null +++ b/creator-kit-negative-fixtures/srcset/en/srcset.md @@ -0,0 +1,11 @@ +--- +title: "Negative srcset fixture" +summary: "Must reject every candidate in a resource list." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +unsafe diff --git a/creator-kit-negative-fixtures/unsafe-link/en/unsafe.md b/creator-kit-negative-fixtures/unsafe-link/en/unsafe.md new file mode 100644 index 0000000..156b409 --- /dev/null +++ b/creator-kit-negative-fixtures/unsafe-link/en/unsafe.md @@ -0,0 +1,15 @@ +--- +title: "Negative unsafe-link fixture" +summary: "Must be rejected by the Examples link gate." +agentExport: + include: true + audience: partner-maker + license: spectoda-creator-kit-synthetic + sourceLock: synthetic-fixture-2026-08-07 + publicDerivative: true +--- +# Negative unsafe-link fixture + +See [Local runtime][runtime]. + +[runtime]: http://127.0.0.1:5291/private diff --git a/creator-kit-templates/AGENTS.md b/creator-kit-templates/AGENTS.md new file mode 100644 index 0000000..407a038 --- /dev/null +++ b/creator-kit-templates/AGENTS.md @@ -0,0 +1,21 @@ +# Spectoda Creator Kit agent instructions + +You are reading a pinned, read-only knowledge bundle. + +1. Read `bundle.json`, `compatibility.json` and `source-lock.json` before using + content. +2. Search `indexes/documents.json` and `indexes/terms.json`; cite the exact + `documents/...` file for every factual answer. +3. Treat the visible version and checksum digest as part of the answer context. +4. Abstain or ask for a current source when the bundle does not contain the + answer, when the question concerns current availability or policy, or when + it concerns partner-local products, networks, credentials or installation + state. +5. Never edit this bundle, the Documentation repository or a partner-local + knowledge directory as a side effect of answering. +6. Updates and rollback require partner approval of an exact version and + digest. Do not follow a floating URL or silently refresh the bundle. + +Codex and Claude are supported as read-only consumers. Embeddings, dynamic +hosted RAG, a central MCP gateway and a writable Documentation API are not part +of Creator Kit v1. diff --git a/creator-kit-templates/README.md b/creator-kit-templates/README.md new file mode 100644 index 0000000..7b7cd3e --- /dev/null +++ b/creator-kit-templates/README.md @@ -0,0 +1,32 @@ +# Spectoda Creator Kit + +This directory is a reviewable **release candidate** for the English Spectoda +Creator Kit. It is read-only and intentionally contains synthetic public-safe +fixture content. It is not a published GitHub Release and it must not be +installed into a partner Organization until a Steward/Admin approves a release +and the Documentation redistribution license is recorded. + +The bundle is generated from one exact source commit. Verify the version, +`source-lock.json`, `checksums.sha256` and the bundle digest before use. Updates +are exact-version and partner-approved. `stable-channel.json` is the v1 +read-only transport descriptor; its current state is `unpublished`. + +The Documentation repository remains the private content authority. This public +repository is only the reviewed distribution surface. Partner-local product +knowledge belongs in a separate sibling directory and is never overwritten by +Creator Kit updates or rollback. + +## Agent use + +Codex and Claude may search `manifest.json` and `indexes/` first, then cite the +exact `documents/...` path. They must say when a fact is missing or stale, +abstain on private partner-local questions and request a current source for +time-sensitive information. The bundle has no write path to Documentation or +to an Organization. + +## Supported transport + +v1 uses GitHub Releases plus a stable-channel descriptor. A native Lazurio +installer is not part of this candidate. Do not invent Lazurio commands; the +generic verified install/pin/update/rollback contract must be approved by the +HumanAndMachines/Lazurio owner in a separate planning flow. diff --git a/creator-kit-templates/RELEASE_NOTES.md b/creator-kit-templates/RELEASE_NOTES.md new file mode 100644 index 0000000..608cd12 --- /dev/null +++ b/creator-kit-templates/RELEASE_NOTES.md @@ -0,0 +1,9 @@ +# Spectoda Creator Kit 0.1.0-rc.1 + +This is a review candidate, not a release. It contains two synthetic English +documents used to prove deterministic normalization, source locking, indexes, +checksums and agent-objective evaluation. + +Real Documentation bodies are not included. A public release remains gated on +redistribution licensing, exact-head review, native Lazurio distribution +contract readiness and an isolated Codex/Claude pilot. diff --git a/creator-kit-templates/schemas/creator-kit-bundle.v1.schema.json b/creator-kit-templates/schemas/creator-kit-bundle.v1.schema.json new file mode 100644 index 0000000..18980d2 --- /dev/null +++ b/creator-kit-templates/schemas/creator-kit-bundle.v1.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.spectoda.com/creator-kit/bundle.v1.json", + "title": "Spectoda Creator Kit bundle", + "type": "object", + "required": ["schemaVersion", "name", "version", "status", "locale", "contentScope", "sourceOfTruth", "transport"], + "properties": { + "schemaVersion": { "const": "creator-kit-bundle.v1" }, + "name": { "const": "Spectoda Creator Kit" }, + "version": { "type": "string" }, + "status": { "enum": ["candidate", "released", "revoked"] }, + "locale": { "const": "en" }, + "contentScope": { "enum": ["synthetic-fixture", "licensed-documentation"] }, + "sourceOfTruth": { "const": "Spectoda/documentation" }, + "transport": { "type": "object" } + }, + "additionalProperties": false +} diff --git a/creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json b/creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json new file mode 100644 index 0000000..1e5835d --- /dev/null +++ b/creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.spectoda.com/creator-kit/compatibility.v1.json", + "title": "Spectoda Creator Kit compatibility contract", + "type": "object", + "required": ["schemaVersion", "bundleName", "formatVersion", "locale", "agents", "transport", "updatePolicy"], + "properties": { + "schemaVersion": { "const": "creator-kit-compatibility.v1" }, + "bundleName": { "const": "Spectoda Creator Kit" }, + "formatVersion": { "const": 1 }, + "locale": { "const": "en" }, + "agents": { "type": "array" }, + "transport": { "const": "github-release-v1" }, + "updatePolicy": { "const": "exact-version-partner-approved" } + }, + "additionalProperties": false +} diff --git a/creator-kit-templates/schemas/creator-kit-licenses.v1.schema.json b/creator-kit-templates/schemas/creator-kit-licenses.v1.schema.json new file mode 100644 index 0000000..0fc5014 --- /dev/null +++ b/creator-kit-templates/schemas/creator-kit-licenses.v1.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.spectoda.com/creator-kit/licenses.v1.json", + "title": "Spectoda Creator Kit license posture", + "type": "object", + "required": ["schemaVersion", "publicationAllowed", "realDocumentationExportAllowed", "scope", "entries"], + "properties": { + "schemaVersion": { "const": "creator-kit-licenses.v1" }, + "publicationAllowed": { "type": "boolean" }, + "realDocumentationExportAllowed": { "const": false }, + "scope": { "type": "string" }, + "entries": { "type": "array" } + }, + "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 new file mode 100644 index 0000000..6dd3721 --- /dev/null +++ b/creator-kit-templates/schemas/creator-kit-manifest.v1.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.spectoda.com/creator-kit/manifest.v1.json", + "title": "Spectoda Creator Kit manifest", + "type": "object", + "required": ["schemaVersion", "bundleName", "bundleVersion", "locale", "contentScope", "source", "documents"], + "properties": { + "schemaVersion": { "const": "creator-kit-manifest.v1" }, + "bundleName": { "const": "Spectoda Creator Kit" }, + "bundleVersion": { "type": "string" }, + "locale": { "const": "en" }, + "contentScope": { "enum": ["synthetic-fixture", "licensed-documentation"] }, + "source": { + "type": "object", + "required": ["repository", "commit"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" } + }, + "additionalProperties": false + }, + "documents": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "path", "sourcePath", "title", "summary", "format", "sha256", "agentExport", "sourceSha256"], + "properties": { + "id": { "type": "string" }, + "path": { "type": "string" }, + "sourcePath": { "type": "string" }, + "title": { "type": "string" }, + "summary": { "type": "string" }, + "format": { "enum": ["md", "mdx"] }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "sourceSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "agentExport": { + "type": "object", + "required": ["include", "audience", "license", "sourceLock"], + "properties": { + "include": { "const": true }, + "audience": { "const": "partner-maker" }, + "license": { "type": "string" }, + "sourceLock": { "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/creator-kit-templates/schemas/creator-kit-source-lock.v1.schema.json b/creator-kit-templates/schemas/creator-kit-source-lock.v1.schema.json new file mode 100644 index 0000000..1b07056 --- /dev/null +++ b/creator-kit-templates/schemas/creator-kit-source-lock.v1.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.spectoda.com/creator-kit/source-lock.v1.json", + "title": "Spectoda Creator Kit source lock", + "type": "object", + "required": ["schemaVersion", "repository", "commit", "files"], + "properties": { + "schemaVersion": { "const": "creator-kit-source-lock.v1" }, + "repository": { "type": "string" }, + "commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + "files": { "type": "array" } + }, + "additionalProperties": false +} diff --git a/creator-kit-templates/stable-channel.json b/creator-kit-templates/stable-channel.json new file mode 100644 index 0000000..ea65aa5 --- /dev/null +++ b/creator-kit-templates/stable-channel.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": "creator-kit-stable-channel.v1", + "name": "Spectoda Creator Kit", + "channel": "stable", + "state": "unpublished", + "version": null, + "digest": null, + "releaseUrl": null, + "requiredHumanApproval": true, + "updatePolicy": "exact-version-partner-approved", + "rollback": { + "supported": true, + "requiresPartnerApproval": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ddc20c5 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "@spectoda/examples-creator-kit-tools", + "private": true, + "type": "module", + "scripts": { + "creator-kit:build": "node scripts/creator-kit/build-release-candidate.mjs --output creator-kit --release-dir .creator-kit-tmp/release", + "creator-kit:validate": "node scripts/creator-kit/validate.mjs creator-kit", + "creator-kit:test": "node --test scripts/creator-kit/creator-kit.test.mjs", + "creator-kit:evaluate": "node scripts/creator-kit/evaluate-agents.mjs", + "creator-kit:check": "bun run creator-kit:test && bun run creator-kit:validate && bun run creator-kit:evaluate" + } +} diff --git a/scripts/creator-kit/build-release-candidate.mjs b/scripts/creator-kit/build-release-candidate.mjs new file mode 100644 index 0000000..afc3829 --- /dev/null +++ b/scripts/creator-kit/build-release-candidate.mjs @@ -0,0 +1,83 @@ +import { execFileSync } from "node:child_process"; +import { readFile, rm, writeFile, mkdir } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +import { buildSyntheticBundle, createDeterministicTar, sha256 } from "./build.mjs"; +import { validateBundle } from "./validate.mjs"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + +function gitHead() { + try { + return execFileSync("git", ["rev-parse", "HEAD"], { cwd: ROOT, encoding: "utf8" }).trim(); + } catch { + return "unknown"; + } +} + +function argValue(argv, name, fallback) { + const index = argv.findIndex((argument) => argument === `--${name}` || argument.startsWith(`--${name}=`)); + if (index === -1) return fallback; + const argument = argv[index]; + return argument.includes("=") ? argument.slice(argument.indexOf("=") + 1) : argv[index + 1]; +} + +export async function buildReleaseCandidate({ + outputDir = path.join(ROOT, "creator-kit"), + releaseDir = path.join(ROOT, ".creator-kit-tmp", "release"), + version = "0.1.0-rc.1", + sourceCommit = gitHead(), +} = {}) { + const build = await buildSyntheticBundle({ outputDir, version, sourceCommit }); + const validation = await validateBundle(outputDir); + await rm(releaseDir, { recursive: true, force: true }); + await mkdir(releaseDir, { recursive: true }); + const archivePath = path.join(releaseDir, `spectoda-creator-kit-${version}.tar`); + const archive = await createDeterministicTar(outputDir, archivePath, 0); + await writeFile(`${archivePath}.sha256`, `${archive.sha256} ${path.basename(archivePath)}\n`, "utf8"); + const provenance = { + schemaVersion: "creator-kit-provenance.v1", + name: "Spectoda Creator Kit", + version, + status: "candidate", + releaseNotPublished: true, + source: { + repository: "synthetic-fixture", + commit: sourceCommit, + examplesCommit: gitHead(), + }, + exporterVersion: build.exporterVersion, + bundleDigest: validation.checksumDigest, + archiveDigest: archive.sha256, + humanReleaseGate: "creator-kit-release", + stableChannelState: validation.stableState, + }; + await writeFile(path.join(releaseDir, "provenance.json"), `${JSON.stringify(provenance, null, 2)}\n`, "utf8"); + return { build, validation, archive, provenance, releaseDir }; +} + +async function main() { + const argv = process.argv.slice(2); + const result = await buildReleaseCandidate({ + outputDir: argValue(argv, "output", path.join(ROOT, "creator-kit")), + releaseDir: argValue(argv, "release-dir", path.join(ROOT, ".creator-kit-tmp", "release")), + version: argValue(argv, "version", "0.1.0-rc.1"), + sourceCommit: argValue(argv, "source-commit", gitHead()), + }); + console.log(JSON.stringify({ + version: result.provenance.version, + bundleDigest: result.provenance.bundleDigest, + archiveDigest: result.provenance.archiveDigest, + archivePath: result.archive.archivePath, + stableChannelState: result.provenance.stableChannelState, + releaseNotPublished: result.provenance.releaseNotPublished, + }, null, 2)); +} + +if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) { + main().catch((error) => { + console.error(`Creator Kit release candidate failed: ${error.message}`); + process.exitCode = 1; + }); +} diff --git a/scripts/creator-kit/build.mjs b/scripts/creator-kit/build.mjs new file mode 100644 index 0000000..cae29e3 --- /dev/null +++ b/scripts/creator-kit/build.mjs @@ -0,0 +1,498 @@ +import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { mkdtemp, readdir, readFile, rename, rm, stat, writeFile, mkdir, copyFile } from "node:fs/promises"; +import { isIP } from "node:net"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +export const EXPORTER_VERSION = "examples-creator-kit-builder/1.0.0"; +export const BUNDLE_NAME = "Spectoda Creator Kit"; +const SCRIPT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const TEMPLATE_ROOT = path.join(SCRIPT_ROOT, "creator-kit-templates"); +const DEFAULT_SOURCE = path.join(SCRIPT_ROOT, "creator-kit-fixtures"); +const NEGATIVE_SOURCE = path.join(SCRIPT_ROOT, "creator-kit-negative-fixtures"); +const DEFAULT_OUTPUT = path.join(SCRIPT_ROOT, "creator-kit"); +const SYNTHETIC_SOURCE_LOCK = "synthetic-fixture-2026-08-07"; + +export class CreatorKitBuildError extends Error { + constructor(code, message) { + super(message); + this.name = "CreatorKitBuildError"; + this.code = code; + } +} + +const fail = (code, message) => { + throw new CreatorKitBuildError(code, message); +}; + +function isWithin(root, candidate) { + const relative = path.relative(root, candidate); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} + +function isStrictDescendant(root, candidate) { + const relative = path.relative(root, candidate); + return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative); +} + +function assertSafeOutputDir(output, source) { + const allowed = output === DEFAULT_OUTPUT || isStrictDescendant(DEFAULT_OUTPUT, output) || isStrictDescendant(tmpdir(), output); + if (!allowed || isWithin(output, SCRIPT_ROOT) || isWithin(output, source) || isWithin(source, output)) { + fail("unsafe_output", "Output must be the Creator Kit distribution directory, a child build directory, or an OS temporary directory"); + } +} +export const sha256 = (value) => createHash("sha256").update(value).digest("hex"); +export const relativePosix = (root, absolute) => path.relative(root, absolute).split(path.sep).join("/"); + +function parseScalar(value) { + const trimmed = value.trim(); + if (!trimmed) return {}; + if (trimmed === "true") return true; + if (trimmed === "false") return false; + if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { + return trimmed.slice(1, -1).replace(/\\([\\"'])/g, "$1"); + } + return trimmed; +} + +export function parseFrontmatter(raw) { + const normalized = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + if (!normalized.startsWith("---\n")) fail("frontmatter_required", "Every exported fixture requires frontmatter"); + const end = normalized.indexOf("\n---", 4); + if (end === -1) fail("invalid_frontmatter", "Frontmatter has no closing delimiter"); + const data = {}; + let section = null; + for (const line of normalized.slice(4, end).split("\n")) { + if (!line.trim()) continue; + const root = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/u); + if (root) { + const [, key, value] = root; + data[key] = value ? parseScalar(value) : {}; + section = value ? null : key; + continue; + } + const nested = line.match(/^\s{2,}([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/u); + if (!nested || !section || typeof data[section] !== "object" || Array.isArray(data[section])) { + fail("invalid_frontmatter", `Unsupported frontmatter line: ${line}`); + } + data[section][nested[1]] = parseScalar(nested[2]); + } + return { data, content: normalized.slice(end + 4) }; +} + +export function normalizeMarkdown(content, sourcePath) { + const lines = content + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n") + .normalize("NFC") + .split("\n"); + const result = []; + const outsideFence = []; + let fence = null; + for (const original of lines) { + const line = original.replace(/[ \t]+$/u, ""); + const trimmed = line.trim(); + const markerMatch = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/u); + const marker = markerMatch ? { character: markerMatch[1][0], length: markerMatch[1].length, remainder: markerMatch[2] } : null; + if (!fence && marker && (marker.character !== "`" || !marker.remainder.includes("`"))) { + fence = { character: marker.character, length: marker.length }; + } else if (fence && marker && /^[ \t]*$/u.test(marker.remainder) && fence.character === marker.character && marker.length >= fence.length) { + fence = null; + } else if (!fence) { + outsideFence.push(trimmed); + if ( + /<\s*\/?\s*(?:script|iframe|object|embed|applet|base|meta|link|style)\b/iu.test(trimmed) || + /<[A-Za-z][^>]*\bon[a-z][A-Za-z0-9:_-]*\s*=/iu.test(trimmed) || + /<[A-Za-z][^>]*\b(?:srcdoc|formaction)\s*=/iu.test(trimmed) || + (trimmed.includes("<") && /\b(?:javascript|vbscript|data):/iu.test(trimmed)) + ) fail("executable_html", `${sourcePath} contains executable or unsafe raw HTML`); + if (/^(?:import|export)\s+/u.test(trimmed)) fail("executable_mdx", `${sourcePath} contains an executable MDX statement`); + if (sourcePath.endsWith(".mdx") && (/\{[^}\n]*\}/u.test(trimmed) || /<[A-Z][A-Za-z0-9_.:-]*(?:\s|\/?>)/u.test(trimmed))) fail("executable_mdx", `${sourcePath} contains an MDX expression or JSX component`); + } + if (line === "" && result.at(-1) === "") continue; + result.push(line); + } + const outsideContent = outsideFence.join("\n"); + if ( + /<\s*\/?\s*(?:script|iframe|object|embed|applet|base|meta|link|style)\b/iu.test(outsideContent) || + /<[A-Za-z][^>]*\bon[a-z][A-Za-z0-9:_-]*\s*=/iu.test(outsideContent) || + /<[A-Za-z][^>]*\b(?:srcdoc|formaction)\s*=/iu.test(outsideContent) || + (outsideContent.includes("<") && /\b(?:javascript|vbscript|data):/iu.test(outsideContent)) + ) fail("executable_html", `${sourcePath} contains executable or unsafe raw HTML`); + if (sourcePath.endsWith(".mdx") && (/[{}]/u.test(outsideContent) || /<[A-Z][A-Za-z0-9_.:-]*(?:\s|\/?>)/u.test(outsideContent))) { + fail("executable_mdx", `${sourcePath} contains an MDX expression or JSX component`); + } + const normalized = `${result.join("\n").trim()}\n`; + if (normalized === "\n") fail("empty_document", `${sourcePath} is empty`); + return normalized; +} + +function credentialFinding(content) { + const patterns = [ + ["private_key", /-----BEGIN [A-Z ]*PRIVATE KEY-----/u], + ["credential_assignment", /\b(?:api[_ -]?key|secret|token|password)\s*[:=]\s*["'`]?[^\s"'`]{16,}/iu], + ["bearer_token", /\bBearer\s+[A-Za-z0-9._-]{16,}/u], + ["github_token", /\b(?:ghp_|github_pat_|sk-)[A-Za-z0-9_-]{12,}/u], + ["local_filesystem_path", /(?:\/Users\/|\/home\/|[A-Z]:\\\\Users\\\\)/u], + ]; + return patterns.find(([, pattern]) => pattern.test(content))?.[0] ?? null; +} + +function metadataContainsUrl(content) { + return /(?:https?:\/\/|\/\/|(?:javascript|vbscript|data):)/iu.test(content); +} + +function links(content) { + const result = []; + const add = (href) => { + if (href) result.push(href.trim()); + }; + const pattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+[^)]*)?\)/gu; + let match; + while ((match = pattern.exec(content)) !== null) add(match[1].replace(/^<|>$/gu, "")); + const referenceDefinition = /^\s{0,3}\[[^\]]+\]:\s*(?:<([^>\n]+)>|(\S+))/gmu; + while ((match = referenceDefinition.exec(content)) !== null) add(match[1] ?? match[2]); + const autolink = /<((?:https?|mailto):[^>\s]+)>/giu; + while ((match = autolink.exec(content)) !== null) add(match[1]); + const htmlAnchor = /]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/giu; + while ((match = htmlAnchor.exec(content)) !== null) add(match[1] ?? match[2] ?? match[3]); + const htmlResource = /<[A-Za-z][^>]*\b(href|src|srcset|poster|action)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/giu; + while ((match = htmlResource.exec(content)) !== null) { + const attribute = match[1].toLowerCase(); + const value = match[2] ?? match[3] ?? match[4]; + if (attribute === "srcset") { + for (const candidate of value.split(",")) add(candidate.trim().split(/\s+/u)[0]); + } else { + add(value); + } + } + const bareUrl = /https?:\/\/[^\s<>)\]}]+/giu; + while ((match = bareUrl.exec(content)) !== null) add(match[0].replace(/[.,;:!?]+$/u, "")); + return result; +} + +function assertLinks(content, sourcePath, sourcePaths) { + if (/<[A-Za-z][^>]*\b(?:href|src|srcset|poster|action)\s*=\s*\{[^}]*\}/iu.test(content)) { + fail("unsafe_link", `${sourcePath} contains a dynamic HTML/MDX resource link`); + } + for (const rawHref of links(content)) { + const href = rawHref.replace(/[\t\n\f\r]/gu, ""); + if (href !== rawHref) fail("unsafe_link", `${sourcePath} contains a URL with control whitespace`); + if (href.startsWith("#")) continue; + if (/^[a-z][a-z0-9+.-]*:/iu.test(href) && !/^https?:\/\//iu.test(href)) { + fail("unsafe_link", `${sourcePath} contains an unsupported external link: ${href}`); + } + if (/^https?:\/\//iu.test(href)) { + const url = new URL(href); + const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/gu, "").replace(/\.$/u, ""); + if ( + url.protocol !== "https:" || + isIP(hostname) !== 0 || + ["localhost", "127.0.0.1", "0.0.0.0"].includes(hostname) || + hostname.endsWith(".invalid") || + hostname.endsWith(".local") || + hostname.endsWith(".internal") || + hostname.endsWith(".lan") || + hostname.endsWith(".home.arpa") + ) { + fail("unsafe_link", `${sourcePath} contains ${href}`); + } + continue; + } + if (href.startsWith("/")) fail("unsafe_link", `${sourcePath} contains an absolute local link`); + const target = path.posix.normalize(path.posix.join(path.posix.dirname(sourcePath), href.split("#")[0])); + const candidates = [target, path.posix.join(target, "index.md"), `${target}.md`, `${target}.mdx`]; + if (!candidates.some((candidate) => sourcePaths.has(candidate))) fail("broken_link", `${sourcePath} points to ${href}`); + } +} + +async function walk(root) { + const files = []; + async function visit(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + entries.sort((left, right) => left.name.localeCompare(right.name)); + for (const entry of entries) { + const absolute = path.join(directory, entry.name); + if (entry.isDirectory()) await visit(absolute); + else if (entry.isFile()) files.push(absolute); + } + } + await visit(root); + return files; +} + +function json(value) { + return `${JSON.stringify(value, null, 2)}\n`; +} + +async function writeJson(file, value) { + await mkdir(path.dirname(file), { recursive: true }); + await writeFile(file, json(value), "utf8"); +} + +export async function bundleFiles(root) { + return (await walk(root)).filter((file) => relativePosix(root, file) !== "checksums.sha256"); +} + +export async function createChecksums(root) { + const lines = []; + for (const file of await bundleFiles(root)) { + lines.push(`${sha256(await readFile(file))} ${relativePosix(root, file)}`); + } + return `${lines.sort().join("\n")}\n`; +} + +function terms(documents) { + const index = new Map(); + for (const document of documents) { + const documentTerms = new Set(document.content.toLowerCase().match(/[a-z][a-z0-9-]{2,}/gu) ?? []); + for (const term of documentTerms) index.set(term, [...(index.get(term) ?? []), document.id]); + } + return [...index.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([term, ids]) => ({ term, documentIds: ids.sort() })); +} + +async function copyTemplates(output) { + for (const file of await walk(TEMPLATE_ROOT)) { + const relative = relativePosix(TEMPLATE_ROOT, file); + const destination = path.join(output, relative); + await mkdir(path.dirname(destination), { recursive: true }); + await copyFile(file, destination); + } +} + +function gitHead() { + try { + return execFileSync("git", ["rev-parse", "HEAD"], { cwd: SCRIPT_ROOT, encoding: "utf8" }).trim(); + } catch { + return "unknown"; + } +} + +function verifySourceAtCommit(file, sourcePath, sourceCommit, raw) { + if (!/^[a-f0-9]{40}$/u.test(sourceCommit)) fail("source_commit_required", `${sourcePath} requires a 40-character source commit`); + const repositoryPath = relativePosix(SCRIPT_ROOT, file); + if (repositoryPath.startsWith("../") || path.isAbsolute(repositoryPath)) fail("source_path_outside_repository", `${sourcePath} is outside the Examples repository`); + let locked; + try { + locked = execFileSync("git", ["show", `${sourceCommit}:${repositoryPath}`], { cwd: SCRIPT_ROOT, maxBuffer: 16 * 1024 * 1024 }); + } catch { + fail("source_lock_unavailable", `${sourcePath} is not available at source commit ${sourceCommit}`); + } + if (sha256(locked) !== sha256(raw)) fail("source_lock_mismatch", `${sourcePath} bytes differ from source commit ${sourceCommit}`); +} + +export async function buildSyntheticBundle({ + sourceRoot = DEFAULT_SOURCE, + outputDir = DEFAULT_OUTPUT, + version = "0.1.0-rc.1", + sourceCommit = gitHead(), + sourceRepository = "synthetic-fixture", +} = {}) { + if (sourceRepository !== "synthetic-fixture") fail("synthetic_only", "The reviewed Examples generator accepts synthetic-fixture as its only source repository"); + const source = path.resolve(sourceRoot); + const output = path.resolve(outputDir); + assertSafeOutputDir(output, source); + if (![DEFAULT_SOURCE, NEGATIVE_SOURCE].some((root) => isWithin(root, source))) { + fail("synthetic_source_required", "The reviewed Examples generator accepts only tracked fixture roots"); + } + if (!/^[a-f0-9]{40}$/u.test(sourceCommit)) fail("source_commit_required", "Synthetic bundles require an exact 40-character source commit"); + const sourceFiles = (await walk(source)).filter((file) => /\.(md|mdx)$/u.test(file)).sort((left, right) => left.localeCompare(right)); + const sourcePaths = new Set(sourceFiles.map((file) => relativePosix(source, file))); + const documents = []; + for (const file of sourceFiles) { + const sourcePath = relativePosix(source, file); + if (!sourcePath.startsWith("en/")) fail("locale_not_allowed", `${sourcePath} is not an English fixture path`); + const raw = await readFile(file, "utf8"); + verifySourceAtCommit(file, sourcePath, sourceCommit, raw); + const parsed = parseFrontmatter(raw); + const agentExport = parsed.data.agentExport; + if (!agentExport || agentExport.include !== true || agentExport.audience !== "partner-maker") fail("invalid_agent_export", `${sourcePath} must explicitly opt into partner-maker export`); + if (agentExport.license !== "spectoda-creator-kit-synthetic" || agentExport.publicDerivative !== true || !agentExport.sourceLock) fail("license_not_allowed", `${sourcePath} has no synthetic redistribution lock`); + if (agentExport.sourceLock !== SYNTHETIC_SOURCE_LOCK) fail("source_lock_mismatch", `${sourcePath} has an unapproved synthetic fixture lock`); + const title = typeof parsed.data.title === "string" ? parsed.data.title.trim() : ""; + const summary = typeof parsed.data.summary === "string" ? parsed.data.summary.trim() : ""; + if (!title || !summary) fail("metadata_required", `${sourcePath} needs title and summary`); + const metadataFinding = credentialFinding(JSON.stringify({ title, summary, agentExport })); + if (metadataFinding) fail("public_safety", `${sourcePath} metadata matched ${metadataFinding}`); + if (metadataContainsUrl(`${title}\n${summary}`)) fail("unsafe_link", `${sourcePath} metadata contains a URL`); + const content = normalizeMarkdown(parsed.content, sourcePath); + const finding = credentialFinding(content); + if (finding) fail("public_safety", `${sourcePath} matched ${finding}`); + documents.push({ + id: `${sourcePath.replace(/\.(md|mdx)$/u, "").replace(/\//g, "__")}__${path.extname(sourcePath).slice(1)}`, + sourcePath, + path: `documents/${sourcePath}`, + title, + summary, + format: path.extname(sourcePath).slice(1), + content, + sourceSha256: sha256(raw), + sha256: sha256(content), + agentExport: { include: true, audience: agentExport.audience, license: agentExport.license, sourceLock: agentExport.sourceLock }, + }); + } + if (documents.length === 0) fail("no_documents", "Synthetic fixture contains no English documents"); + const documentIds = new Set(); + for (const document of documents) { + if (documentIds.has(document.id)) fail("duplicate_document_id", `Synthetic documents produce a duplicate ID: ${document.id}`); + documentIds.add(document.id); + } + for (const document of documents) assertLinks(document.content, document.sourcePath, sourcePaths); + documents.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath)); + let staging; + try { + await mkdir(path.dirname(output), { recursive: true }); + staging = await mkdtemp(path.join(path.dirname(output), ".creator-kit-staging-")); + await copyTemplates(staging); + for (const document of documents) { + const destination = path.join(staging, document.path); + await mkdir(path.dirname(destination), { recursive: true }); + await writeFile(destination, document.content, "utf8"); + } + const manifest = { + schemaVersion: "creator-kit-manifest.v1", + bundleName: BUNDLE_NAME, + bundleVersion: version, + locale: "en", + contentScope: "synthetic-fixture", + source: { repository: sourceRepository, commit: sourceCommit }, + documents: documents.map(({ content: _content, ...document }) => document), + }; + const sourceLock = { + schemaVersion: "creator-kit-source-lock.v1", + repository: sourceRepository, + commit: sourceCommit, + files: documents.map((document) => ({ sourcePath: document.sourcePath, sourceSha256: document.sourceSha256, normalizedSha256: document.sha256, license: document.agentExport.license })), + }; + await writeJson(path.join(staging, "bundle.json"), { + schemaVersion: "creator-kit-bundle.v1", + name: BUNDLE_NAME, + version, + status: "candidate", + locale: "en", + contentScope: "synthetic-fixture", + sourceOfTruth: "Spectoda/documentation", + transport: { apiVersion: "github-release-v1", stableChannelDescriptor: "stable-channel.json" }, + }); + await writeJson(path.join(staging, "manifest.json"), manifest); + await writeJson(path.join(staging, "source-lock.json"), sourceLock); + await writeJson(path.join(staging, "licenses.json"), { + schemaVersion: "creator-kit-licenses.v1", + publicationAllowed: false, + realDocumentationExportAllowed: false, + scope: "synthetic-fixture-only", + entries: ["spectoda-creator-kit-synthetic"], + }); + await writeJson(path.join(staging, "compatibility.json"), { + schemaVersion: "creator-kit-compatibility.v1", + bundleName: BUNDLE_NAME, + formatVersion: 1, + locale: "en", + agents: ["codex", "claude"], + transport: "github-release-v1", + updatePolicy: "exact-version-partner-approved", + }); + await writeJson(path.join(staging, "selection.json"), { + schemaVersion: "creator-kit-selection.v1", + failClosed: true, + selected: documents.map((document) => ({ sourcePath: document.sourcePath, agentExport: document.agentExport })), + }); + await writeJson(path.join(staging, "indexes", "documents.json"), { + schemaVersion: "creator-kit-document-index.v1", + documents: documents.map(({ content: _content, sourceSha256: _sourceSha256, ...document }) => document), + }); + await writeJson(path.join(staging, "indexes", "terms.json"), { + schemaVersion: "creator-kit-term-index.v1", + terms: terms(documents), + }); + await writeFile(path.join(staging, "checksums.sha256"), await createChecksums(staging), "utf8"); + const files = await bundleFiles(staging); + const totalBytes = (await Promise.all(files.map(async (file) => (await stat(file)).size))).reduce((total, size) => total + size, 0); + if (totalBytes > 5242880) fail("bundle_too_large", `Bundle exceeds the 5 MiB candidate limit: ${totalBytes}`); + const checksumDigest = sha256(await readFile(path.join(staging, "checksums.sha256"))); + await rm(output, { recursive: true, force: true }); + await rename(staging, output); + staging = null; + return { + version, + sourceCommit, + documentCount: documents.length, + totalBytes, + fileCount: files.length, + checksumDigest, + exporterVersion: EXPORTER_VERSION, + }; + } finally { + if (staging) await rm(staging, { recursive: true, force: true }); + } +} + +function octal(value, width) { + const digits = Math.floor(value).toString(8); + return `${digits.padStart(width - 1, "0")}\0`; +} + +function field(buffer, offset, length, value) { + Buffer.from(value).copy(buffer, offset, 0, Math.min(Buffer.byteLength(value), length)); +} + +export async function createDeterministicTar(sourceRoot, archivePath, epoch = 0) { + const files = await walk(sourceRoot); + const chunks = []; + for (const file of files.sort((left, right) => relativePosix(sourceRoot, left).localeCompare(relativePosix(sourceRoot, right)))) { + const relative = relativePosix(sourceRoot, file); + if (Buffer.byteLength(relative) > 100) fail("archive_path", `Archive path is longer than ustar allows: ${relative}`); + const content = await readFile(file); + const header = Buffer.alloc(512); + field(header, 0, 100, relative); + field(header, 100, 8, "0000644\0"); + field(header, 108, 8, "0000000\0"); + field(header, 116, 8, "0000000\0"); + field(header, 124, 12, octal(content.length, 12)); + field(header, 136, 12, octal(epoch, 12)); + header.fill(0x20, 148, 156); + header[156] = 0x30; + field(header, 257, 6, "ustar\0"); + field(header, 263, 2, "00"); + const checksum = [...header].reduce((total, byte) => total + byte, 0); + field(header, 148, 8, `${checksum.toString(8).padStart(6, "0")}\0 `); + chunks.push(header, content); + const padding = (512 - (content.length % 512)) % 512; + if (padding) chunks.push(Buffer.alloc(padding)); + } + chunks.push(Buffer.alloc(1024)); + await mkdir(path.dirname(archivePath), { recursive: true }); + await writeFile(archivePath, Buffer.concat(chunks)); + return { archivePath, sha256: sha256(await readFile(archivePath)), fileCount: files.length }; +} + +function parseArgs(argv) { + const options = {}; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (!argument.startsWith("--")) continue; + const [key, inline] = argument.slice(2).split("=", 2); + options[key] = inline ?? argv[++index]; + } + return options; +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + const result = await buildSyntheticBundle({ + sourceRoot: options.source ?? DEFAULT_SOURCE, + outputDir: options.output ?? DEFAULT_OUTPUT, + version: options.version ?? "0.1.0-rc.1", + sourceCommit: options["source-commit"] ?? gitHead(), + }); + console.log(JSON.stringify(result, null, 2)); +} + +if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) { + main().catch((error) => { + console.error(`Creator Kit Examples build failed [${error.code ?? "unknown"}]: ${error.message}`); + process.exitCode = 1; + }); +} diff --git a/scripts/creator-kit/creator-kit.test.mjs b/scripts/creator-kit/creator-kit.test.mjs new file mode 100644 index 0000000..7be6263 --- /dev/null +++ b/scripts/creator-kit/creator-kit.test.mjs @@ -0,0 +1,136 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import path from "node:path"; +import { tmpdir } from "node:os"; +import test from "node:test"; + +import { buildReleaseCandidate } from "./build-release-candidate.mjs"; +import { CreatorKitBuildError, buildSyntheticBundle } from "./build.mjs"; +import { evaluateAllAgents } from "./evaluate-agents.mjs"; +import { promoteStableChannel } from "./promote-stable-channel.mjs"; +import { validateBundle } from "./validate.mjs"; + +const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../.."); +const FIXTURE_ROOT = path.join(ROOT, "creator-kit-fixtures"); +const NEGATIVE_ROOT = path.join(ROOT, "creator-kit-negative-fixtures"); + +test("double-builds a reproducible candidate and deterministic tar archive", async () => { + const root = await mkdtemp(path.join(tmpdir(), "creator-kit-examples-")); + try { + const first = await buildReleaseCandidate({ + outputDir: path.join(root, "first-bundle"), + releaseDir: path.join(root, "first-release"), + }); + const second = await buildReleaseCandidate({ + outputDir: path.join(root, "second-bundle"), + releaseDir: path.join(root, "second-release"), + }); + assert.equal(first.validation.checksumDigest, second.validation.checksumDigest); + assert.equal(first.archive.sha256, second.archive.sha256); + assert.equal(first.provenance.releaseNotPublished, true); + assert.equal(first.provenance.stableChannelState, "unpublished"); + assert.equal((await validateBundle(path.join(root, "first-bundle"))).documentCount, 2); + assert.equal(await readFile(first.archive.archivePath).then((value) => value.length), await readFile(second.archive.archivePath).then((value) => value.length)); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("agent-objective harness passes Codex and Claude-shaped fixtures without claiming a pilot", async () => { + const result = await evaluateAllAgents(); + assert.equal(result.passed, true); + assert.equal(result.pilotStatus, "not-run"); + assert.deepEqual(result.evaluations.map((evaluation) => evaluation.agent), ["codex", "claude"]); +}); + +test("refuses synthetic sources outside the tracked fixture roots", async () => { + const root = await mkdtemp(path.join(tmpdir(), "creator-kit-source-boundary-")); + try { + await assert.rejects( + buildSyntheticBundle({ sourceRoot: ROOT, outputDir: path.join(root, "out") }), + (error) => error instanceof CreatorKitBuildError && error.code === "synthetic_source_required", + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("rejects output paths outside the reviewed distribution or temporary directories", async () => { + const root = await mkdtemp(path.join(tmpdir(), "creator-kit-output-")); + try { + await assert.rejects( + buildSyntheticBundle({ sourceRoot: FIXTURE_ROOT, outputDir: ROOT }), + (error) => error instanceof CreatorKitBuildError && error.code === "unsafe_output", + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("stable-channel promotion is human-gated and deterministic", async () => { + const root = await mkdtemp(path.join(tmpdir(), "creator-kit-stable-")); + try { + const candidate = await buildReleaseCandidate({ outputDir: path.join(root, "bundle"), releaseDir: path.join(root, "release") }); + await assert.rejects( + promoteStableChannel({ + bundleRoot: path.join(root, "bundle"), + version: "0.1.0-rc.1", + digest: candidate.validation.checksumDigest, + releaseUrl: "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/Spectoda/examples/releases/tag/creator-kit-v0.1.0-rc.1", + outputPath: path.join(root, "stable-channel.json"), + confirmation: "", + }), + /protected human confirmation/u, + ); + const stable = await promoteStableChannel({ + bundleRoot: path.join(root, "bundle"), + version: "0.1.0-rc.1", + digest: candidate.validation.checksumDigest, + releaseUrl: "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/Spectoda/examples/releases/tag/creator-kit-v0.1.0-rc.1", + outputPath: path.join(root, "stable-channel.json"), + confirmation: "PUBLISH_CREATOR_KIT_RELEASE", + }); + assert.equal(stable.state, "published"); + assert.equal(stable.digest, candidate.validation.checksumDigest); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("tracked negative fixtures fail closed for every gate", async () => { + const root = await mkdtemp(path.join(tmpdir(), "creator-kit-negative-")); + try { + const cases = [ + ["secret", "public_safety"], + ["executable", "executable_mdx"], + ["executable-html", "executable_html"], + ["multiline-expression", "executable_mdx"], + ["multiline-jsx", "executable_mdx"], + ["multiline-html", "executable_html"], + ["unsafe-link", "unsafe_link"], + ["metadata", "public_safety"], + ["metadata-link", "unsafe_link"], + ["indented-pseudo-fence", "executable_html"], + ["invalid-fence", "executable_html"], + ["multiline-js-url", "unsafe_link"], + ["reference", "unsafe_link"], + ["autolink", "unsafe_link"], + ["html", "unsafe_link"], + ["srcset", "unsafe_link"], + ["bare-url", "unsafe_link"], + ["private-https", "unsafe_link"], + ["localhost-dot", "unsafe_link"], + ["resource", "unsafe_link"], + ["dynamic-resource", "unsafe_link"], + ["collision", "duplicate_document_id"], + ]; + for (const [name, code] of cases) { + await assert.rejects( + buildSyntheticBundle({ sourceRoot: path.join(NEGATIVE_ROOT, name), outputDir: path.join(root, name) }), + (error) => error instanceof CreatorKitBuildError && error.code === code, + ); + } + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/scripts/creator-kit/evaluate-agents.mjs b/scripts/creator-kit/evaluate-agents.mjs new file mode 100644 index 0000000..eadc127 --- /dev/null +++ b/scripts/creator-kit/evaluate-agents.mjs @@ -0,0 +1,44 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const EVALUATION_ROOT = path.join(ROOT, "creator-kit-evaluations"); + +async function json(file) { + return JSON.parse(await readFile(file, "utf8")); +} + +export async function evaluateAgent(agent) { + const questions = await json(path.join(EVALUATION_ROOT, "questions.json")); + const responseDocument = await json(path.join(EVALUATION_ROOT, "responses", `${agent}.json`)); + const responses = new Map(responseDocument.responses.map((response) => [response.id, response])); + const results = questions.map((question) => { + const response = responses.get(question.id); + const citationOk = question.expected.citation ? response?.citations?.includes(question.expected.citation) : true; + const abstentionOk = response?.abstained === question.expected.abstain; + return { + id: question.id, + passed: Boolean(response && citationOk && abstentionOk), + citationOk, + abstentionOk, + }; + }); + return { agent, passed: results.every((result) => result.passed), results }; +} + +export async function evaluateAllAgents() { + const evaluations = await Promise.all([evaluateAgent("codex"), evaluateAgent("claude")]); + return { harness: "synthetic-agent-objective-v1", pilotStatus: "not-run", passed: evaluations.every((evaluation) => evaluation.passed), evaluations }; +} + +async function main() { + const result = await evaluateAllAgents(); + console.log(JSON.stringify(result, null, 2)); + if (!result.passed) process.exitCode = 1; +} + +if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) main().catch((error) => { + console.error(`Creator Kit agent evaluation failed: ${error.message}`); + process.exitCode = 1; +}); diff --git a/scripts/creator-kit/promote-stable-channel.mjs b/scripts/creator-kit/promote-stable-channel.mjs new file mode 100644 index 0000000..df841a5 --- /dev/null +++ b/scripts/creator-kit/promote-stable-channel.mjs @@ -0,0 +1,56 @@ +import { readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + +export async function promoteStableChannel({ + bundleRoot = path.join(ROOT, "creator-kit"), + version, + digest, + releaseUrl, + outputPath, + confirmation, +} = {}) { + if (confirmation !== "PUBLISH_CREATOR_KIT_RELEASE") throw new Error("Stable-channel promotion requires the protected human confirmation"); + if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version ?? "")) throw new Error("Stable-channel promotion requires a semver version"); + if (!/^[a-f0-9]{64}$/u.test(digest ?? "")) throw new Error("Stable-channel promotion requires a 64-character bundle digest"); + if (!/^https:\/\/github\.com\/Spectoda\/examples\/releases\/tag\/creator-kit-v[^/]+$/u.test(releaseUrl ?? "")) throw new Error("Stable-channel release URL must point to a versioned Spectoda/examples GitHub Release"); + const current = JSON.parse(await readFile(path.join(bundleRoot, "stable-channel.json"), "utf8")); + if (current.state !== "unpublished" || current.requiredHumanApproval !== true) throw new Error("Stable-channel source is not an unpublished human-gated descriptor"); + const next = { + ...current, + state: "published", + version, + digest, + releaseUrl, + rollback: { ...current.rollback, previous: current.version ?? null }, + }; + await writeFile(outputPath, `${JSON.stringify(next, null, 2)}\n`, "utf8"); + return next; +} + +function argValue(argv, name) { + const index = argv.findIndex((argument) => argument === `--${name}` || argument.startsWith(`--${name}=`)); + if (index === -1) return undefined; + const argument = argv[index]; + return argument.includes("=") ? argument.slice(argument.indexOf("=") + 1) : argv[index + 1]; +} + +async function main() { + const argv = process.argv.slice(2); + const result = await promoteStableChannel({ + bundleRoot: argValue(argv, "bundle") ?? path.join(ROOT, "creator-kit"), + version: argValue(argv, "version"), + digest: argValue(argv, "digest"), + releaseUrl: argValue(argv, "release-url"), + outputPath: argValue(argv, "output"), + confirmation: argValue(argv, "confirm"), + }); + console.log(JSON.stringify(result, null, 2)); +} + +if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) main().catch((error) => { + console.error(`Creator Kit stable-channel promotion failed: ${error.message}`); + process.exitCode = 1; +}); diff --git a/scripts/creator-kit/validate.mjs b/scripts/creator-kit/validate.mjs new file mode 100644 index 0000000..a2b05be --- /dev/null +++ b/scripts/creator-kit/validate.mjs @@ -0,0 +1,181 @@ +import assert from "node:assert/strict"; +import { readFile, stat } from "node:fs/promises"; +import { isIP } from "node:net"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +import { bundleFiles, createChecksums, sha256 } from "./build.mjs"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const DEFAULT_BUNDLE = path.join(ROOT, "creator-kit"); +const SYNTHETIC_SOURCE_LOCK = "synthetic-fixture-2026-08-07"; + +function check(condition, message) { + if (!condition) throw new Error(message); +} + +async function readJson(file) { + try { + return JSON.parse(await readFile(file, "utf8")); + } catch (error) { + throw new Error(`Invalid JSON ${file}: ${error.message}`); + } +} + +function findLinks(content) { + const links = []; + const add = (href) => { + if (href) links.push(href.trim()); + }; + const pattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+[^)]*)?\)/gu; + let match; + while ((match = pattern.exec(content)) !== null) add(match[1].replace(/^<|>$/gu, "")); + const referenceDefinition = /^\s{0,3}\[[^\]]+\]:\s*(?:<([^>\n]+)>|(\S+))/gmu; + while ((match = referenceDefinition.exec(content)) !== null) add(match[1] ?? match[2]); + const autolink = /<((?:https?|mailto):[^>\s]+)>/giu; + while ((match = autolink.exec(content)) !== null) add(match[1]); + const htmlAnchor = /]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/giu; + while ((match = htmlAnchor.exec(content)) !== null) add(match[1] ?? match[2] ?? match[3]); + const htmlResource = /<[A-Za-z][^>]*\b(href|src|srcset|poster|action)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/giu; + while ((match = htmlResource.exec(content)) !== null) { + const attribute = match[1].toLowerCase(); + const value = match[2] ?? match[3] ?? match[4]; + if (attribute === "srcset") { + for (const candidate of value.split(",")) add(candidate.trim().split(/\s+/u)[0]); + } else { + add(value); + } + } + const bareUrl = /https?:\/\/[^\s<>)\]}]+/giu; + while ((match = bareUrl.exec(content)) !== null) add(match[0].replace(/[.,;:!?]+$/u, "")); + return links; +} + +function publicSafetyFinding(content) { + const patterns = [ + ["private_key", /-----BEGIN [A-Z ]*PRIVATE KEY-----/u], + ["credential_assignment", /\b(?:api[_ -]?key|secret|token|password)\s*[:=]\s*["'`]?[^\s"'`]{16,}/iu], + ["bearer_token", /\bBearer\s+[A-Za-z0-9._-]{16,}/u], + ["github_token", /\b(?:ghp_|github_pat_|sk-)[A-Za-z0-9_-]{12,}/u], + ["local_filesystem_path", /(?:\/Users\/|\/home\/|[A-Z]:\\\\Users\\\\)/u], + ]; + return patterns.find(([, pattern]) => pattern.test(content))?.[0] ?? null; +} + +function metadataContainsUrl(content) { + return /(?:https?:\/\/|\/\/|(?:javascript|vbscript|data):)/iu.test(content); +} + +export async function validateBundle(bundleRoot = DEFAULT_BUNDLE) { + const root = path.resolve(bundleRoot); + const required = [ + "README.md", + "AGENTS.md", + "RELEASE_NOTES.md", + "stable-channel.json", + "bundle.json", + "manifest.json", + "source-lock.json", + "licenses.json", + "compatibility.json", + "selection.json", + "indexes/documents.json", + "indexes/terms.json", + "checksums.sha256", + ]; + for (const relative of required) check(await stat(path.join(root, relative)).then(() => true).catch(() => false), `Missing bundle file ${relative}`); + const bundle = await readJson(path.join(root, "bundle.json")); + const manifest = await readJson(path.join(root, "manifest.json")); + const sourceLock = await readJson(path.join(root, "source-lock.json")); + const licenses = await readJson(path.join(root, "licenses.json")); + const compatibility = await readJson(path.join(root, "compatibility.json")); + const selection = await readJson(path.join(root, "selection.json")); + const stable = await readJson(path.join(root, "stable-channel.json")); + const documentIndex = await readJson(path.join(root, "indexes/documents.json")); + const termIndex = await readJson(path.join(root, "indexes/terms.json")); + check(bundle.schemaVersion === "creator-kit-bundle.v1", "Wrong bundle schema"); + check(bundle.name === "Spectoda Creator Kit" && bundle.status === "candidate" && bundle.locale === "en", "Bundle identity is invalid"); + check(bundle.contentScope === "synthetic-fixture" && bundle.sourceOfTruth === "Spectoda/documentation", "Bundle must be synthetic-only and Documentation-sourced"); + check(bundle.transport?.apiVersion === "github-release-v1", "Bundle transport is not the v1 GitHub Release contract"); + check(manifest.schemaVersion === "creator-kit-manifest.v1" && manifest.locale === "en", "Manifest schema/locale is invalid"); + check(manifest.source?.repository === "synthetic-fixture" && /^[a-f0-9]{40}$/u.test(manifest.source.commit ?? ""), "Source lock must name the synthetic source and exact commit"); + check(sourceLock.schemaVersion === "creator-kit-source-lock.v1" && sourceLock.commit === manifest.source.commit, "Source lock does not match manifest source"); + check(licenses.schemaVersion === "creator-kit-licenses.v1" && licenses.scope === "synthetic-fixture-only", "License posture is not synthetic-only"); + check(licenses.realDocumentationExportAllowed === false, "Real Documentation export must remain disabled"); + check(licenses.publicationAllowed === false, "Synthetic candidate publication must remain disabled"); + check(compatibility.schemaVersion === "creator-kit-compatibility.v1" && compatibility.updatePolicy === "exact-version-partner-approved", "Compatibility policy is invalid"); + check(selection.schemaVersion === "creator-kit-selection.v1" && selection.failClosed === true, "Selection must be fail-closed"); + check(stable.schemaVersion === "creator-kit-stable-channel.v1" && stable.state === "unpublished" && stable.requiredHumanApproval === true, "Stable channel must remain unpublished behind a human gate"); + check(stable.version === null && stable.digest === null && stable.releaseUrl === null, "Unpublished stable channel must not contain release coordinates"); + check(Array.isArray(manifest.documents) && manifest.documents.length === 2, "Candidate must contain the two synthetic documents"); + check(new Set(manifest.documents.map((document) => document.id)).size === manifest.documents.length, "Manifest document IDs must be unique"); + check(Array.isArray(documentIndex.documents) && documentIndex.documents.length === manifest.documents.length, "Document index does not match manifest"); + check(Array.isArray(termIndex.terms), "Term index is not an array"); + check(manifest.documents.map((document) => document.sourcePath).join("\n") === [...manifest.documents].sort((left, right) => left.sourcePath.localeCompare(right.sourcePath)).map((document) => document.sourcePath).join("\n"), "Manifest documents are not sorted"); + const sourceLockByPath = new Map(sourceLock.files.map((file) => [file.sourcePath, file])); + for (const document of manifest.documents) { + check(document.agentExport?.include === true && document.agentExport.audience === "partner-maker", `${document.sourcePath} is not explicitly selected`); + check(document.agentExport.license === "spectoda-creator-kit-synthetic", `${document.sourcePath} has an invalid license`); + check(document.agentExport.sourceLock === SYNTHETIC_SOURCE_LOCK, `${document.sourcePath} has an invalid synthetic fixture lock`); + const body = await readFile(path.join(root, document.path), "utf8"); + check(!body.startsWith("---\n"), `${document.path} still contains frontmatter`); + check(sha256(body) === document.sha256, `${document.path} content hash does not match manifest`); + check(!publicSafetyFinding(body), `${document.path} failed public-safety scan`); + check(!publicSafetyFinding(JSON.stringify({ title: document.title, summary: document.summary, agentExport: document.agentExport })), `${document.path} metadata failed public-safety scan`); + check(!metadataContainsUrl(`${document.title}\n${document.summary}`), `${document.path} metadata contains a URL`); + check(!/<[A-Za-z][^>]*\b(?:href|src|srcset|poster|action)\s*=\s*\{[^}]*\}/iu.test(body), `${document.path} contains a dynamic HTML/MDX resource link`); + const lock = sourceLockByPath.get(document.sourcePath); + check(lock?.normalizedSha256 === document.sha256, `${document.sourcePath} source lock does not match normalized body`); + for (const rawHref of findLinks(body)) { + const href = rawHref.replace(/[\t\n\f\r]/gu, ""); + check(href === rawHref, `${document.path} contains a URL with control whitespace`); + if (href.startsWith("#")) continue; + if (/^https?:\/\//u.test(href)) { + const url = new URL(href); + const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/gu, "").replace(/\.$/u, ""); + check( + url.protocol === "https:" && + isIP(hostname) === 0 && + !["localhost", "127.0.0.1", "0.0.0.0"].includes(hostname) && + !hostname.endsWith(".invalid") && + !hostname.endsWith(".local") && + !hostname.endsWith(".internal") && + !hostname.endsWith(".lan") && + !hostname.endsWith(".home.arpa"), + `${document.path} contains a non-public HTTPS link ${href}`, + ); + continue; + } + if (/^[a-z][a-z0-9+.-]*:/iu.test(href)) throw new Error(`${document.path} contains an unsupported external link`); + check(!href.startsWith("/"), `${document.path} contains an absolute local link`); + const target = path.posix.normalize(path.posix.join(path.posix.dirname(document.path), href.split("#")[0])); + check(await stat(path.join(root, target)).then(() => true).catch(() => false), `${document.path} points to missing bundle link ${href}`); + } + } + const expectedChecksums = (await readFile(path.join(root, "checksums.sha256"), "utf8")).trim(); + const actualChecksums = (await createChecksums(root)).trim(); + check(expectedChecksums === actualChecksums, "checksums.sha256 does not match bundle files"); + const files = await bundleFiles(root); + const totalBytes = (await Promise.all(files.map(async (file) => (await stat(file)).size))).reduce((total, size) => total + size, 0); + check(totalBytes <= 5242880, `Bundle is larger than 5 MiB: ${totalBytes}`); + return { + bundleVersion: bundle.version, + documentCount: manifest.documents.length, + totalBytes, + fileCount: files.length, + checksumDigest: sha256(Buffer.from(`${expectedChecksums}\n`)), + stableState: stable.state, + }; +} + +async function main() { + const result = await validateBundle(process.argv[2] ?? DEFAULT_BUNDLE); + console.log(JSON.stringify(result, null, 2)); +} + +if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) { + main().catch((error) => { + console.error(`Creator Kit Examples validation failed: ${error.message}`); + process.exitCode = 1; + }); +} From a9551ba4787ec1c98f01ecb8238f1c7c8398a916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sat, 8 Aug 2026 01:46:25 +0200 Subject: [PATCH 02/10] Add reproducible Creator Kit candidate --- creator-kit/AGENTS.md | 21 ++ creator-kit/README.md | 32 +++ creator-kit/RELEASE_NOTES.md | 9 + creator-kit/bundle.json | 13 ++ creator-kit/checksums.sha256 | 19 ++ creator-kit/compatibility.json | 12 + creator-kit/documents/en/synthetic-creator.md | 6 + .../documents/en/synthetic-creator.mdx | 4 + creator-kit/indexes/documents.json | 35 +++ creator-kit/indexes/terms.json | 215 ++++++++++++++++++ creator-kit/licenses.json | 9 + creator-kit/manifest.json | 45 ++++ .../schemas/creator-kit-bundle.v1.schema.json | 18 ++ .../creator-kit-compatibility.v1.schema.json | 17 ++ .../creator-kit-licenses.v1.schema.json | 15 ++ .../creator-kit-manifest.v1.schema.json | 53 +++++ .../creator-kit-source-lock.v1.schema.json | 14 ++ creator-kit/selection.json | 24 ++ creator-kit/source-lock.json | 19 ++ creator-kit/stable-channel.json | 15 ++ 20 files changed, 595 insertions(+) create mode 100644 creator-kit/AGENTS.md create mode 100644 creator-kit/README.md create mode 100644 creator-kit/RELEASE_NOTES.md create mode 100644 creator-kit/bundle.json create mode 100644 creator-kit/checksums.sha256 create mode 100644 creator-kit/compatibility.json create mode 100644 creator-kit/documents/en/synthetic-creator.md create mode 100644 creator-kit/documents/en/synthetic-creator.mdx create mode 100644 creator-kit/indexes/documents.json create mode 100644 creator-kit/indexes/terms.json create mode 100644 creator-kit/licenses.json create mode 100644 creator-kit/manifest.json create mode 100644 creator-kit/schemas/creator-kit-bundle.v1.schema.json create mode 100644 creator-kit/schemas/creator-kit-compatibility.v1.schema.json create mode 100644 creator-kit/schemas/creator-kit-licenses.v1.schema.json create mode 100644 creator-kit/schemas/creator-kit-manifest.v1.schema.json create mode 100644 creator-kit/schemas/creator-kit-source-lock.v1.schema.json create mode 100644 creator-kit/selection.json create mode 100644 creator-kit/source-lock.json create mode 100644 creator-kit/stable-channel.json diff --git a/creator-kit/AGENTS.md b/creator-kit/AGENTS.md new file mode 100644 index 0000000..407a038 --- /dev/null +++ b/creator-kit/AGENTS.md @@ -0,0 +1,21 @@ +# Spectoda Creator Kit agent instructions + +You are reading a pinned, read-only knowledge bundle. + +1. Read `bundle.json`, `compatibility.json` and `source-lock.json` before using + content. +2. Search `indexes/documents.json` and `indexes/terms.json`; cite the exact + `documents/...` file for every factual answer. +3. Treat the visible version and checksum digest as part of the answer context. +4. Abstain or ask for a current source when the bundle does not contain the + answer, when the question concerns current availability or policy, or when + it concerns partner-local products, networks, credentials or installation + state. +5. Never edit this bundle, the Documentation repository or a partner-local + knowledge directory as a side effect of answering. +6. Updates and rollback require partner approval of an exact version and + digest. Do not follow a floating URL or silently refresh the bundle. + +Codex and Claude are supported as read-only consumers. Embeddings, dynamic +hosted RAG, a central MCP gateway and a writable Documentation API are not part +of Creator Kit v1. diff --git a/creator-kit/README.md b/creator-kit/README.md new file mode 100644 index 0000000..7b7cd3e --- /dev/null +++ b/creator-kit/README.md @@ -0,0 +1,32 @@ +# Spectoda Creator Kit + +This directory is a reviewable **release candidate** for the English Spectoda +Creator Kit. It is read-only and intentionally contains synthetic public-safe +fixture content. It is not a published GitHub Release and it must not be +installed into a partner Organization until a Steward/Admin approves a release +and the Documentation redistribution license is recorded. + +The bundle is generated from one exact source commit. Verify the version, +`source-lock.json`, `checksums.sha256` and the bundle digest before use. Updates +are exact-version and partner-approved. `stable-channel.json` is the v1 +read-only transport descriptor; its current state is `unpublished`. + +The Documentation repository remains the private content authority. This public +repository is only the reviewed distribution surface. Partner-local product +knowledge belongs in a separate sibling directory and is never overwritten by +Creator Kit updates or rollback. + +## Agent use + +Codex and Claude may search `manifest.json` and `indexes/` first, then cite the +exact `documents/...` path. They must say when a fact is missing or stale, +abstain on private partner-local questions and request a current source for +time-sensitive information. The bundle has no write path to Documentation or +to an Organization. + +## Supported transport + +v1 uses GitHub Releases plus a stable-channel descriptor. A native Lazurio +installer is not part of this candidate. Do not invent Lazurio commands; the +generic verified install/pin/update/rollback contract must be approved by the +HumanAndMachines/Lazurio owner in a separate planning flow. diff --git a/creator-kit/RELEASE_NOTES.md b/creator-kit/RELEASE_NOTES.md new file mode 100644 index 0000000..608cd12 --- /dev/null +++ b/creator-kit/RELEASE_NOTES.md @@ -0,0 +1,9 @@ +# Spectoda Creator Kit 0.1.0-rc.1 + +This is a review candidate, not a release. It contains two synthetic English +documents used to prove deterministic normalization, source locking, indexes, +checksums and agent-objective evaluation. + +Real Documentation bodies are not included. A public release remains gated on +redistribution licensing, exact-head review, native Lazurio distribution +contract readiness and an isolated Codex/Claude pilot. diff --git a/creator-kit/bundle.json b/creator-kit/bundle.json new file mode 100644 index 0000000..913c5de --- /dev/null +++ b/creator-kit/bundle.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "creator-kit-bundle.v1", + "name": "Spectoda Creator Kit", + "version": "0.1.0-rc.1", + "status": "candidate", + "locale": "en", + "contentScope": "synthetic-fixture", + "sourceOfTruth": "Spectoda/documentation", + "transport": { + "apiVersion": "github-release-v1", + "stableChannelDescriptor": "stable-channel.json" + } +} diff --git a/creator-kit/checksums.sha256 b/creator-kit/checksums.sha256 new file mode 100644 index 0000000..08642bd --- /dev/null +++ b/creator-kit/checksums.sha256 @@ -0,0 +1,19 @@ +0605e1067514814ba301f214cf32e1db875a0b8649d4ae1e3cd1ecd6417fe6a8 RELEASE_NOTES.md +1d90d0e12413a8dc658ac750ebb7f903217b146c2141f41d41a870a2f6a56bd8 schemas/creator-kit-source-lock.v1.schema.json +2376f5bbad4d4696a43c635180908bd5c128f8e5a58c2eea544cbce7ac188e6b schemas/creator-kit-bundle.v1.schema.json +242b8ea3d8343e0479aafa7abcea3c5d02298b083a449e4feee1516a9f6a7494 AGENTS.md +25090f867683901852fb23075829dff154741a7427acd8592266dd79765d7082 indexes/documents.json +29be85035a878a415590ea1578068337f552e0b8eae729745e2a3427ed7ce4a7 schemas/creator-kit-licenses.v1.schema.json +365db466876730d311e98f5eac2b87309fe81aa1ecbc8caa849cdd372b23e291 manifest.json +41f576a534c5277269c3c969eb30d6f3e9b8f8112f04eb2ea28c0875a3a0aeb2 stable-channel.json +7a2583955a991b5d5bdebcc505ab8e783831310f308be01b976cd77a46e8af6e indexes/terms.json +8f347465f826e28d373624f0622394b78a8854ad7be24d088973a192be268bf3 README.md +9f9e115705c24bece3a9af6f1ae9473cb67a45284a4f2e79dae45c995401e324 compatibility.json +ae630cdfe9c3ed6afb970a305100fca7aaf2c0d53fb918d0ae3cdd593ff24aa0 licenses.json +b673c402a7494c1a6f70ab5343e2a502aa1b818927916424c7624d44399e3035 selection.json +bae96c2102fcbfc9ff5afc62605232193a308761d5bdb62df1995c03aea68fb8 documents/en/synthetic-creator.mdx +c17d152a03d506fad0613ea55dc32a34b091bda200e699f873c6116ba03eb626 source-lock.json +c79a442b75413ee3f2c5db22ddb5b6bda6332db0c1f5e73531ef80c9092ee6ad documents/en/synthetic-creator.md +d2cc422c32a525f1834ef7fe997bf291c45aa783afe95e3a2af6439b0254bce8 bundle.json +f79f7ba16e5bfd8598d95dd778cccb2377a3c123913cc0a74a1c8517e6b261ad schemas/creator-kit-manifest.v1.schema.json +f93711fe6420a97834eae2b9f48d43e595077aea880f5b21a81657ab9c024d2d schemas/creator-kit-compatibility.v1.schema.json diff --git a/creator-kit/compatibility.json b/creator-kit/compatibility.json new file mode 100644 index 0000000..ff23af3 --- /dev/null +++ b/creator-kit/compatibility.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": "creator-kit-compatibility.v1", + "bundleName": "Spectoda Creator Kit", + "formatVersion": 1, + "locale": "en", + "agents": [ + "codex", + "claude" + ], + "transport": "github-release-v1", + "updatePolicy": "exact-version-partner-approved" +} diff --git a/creator-kit/documents/en/synthetic-creator.md b/creator-kit/documents/en/synthetic-creator.md new file mode 100644 index 0000000..6c56e2f --- /dev/null +++ b/creator-kit/documents/en/synthetic-creator.md @@ -0,0 +1,6 @@ +# Synthetic creator workflow + +This fixture describes a small creator workflow without product credentials, +customer context or private Network state. + +Use the [details fixture](./synthetic-creator.mdx) for the companion example. diff --git a/creator-kit/documents/en/synthetic-creator.mdx b/creator-kit/documents/en/synthetic-creator.mdx new file mode 100644 index 0000000..16e17dd --- /dev/null +++ b/creator-kit/documents/en/synthetic-creator.mdx @@ -0,0 +1,4 @@ +# Synthetic creator workflow details + +MDX is treated as Markdown-shaped input. The bundle never evaluates components +or executes imports and exports. diff --git a/creator-kit/indexes/documents.json b/creator-kit/indexes/documents.json new file mode 100644 index 0000000..2c001bc --- /dev/null +++ b/creator-kit/indexes/documents.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": "creator-kit-document-index.v1", + "documents": [ + { + "id": "en__synthetic-creator__md", + "sourcePath": "en/synthetic-creator.md", + "path": "documents/en/synthetic-creator.md", + "title": "Synthetic creator workflow", + "summary": "A public-safe fixture describing a deterministic creator workflow.", + "format": "md", + "sha256": "c79a442b75413ee3f2c5db22ddb5b6bda6332db0c1f5e73531ef80c9092ee6ad", + "agentExport": { + "include": true, + "audience": "partner-maker", + "license": "spectoda-creator-kit-synthetic", + "sourceLock": "synthetic-fixture-2026-08-07" + } + }, + { + "id": "en__synthetic-creator__mdx", + "sourcePath": "en/synthetic-creator.mdx", + "path": "documents/en/synthetic-creator.mdx", + "title": "Synthetic creator workflow details", + "summary": "A second public-safe fixture proving MDX is normalized without executing components.", + "format": "mdx", + "sha256": "bae96c2102fcbfc9ff5afc62605232193a308761d5bdb62df1995c03aea68fb8", + "agentExport": { + "include": true, + "audience": "partner-maker", + "license": "spectoda-creator-kit-synthetic", + "sourceLock": "synthetic-fixture-2026-08-07" + } + } + ] +} diff --git a/creator-kit/indexes/terms.json b/creator-kit/indexes/terms.json new file mode 100644 index 0000000..5822280 --- /dev/null +++ b/creator-kit/indexes/terms.json @@ -0,0 +1,215 @@ +{ + "schemaVersion": "creator-kit-term-index.v1", + "terms": [ + { + "term": "and", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "bundle", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "companion", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "components", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "context", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "creator", + "documentIds": [ + "en__synthetic-creator__md", + "en__synthetic-creator__mdx" + ] + }, + { + "term": "credentials", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "customer", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "describes", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "details", + "documentIds": [ + "en__synthetic-creator__md", + "en__synthetic-creator__mdx" + ] + }, + { + "term": "evaluates", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "example", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "executes", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "exports", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "fixture", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "for", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "imports", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "input", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "markdown-shaped", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "mdx", + "documentIds": [ + "en__synthetic-creator__md", + "en__synthetic-creator__mdx" + ] + }, + { + "term": "network", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "never", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "private", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "product", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "small", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "state", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "synthetic", + "documentIds": [ + "en__synthetic-creator__md", + "en__synthetic-creator__mdx" + ] + }, + { + "term": "synthetic-creator", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "the", + "documentIds": [ + "en__synthetic-creator__md", + "en__synthetic-creator__mdx" + ] + }, + { + "term": "this", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "treated", + "documentIds": [ + "en__synthetic-creator__mdx" + ] + }, + { + "term": "use", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "without", + "documentIds": [ + "en__synthetic-creator__md" + ] + }, + { + "term": "workflow", + "documentIds": [ + "en__synthetic-creator__md", + "en__synthetic-creator__mdx" + ] + } + ] +} diff --git a/creator-kit/licenses.json b/creator-kit/licenses.json new file mode 100644 index 0000000..58fe42d --- /dev/null +++ b/creator-kit/licenses.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": "creator-kit-licenses.v1", + "publicationAllowed": false, + "realDocumentationExportAllowed": false, + "scope": "synthetic-fixture-only", + "entries": [ + "spectoda-creator-kit-synthetic" + ] +} diff --git a/creator-kit/manifest.json b/creator-kit/manifest.json new file mode 100644 index 0000000..0457b20 --- /dev/null +++ b/creator-kit/manifest.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": "creator-kit-manifest.v1", + "bundleName": "Spectoda Creator Kit", + "bundleVersion": "0.1.0-rc.1", + "locale": "en", + "contentScope": "synthetic-fixture", + "source": { + "repository": "synthetic-fixture", + "commit": "6142c40b0868298150e93e6ed2bf19de46dc1380" + }, + "documents": [ + { + "id": "en__synthetic-creator__md", + "sourcePath": "en/synthetic-creator.md", + "path": "documents/en/synthetic-creator.md", + "title": "Synthetic creator workflow", + "summary": "A public-safe fixture describing a deterministic creator workflow.", + "format": "md", + "sourceSha256": "ee17f526b4a658f911459f877c942a087f946758cbe38abd3eaed766a42d5cc6", + "sha256": "c79a442b75413ee3f2c5db22ddb5b6bda6332db0c1f5e73531ef80c9092ee6ad", + "agentExport": { + "include": true, + "audience": "partner-maker", + "license": "spectoda-creator-kit-synthetic", + "sourceLock": "synthetic-fixture-2026-08-07" + } + }, + { + "id": "en__synthetic-creator__mdx", + "sourcePath": "en/synthetic-creator.mdx", + "path": "documents/en/synthetic-creator.mdx", + "title": "Synthetic creator workflow details", + "summary": "A second public-safe fixture proving MDX is normalized without executing components.", + "format": "mdx", + "sourceSha256": "2aedb71926e038227aede2f41196fe0845747bf020ff150ab0c68a18cb846136", + "sha256": "bae96c2102fcbfc9ff5afc62605232193a308761d5bdb62df1995c03aea68fb8", + "agentExport": { + "include": true, + "audience": "partner-maker", + "license": "spectoda-creator-kit-synthetic", + "sourceLock": "synthetic-fixture-2026-08-07" + } + } + ] +} diff --git a/creator-kit/schemas/creator-kit-bundle.v1.schema.json b/creator-kit/schemas/creator-kit-bundle.v1.schema.json new file mode 100644 index 0000000..18980d2 --- /dev/null +++ b/creator-kit/schemas/creator-kit-bundle.v1.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.spectoda.com/creator-kit/bundle.v1.json", + "title": "Spectoda Creator Kit bundle", + "type": "object", + "required": ["schemaVersion", "name", "version", "status", "locale", "contentScope", "sourceOfTruth", "transport"], + "properties": { + "schemaVersion": { "const": "creator-kit-bundle.v1" }, + "name": { "const": "Spectoda Creator Kit" }, + "version": { "type": "string" }, + "status": { "enum": ["candidate", "released", "revoked"] }, + "locale": { "const": "en" }, + "contentScope": { "enum": ["synthetic-fixture", "licensed-documentation"] }, + "sourceOfTruth": { "const": "Spectoda/documentation" }, + "transport": { "type": "object" } + }, + "additionalProperties": false +} diff --git a/creator-kit/schemas/creator-kit-compatibility.v1.schema.json b/creator-kit/schemas/creator-kit-compatibility.v1.schema.json new file mode 100644 index 0000000..1e5835d --- /dev/null +++ b/creator-kit/schemas/creator-kit-compatibility.v1.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.spectoda.com/creator-kit/compatibility.v1.json", + "title": "Spectoda Creator Kit compatibility contract", + "type": "object", + "required": ["schemaVersion", "bundleName", "formatVersion", "locale", "agents", "transport", "updatePolicy"], + "properties": { + "schemaVersion": { "const": "creator-kit-compatibility.v1" }, + "bundleName": { "const": "Spectoda Creator Kit" }, + "formatVersion": { "const": 1 }, + "locale": { "const": "en" }, + "agents": { "type": "array" }, + "transport": { "const": "github-release-v1" }, + "updatePolicy": { "const": "exact-version-partner-approved" } + }, + "additionalProperties": false +} diff --git a/creator-kit/schemas/creator-kit-licenses.v1.schema.json b/creator-kit/schemas/creator-kit-licenses.v1.schema.json new file mode 100644 index 0000000..0fc5014 --- /dev/null +++ b/creator-kit/schemas/creator-kit-licenses.v1.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.spectoda.com/creator-kit/licenses.v1.json", + "title": "Spectoda Creator Kit license posture", + "type": "object", + "required": ["schemaVersion", "publicationAllowed", "realDocumentationExportAllowed", "scope", "entries"], + "properties": { + "schemaVersion": { "const": "creator-kit-licenses.v1" }, + "publicationAllowed": { "type": "boolean" }, + "realDocumentationExportAllowed": { "const": false }, + "scope": { "type": "string" }, + "entries": { "type": "array" } + }, + "additionalProperties": false +} diff --git a/creator-kit/schemas/creator-kit-manifest.v1.schema.json b/creator-kit/schemas/creator-kit-manifest.v1.schema.json new file mode 100644 index 0000000..6dd3721 --- /dev/null +++ b/creator-kit/schemas/creator-kit-manifest.v1.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.spectoda.com/creator-kit/manifest.v1.json", + "title": "Spectoda Creator Kit manifest", + "type": "object", + "required": ["schemaVersion", "bundleName", "bundleVersion", "locale", "contentScope", "source", "documents"], + "properties": { + "schemaVersion": { "const": "creator-kit-manifest.v1" }, + "bundleName": { "const": "Spectoda Creator Kit" }, + "bundleVersion": { "type": "string" }, + "locale": { "const": "en" }, + "contentScope": { "enum": ["synthetic-fixture", "licensed-documentation"] }, + "source": { + "type": "object", + "required": ["repository", "commit"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" } + }, + "additionalProperties": false + }, + "documents": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "path", "sourcePath", "title", "summary", "format", "sha256", "agentExport", "sourceSha256"], + "properties": { + "id": { "type": "string" }, + "path": { "type": "string" }, + "sourcePath": { "type": "string" }, + "title": { "type": "string" }, + "summary": { "type": "string" }, + "format": { "enum": ["md", "mdx"] }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "sourceSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "agentExport": { + "type": "object", + "required": ["include", "audience", "license", "sourceLock"], + "properties": { + "include": { "const": true }, + "audience": { "const": "partner-maker" }, + "license": { "type": "string" }, + "sourceLock": { "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/creator-kit/schemas/creator-kit-source-lock.v1.schema.json b/creator-kit/schemas/creator-kit-source-lock.v1.schema.json new file mode 100644 index 0000000..1b07056 --- /dev/null +++ b/creator-kit/schemas/creator-kit-source-lock.v1.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.spectoda.com/creator-kit/source-lock.v1.json", + "title": "Spectoda Creator Kit source lock", + "type": "object", + "required": ["schemaVersion", "repository", "commit", "files"], + "properties": { + "schemaVersion": { "const": "creator-kit-source-lock.v1" }, + "repository": { "type": "string" }, + "commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + "files": { "type": "array" } + }, + "additionalProperties": false +} diff --git a/creator-kit/selection.json b/creator-kit/selection.json new file mode 100644 index 0000000..ff7a002 --- /dev/null +++ b/creator-kit/selection.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": "creator-kit-selection.v1", + "failClosed": true, + "selected": [ + { + "sourcePath": "en/synthetic-creator.md", + "agentExport": { + "include": true, + "audience": "partner-maker", + "license": "spectoda-creator-kit-synthetic", + "sourceLock": "synthetic-fixture-2026-08-07" + } + }, + { + "sourcePath": "en/synthetic-creator.mdx", + "agentExport": { + "include": true, + "audience": "partner-maker", + "license": "spectoda-creator-kit-synthetic", + "sourceLock": "synthetic-fixture-2026-08-07" + } + } + ] +} diff --git a/creator-kit/source-lock.json b/creator-kit/source-lock.json new file mode 100644 index 0000000..a3679e2 --- /dev/null +++ b/creator-kit/source-lock.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": "creator-kit-source-lock.v1", + "repository": "synthetic-fixture", + "commit": "6142c40b0868298150e93e6ed2bf19de46dc1380", + "files": [ + { + "sourcePath": "en/synthetic-creator.md", + "sourceSha256": "ee17f526b4a658f911459f877c942a087f946758cbe38abd3eaed766a42d5cc6", + "normalizedSha256": "c79a442b75413ee3f2c5db22ddb5b6bda6332db0c1f5e73531ef80c9092ee6ad", + "license": "spectoda-creator-kit-synthetic" + }, + { + "sourcePath": "en/synthetic-creator.mdx", + "sourceSha256": "2aedb71926e038227aede2f41196fe0845747bf020ff150ab0c68a18cb846136", + "normalizedSha256": "bae96c2102fcbfc9ff5afc62605232193a308761d5bdb62df1995c03aea68fb8", + "license": "spectoda-creator-kit-synthetic" + } + ] +} diff --git a/creator-kit/stable-channel.json b/creator-kit/stable-channel.json new file mode 100644 index 0000000..ea65aa5 --- /dev/null +++ b/creator-kit/stable-channel.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": "creator-kit-stable-channel.v1", + "name": "Spectoda Creator Kit", + "channel": "stable", + "state": "unpublished", + "version": null, + "digest": null, + "releaseUrl": null, + "requiredHumanApproval": true, + "updatePolicy": "exact-version-partner-approved", + "rollback": { + "supported": true, + "requiresPartnerApproval": true + } +} From 59ccf66f5cb8eec758dd869655c4f733e8d66782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sat, 8 Aug 2026 01:51:51 +0200 Subject: [PATCH 03/10] CI: validate locked source history in candidate builds Fetch the complete repository history before checking that the committed Creator Kit source lock is an ancestor of the reviewed head. This keeps the candidate workflow fail-closed on fresh GitHub Actions checkouts and documents the guard for future maintenance. --- .github/workflows/creator-kit-candidate.yml | 1 + CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/creator-kit-candidate.yml b/.github/workflows/creator-kit-candidate.yml index aac2da8..bea89c0 100644 --- a/.github/workflows/creator-kit-candidate.yml +++ b/.github/workflows/creator-kit-candidate.yml @@ -23,6 +23,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + fetch-depth: 0 persist-credentials: false - uses: oven-sh/setup-bun@v2 - name: Resolve the committed source lock diff --git a/CHANGELOG.md b/CHANGELOG.md index c626c90..7d52dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ Date-based versions use `YYYYMMDD`. - `bun run creator-kit:check` - `bun run creator-kit:build` +- Candidate CI fetches full history before checking the committed source lock, + so ancestry validation remains fail-closed on a fresh GitHub Actions checkout. ## 20260702 From 8c4641d94d825be45e83d114c3c08666d2c00a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sat, 8 Aug 2026 15:02:23 +0200 Subject: [PATCH 04/10] Creator Kit: Source the Event Player plugin from Examples Make the public Event Player example the single editable plugin source and copy its reviewed files byte-for-byte into Creator Kit candidates from one exact commit.\n\nThe fail-closed selection, source hashes, compatibility metadata, agent guidance, and release license gate keep generated bundles reproducible and prevent Studio or Creator Kit from becoming a second authoring copy. --- .github/workflows/creator-kit-candidate.yml | 1 + .github/workflows/creator-kit-release.yml | 4 + creator-kit-evaluations/questions.json | 8 ++ creator-kit-evaluations/responses/claude.json | 6 + creator-kit-evaluations/responses/codex.json | 6 + creator-kit-templates/AGENTS.md | 7 +- creator-kit-templates/README.md | 13 +- creator-kit-templates/RELEASE_NOTES.md | 8 +- .../schemas/creator-kit-bundle.v1.schema.json | 12 +- .../creator-kit-compatibility.v1.schema.json | 6 +- .../creator-kit-manifest.v1.schema.json | 45 +++++- .../creator-kit-source-lock.v1.schema.json | 16 ++- .../creator-kit/build-release-candidate.mjs | 12 +- scripts/creator-kit/build.mjs | 132 ++++++++++++++++-- scripts/creator-kit/creator-kit.test.mjs | 18 ++- scripts/creator-kit/validate.mjs | 38 ++++- 16 files changed, 290 insertions(+), 42 deletions(-) diff --git a/.github/workflows/creator-kit-candidate.yml b/.github/workflows/creator-kit-candidate.yml index bea89c0..3a12c98 100644 --- a/.github/workflows/creator-kit-candidate.yml +++ b/.github/workflows/creator-kit-candidate.yml @@ -7,6 +7,7 @@ on: - "creator-kit-templates/**" - "scripts/creator-kit/**" - "creator-kit/**" + - "data/v2/examples/player-bundle-ab-complete-cue-tracks/**" - "package.json" - ".github/workflows/creator-kit-*.yml" diff --git a/.github/workflows/creator-kit-release.yml b/.github/workflows/creator-kit-release.yml index 17659a1..d644631 100644 --- a/.github/workflows/creator-kit-release.yml +++ b/.github/workflows/creator-kit-release.yml @@ -64,6 +64,10 @@ jobs: - name: Verify committed candidate matches the reviewed generator run: git diff --exit-code -- creator-kit - run: bun run creator-kit:check + - name: Require an explicit publishable license posture + run: | + set -euo pipefail + jq -e '.publicationAllowed == true' creator-kit/licenses.json - name: Create release assets locally env: CREATOR_KIT_VERSION: ${{ inputs.version }} diff --git a/creator-kit-evaluations/questions.json b/creator-kit-evaluations/questions.json index 9ad2a4c..fcf0f6e 100644 --- a/creator-kit-evaluations/questions.json +++ b/creator-kit-evaluations/questions.json @@ -1,4 +1,12 @@ [ + { + "id": "event-player-plugin", + "prompt": "Where is the Berry plugin required by the Studio Event Player?", + "expected": { + "abstain": false, + "citation": "examples/player-bundle-ab-complete-cue-tracks/README.md" + } + }, { "id": "bundle-scope", "prompt": "What is the synthetic creator workflow fixture?", diff --git a/creator-kit-evaluations/responses/claude.json b/creator-kit-evaluations/responses/claude.json index 213cf56..7af82e3 100644 --- a/creator-kit-evaluations/responses/claude.json +++ b/creator-kit-evaluations/responses/claude.json @@ -1,6 +1,12 @@ { "agent": "claude", "responses": [ + { + "id": "event-player-plugin", + "answer": "The required Project Berry source is player-bundle.be in the locked Event Player example; it must be added to each Controller Project.", + "citations": ["examples/player-bundle-ab-complete-cue-tracks/README.md"], + "abstained": false + }, { "id": "bundle-scope", "answer": "The fixture is a small creator workflow with no private Network state.", diff --git a/creator-kit-evaluations/responses/codex.json b/creator-kit-evaluations/responses/codex.json index 40f29b3..dc65c29 100644 --- a/creator-kit-evaluations/responses/codex.json +++ b/creator-kit-evaluations/responses/codex.json @@ -1,6 +1,12 @@ { "agent": "codex", "responses": [ + { + "id": "event-player-plugin", + "answer": "Add the locked player-bundle.be Project Berry source to every Controller Project; Studio bundle upload does not install it.", + "citations": ["examples/player-bundle-ab-complete-cue-tracks/README.md"], + "abstained": false + }, { "id": "bundle-scope", "answer": "The bundle describes a small public-safe creator workflow.", diff --git a/creator-kit-templates/AGENTS.md b/creator-kit-templates/AGENTS.md index 407a038..740b32d 100644 --- a/creator-kit-templates/AGENTS.md +++ b/creator-kit-templates/AGENTS.md @@ -5,7 +5,7 @@ You are reading a pinned, read-only knowledge bundle. 1. Read `bundle.json`, `compatibility.json` and `source-lock.json` before using content. 2. Search `indexes/documents.json` and `indexes/terms.json`; cite the exact - `documents/...` file for every factual answer. + `documents/...` or `examples/...` file for every factual answer. 3. Treat the visible version and checksum digest as part of the answer context. 4. Abstain or ask for a current source when the bundle does not contain the answer, when the question concerns current availability or policy, or when @@ -19,3 +19,8 @@ You are reading a pinned, read-only knowledge bundle. Codex and Claude are supported as read-only consumers. Embeddings, dynamic hosted RAG, a central MCP gateway and a writable Documentation API are not part of Creator Kit v1. + +The canonical copy-ready firmware 0.12.11 Event Player integration is exposed +at `examples/player-bundle-ab-complete-cue-tracks/`. Use its README for setup +and its `player-bundle.be` as the Project Berry source. Do not rewrite or +silently modernize that locked example. diff --git a/creator-kit-templates/README.md b/creator-kit-templates/README.md index 7b7cd3e..357363b 100644 --- a/creator-kit-templates/README.md +++ b/creator-kit-templates/README.md @@ -2,7 +2,7 @@ This directory is a reviewable **release candidate** for the English Spectoda Creator Kit. It is read-only and intentionally contains synthetic public-safe -fixture content. It is not a published GitHub Release and it must not be +fixture content plus one public MIT-licensed Event Player example. It is not a published GitHub Release and it must not be installed into a partner Organization until a Steward/Admin approves a release and the Documentation redistribution license is recorded. @@ -11,8 +11,10 @@ The bundle is generated from one exact source commit. Verify the version, are exact-version and partner-approved. `stable-channel.json` is the v1 read-only transport descriptor; its current state is `unpublished`. -The Documentation repository remains the private content authority. This public -repository is only the reviewed distribution surface. Partner-local product +The Documentation repository remains the private document authority. The +copy-ready Event Player plugin under +`examples/player-bundle-ab-complete-cue-tracks/` is copied byte-for-byte from +its canonical `Spectoda/examples` source at the locked commit. Partner-local product knowledge belongs in a separate sibling directory and is never overwritten by Creator Kit updates or rollback. @@ -24,6 +26,11 @@ abstain on private partner-local questions and request a current source for time-sensitive information. The bundle has no write path to Documentation or to an Organization. +For the firmware 0.12.11 Event Player setup, cite and follow +`examples/player-bundle-ab-complete-cue-tracks/README.md`. Add the accompanying +`player-bundle.be` to each Controller Project; uploading a Player bundle does +not install the Project Berry plugin. + ## Supported transport v1 uses GitHub Releases plus a stable-channel descriptor. A native Lazurio diff --git a/creator-kit-templates/RELEASE_NOTES.md b/creator-kit-templates/RELEASE_NOTES.md index 608cd12..68c5123 100644 --- a/creator-kit-templates/RELEASE_NOTES.md +++ b/creator-kit-templates/RELEASE_NOTES.md @@ -1,8 +1,10 @@ -# Spectoda Creator Kit 0.1.0-rc.1 +# Spectoda Creator Kit candidate This is a review candidate, not a release. It contains two synthetic English -documents used to prove deterministic normalization, source locking, indexes, -checksums and agent-objective evaluation. +documents used to prove deterministic normalization, plus the exact +MIT-licensed firmware 0.12.11 Event Player example selected from +`Spectoda/examples`. Both are source locked, checksummed, and covered by +agent-objective evaluation. Real Documentation bodies are not included. A public release remains gated on redistribution licensing, exact-head review, native Lazurio distribution diff --git a/creator-kit-templates/schemas/creator-kit-bundle.v1.schema.json b/creator-kit-templates/schemas/creator-kit-bundle.v1.schema.json index 18980d2..920117f 100644 --- a/creator-kit-templates/schemas/creator-kit-bundle.v1.schema.json +++ b/creator-kit-templates/schemas/creator-kit-bundle.v1.schema.json @@ -10,8 +10,16 @@ "version": { "type": "string" }, "status": { "enum": ["candidate", "released", "revoked"] }, "locale": { "const": "en" }, - "contentScope": { "enum": ["synthetic-fixture", "licensed-documentation"] }, - "sourceOfTruth": { "const": "Spectoda/documentation" }, + "contentScope": { "enum": ["synthetic-fixture-with-public-examples", "licensed-documentation"] }, + "sourceOfTruth": { + "type": "object", + "required": ["documents", "examples"], + "properties": { + "documents": { "type": "string" }, + "examples": { "const": "Spectoda/examples" } + }, + "additionalProperties": false + }, "transport": { "type": "object" } }, "additionalProperties": false 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 1e5835d..b1ba10d 100644 --- a/creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json +++ b/creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json @@ -3,7 +3,7 @@ "$id": "https://schemas.spectoda.com/creator-kit/compatibility.v1.json", "title": "Spectoda Creator Kit compatibility contract", "type": "object", - "required": ["schemaVersion", "bundleName", "formatVersion", "locale", "agents", "transport", "updatePolicy"], + "required": ["schemaVersion", "bundleName", "formatVersion", "locale", "agents", "transport", "updatePolicy", "firmware", "wasm"], "properties": { "schemaVersion": { "const": "creator-kit-compatibility.v1" }, "bundleName": { "const": "Spectoda Creator Kit" }, @@ -11,7 +11,9 @@ "locale": { "const": "en" }, "agents": { "type": "array" }, "transport": { "const": "github-release-v1" }, - "updatePolicy": { "const": "exact-version-partner-approved" } + "updatePolicy": { "const": "exact-version-partner-approved" }, + "firmware": { "const": "0.12.11" }, + "wasm": { "const": "DEBUG_UNIVERSAL_0.12.11_20260808" } }, "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 6dd3721..5c5d7a4 100644 --- a/creator-kit-templates/schemas/creator-kit-manifest.v1.schema.json +++ b/creator-kit-templates/schemas/creator-kit-manifest.v1.schema.json @@ -3,13 +3,13 @@ "$id": "https://schemas.spectoda.com/creator-kit/manifest.v1.json", "title": "Spectoda Creator Kit manifest", "type": "object", - "required": ["schemaVersion", "bundleName", "bundleVersion", "locale", "contentScope", "source", "documents"], + "required": ["schemaVersion", "bundleName", "bundleVersion", "locale", "contentScope", "source", "documents", "examples"], "properties": { "schemaVersion": { "const": "creator-kit-manifest.v1" }, "bundleName": { "const": "Spectoda Creator Kit" }, "bundleVersion": { "type": "string" }, "locale": { "const": "en" }, - "contentScope": { "enum": ["synthetic-fixture", "licensed-documentation"] }, + "contentScope": { "enum": ["synthetic-fixture-with-public-examples", "licensed-documentation"] }, "source": { "type": "object", "required": ["repository", "commit"], @@ -47,6 +47,47 @@ }, "additionalProperties": false } + }, + "examples": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "path", "title", "topic", "audience", "license", "stability", "compatibility", "files"], + "properties": { + "id": { "type": "string" }, + "path": { "type": "string" }, + "title": { "type": "string" }, + "topic": { "type": "string" }, + "audience": { "const": "partner-maker" }, + "license": { "const": "MIT" }, + "stability": { "const": "firmware-0.12.11" }, + "compatibility": { + "type": "object", + "required": ["firmware", "wasm"], + "properties": { + "firmware": { "const": "0.12.11" }, + "wasm": { "const": "DEBUG_UNIVERSAL_0.12.11_20260808" } + }, + "additionalProperties": false + }, + "files": { + "type": "array", + "items": { + "type": "object", + "required": ["sourcePath", "path", "format", "sourceSha256", "sha256"], + "properties": { + "sourcePath": { "type": "string" }, + "path": { "type": "string" }, + "format": { "enum": ["md", "yaml", "be"] }, + "sourceSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } } }, "additionalProperties": false diff --git a/creator-kit-templates/schemas/creator-kit-source-lock.v1.schema.json b/creator-kit-templates/schemas/creator-kit-source-lock.v1.schema.json index 1b07056..4b7ed8e 100644 --- a/creator-kit-templates/schemas/creator-kit-source-lock.v1.schema.json +++ b/creator-kit-templates/schemas/creator-kit-source-lock.v1.schema.json @@ -8,7 +8,21 @@ "schemaVersion": { "const": "creator-kit-source-lock.v1" }, "repository": { "type": "string" }, "commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, - "files": { "type": "array" } + "files": { + "type": "array", + "items": { + "type": "object", + "required": ["sourcePath", "sourceSha256", "bundleSha256", "license"], + "properties": { + "sourcePath": { "type": "string" }, + "sourceSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "bundleSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "license": { "type": "string" }, + "role": { "const": "selection" } + }, + "additionalProperties": false + } + } }, "additionalProperties": false } diff --git a/scripts/creator-kit/build-release-candidate.mjs b/scripts/creator-kit/build-release-candidate.mjs index afc3829..dee697a 100644 --- a/scripts/creator-kit/build-release-candidate.mjs +++ b/scripts/creator-kit/build-release-candidate.mjs @@ -3,7 +3,7 @@ import { readFile, rm, writeFile, mkdir } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { buildSyntheticBundle, createDeterministicTar, sha256 } from "./build.mjs"; +import { buildCreatorKitCandidate, createDeterministicTar, sha256 } from "./build.mjs"; import { validateBundle } from "./validate.mjs"; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); @@ -26,10 +26,10 @@ function argValue(argv, name, fallback) { export async function buildReleaseCandidate({ outputDir = path.join(ROOT, "creator-kit"), releaseDir = path.join(ROOT, ".creator-kit-tmp", "release"), - version = "0.1.0-rc.1", + version = "0.1.0-rc.2", sourceCommit = gitHead(), } = {}) { - const build = await buildSyntheticBundle({ outputDir, version, sourceCommit }); + const build = await buildCreatorKitCandidate({ outputDir, version, sourceCommit }); const validation = await validateBundle(outputDir); await rm(releaseDir, { recursive: true, force: true }); await mkdir(releaseDir, { recursive: true }); @@ -43,9 +43,9 @@ export async function buildReleaseCandidate({ status: "candidate", releaseNotPublished: true, source: { - repository: "synthetic-fixture", + repository: "Spectoda/examples", commit: sourceCommit, - examplesCommit: gitHead(), + generatorCommit: gitHead(), }, exporterVersion: build.exporterVersion, bundleDigest: validation.checksumDigest, @@ -62,7 +62,7 @@ async function main() { const result = await buildReleaseCandidate({ outputDir: argValue(argv, "output", path.join(ROOT, "creator-kit")), releaseDir: argValue(argv, "release-dir", path.join(ROOT, ".creator-kit-tmp", "release")), - version: argValue(argv, "version", "0.1.0-rc.1"), + version: argValue(argv, "version", "0.1.0-rc.2"), sourceCommit: argValue(argv, "source-commit", gitHead()), }); console.log(JSON.stringify({ diff --git a/scripts/creator-kit/build.mjs b/scripts/creator-kit/build.mjs index cae29e3..dcc1aac 100644 --- a/scripts/creator-kit/build.mjs +++ b/scripts/creator-kit/build.mjs @@ -14,6 +14,10 @@ const DEFAULT_SOURCE = path.join(SCRIPT_ROOT, "creator-kit-fixtures"); const NEGATIVE_SOURCE = path.join(SCRIPT_ROOT, "creator-kit-negative-fixtures"); const DEFAULT_OUTPUT = path.join(SCRIPT_ROOT, "creator-kit"); const SYNTHETIC_SOURCE_LOCK = "synthetic-fixture-2026-08-07"; +const PUBLIC_EXAMPLE_ROOT = path.join(SCRIPT_ROOT, "data/v2/examples/player-bundle-ab-complete-cue-tracks"); +const PUBLIC_EXAMPLE_SELECTION = path.join(PUBLIC_EXAMPLE_ROOT, "creator-kit.json"); +const PUBLIC_EXAMPLE_DESTINATION = "examples/player-bundle-ab-complete-cue-tracks"; +const EXAMPLES_REPOSITORY = "Spectoda/examples"; export class CreatorKitBuildError extends Error { constructor(code, message) { @@ -283,14 +287,101 @@ function verifySourceAtCommit(file, sourcePath, sourceCommit, raw) { if (sha256(locked) !== sha256(raw)) fail("source_lock_mismatch", `${sourcePath} bytes differ from source commit ${sourceCommit}`); } -export async function buildSyntheticBundle({ +function assertExactKeys(value, expected, sourcePath) { + if (!value || typeof value !== "object" || Array.isArray(value)) fail("invalid_example_selection", `${sourcePath} must be an object`); + const actual = Object.keys(value).sort(); + const wanted = [...expected].sort(); + if (actual.join("\n") !== wanted.join("\n")) fail("invalid_example_selection", `${sourcePath} has unsupported or missing fields`); +} + +async function loadPublicExamples(sourceCommit) { + const selectionRaw = await readFile(PUBLIC_EXAMPLE_SELECTION); + verifySourceAtCommit(PUBLIC_EXAMPLE_SELECTION, relativePosix(SCRIPT_ROOT, PUBLIC_EXAMPLE_SELECTION), sourceCommit, selectionRaw); + let selection; + try { + selection = JSON.parse(selectionRaw.toString("utf8")); + } catch { + fail("invalid_example_selection", "The public example selection is not valid JSON"); + } + assertExactKeys(selection, ["schemaVersion", "include", "audience", "license", "topic", "stability", "compatibility", "files"], "creator-kit.json"); + assertExactKeys(selection.compatibility ?? {}, ["firmware", "wasm"], "creator-kit.json compatibility"); + if ( + selection.schemaVersion !== "creator-kit-example.v1" || + selection.include !== true || + selection.audience !== "partner-maker" || + selection.license !== "MIT" || + selection.topic !== "studio-event-player" || + selection.stability !== "firmware-0.12.11" || + selection.compatibility.firmware !== "0.12.11" || + selection.compatibility.wasm !== "DEBUG_UNIVERSAL_0.12.11_20260808" || + !Array.isArray(selection.files) || + selection.files.length === 0 + ) { + fail("invalid_example_selection", "The public Event Player example has not explicitly opted into the reviewed Creator Kit contract"); + } + const uniqueFiles = new Set(selection.files); + if (uniqueFiles.size !== selection.files.length) fail("invalid_example_selection", "The public example selection contains duplicate files"); + const files = []; + for (const relative of selection.files) { + if ( + typeof relative !== "string" || + relative !== path.posix.basename(relative) || + !/^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:md|yaml|be)$/u.test(relative) + ) { + fail("invalid_example_selection", `The public example selection contains an unsafe path: ${String(relative)}`); + } + const source = path.join(PUBLIC_EXAMPLE_ROOT, relative); + if (!isStrictDescendant(PUBLIC_EXAMPLE_ROOT, source)) fail("invalid_example_selection", `${relative} escapes the public example directory`); + const raw = await readFile(source); + const sourcePath = relativePosix(SCRIPT_ROOT, source); + verifySourceAtCommit(source, sourcePath, sourceCommit, raw); + const finding = credentialFinding(raw.toString("utf8")); + if (finding) fail("public_safety", `${sourcePath} matched ${finding}`); + files.push({ + source, + sourcePath, + path: `${PUBLIC_EXAMPLE_DESTINATION}/${relative}`, + format: path.extname(relative).slice(1), + sourceSha256: sha256(raw), + sha256: sha256(raw), + bytes: raw, + }); + } + files.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath)); + const readme = files.find((file) => file.sourcePath.endsWith("/README.md")); + const title = readme?.bytes.toString("utf8").match(/^#\s+(.+)$/mu)?.[1]?.trim(); + if (!title) fail("invalid_example_selection", "The public Event Player example README needs a title"); + return { + selection: { + sourcePath: relativePosix(SCRIPT_ROOT, PUBLIC_EXAMPLE_SELECTION), + sourceSha256: sha256(selectionRaw), + bundleSha256: sha256(selectionRaw), + license: selection.license, + }, + example: { + id: "player-bundle-ab-complete-cue-tracks", + path: PUBLIC_EXAMPLE_DESTINATION, + title, + topic: selection.topic, + audience: selection.audience, + license: selection.license, + stability: selection.stability, + compatibility: selection.compatibility, + files: files.map(({ source: _source, bytes: _bytes, ...file }) => file), + }, + files, + }; +} + +export async function buildCreatorKitCandidate({ sourceRoot = DEFAULT_SOURCE, outputDir = DEFAULT_OUTPUT, - version = "0.1.0-rc.1", + version = "0.1.0-rc.2", sourceCommit = gitHead(), - sourceRepository = "synthetic-fixture", + sourceRepository = EXAMPLES_REPOSITORY, + includePublicExamples = true, } = {}) { - if (sourceRepository !== "synthetic-fixture") fail("synthetic_only", "The reviewed Examples generator accepts synthetic-fixture as its only source repository"); + if (sourceRepository !== EXAMPLES_REPOSITORY) fail("source_repository", `The reviewed generator accepts ${EXAMPLES_REPOSITORY} as its only source repository`); const source = path.resolve(sourceRoot); const output = path.resolve(outputDir); assertSafeOutputDir(output, source); @@ -341,6 +432,7 @@ export async function buildSyntheticBundle({ } for (const document of documents) assertLinks(document.content, document.sourcePath, sourcePaths); documents.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath)); + const publicExamples = includePublicExamples ? await loadPublicExamples(sourceCommit) : { selection: null, example: null, files: [] }; let staging; try { await mkdir(path.dirname(output), { recursive: true }); @@ -351,20 +443,30 @@ export async function buildSyntheticBundle({ await mkdir(path.dirname(destination), { recursive: true }); await writeFile(destination, document.content, "utf8"); } + for (const file of publicExamples.files) { + const destination = path.join(staging, file.path); + await mkdir(path.dirname(destination), { recursive: true }); + await writeFile(destination, file.bytes); + } const manifest = { schemaVersion: "creator-kit-manifest.v1", bundleName: BUNDLE_NAME, bundleVersion: version, locale: "en", - contentScope: "synthetic-fixture", + contentScope: "synthetic-fixture-with-public-examples", source: { repository: sourceRepository, commit: sourceCommit }, documents: documents.map(({ content: _content, ...document }) => document), + examples: publicExamples.example ? [publicExamples.example] : [], }; const sourceLock = { schemaVersion: "creator-kit-source-lock.v1", repository: sourceRepository, commit: sourceCommit, - files: documents.map((document) => ({ sourcePath: document.sourcePath, sourceSha256: document.sourceSha256, normalizedSha256: document.sha256, license: document.agentExport.license })), + files: [ + ...documents.map((document) => ({ sourcePath: document.sourcePath, sourceSha256: document.sourceSha256, bundleSha256: document.sha256, license: document.agentExport.license })), + ...(publicExamples.selection ? [{ ...publicExamples.selection, role: "selection" }] : []), + ...publicExamples.files.map((file) => ({ sourcePath: file.sourcePath, sourceSha256: file.sourceSha256, bundleSha256: file.sha256, license: publicExamples.example.license })), + ].sort((left, right) => left.sourcePath.localeCompare(right.sourcePath)), }; await writeJson(path.join(staging, "bundle.json"), { schemaVersion: "creator-kit-bundle.v1", @@ -372,8 +474,8 @@ export async function buildSyntheticBundle({ version, status: "candidate", locale: "en", - contentScope: "synthetic-fixture", - sourceOfTruth: "Spectoda/documentation", + contentScope: "synthetic-fixture-with-public-examples", + sourceOfTruth: { documents: "synthetic-fixture", examples: EXAMPLES_REPOSITORY }, transport: { apiVersion: "github-release-v1", stableChannelDescriptor: "stable-channel.json" }, }); await writeJson(path.join(staging, "manifest.json"), manifest); @@ -382,8 +484,8 @@ export async function buildSyntheticBundle({ schemaVersion: "creator-kit-licenses.v1", publicationAllowed: false, realDocumentationExportAllowed: false, - scope: "synthetic-fixture-only", - entries: ["spectoda-creator-kit-synthetic"], + scope: "synthetic-fixture-and-public-examples", + entries: ["spectoda-creator-kit-synthetic", "MIT"], }); await writeJson(path.join(staging, "compatibility.json"), { schemaVersion: "creator-kit-compatibility.v1", @@ -393,11 +495,14 @@ export async function buildSyntheticBundle({ agents: ["codex", "claude"], transport: "github-release-v1", updatePolicy: "exact-version-partner-approved", + firmware: publicExamples.example?.compatibility.firmware ?? null, + wasm: publicExamples.example?.compatibility.wasm ?? null, }); await writeJson(path.join(staging, "selection.json"), { schemaVersion: "creator-kit-selection.v1", failClosed: true, - selected: documents.map((document) => ({ sourcePath: document.sourcePath, agentExport: document.agentExport })), + selectedDocuments: documents.map((document) => ({ sourcePath: document.sourcePath, agentExport: document.agentExport })), + selectedExamples: publicExamples.example ? [{ id: publicExamples.example.id, selectionSourcePath: publicExamples.selection.sourcePath, files: publicExamples.example.files.map((file) => file.sourcePath) }] : [], }); await writeJson(path.join(staging, "indexes", "documents.json"), { schemaVersion: "creator-kit-document-index.v1", @@ -419,6 +524,7 @@ export async function buildSyntheticBundle({ version, sourceCommit, documentCount: documents.length, + exampleCount: publicExamples.example ? 1 : 0, totalBytes, fileCount: files.length, checksumDigest, @@ -481,10 +587,10 @@ function parseArgs(argv) { async function main() { const options = parseArgs(process.argv.slice(2)); - const result = await buildSyntheticBundle({ + const result = await buildCreatorKitCandidate({ sourceRoot: options.source ?? DEFAULT_SOURCE, outputDir: options.output ?? DEFAULT_OUTPUT, - version: options.version ?? "0.1.0-rc.1", + version: options.version ?? "0.1.0-rc.2", sourceCommit: options["source-commit"] ?? gitHead(), }); console.log(JSON.stringify(result, null, 2)); diff --git a/scripts/creator-kit/creator-kit.test.mjs b/scripts/creator-kit/creator-kit.test.mjs index 7be6263..5060ba3 100644 --- a/scripts/creator-kit/creator-kit.test.mjs +++ b/scripts/creator-kit/creator-kit.test.mjs @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import test from "node:test"; import { buildReleaseCandidate } from "./build-release-candidate.mjs"; -import { CreatorKitBuildError, buildSyntheticBundle } from "./build.mjs"; +import { CreatorKitBuildError, buildCreatorKitCandidate, sha256 } from "./build.mjs"; import { evaluateAllAgents } from "./evaluate-agents.mjs"; import { promoteStableChannel } from "./promote-stable-channel.mjs"; import { validateBundle } from "./validate.mjs"; @@ -30,6 +30,16 @@ test("double-builds a reproducible candidate and deterministic tar archive", asy assert.equal(first.provenance.releaseNotPublished, true); assert.equal(first.provenance.stableChannelState, "unpublished"); assert.equal((await validateBundle(path.join(root, "first-bundle"))).documentCount, 2); + assert.equal(first.validation.exampleCount, 1); + const sourcePlugin = await readFile(path.join(ROOT, "data/v2/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be")); + const bundledPlugin = await readFile(path.join(root, "first-bundle/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be")); + assert.deepEqual(bundledPlugin, sourcePlugin); + assert.match(bundledPlugin.toString("utf8"), /timeline[.]at/u); + assert.doesNotMatch(bundledPlugin.toString("utf8"), /timeline[.]toMillis/u); + const sourceLock = JSON.parse(await readFile(path.join(root, "first-bundle/source-lock.json"), "utf8")); + const pluginLock = sourceLock.files.find((file) => file.sourcePath.endsWith("/player-bundle.be")); + assert.equal(pluginLock.sourceSha256, sha256(sourcePlugin)); + assert.equal(pluginLock.bundleSha256, sha256(bundledPlugin)); assert.equal(await readFile(first.archive.archivePath).then((value) => value.length), await readFile(second.archive.archivePath).then((value) => value.length)); } finally { await rm(root, { recursive: true, force: true }); @@ -47,7 +57,7 @@ test("refuses synthetic sources outside the tracked fixture roots", async () => const root = await mkdtemp(path.join(tmpdir(), "creator-kit-source-boundary-")); try { await assert.rejects( - buildSyntheticBundle({ sourceRoot: ROOT, outputDir: path.join(root, "out") }), + buildCreatorKitCandidate({ sourceRoot: ROOT, outputDir: path.join(root, "out"), includePublicExamples: false }), (error) => error instanceof CreatorKitBuildError && error.code === "synthetic_source_required", ); } finally { @@ -59,7 +69,7 @@ test("rejects output paths outside the reviewed distribution or temporary direct const root = await mkdtemp(path.join(tmpdir(), "creator-kit-output-")); try { await assert.rejects( - buildSyntheticBundle({ sourceRoot: FIXTURE_ROOT, outputDir: ROOT }), + buildCreatorKitCandidate({ sourceRoot: FIXTURE_ROOT, outputDir: ROOT, includePublicExamples: false }), (error) => error instanceof CreatorKitBuildError && error.code === "unsafe_output", ); } finally { @@ -126,7 +136,7 @@ test("tracked negative fixtures fail closed for every gate", async () => { ]; for (const [name, code] of cases) { await assert.rejects( - buildSyntheticBundle({ sourceRoot: path.join(NEGATIVE_ROOT, name), outputDir: path.join(root, name) }), + buildCreatorKitCandidate({ sourceRoot: path.join(NEGATIVE_ROOT, name), outputDir: path.join(root, name), includePublicExamples: false }), (error) => error instanceof CreatorKitBuildError && error.code === code, ); } diff --git a/scripts/creator-kit/validate.mjs b/scripts/creator-kit/validate.mjs index a2b05be..c1cc31e 100644 --- a/scripts/creator-kit/validate.mjs +++ b/scripts/creator-kit/validate.mjs @@ -95,12 +95,13 @@ export async function validateBundle(bundleRoot = DEFAULT_BUNDLE) { const termIndex = await readJson(path.join(root, "indexes/terms.json")); check(bundle.schemaVersion === "creator-kit-bundle.v1", "Wrong bundle schema"); check(bundle.name === "Spectoda Creator Kit" && bundle.status === "candidate" && bundle.locale === "en", "Bundle identity is invalid"); - check(bundle.contentScope === "synthetic-fixture" && bundle.sourceOfTruth === "Spectoda/documentation", "Bundle must be synthetic-only and Documentation-sourced"); + check(bundle.contentScope === "synthetic-fixture-with-public-examples", "Bundle content scope is invalid"); + check(bundle.sourceOfTruth?.documents === "synthetic-fixture" && bundle.sourceOfTruth?.examples === "Spectoda/examples", "Bundle source ownership is invalid"); check(bundle.transport?.apiVersion === "github-release-v1", "Bundle transport is not the v1 GitHub Release contract"); check(manifest.schemaVersion === "creator-kit-manifest.v1" && manifest.locale === "en", "Manifest schema/locale is invalid"); - check(manifest.source?.repository === "synthetic-fixture" && /^[a-f0-9]{40}$/u.test(manifest.source.commit ?? ""), "Source lock must name the synthetic source and exact commit"); - check(sourceLock.schemaVersion === "creator-kit-source-lock.v1" && sourceLock.commit === manifest.source.commit, "Source lock does not match manifest source"); - check(licenses.schemaVersion === "creator-kit-licenses.v1" && licenses.scope === "synthetic-fixture-only", "License posture is not synthetic-only"); + check(manifest.source?.repository === "Spectoda/examples" && /^[a-f0-9]{40}$/u.test(manifest.source.commit ?? ""), "Source lock must name Examples and an exact commit"); + check(sourceLock.schemaVersion === "creator-kit-source-lock.v1" && sourceLock.repository === manifest.source.repository && sourceLock.commit === manifest.source.commit, "Source lock does not match manifest source"); + check(licenses.schemaVersion === "creator-kit-licenses.v1" && licenses.scope === "synthetic-fixture-and-public-examples", "License posture does not cover the candidate scope"); check(licenses.realDocumentationExportAllowed === false, "Real Documentation export must remain disabled"); check(licenses.publicationAllowed === false, "Synthetic candidate publication must remain disabled"); check(compatibility.schemaVersion === "creator-kit-compatibility.v1" && compatibility.updatePolicy === "exact-version-partner-approved", "Compatibility policy is invalid"); @@ -108,11 +109,13 @@ export async function validateBundle(bundleRoot = DEFAULT_BUNDLE) { check(stable.schemaVersion === "creator-kit-stable-channel.v1" && stable.state === "unpublished" && stable.requiredHumanApproval === true, "Stable channel must remain unpublished behind a human gate"); check(stable.version === null && stable.digest === null && stable.releaseUrl === null, "Unpublished stable channel must not contain release coordinates"); check(Array.isArray(manifest.documents) && manifest.documents.length === 2, "Candidate must contain the two synthetic documents"); + check(Array.isArray(manifest.examples) && manifest.examples.length === 1, "Candidate must contain the selected public Event Player example"); check(new Set(manifest.documents.map((document) => document.id)).size === manifest.documents.length, "Manifest document IDs must be unique"); check(Array.isArray(documentIndex.documents) && documentIndex.documents.length === manifest.documents.length, "Document index does not match manifest"); check(Array.isArray(termIndex.terms), "Term index is not an array"); check(manifest.documents.map((document) => document.sourcePath).join("\n") === [...manifest.documents].sort((left, right) => left.sourcePath.localeCompare(right.sourcePath)).map((document) => document.sourcePath).join("\n"), "Manifest documents are not sorted"); const sourceLockByPath = new Map(sourceLock.files.map((file) => [file.sourcePath, file])); + check(sourceLockByPath.size === sourceLock.files.length, "Source lock contains duplicate paths"); for (const document of manifest.documents) { check(document.agentExport?.include === true && document.agentExport.audience === "partner-maker", `${document.sourcePath} is not explicitly selected`); check(document.agentExport.license === "spectoda-creator-kit-synthetic", `${document.sourcePath} has an invalid license`); @@ -125,7 +128,7 @@ export async function validateBundle(bundleRoot = DEFAULT_BUNDLE) { check(!metadataContainsUrl(`${document.title}\n${document.summary}`), `${document.path} metadata contains a URL`); check(!/<[A-Za-z][^>]*\b(?:href|src|srcset|poster|action)\s*=\s*\{[^}]*\}/iu.test(body), `${document.path} contains a dynamic HTML/MDX resource link`); const lock = sourceLockByPath.get(document.sourcePath); - check(lock?.normalizedSha256 === document.sha256, `${document.sourcePath} source lock does not match normalized body`); + check(lock?.bundleSha256 === document.sha256, `${document.sourcePath} source lock does not match normalized body`); for (const rawHref of findLinks(body)) { const href = rawHref.replace(/[\t\n\f\r]/gu, ""); check(href === rawHref, `${document.path} contains a URL with control whitespace`); @@ -152,6 +155,30 @@ export async function validateBundle(bundleRoot = DEFAULT_BUNDLE) { check(await stat(path.join(root, target)).then(() => true).catch(() => false), `${document.path} points to missing bundle link ${href}`); } } + const example = manifest.examples[0]; + check(example.id === "player-bundle-ab-complete-cue-tracks" && example.license === "MIT", "Public example identity/license is invalid"); + check(example.compatibility?.firmware === "0.12.11" && example.compatibility?.wasm === "DEBUG_UNIVERSAL_0.12.11_20260808", "Public example compatibility is invalid"); + check(Array.isArray(example.files) && example.files.length === 3, "Public example must contain exactly the reviewed three files"); + check( + example.files.map((file) => file.path).sort().join("\n") === [ + "examples/player-bundle-ab-complete-cue-tracks/README.md", + "examples/player-bundle-ab-complete-cue-tracks/example.yaml", + "examples/player-bundle-ab-complete-cue-tracks/player-bundle.be", + ].sort().join("\n"), + "Public example file selection is invalid", + ); + for (const file of example.files) { + const body = await readFile(path.join(root, file.path)); + check(sha256(body) === file.sha256 && file.sha256 === file.sourceSha256, `${file.path} is not a verbatim source copy`); + check(!publicSafetyFinding(body.toString("utf8")), `${file.path} failed public-safety scan`); + const lock = sourceLockByPath.get(file.sourcePath); + check(lock?.sourceSha256 === file.sourceSha256 && lock?.bundleSha256 === file.sha256 && lock?.license === "MIT", `${file.sourcePath} source lock is invalid`); + } + const plugin = await readFile(path.join(root, "examples/player-bundle-ab-complete-cue-tracks/player-bundle.be"), "utf8"); + check(plugin.includes("timeline.at") && !plugin.includes("timeline.toMillis"), "Bundled Player plugin does not use the final timeline.at API"); + check(Array.isArray(selection.selectedExamples) && selection.selectedExamples.length === 1, "Selection does not expose the public example"); + const selectionLock = sourceLockByPath.get("data/v2/examples/player-bundle-ab-complete-cue-tracks/creator-kit.json"); + check(selectionLock?.role === "selection" && selectionLock.sourceSha256 === selectionLock.bundleSha256 && selectionLock.license === "MIT", "Public example selection lock is invalid"); const expectedChecksums = (await readFile(path.join(root, "checksums.sha256"), "utf8")).trim(); const actualChecksums = (await createChecksums(root)).trim(); check(expectedChecksums === actualChecksums, "checksums.sha256 does not match bundle files"); @@ -161,6 +188,7 @@ export async function validateBundle(bundleRoot = DEFAULT_BUNDLE) { return { bundleVersion: bundle.version, documentCount: manifest.documents.length, + exampleCount: manifest.examples.length, totalBytes, fileCount: files.length, checksumDigest: sha256(Buffer.from(`${expectedChecksums}\n`)), From 25f6267865dd747d62fd4b8360accfccfa0a1c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sat, 8 Aug 2026 15:02:39 +0200 Subject: [PATCH 05/10] Creator Kit: Add the locked Event Player example Regenerate the read-only 0.1.0-rc.2 candidate from the exact reviewed Examples source commit.\n\nThe bundle now includes the firmware 0.12.11 Project Berry player, setup metadata, source hashes, compatibility coordinates, and agent guidance while remaining unpublished behind the explicit license gate. --- creator-kit/AGENTS.md | 7 +- creator-kit/README.md | 13 +- creator-kit/RELEASE_NOTES.md | 8 +- creator-kit/bundle.json | 9 +- creator-kit/checksums.sha256 | 29 +- creator-kit/compatibility.json | 4 +- .../README.md | 157 ++++ .../example.yaml | 44 + .../player-bundle.be | 774 ++++++++++++++++++ creator-kit/licenses.json | 5 +- creator-kit/manifest.json | 46 +- .../schemas/creator-kit-bundle.v1.schema.json | 12 +- .../creator-kit-compatibility.v1.schema.json | 6 +- .../creator-kit-manifest.v1.schema.json | 45 +- .../creator-kit-source-lock.v1.schema.json | 16 +- creator-kit/selection.json | 13 +- creator-kit/source-lock.json | 33 +- 17 files changed, 1179 insertions(+), 42 deletions(-) create mode 100644 creator-kit/examples/player-bundle-ab-complete-cue-tracks/README.md create mode 100644 creator-kit/examples/player-bundle-ab-complete-cue-tracks/example.yaml create mode 100644 creator-kit/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be diff --git a/creator-kit/AGENTS.md b/creator-kit/AGENTS.md index 407a038..740b32d 100644 --- a/creator-kit/AGENTS.md +++ b/creator-kit/AGENTS.md @@ -5,7 +5,7 @@ You are reading a pinned, read-only knowledge bundle. 1. Read `bundle.json`, `compatibility.json` and `source-lock.json` before using content. 2. Search `indexes/documents.json` and `indexes/terms.json`; cite the exact - `documents/...` file for every factual answer. + `documents/...` or `examples/...` file for every factual answer. 3. Treat the visible version and checksum digest as part of the answer context. 4. Abstain or ask for a current source when the bundle does not contain the answer, when the question concerns current availability or policy, or when @@ -19,3 +19,8 @@ You are reading a pinned, read-only knowledge bundle. Codex and Claude are supported as read-only consumers. Embeddings, dynamic hosted RAG, a central MCP gateway and a writable Documentation API are not part of Creator Kit v1. + +The canonical copy-ready firmware 0.12.11 Event Player integration is exposed +at `examples/player-bundle-ab-complete-cue-tracks/`. Use its README for setup +and its `player-bundle.be` as the Project Berry source. Do not rewrite or +silently modernize that locked example. diff --git a/creator-kit/README.md b/creator-kit/README.md index 7b7cd3e..357363b 100644 --- a/creator-kit/README.md +++ b/creator-kit/README.md @@ -2,7 +2,7 @@ This directory is a reviewable **release candidate** for the English Spectoda Creator Kit. It is read-only and intentionally contains synthetic public-safe -fixture content. It is not a published GitHub Release and it must not be +fixture content plus one public MIT-licensed Event Player example. It is not a published GitHub Release and it must not be installed into a partner Organization until a Steward/Admin approves a release and the Documentation redistribution license is recorded. @@ -11,8 +11,10 @@ The bundle is generated from one exact source commit. Verify the version, are exact-version and partner-approved. `stable-channel.json` is the v1 read-only transport descriptor; its current state is `unpublished`. -The Documentation repository remains the private content authority. This public -repository is only the reviewed distribution surface. Partner-local product +The Documentation repository remains the private document authority. The +copy-ready Event Player plugin under +`examples/player-bundle-ab-complete-cue-tracks/` is copied byte-for-byte from +its canonical `Spectoda/examples` source at the locked commit. Partner-local product knowledge belongs in a separate sibling directory and is never overwritten by Creator Kit updates or rollback. @@ -24,6 +26,11 @@ abstain on private partner-local questions and request a current source for time-sensitive information. The bundle has no write path to Documentation or to an Organization. +For the firmware 0.12.11 Event Player setup, cite and follow +`examples/player-bundle-ab-complete-cue-tracks/README.md`. Add the accompanying +`player-bundle.be` to each Controller Project; uploading a Player bundle does +not install the Project Berry plugin. + ## Supported transport v1 uses GitHub Releases plus a stable-channel descriptor. A native Lazurio diff --git a/creator-kit/RELEASE_NOTES.md b/creator-kit/RELEASE_NOTES.md index 608cd12..68c5123 100644 --- a/creator-kit/RELEASE_NOTES.md +++ b/creator-kit/RELEASE_NOTES.md @@ -1,8 +1,10 @@ -# Spectoda Creator Kit 0.1.0-rc.1 +# Spectoda Creator Kit candidate This is a review candidate, not a release. It contains two synthetic English -documents used to prove deterministic normalization, source locking, indexes, -checksums and agent-objective evaluation. +documents used to prove deterministic normalization, plus the exact +MIT-licensed firmware 0.12.11 Event Player example selected from +`Spectoda/examples`. Both are source locked, checksummed, and covered by +agent-objective evaluation. Real Documentation bodies are not included. A public release remains gated on redistribution licensing, exact-head review, native Lazurio distribution diff --git a/creator-kit/bundle.json b/creator-kit/bundle.json index 913c5de..33eaac7 100644 --- a/creator-kit/bundle.json +++ b/creator-kit/bundle.json @@ -1,11 +1,14 @@ { "schemaVersion": "creator-kit-bundle.v1", "name": "Spectoda Creator Kit", - "version": "0.1.0-rc.1", + "version": "0.1.0-rc.2", "status": "candidate", "locale": "en", - "contentScope": "synthetic-fixture", - "sourceOfTruth": "Spectoda/documentation", + "contentScope": "synthetic-fixture-with-public-examples", + "sourceOfTruth": { + "documents": "synthetic-fixture", + "examples": "Spectoda/examples" + }, "transport": { "apiVersion": "github-release-v1", "stableChannelDescriptor": "stable-channel.json" diff --git a/creator-kit/checksums.sha256 b/creator-kit/checksums.sha256 index 08642bd..7b59842 100644 --- a/creator-kit/checksums.sha256 +++ b/creator-kit/checksums.sha256 @@ -1,19 +1,22 @@ -0605e1067514814ba301f214cf32e1db875a0b8649d4ae1e3cd1ecd6417fe6a8 RELEASE_NOTES.md -1d90d0e12413a8dc658ac750ebb7f903217b146c2141f41d41a870a2f6a56bd8 schemas/creator-kit-source-lock.v1.schema.json -2376f5bbad4d4696a43c635180908bd5c128f8e5a58c2eea544cbce7ac188e6b schemas/creator-kit-bundle.v1.schema.json -242b8ea3d8343e0479aafa7abcea3c5d02298b083a449e4feee1516a9f6a7494 AGENTS.md +1d5899938769871ada19da20f42f26d94cecb6161eff5fb33660d8e9f088bbc4 schemas/creator-kit-compatibility.v1.schema.json +1ee047eb0cbf291d6ccf7d61342f5da6af64119dab2b8e41c337b350b70bc732 selection.json 25090f867683901852fb23075829dff154741a7427acd8592266dd79765d7082 indexes/documents.json 29be85035a878a415590ea1578068337f552e0b8eae729745e2a3427ed7ce4a7 schemas/creator-kit-licenses.v1.schema.json -365db466876730d311e98f5eac2b87309fe81aa1ecbc8caa849cdd372b23e291 manifest.json +2a5ba31e24c366e1ebd5e81f7c73c95d57220d354464cd34074f0301973b10e0 examples/player-bundle-ab-complete-cue-tracks/player-bundle.be +2e59802f09261f6ae78d5e275798680ee76237ff5dc4fd68cbe2f4378837234a source-lock.json +3488e911f2e41d29518e32ff67485ce4dd310f981133028326b8e77a60254822 schemas/creator-kit-manifest.v1.schema.json 41f576a534c5277269c3c969eb30d6f3e9b8f8112f04eb2ea28c0875a3a0aeb2 stable-channel.json +4d19b2085b9823627e9b010cbb33683d33116b306a7aa96ca4b4e7bfbec622f4 examples/player-bundle-ab-complete-cue-tracks/example.yaml +518226851b88ae01e595de3f28f6aedc32450bf2bec8f6036dcde95c1972878d RELEASE_NOTES.md +62c1d40c752ef23c11eb7603945c3e3c64fdde950833a92e2553a0804d3c2fd3 licenses.json +63ab2aa1cc0f51f58adbc5143af432d49e0c57944269033b213199d22b69cb26 manifest.json +78fd74f9223f9afd727c3a220eeaafca2fa63e04183784cf5a4147bae1c001dc AGENTS.md 7a2583955a991b5d5bdebcc505ab8e783831310f308be01b976cd77a46e8af6e indexes/terms.json -8f347465f826e28d373624f0622394b78a8854ad7be24d088973a192be268bf3 README.md -9f9e115705c24bece3a9af6f1ae9473cb67a45284a4f2e79dae45c995401e324 compatibility.json -ae630cdfe9c3ed6afb970a305100fca7aaf2c0d53fb918d0ae3cdd593ff24aa0 licenses.json -b673c402a7494c1a6f70ab5343e2a502aa1b818927916424c7624d44399e3035 selection.json +7c09ed10e2904a7b01cd82da7c4c7c2cc4e7840a3f9571e28de31ce135ec180d compatibility.json +8687920a58f1e19f132913a327da1ef287a9b291429e7fc781fc54f2ab15ef4a examples/player-bundle-ab-complete-cue-tracks/README.md +86eb826e97162c2f76f47c4cf7e92f42bf9401f5a9c58cc606145b6a64f3423d README.md bae96c2102fcbfc9ff5afc62605232193a308761d5bdb62df1995c03aea68fb8 documents/en/synthetic-creator.mdx -c17d152a03d506fad0613ea55dc32a34b091bda200e699f873c6116ba03eb626 source-lock.json +c0bc32a6efdea1a865f5dbba4f6bc94cf115b2350ab065cd031226a7b6e75cb8 bundle.json c79a442b75413ee3f2c5db22ddb5b6bda6332db0c1f5e73531ef80c9092ee6ad documents/en/synthetic-creator.md -d2cc422c32a525f1834ef7fe997bf291c45aa783afe95e3a2af6439b0254bce8 bundle.json -f79f7ba16e5bfd8598d95dd778cccb2377a3c123913cc0a74a1c8517e6b261ad schemas/creator-kit-manifest.v1.schema.json -f93711fe6420a97834eae2b9f48d43e595077aea880f5b21a81657ab9c024d2d schemas/creator-kit-compatibility.v1.schema.json +e5d14caad4ff80aff41ab60b7b2a06ca5adec236d3820083d1fa780ed10ebc7b schemas/creator-kit-bundle.v1.schema.json +fe3c496bf794676fe9d8c4854f1a29272c6cf6a22d2dd7cc1422b17caf7e33cb schemas/creator-kit-source-lock.v1.schema.json diff --git a/creator-kit/compatibility.json b/creator-kit/compatibility.json index ff23af3..9bf5b11 100644 --- a/creator-kit/compatibility.json +++ b/creator-kit/compatibility.json @@ -8,5 +8,7 @@ "claude" ], "transport": "github-release-v1", - "updatePolicy": "exact-version-partner-approved" + "updatePolicy": "exact-version-partner-approved", + "firmware": "0.12.11", + "wasm": "DEBUG_UNIVERSAL_0.12.11_20260808" } diff --git a/creator-kit/examples/player-bundle-ab-complete-cue-tracks/README.md b/creator-kit/examples/player-bundle-ab-complete-cue-tracks/README.md new file mode 100644 index 0000000..09ef74e --- /dev/null +++ b/creator-kit/examples/player-bundle-ab-complete-cue-tracks/README.md @@ -0,0 +1,157 @@ +# A/B Complete-Cue Track Player from NetworkStorage + +This copy-ready Project Berry example plays the final SPM/SPT v1 bundle format +on firmware **0.12.11**. The shared Spectoda timeline is the only transport: +play, pause, seek, rewind, and loop require no separate Player events. + +Each SPT is one Track for one Spectoda ID; the Track itself has no event label. +A Track defines stable typed tracked events, and every Cue supplies a complete +value for each of them. The plugin automatically includes a global ID255 Track +when the selected generation contains one, then adds only the local IDs +configured for this Controller. + +```berry +PlayerBundle({"namespace": "demo", "ids": [1], "debug": false}) +``` + +## Canonical source + +`player-bundle.be` in this directory is the single editable, copy-ready source +for Studio, Documentation, and the generated Spectoda Creator Kit. Keep this +public path stable. Creator Kit releases copy its exact bytes from a locked +Examples commit according to the fail-closed `creator-kit.json` selection; the +generated bundle is never a second authoring copy. + +Configure local IDs only in the range 0–254. Never put `255` in `ids`; global +ID255 is discovered automatically. A Controller which owns IDs 1 and 7 uses +`{"ids": [1, 7]}`. Another Controller may use `{"ids": [2]}` while all of the +same NetworkStorage files still synchronize to both Controllers. + +## Files and publication order + +For namespace `demo`, the manifest is `demo.spm`; Track files are named +`demo..<000-255>.spt`. `player-bundle-artifacts.json` contains a synthetic +two-generation show as exact binary hex: + +- slot A is the fallback generation; +- slot B is the preferred generation; +- both generations contain ID255 and ID1; +- the ID1 brightness Cue values change in generation B. + +Convert each `hex` value to exact bytes and upload it with the listed filename +and `version`. A real Studio deployment writes every changed inactive-slot SPT, +reads back its exact bytes/fingerprint, and writes the SPM **last**. Do not hand +edit revisions or fingerprints. + +Studio authors Tracks, tracked events, and complete Cues. **Capture** is the +native WASM operation which reads current EventStore values into a complete Cue; +this Berry plugin does not implement Capture. SEB exists here only as compiled +binary segments inside SPT files, not as an authoring concept. + +The plugin watches only the active manifest and relevant active Track files. +Inactive-slot uploads therefore do not interrupt a running show. Once SPM +changes, playback halts. The new preferred generation is selected only after +ID255 (when present) and every configured local SPT match their manifest ID, +artifact revision, exact size, and NetworkStorage fingerprint. An incomplete +preferred generation keeps a complete fallback. Reconciliation after an +artifact change deliberately waits for the shared timeline to be paused. + +## Runtime behavior + +The plugin range-reads the manifest, SPT headers/directories, and individual +SEB segments. It never loads a whole large SPT into one Berry `bytes()` value. +Every embedded SEB receives native fail-closed validation before a generation +can activate. Seek, rewind, and loop binary-search the last Cue at or before the +timeline target and land that complete Cue at the transport's causal local +millis. Forward playback keeps the exact `(SEB bytes, at, cursor)` tuple until +the segment changes. One local timeline-zero mapping is frozen per +epoch/discontinuity and refreshed when a paused timeline resumes. Equal Cue +times in separate Track files therefore receive the same causal timestamp even +when those Tracks execute on different Plugin turns, while time spent paused is +still reflected in later Cue clocks. + +At most one `SEB.land` execution occurs per Plugin turn. A complete Cue can +therefore use the full firmware 0.12.11 limit of **340 event values** without +competing with another Track in the same transient queue. A Cue is never split. +Future records remain outside EventStore until their timeline offsets are due. + +## Requirements and limits + +- firmware exactly 0.12.11 with final SEB v1 and SPM/SPT v1 support; +- every enabled Track starts with a complete Cue at timeline `0 ms`; +- one SEB segment is at most 4,092 bytes, 340 records, and 65,535 ms; +- one SPT is at most 65,535 bytes, but Studio's measured + `maxTrackPayloadBytes`, bundle, metadata, and free-flash gates may be lower; +- NetworkStorage fingerprints are HMAC-SHA256 with UTF-8 key `fingerprint`; +- this distributed Player uses `SEB.land`; do not replace it with `SEB.emit` + on every Controller. + +Turn `debug` on only while commissioning. The example contains no network key, +MAC address, customer name, or other private installation data. + +## Isolated DEVKIT end-to-end runner + +`player-bundle-devkit-smoke.mts` uses the real NodeSerial connector and App +Controller API. It refuses all writes unless you pass an explicit serial port, +the exact monorepo checkout, and `--confirm-isolated-devkit`: + +```bash +cd /path/to/Spectoda-monorepo +SPECTODA_WASM_VERSION=DEBUG_UNIVERSAL_0.12.11_20260808 \ +node_modules/.bin/tsx \ + /path/to/examples/data/v2/examples/player-bundle-ab-complete-cue-tracks/player-bundle-devkit-smoke.mts \ + --monorepo=/path/to/Spectoda-monorepo \ + --path=/dev/cu.usbserial-XXXX \ + --confirm-isolated-devkit \ + --evidence=/tmp/player-bundle-smoke.json +``` + +Equivalent environment options are `SPECTODA_MONOREPO`, +`SPECTODA_SERIAL_PATH`, and `SPECTODA_WASM_VERSION`. Optional CLI settings are +`--baudrate`, `--scan-timeout`, `--poll-timeout`, `--namespace`, +`--skip-capacity-probe`, and `--evidence`. + +For a `DEBUG_DEV_*` version, copy that exact local build's `.js` and `.wasm` +files into `.webassembly/` below the runner's current directory. The runner +fails before any Controller write when either file is absent; this prevents the +JS loader from silently using an older universal WASM without the Player APIs. + +The runner derives fresh unsigned-48 revisions above every observed namespace +version. It first writes slot A SPTs with exact readback, then writes the A-only +SPM last. It removes copy-only comment lines and indentation, matching Studio's +Project preprocessing, uploads the Berry as `BERRY(...)` through `writeTngl`, +then resumes automatically in a fresh Node/WASM process after the expected +Controller serial restart. The resumed phase physically reads the TNGL bytecode +into a fresh WASM runtime and requires its fingerprint to equal the pre-restart +compiler fingerprint. It then checks global ID255 and local ID1 values plus +identical causal clocks across pause, external-authority, play, seek, rewind, +and a running-rewind loop. It then writes slot B +SPTs, proves that inactive files did not alter playback, writes the new SPM +last, verifies the preferred switch without mixed Cue state, and repeats the +transport checks. + +By default the same isolated Controller also gets a deterministic 60,000-byte +NetworkStorage transport/readback probe. The deploy gate accepts 60,000 and +rejects 60,001 before the write function can run. Use +`--skip-capacity-probe` when flash headroom has already been measured elsewhere. +After the playback checks, the runner retires the probe with a higher-version +zero-byte marker so that its payload does not remain on flash. +The runner prints and optionally saves revisions, exact sizes, fingerprints, +the final namespace listing, firmware version, serial port, monorepo head, and +all observed causal clocks. It leaves the example Project Berry and named +NetworkStorage files installed; restore the intended Project after testing. + +## Host Berry regression + +From the Examples repository root, run the deterministic lifecycle/failure-path +test with the Berry host interpreter built by the matching firmware checkout: + +```bash +/path/to/firmware/components/spectoda-scripting/berry/berry \ + tests/player-bundle-stub.be +``` + +The test reads this example's fixture corpus and covers preferred/fallback +selection, bounded range reads, transient metadata and timeline-projection failures, +pause/resume clocks, seek/loop reconstruction, and local-ID validation without +writing to a Controller. diff --git a/creator-kit/examples/player-bundle-ab-complete-cue-tracks/example.yaml b/creator-kit/examples/player-bundle-ab-complete-cue-tracks/example.yaml new file mode 100644 index 0000000..b0805c8 --- /dev/null +++ b/creator-kit/examples/player-bundle-ab-complete-cue-tracks/example.yaml @@ -0,0 +1,44 @@ +schemaVersion: example.v2 +slug: player-bundle-ab-complete-cue-tracks +title: A/B Complete-Cue Track Event Player +category: controller +summary: >- + Copy-ready firmware 0.12.11 Project Berry player for SPM/SPT v1 bundles. It + selects a complete preferred or fallback generation, automatically lands the + global ID255 Track plus configured local-ID Tracks, and follows the shared + timeline through play, pause, seek, rewind, and loop. Each Track defines + stable typed tracked events, and every Cue supplies all their values. +tags: + - berry + - event-player + - network-storage + - seb + - spm + - spt + - timeline + - ab-deployment +hardware: >- + Requires exactly firmware 0.12.11 with final SEB v1 and SPM/SPT v1 support. + Configure Project Berry with only this Controller's local Spectoda IDs 0-254; + the global ID255 Track is automatic. Upload the SPM/SPT bundle to network-wide + NetworkStorage. The player-bundle.be path is a stable public integration + contract used by Studio, Documentation, and Spectoda Creator Kit releases. +files: + - path: README.md + role: Setup, publication, validation, and runtime behavior + language: markdown + - path: creator-kit.json + role: Explicit fail-closed Spectoda Creator Kit export selection + language: json + - path: player-bundle.be + role: Copy-ready Project Berry SPM/SPT Player + language: berry + - path: player-bundle-artifacts.json + role: Synthetic two-generation A/B NetworkStorage artifact corpus + language: json + - path: player-bundle-devkit-smoke.mts + role: Explicit isolated-DEVKIT NodeSerial/App Controller end-to-end runner + language: typescript +related: + - seb-causal-scene-recall + - seb-segmented-id-track-player diff --git a/creator-kit/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be b/creator-kit/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be new file mode 100644 index 0000000..eecce25 --- /dev/null +++ b/creator-kit/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be @@ -0,0 +1,774 @@ +# Project Berry Player for firmware 0.12.11 and SPM/SPT v1. +# +# Public API: +# PlayerBundle({"namespace": "demo", "ids": [1], "debug": false}) +# +# ID255 is discovered automatically when it exists in a generation. Configure +# only Controller-local IDs 0..254. NetworkStorage remains a global union: this +# Controller validates and opens only the global Track and its configured local +# Tracks. +def PlayerBundle(S) + import string + + var ns = S.find("namespace", "player") + var ids = S.find("ids", []) + var dbg = S.find("debug", false) + var integrity_polls = S.find("integrityPolls", 25) + var retry_polls = S.find("retryPolls", 100) + + # The namespace is also part of every NetworkStorage filename. Keeping the + # validation here makes an invalid Project fail before registering Plugin. + if type(ns) != "string" || size(ns) < 1 || size(ns) > 14 + raise "value_error", "Player namespace must contain 1-14 ASCII characters" + end + var allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-" + for i : 0..size(ns) - 1 + if string.find(allowed, ns[i]) < 0 + raise "value_error", "Player namespace contains an invalid character" + end + end + var manifest_file = ns + ".spm" + if type(ids) != "instance" || classname(ids) != "list" || + type(integrity_polls) != "int" || type(retry_polls) != "int" || + integrity_polls < 1 || retry_polls < 1 + raise "value_error", "Player ids/poll configuration is invalid" + end + + # local_ids is sorted once so activation and playback stay deterministic. + var local_ids = [] + for id : ids + if type(id) != "int" || id < 0 || id > 254 + raise "value_error", "Player local IDs must be integers in 0..254" + end + var at = 0 + while at < local_ids.size() && local_ids[at] < id + at += 1 + end + if at < local_ids.size() && local_ids[at] == id + raise "value_error", "Player local IDs must be unique" + end + local_ids.insert(at, id) + end + + # Mutable state lives in one map because Berry closures capture scalar + # upvalues by value. active is kept when a newer upload is incomplete. + var R = { + "active": nil, + "armed": false, + "storage_dirty": false, + "reconcile": nil, + "last_time": 0, + "last_epoch": 0, + "last_paused": true, + "timeline_zero": nil, + "poll": 0, + "round": 0, + "error": nil + } + + def log_error(message) + if message != R["error"] + R["error"] = message + if dbg + print("PlayerBundle", message) + end + end + end + + def track_name(slot, id) + var digits = str(id) + if id < 10 + digits = "00" + digits + elif id < 100 + digits = "0" + digits + end + return ns + "." + slot + "." + digits + ".spt" + end + + # getNetworkStorageData(name, offset, length) is bounded to 8192 bytes. + # Every read in this player is a header, directory entry, or <=4092B SEB. + def read_exact(name, offset, length) + var b = spectoda.getNetworkStorageData(name, offset, length) + if b == nil || b.size() != length + return nil + end + return b + end + + def exact_size(name, size) + var tail = spectoda.getNetworkStorageData(name, size, 1) + return tail != nil && tail.size() == 0 + end + + def bytes_zero(b, first, last) + for i : first..last + if b.get(i) != 0 + return false + end + end + return true + end + + def bytes_equal(a, ao, b, bo, count) + for i : 0..count - 1 + if a.get(ao + i) != b.get(bo + i) + return false + end + end + return true + end + + def fingerprint_hex(b, offset) + var h = "0123456789abcdef" + var result = "" + for i : 0..31 + var v = b.get(offset + i) + result += h[(v >> 4) & 15] + h[v & 15] + end + return result + end + + def metadata() + var result = {} + var entries = spectoda.listNetworkStorageData() + if entries == nil + return nil + end + for entry : entries + var name = entry.find("name", nil) + var fingerprint = entry.find("fingerprint", nil) + if name != nil && fingerprint != nil + result[name] = fingerprint + end + end + return result + end + + def generation(header, descriptor_offset, slot, expected_table) + var count = header.get(descriptor_offset + 6, 2) + var duration = header.get(descriptor_offset + 8, 4) + var table = header.get(descriptor_offset + 12, 2) + var table_size = header.get(descriptor_offset + 14, 2) + if duration < 0 || table != expected_table || table_size != count * 44 + return nil + end + return { + "slot": slot, + "duration": duration, + "count": count, + "table": table, + "table_size": table_size, + "tracks": {} + } + end + + # Parse both generation tables so gaps, overlap, duplicates, unsorted IDs, + # reserved fields, and trailing bytes fail before any artifact is opened. + def parse_manifest(meta) + var name = manifest_file + var fp = meta.find(name, nil) + var h = read_exact(name, 0, 112) + if fp == nil || h == nil || h.get(0) != 83 || h.get(1) != 80 || h.get(2) != 77 + return nil + end + var file_size = h.get(6, 2) + var ps = h.get(8) + var fs = h.get(9) + if h.get(3) != 1 || h.get(4) != 16 || h.get(5) != 0 || + (ps != 0 && ps != 1) || (fs != 0 && fs != 1 && fs != 255) || + fs == ps || h.get(10) != 48 || h.get(11) != 44 || + !bytes_zero(h, 12, 15) || file_size < 112 || !exact_size(name, file_size) + return nil + end + + var preferred = generation(h, 16, ps == 0 ? "a" : "b", 112) + if preferred == nil + return nil + end + var expected = 112 + preferred["table_size"] + var fallback = nil + if fs == 255 + if !bytes_zero(h, 64, 111) + return nil + end + else + fallback = generation(h, 64, fs == 0 ? "a" : "b", expected) + if fallback == nil + return nil + end + expected += fallback["table_size"] + end + if expected != file_size + return nil + end + + var generations = [preferred] + if fallback != nil + generations.push(fallback) + end + for g : generations + var previous_id = -1 + for i : 0..g["count"] - 1 + var entry = read_exact(name, g["table"] + i * 44, 44) + if entry == nil + return nil + end + var id = entry.get(0) + if id <= previous_id || entry.get(1) != 0 || entry.get(10, 2) != 0 + return nil + end + previous_id = id + g["tracks"][id] = { + "id": id, + "size": entry.get(2, 2), + "revision": entry[4..9], + "fingerprint": fingerprint_hex(entry, 12) + } + end + end + return {"fingerprint": fp, "preferred": preferred, "fallback": fallback} + end + + def read_segment(track, index) + if index < 0 || index >= track["segment_count"] + return nil + end + var b = read_exact(track["name"], track["segment_offset"] + index * 12, 12) + if b == nil + return nil + end + return { + "origin": b.get(0, 4), + "first_cue": b.get(4, 2), + "cue_count": b.get(6, 2), + "seb_offset": b.get(8, 2), + "seb_size": b.get(10, 2) + } + end + + def read_cue(track, index) + if index < 0 || index >= track["cue_count"] + return nil + end + var b = read_exact(track["name"], track["cue_offset"] + index * 12, 12) + if b == nil + return nil + end + return { + "time": b.get(0, 4), + "segment": b.get(4, 2), + "cursor": b.get(6, 2), + "count": b.get(8, 2), + "offset": b.get(10, 2) + } + end + + # Validate the full SPT directory and each embedded SEB without retaining + # a whole SPT in Berry RAM. SEB.land at cursor=record_count is a native, + # mutation-free full-blob validation call. + def validate_track(descriptor, slot, duration, meta) + var name = track_name(slot, descriptor["id"]) + if descriptor["size"] < 36 || descriptor["size"] > 65535 || + meta.find(name, nil) != descriptor["fingerprint"] || + !exact_size(name, descriptor["size"]) + return nil + end + var h = read_exact(name, 0, 36) + if h == nil || h.get(0) != 83 || h.get(1) != 80 || h.get(2) != 84 || + h.get(3) != 1 || h.get(4) != 36 || h.get(5) != 0 || + h.get(6) != descriptor["id"] || h.get(7) != 0 || + !bytes_equal(h, 8, descriptor["revision"], 0, 6) || + h.get(14) != 12 || h.get(15) != 12 || h.get(16, 4) != duration || + h.get(20, 2) < 1 || h.get(20, 2) > 340 || h.get(22, 2) < 1 || + h.get(24, 2) < 1 || h.get(26, 2) != 36 || h.get(34, 2) != 0 || + h.get(32, 2) != descriptor["size"] + return nil + end + var cue_count = h.get(22, 2) + var segment_count = h.get(24, 2) + var segment_offset = h.get(28, 2) + var data_offset = h.get(30, 2) + if segment_offset != 36 + cue_count * 12 || + data_offset != segment_offset + segment_count * 12 || + data_offset > descriptor["size"] + return nil + end + + var track = { + "id": descriptor["id"], + "name": name, + "fingerprint": descriptor["fingerprint"], + "size": descriptor["size"], + "duration": duration, + "event_count": h.get(20, 2), + "cue_count": cue_count, + "segment_count": segment_count, + "cue_offset": 36, + "segment_offset": segment_offset, + "data_offset": data_offset, + "play_segment": 0, + "cursor": 0, + "bytes": nil, + "at": nil + } + + var expected_cue = 0 + var expected_data = data_offset + var previous_time = -1 + for segment_index : 0..segment_count - 1 + var segment = read_segment(track, segment_index) + if segment == nil || segment["origin"] < 0 || + segment["first_cue"] != expected_cue || segment["cue_count"] < 1 || + segment["first_cue"] + segment["cue_count"] > cue_count || + segment["seb_offset"] != expected_data || segment["seb_size"] < 12 || + segment["seb_size"] > 4092 || + segment["seb_offset"] + segment["seb_size"] > descriptor["size"] + return nil + end + var seb = read_exact(name, segment["seb_offset"], segment["seb_size"]) + if seb == nil || seb.get(0) != 83 || seb.get(1) != 69 || seb.get(2) != 66 || + seb.get(3) != 1 || seb.get(4) != 12 || seb.get(5) != 0 || + seb.get(10) != 12 || seb.get(11) != 0 + return nil + end + var records = seb.get(6, 2) + var seb_duration = seb.get(8, 2) + if records < 1 || records > 340 || segment["seb_size"] != 12 + records * 12 || + records != segment["cue_count"] * track["event_count"] + return nil + end + var inspected = SEB.land(seb, { + "at": controller.millis(), + "cursor": records, + "until": 0 + }) + if !inspected["ok"] || !inspected["done"] || inspected["cursor"] != records || + inspected["consumed"] != 0 + return nil + end + + var expected_record = 0 + for local_cue : 0..segment["cue_count"] - 1 + var cue_index = segment["first_cue"] + local_cue + var cue = read_cue(track, cue_index) + if cue == nil || cue["time"] <= previous_time || cue["time"] > duration || + cue["segment"] != segment_index || cue["cursor"] != expected_record || + cue["count"] != track["event_count"] || + cue["offset"] != cue["time"] - segment["origin"] || + cue["offset"] > seb_duration || + (local_cue == 0 && cue["time"] != segment["origin"]) + return nil + end + if cue_index == 0 && cue["time"] != 0 + return nil + end + for record : 0..cue["count"] - 1 + var record_offset = 12 + (cue["cursor"] + record) * 12 + if seb.get(record_offset + 8, 2) != cue["offset"] || + seb.get(record_offset + 10) != track["id"] + return nil + end + end + expected_record += cue["count"] + previous_time = cue["time"] + end + if expected_record != records + return nil + end + expected_cue += segment["cue_count"] + expected_data += segment["seb_size"] + end + if expected_cue != cue_count || expected_data != descriptor["size"] + return nil + end + return track + end + + def validate_generation(manifest, generation, meta) + if generation == nil + return nil + end + var requested = [] + if generation["tracks"].contains(255) + requested.push(255) + end + for id : local_ids + if !generation["tracks"].contains(id) + return nil + end + requested.push(id) + end + + var tracks = [] + for id : requested + var track = validate_track( + generation["tracks"][id], generation["slot"], generation["duration"], meta) + if track == nil + return nil + end + tracks.push(track) + end + return { + "slot": generation["slot"], + "duration": generation["duration"], + "manifest_fingerprint": manifest["fingerprint"], + "tracks": tracks + } + end + + def active_matches(active, meta) + if meta == nil + return nil + end + if active == nil || + meta.find(manifest_file, nil) != active["manifest_fingerprint"] + return false + end + for track : active["tracks"] + if meta.find(track["name"], nil) != track["fingerprint"] + return false + end + end + return true + end + + # Preferred is tried first. A failed preferred never replaces the current + # generation; a fully valid fallback is the only startup/recovery choice. + def load_bundle() + var before = metadata() + if before == nil + return nil + end + var manifest = parse_manifest(before) + if manifest == nil + return nil + end + var candidate = validate_generation(manifest, manifest["preferred"], before) + if candidate == nil + candidate = validate_generation(manifest, manifest["fallback"], before) + end + if candidate == nil + return nil + end + var after = metadata() + if !active_matches(candidate, after) + return nil + end + return candidate + end + + def subtract_local_millis(local_millis, offset) + # Avoid relying on signed-overflow behavior at the 32-bit millis wrap. + var minimum = -2147483648 + if local_millis >= minimum + offset + return local_millis - offset + end + var underflow = offset - (local_millis - minimum) + return 2147483647 - underflow + 1 + end + + def add_local_millis(local_millis, offset) + # Mirror subtract_local_millis while keeping the result in signed + # 32-bit millis space across controller.millis() wrap. + var maximum = 2147483647 + var room = maximum - local_millis + if offset <= room + return local_millis + offset + end + return -2147483648 + (offset - room - 1) + end + + def find_cue(track, target) + var low = 0 + var high = track["cue_count"] - 1 + var found = nil + while low <= high + var middle = (low + high) >> 1 + var cue = read_cue(track, middle) + if cue == nil + return nil + end + if cue["time"] <= target + found = cue + low = middle + 1 + else + high = middle - 1 + end + end + return found + end + + def prepare_seek(track, target, local_now) + var cue = find_cue(track, target) + if cue == nil + return nil + end + var segment = read_segment(track, cue["segment"]) + if segment == nil + return nil + end + return { + "track": track, + "cue": cue, + "segment": segment, + "at": subtract_local_millis(local_now, cue["offset"]) + } + end + + def begin_reconcile(state) + var active = R["active"] + if active == nil || state["time"] < 0 || state["time"] > active["duration"] + R["armed"] = false + R["reconcile"] = nil + log_error("timeline is outside the active show") + return false + end + # Keep the discontinuity trigger armed until reconciliation completes, + # so a transient projection/directory read failure retries next turn. + R["last_epoch"] = state["epoch"] - 1 + var local_now = timeline.at(state["time"]) + if local_now == nil + R["armed"] = false + log_error("timeline cannot project local millis") + return false + end + R["timeline_zero"] = subtract_local_millis(local_now, state["time"]) + # Freeze one mapping per timeline epoch/discontinuity. Calling + # timeline.at(segment origin) separately for each Track can + # quantize equal Cues one millisecond apart on real Controllers. + # Resolve every Cue and segment directory before the first Cue value is + # landed. SEB bytes are loaded one Track at a time so a large + # multi-ID bundle does not remain resident in Berry RAM. + var operations = [] + for track : active["tracks"] + var operation = prepare_seek(track, state["time"], local_now) + if operation == nil + R["armed"] = false + log_error("failed to prepare Cue") + return false + end + operations.push(operation) + end + R["armed"] = false + R["reconcile"] = { + "target": state["time"], + "epoch": state["epoch"], + "operations": operations, + "index": 0 + } + return true + end + + def reconcile_step(state) + var reconciliation = R["reconcile"] + if reconciliation == nil + return false + end + if state["epoch"] != reconciliation["epoch"] + R["reconcile"] = nil + return begin_reconcile(state) + end + if reconciliation["index"] < reconciliation["operations"].size() + var operation = reconciliation["operations"][reconciliation["index"]] + var matches = active_matches(R["active"], metadata()) + if matches == nil + return false + end + if !matches + R["storage_dirty"] = true + R["reconcile"] = nil + log_error("Player artifacts changed") + return false + end + var cue = operation["cue"] + var segment = operation["segment"] + var seb = read_exact(operation["track"]["name"], + segment["seb_offset"], segment["seb_size"]) + if seb == nil + log_error("failed to range-read Cue SEB") + return false + end + var result = SEB.land(seb, { + "at": operation["at"], + "cursor": cue["cursor"], + "until": cue["offset"] + }) + if !result["ok"] || result["consumed"] != cue["count"] + log_error("Complete Cue failed: " + str(result["error"])) + return false + end + var track = operation["track"] + var next_cursor = result["cursor"] + if result["done"] + track["play_segment"] = cue["segment"] + 1 + track["cursor"] = 0 + else + track["play_segment"] = cue["segment"] + track["cursor"] = next_cursor + end + track["bytes"] = nil + track["at"] = nil + reconciliation["index"] += 1 + return true + end + + R["reconcile"] = nil + R["armed"] = true + R["last_time"] = state["time"] + R["last_epoch"] = state["epoch"] + R["last_paused"] = state["paused"] + R["error"] = nil + return true + end + + # Return 1 after one SEB call, 0 when this Track has nothing due, and -1 on + # a read/projection failure. Cursor advances only from a successful native call. + def process_track(track, target) + if track["play_segment"] >= track["segment_count"] + return 0 + end + var segment = read_segment(track, track["play_segment"]) + if segment == nil || target < segment["origin"] + return segment == nil ? -1 : 0 + end + if track["bytes"] == nil + track["bytes"] = read_exact(track["name"], segment["seb_offset"], segment["seb_size"]) + if R["timeline_zero"] != nil + track["at"] = add_local_millis(R["timeline_zero"], segment["origin"]) + end + if track["bytes"] == nil || track["at"] == nil + track["bytes"] = nil + track["at"] = nil + return -1 + end + end + var until = target - segment["origin"] + var duration = track["bytes"].get(8, 2) + if until > duration + until = duration + end + var result = SEB.land(track["bytes"], { + "at": track["at"], + "cursor": track["cursor"], + "until": until + }) + if !result["ok"] + log_error("forward SEB failed: " + str(result["error"])) + return 1 + end + track["cursor"] = result["cursor"] + if result["done"] + track["play_segment"] += 1 + track["cursor"] = 0 + track["bytes"] = nil + track["at"] = nil + end + R["error"] = nil + return 1 + end + + # At most one SEB call is issued per Plugin turn. This makes the native + # 340-value atomic queue available to one complete Cue even with many IDs. + def process_forward(target) + var tracks = R["active"]["tracks"] + if tracks.size() == 0 + return + end + for checked : 0..tracks.size() - 1 + var index = (R["round"] + checked) % tracks.size() + var status = process_track(tracks[index], target) + if status != 0 + R["round"] = (index + 1) % tracks.size() + if status < 0 + log_error("failed to range-read active SPT") + end + return + end + end + end + + return Plugin(def() + var state = timeline.getState() + R["poll"] += 1 + + # Integrity polling watches only the manifest and currently active + # relevant SPTs. Writes to the inactive slot do not interrupt playback; + # the SPM-last publication is the change that arms reconciliation. + if R["active"] != nil && R["poll"] % integrity_polls == 0 + if active_matches(R["active"], metadata()) == false + R["storage_dirty"] = true + R["armed"] = false + R["reconcile"] = nil + log_error("Player artifacts changed; pause") + end + end + + if R["storage_dirty"] + if state["paused"] && R["poll"] % retry_polls == 0 + var candidate = load_bundle() + if candidate != nil + R["active"] = candidate + R["storage_dirty"] = false + begin_reconcile(state) + else + log_error("Player generation incomplete") + end + end + return + end + + if R["active"] == nil + if R["poll"] == 1 || R["poll"] % retry_polls == 0 + var candidate = load_bundle() + if candidate != nil + R["active"] = candidate + begin_reconcile(state) + else + log_error("no complete Player generation") + end + end + return + end + + if R["reconcile"] != nil + reconcile_step(state) + return + end + + # A paused timeline stops advancing while the local/network clock does + # not. Refresh the mapping on resume without landing the current Cue + # again: an external event remains authoritative until the next Cue, + # whose causal clock must still reflect the time spent paused. + if R["last_paused"] && !state["paused"] + var local_now = timeline.at(state["time"]) + if local_now == nil + log_error("timeline resume has no local projection") + return + end + R["timeline_zero"] = subtract_local_millis(local_now, state["time"]) + for track : R["active"]["tracks"] + track["bytes"] = nil + track["at"] = nil + end + end + + # Rewind, seek, and timeline loop are all reconstructed from the last + # complete Cue at/before the new shared timeline position. + if state["epoch"] != R["last_epoch"] || state["time"] < R["last_time"] + if begin_reconcile(state) + reconcile_step(state) + end + return + end + + if !state["paused"] && R["armed"] + process_forward(state["time"]) + end + R["last_time"] = state["time"] + R["last_epoch"] = state["epoch"] + R["last_paused"] = state["paused"] + end) +end + +# Copy this call into each Controller's Project and configure only its local IDs. +PlayerBundle({"namespace": "demo", "ids": [1], "debug": false}) diff --git a/creator-kit/licenses.json b/creator-kit/licenses.json index 58fe42d..a8e92b7 100644 --- a/creator-kit/licenses.json +++ b/creator-kit/licenses.json @@ -2,8 +2,9 @@ "schemaVersion": "creator-kit-licenses.v1", "publicationAllowed": false, "realDocumentationExportAllowed": false, - "scope": "synthetic-fixture-only", + "scope": "synthetic-fixture-and-public-examples", "entries": [ - "spectoda-creator-kit-synthetic" + "spectoda-creator-kit-synthetic", + "MIT" ] } diff --git a/creator-kit/manifest.json b/creator-kit/manifest.json index 0457b20..a3edf32 100644 --- a/creator-kit/manifest.json +++ b/creator-kit/manifest.json @@ -1,12 +1,12 @@ { "schemaVersion": "creator-kit-manifest.v1", "bundleName": "Spectoda Creator Kit", - "bundleVersion": "0.1.0-rc.1", + "bundleVersion": "0.1.0-rc.2", "locale": "en", - "contentScope": "synthetic-fixture", + "contentScope": "synthetic-fixture-with-public-examples", "source": { - "repository": "synthetic-fixture", - "commit": "6142c40b0868298150e93e6ed2bf19de46dc1380" + "repository": "Spectoda/examples", + "commit": "08339b4e840ba8c8b0a7d2f4c93eb1a5ba075f92" }, "documents": [ { @@ -41,5 +41,43 @@ "sourceLock": "synthetic-fixture-2026-08-07" } } + ], + "examples": [ + { + "id": "player-bundle-ab-complete-cue-tracks", + "path": "examples/player-bundle-ab-complete-cue-tracks", + "title": "A/B Complete-Cue Track Player from NetworkStorage", + "topic": "studio-event-player", + "audience": "partner-maker", + "license": "MIT", + "stability": "firmware-0.12.11", + "compatibility": { + "firmware": "0.12.11", + "wasm": "DEBUG_UNIVERSAL_0.12.11_20260808" + }, + "files": [ + { + "sourcePath": "data/v2/examples/player-bundle-ab-complete-cue-tracks/example.yaml", + "path": "examples/player-bundle-ab-complete-cue-tracks/example.yaml", + "format": "yaml", + "sourceSha256": "4d19b2085b9823627e9b010cbb33683d33116b306a7aa96ca4b4e7bfbec622f4", + "sha256": "4d19b2085b9823627e9b010cbb33683d33116b306a7aa96ca4b4e7bfbec622f4" + }, + { + "sourcePath": "data/v2/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be", + "path": "examples/player-bundle-ab-complete-cue-tracks/player-bundle.be", + "format": "be", + "sourceSha256": "2a5ba31e24c366e1ebd5e81f7c73c95d57220d354464cd34074f0301973b10e0", + "sha256": "2a5ba31e24c366e1ebd5e81f7c73c95d57220d354464cd34074f0301973b10e0" + }, + { + "sourcePath": "data/v2/examples/player-bundle-ab-complete-cue-tracks/README.md", + "path": "examples/player-bundle-ab-complete-cue-tracks/README.md", + "format": "md", + "sourceSha256": "8687920a58f1e19f132913a327da1ef287a9b291429e7fc781fc54f2ab15ef4a", + "sha256": "8687920a58f1e19f132913a327da1ef287a9b291429e7fc781fc54f2ab15ef4a" + } + ] + } ] } diff --git a/creator-kit/schemas/creator-kit-bundle.v1.schema.json b/creator-kit/schemas/creator-kit-bundle.v1.schema.json index 18980d2..920117f 100644 --- a/creator-kit/schemas/creator-kit-bundle.v1.schema.json +++ b/creator-kit/schemas/creator-kit-bundle.v1.schema.json @@ -10,8 +10,16 @@ "version": { "type": "string" }, "status": { "enum": ["candidate", "released", "revoked"] }, "locale": { "const": "en" }, - "contentScope": { "enum": ["synthetic-fixture", "licensed-documentation"] }, - "sourceOfTruth": { "const": "Spectoda/documentation" }, + "contentScope": { "enum": ["synthetic-fixture-with-public-examples", "licensed-documentation"] }, + "sourceOfTruth": { + "type": "object", + "required": ["documents", "examples"], + "properties": { + "documents": { "type": "string" }, + "examples": { "const": "Spectoda/examples" } + }, + "additionalProperties": false + }, "transport": { "type": "object" } }, "additionalProperties": false diff --git a/creator-kit/schemas/creator-kit-compatibility.v1.schema.json b/creator-kit/schemas/creator-kit-compatibility.v1.schema.json index 1e5835d..b1ba10d 100644 --- a/creator-kit/schemas/creator-kit-compatibility.v1.schema.json +++ b/creator-kit/schemas/creator-kit-compatibility.v1.schema.json @@ -3,7 +3,7 @@ "$id": "https://schemas.spectoda.com/creator-kit/compatibility.v1.json", "title": "Spectoda Creator Kit compatibility contract", "type": "object", - "required": ["schemaVersion", "bundleName", "formatVersion", "locale", "agents", "transport", "updatePolicy"], + "required": ["schemaVersion", "bundleName", "formatVersion", "locale", "agents", "transport", "updatePolicy", "firmware", "wasm"], "properties": { "schemaVersion": { "const": "creator-kit-compatibility.v1" }, "bundleName": { "const": "Spectoda Creator Kit" }, @@ -11,7 +11,9 @@ "locale": { "const": "en" }, "agents": { "type": "array" }, "transport": { "const": "github-release-v1" }, - "updatePolicy": { "const": "exact-version-partner-approved" } + "updatePolicy": { "const": "exact-version-partner-approved" }, + "firmware": { "const": "0.12.11" }, + "wasm": { "const": "DEBUG_UNIVERSAL_0.12.11_20260808" } }, "additionalProperties": false } diff --git a/creator-kit/schemas/creator-kit-manifest.v1.schema.json b/creator-kit/schemas/creator-kit-manifest.v1.schema.json index 6dd3721..5c5d7a4 100644 --- a/creator-kit/schemas/creator-kit-manifest.v1.schema.json +++ b/creator-kit/schemas/creator-kit-manifest.v1.schema.json @@ -3,13 +3,13 @@ "$id": "https://schemas.spectoda.com/creator-kit/manifest.v1.json", "title": "Spectoda Creator Kit manifest", "type": "object", - "required": ["schemaVersion", "bundleName", "bundleVersion", "locale", "contentScope", "source", "documents"], + "required": ["schemaVersion", "bundleName", "bundleVersion", "locale", "contentScope", "source", "documents", "examples"], "properties": { "schemaVersion": { "const": "creator-kit-manifest.v1" }, "bundleName": { "const": "Spectoda Creator Kit" }, "bundleVersion": { "type": "string" }, "locale": { "const": "en" }, - "contentScope": { "enum": ["synthetic-fixture", "licensed-documentation"] }, + "contentScope": { "enum": ["synthetic-fixture-with-public-examples", "licensed-documentation"] }, "source": { "type": "object", "required": ["repository", "commit"], @@ -47,6 +47,47 @@ }, "additionalProperties": false } + }, + "examples": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "path", "title", "topic", "audience", "license", "stability", "compatibility", "files"], + "properties": { + "id": { "type": "string" }, + "path": { "type": "string" }, + "title": { "type": "string" }, + "topic": { "type": "string" }, + "audience": { "const": "partner-maker" }, + "license": { "const": "MIT" }, + "stability": { "const": "firmware-0.12.11" }, + "compatibility": { + "type": "object", + "required": ["firmware", "wasm"], + "properties": { + "firmware": { "const": "0.12.11" }, + "wasm": { "const": "DEBUG_UNIVERSAL_0.12.11_20260808" } + }, + "additionalProperties": false + }, + "files": { + "type": "array", + "items": { + "type": "object", + "required": ["sourcePath", "path", "format", "sourceSha256", "sha256"], + "properties": { + "sourcePath": { "type": "string" }, + "path": { "type": "string" }, + "format": { "enum": ["md", "yaml", "be"] }, + "sourceSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } } }, "additionalProperties": false diff --git a/creator-kit/schemas/creator-kit-source-lock.v1.schema.json b/creator-kit/schemas/creator-kit-source-lock.v1.schema.json index 1b07056..4b7ed8e 100644 --- a/creator-kit/schemas/creator-kit-source-lock.v1.schema.json +++ b/creator-kit/schemas/creator-kit-source-lock.v1.schema.json @@ -8,7 +8,21 @@ "schemaVersion": { "const": "creator-kit-source-lock.v1" }, "repository": { "type": "string" }, "commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, - "files": { "type": "array" } + "files": { + "type": "array", + "items": { + "type": "object", + "required": ["sourcePath", "sourceSha256", "bundleSha256", "license"], + "properties": { + "sourcePath": { "type": "string" }, + "sourceSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "bundleSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "license": { "type": "string" }, + "role": { "const": "selection" } + }, + "additionalProperties": false + } + } }, "additionalProperties": false } diff --git a/creator-kit/selection.json b/creator-kit/selection.json index ff7a002..3f4a5cd 100644 --- a/creator-kit/selection.json +++ b/creator-kit/selection.json @@ -1,7 +1,7 @@ { "schemaVersion": "creator-kit-selection.v1", "failClosed": true, - "selected": [ + "selectedDocuments": [ { "sourcePath": "en/synthetic-creator.md", "agentExport": { @@ -20,5 +20,16 @@ "sourceLock": "synthetic-fixture-2026-08-07" } } + ], + "selectedExamples": [ + { + "id": "player-bundle-ab-complete-cue-tracks", + "selectionSourcePath": "data/v2/examples/player-bundle-ab-complete-cue-tracks/creator-kit.json", + "files": [ + "data/v2/examples/player-bundle-ab-complete-cue-tracks/example.yaml", + "data/v2/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be", + "data/v2/examples/player-bundle-ab-complete-cue-tracks/README.md" + ] + } ] } diff --git a/creator-kit/source-lock.json b/creator-kit/source-lock.json index a3679e2..708d0cf 100644 --- a/creator-kit/source-lock.json +++ b/creator-kit/source-lock.json @@ -1,18 +1,43 @@ { "schemaVersion": "creator-kit-source-lock.v1", - "repository": "synthetic-fixture", - "commit": "6142c40b0868298150e93e6ed2bf19de46dc1380", + "repository": "Spectoda/examples", + "commit": "08339b4e840ba8c8b0a7d2f4c93eb1a5ba075f92", "files": [ + { + "sourcePath": "data/v2/examples/player-bundle-ab-complete-cue-tracks/creator-kit.json", + "sourceSha256": "3f470060dfa277f33cdf0f342d49fe05d63a1f97c3596e01e058caf2f21fe24b", + "bundleSha256": "3f470060dfa277f33cdf0f342d49fe05d63a1f97c3596e01e058caf2f21fe24b", + "license": "MIT", + "role": "selection" + }, + { + "sourcePath": "data/v2/examples/player-bundle-ab-complete-cue-tracks/example.yaml", + "sourceSha256": "4d19b2085b9823627e9b010cbb33683d33116b306a7aa96ca4b4e7bfbec622f4", + "bundleSha256": "4d19b2085b9823627e9b010cbb33683d33116b306a7aa96ca4b4e7bfbec622f4", + "license": "MIT" + }, + { + "sourcePath": "data/v2/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be", + "sourceSha256": "2a5ba31e24c366e1ebd5e81f7c73c95d57220d354464cd34074f0301973b10e0", + "bundleSha256": "2a5ba31e24c366e1ebd5e81f7c73c95d57220d354464cd34074f0301973b10e0", + "license": "MIT" + }, + { + "sourcePath": "data/v2/examples/player-bundle-ab-complete-cue-tracks/README.md", + "sourceSha256": "8687920a58f1e19f132913a327da1ef287a9b291429e7fc781fc54f2ab15ef4a", + "bundleSha256": "8687920a58f1e19f132913a327da1ef287a9b291429e7fc781fc54f2ab15ef4a", + "license": "MIT" + }, { "sourcePath": "en/synthetic-creator.md", "sourceSha256": "ee17f526b4a658f911459f877c942a087f946758cbe38abd3eaed766a42d5cc6", - "normalizedSha256": "c79a442b75413ee3f2c5db22ddb5b6bda6332db0c1f5e73531ef80c9092ee6ad", + "bundleSha256": "c79a442b75413ee3f2c5db22ddb5b6bda6332db0c1f5e73531ef80c9092ee6ad", "license": "spectoda-creator-kit-synthetic" }, { "sourcePath": "en/synthetic-creator.mdx", "sourceSha256": "2aedb71926e038227aede2f41196fe0845747bf020ff150ab0c68a18cb846136", - "normalizedSha256": "bae96c2102fcbfc9ff5afc62605232193a308761d5bdb62df1995c03aea68fb8", + "bundleSha256": "bae96c2102fcbfc9ff5afc62605232193a308761d5bdb62df1995c03aea68fb8", "license": "spectoda-creator-kit-synthetic" } ] From 736062bfdb93d67fdd35d6f0511e07b53d1bbf2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sat, 8 Aug 2026 15:29:10 +0200 Subject: [PATCH 06/10] Creator Kit: Harden release candidate verification Run candidate checks whenever a negative fixture changes and verify that release asset generation leaves the reviewed bundle untouched.\n\nThis keeps the fail-closed publication boundary intact without changing the generated kit or its licensing posture. --- .github/workflows/creator-kit-candidate.yml | 1 + .github/workflows/creator-kit-release.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/creator-kit-candidate.yml b/.github/workflows/creator-kit-candidate.yml index 3a12c98..160b943 100644 --- a/.github/workflows/creator-kit-candidate.yml +++ b/.github/workflows/creator-kit-candidate.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - "creator-kit-fixtures/**" + - "creator-kit-negative-fixtures/**" - "creator-kit-templates/**" - "scripts/creator-kit/**" - "creator-kit/**" diff --git a/.github/workflows/creator-kit-release.yml b/.github/workflows/creator-kit-release.yml index d644631..70f8295 100644 --- a/.github/workflows/creator-kit-release.yml +++ b/.github/workflows/creator-kit-release.yml @@ -73,6 +73,8 @@ jobs: CREATOR_KIT_VERSION: ${{ inputs.version }} SOURCE_COMMIT: ${{ steps.source.outputs.source_commit }} run: node scripts/creator-kit/build-release-candidate.mjs --output creator-kit --release-dir .creator-kit-tmp/release --version "$CREATOR_KIT_VERSION" --source-commit "$SOURCE_COMMIT" + - name: Verify release asset build preserved the reviewed candidate + run: git diff --exit-code -- creator-kit - name: Create the approved stable-channel descriptor locally env: CREATOR_KIT_VERSION: ${{ inputs.version }} From 8c33c51021701dfd8fedf945656792e6a3721c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sat, 8 Aug 2026 18:35:55 +0200 Subject: [PATCH 07/10] Creator Kit: Point Event Player guidance at player.show Move the locked integration source, validation, evaluation prompts and candidate trigger to the canonical static player.show example. The kit now teaches one Project Berry plugin and no longer preserves the unreleased A/B bundle path. --- .github/workflows/creator-kit-candidate.yml | 2 +- creator-kit-evaluations/questions.json | 2 +- creator-kit-evaluations/responses/claude.json | 4 ++-- creator-kit-evaluations/responses/codex.json | 4 ++-- creator-kit-templates/AGENTS.md | 4 ++-- creator-kit-templates/README.md | 6 +++--- scripts/creator-kit/build.mjs | 6 +++--- scripts/creator-kit/creator-kit.test.mjs | 6 +++--- scripts/creator-kit/validate.mjs | 12 ++++++------ 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/creator-kit-candidate.yml b/.github/workflows/creator-kit-candidate.yml index 160b943..d0ac83b 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-bundle-ab-complete-cue-tracks/**" + - "data/v2/examples/player-show-complete-cue-tracks/**" - "package.json" - ".github/workflows/creator-kit-*.yml" diff --git a/creator-kit-evaluations/questions.json b/creator-kit-evaluations/questions.json index fcf0f6e..944eab2 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-bundle-ab-complete-cue-tracks/README.md" + "citation": "examples/player-show-complete-cue-tracks/README.md" } }, { diff --git a/creator-kit-evaluations/responses/claude.json b/creator-kit-evaluations/responses/claude.json index 7af82e3..f6cd5cd 100644 --- a/creator-kit-evaluations/responses/claude.json +++ b/creator-kit-evaluations/responses/claude.json @@ -3,8 +3,8 @@ "responses": [ { "id": "event-player-plugin", - "answer": "The required Project Berry source is player-bundle.be in the locked Event Player example; it must be added to each Controller Project.", - "citations": ["examples/player-bundle-ab-complete-cue-tracks/README.md"], + "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"], "abstained": false }, { diff --git a/creator-kit-evaluations/responses/codex.json b/creator-kit-evaluations/responses/codex.json index dc65c29..f1a1c08 100644 --- a/creator-kit-evaluations/responses/codex.json +++ b/creator-kit-evaluations/responses/codex.json @@ -3,8 +3,8 @@ "responses": [ { "id": "event-player-plugin", - "answer": "Add the locked player-bundle.be Project Berry source to every Controller Project; Studio bundle upload does not install it.", - "citations": ["examples/player-bundle-ab-complete-cue-tracks/README.md"], + "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"], "abstained": false }, { diff --git a/creator-kit-templates/AGENTS.md b/creator-kit-templates/AGENTS.md index 740b32d..d6f755b 100644 --- a/creator-kit-templates/AGENTS.md +++ b/creator-kit-templates/AGENTS.md @@ -21,6 +21,6 @@ hosted RAG, a central MCP gateway and a writable Documentation API are not part of Creator Kit v1. The canonical copy-ready firmware 0.12.11 Event Player integration is exposed -at `examples/player-bundle-ab-complete-cue-tracks/`. Use its README for setup -and its `player-bundle.be` as the Project Berry source. Do not rewrite or +at `examples/player-show-complete-cue-tracks/`. Use its README for setup and +its `player.be` as the Project Berry source. Do not rewrite or silently modernize that locked example. diff --git a/creator-kit-templates/README.md b/creator-kit-templates/README.md index 357363b..c289244 100644 --- a/creator-kit-templates/README.md +++ b/creator-kit-templates/README.md @@ -13,7 +13,7 @@ read-only transport descriptor; its current state is `unpublished`. The Documentation repository remains the private document authority. The copy-ready Event Player plugin under -`examples/player-bundle-ab-complete-cue-tracks/` is copied byte-for-byte from +`examples/player-show-complete-cue-tracks/` is copied byte-for-byte from its canonical `Spectoda/examples` source at the locked commit. Partner-local product knowledge belongs in a separate sibling directory and is never overwritten by Creator Kit updates or rollback. @@ -27,8 +27,8 @@ time-sensitive information. The bundle has no write path to Documentation or to an Organization. For the firmware 0.12.11 Event Player setup, cite and follow -`examples/player-bundle-ab-complete-cue-tracks/README.md`. Add the accompanying -`player-bundle.be` to each Controller Project; uploading a Player bundle does +`examples/player-show-complete-cue-tracks/README.md`. Add the accompanying +`player.be` to each Controller Project; uploading Event Player files does not install the Project Berry plugin. ## Supported transport diff --git a/scripts/creator-kit/build.mjs b/scripts/creator-kit/build.mjs index dcc1aac..b2c99e0 100644 --- a/scripts/creator-kit/build.mjs +++ b/scripts/creator-kit/build.mjs @@ -14,9 +14,9 @@ const DEFAULT_SOURCE = path.join(SCRIPT_ROOT, "creator-kit-fixtures"); const NEGATIVE_SOURCE = path.join(SCRIPT_ROOT, "creator-kit-negative-fixtures"); const DEFAULT_OUTPUT = path.join(SCRIPT_ROOT, "creator-kit"); const SYNTHETIC_SOURCE_LOCK = "synthetic-fixture-2026-08-07"; -const PUBLIC_EXAMPLE_ROOT = path.join(SCRIPT_ROOT, "data/v2/examples/player-bundle-ab-complete-cue-tracks"); +const PUBLIC_EXAMPLE_ROOT = path.join(SCRIPT_ROOT, "data/v2/examples/player-show-complete-cue-tracks"); const PUBLIC_EXAMPLE_SELECTION = path.join(PUBLIC_EXAMPLE_ROOT, "creator-kit.json"); -const PUBLIC_EXAMPLE_DESTINATION = "examples/player-bundle-ab-complete-cue-tracks"; +const PUBLIC_EXAMPLE_DESTINATION = "examples/player-show-complete-cue-tracks"; const EXAMPLES_REPOSITORY = "Spectoda/examples"; export class CreatorKitBuildError extends Error { @@ -359,7 +359,7 @@ async function loadPublicExamples(sourceCommit) { license: selection.license, }, example: { - id: "player-bundle-ab-complete-cue-tracks", + id: "player-show-complete-cue-tracks", path: PUBLIC_EXAMPLE_DESTINATION, title, topic: selection.topic, diff --git a/scripts/creator-kit/creator-kit.test.mjs b/scripts/creator-kit/creator-kit.test.mjs index 5060ba3..30a3d56 100644 --- a/scripts/creator-kit/creator-kit.test.mjs +++ b/scripts/creator-kit/creator-kit.test.mjs @@ -31,13 +31,13 @@ test("double-builds a reproducible candidate and deterministic tar archive", asy assert.equal(first.provenance.stableChannelState, "unpublished"); assert.equal((await validateBundle(path.join(root, "first-bundle"))).documentCount, 2); assert.equal(first.validation.exampleCount, 1); - const sourcePlugin = await readFile(path.join(ROOT, "data/v2/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be")); - const bundledPlugin = await readFile(path.join(root, "first-bundle/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be")); + const sourcePlugin = await readFile(path.join(ROOT, "data/v2/examples/player-show-complete-cue-tracks/player.be")); + const bundledPlugin = await readFile(path.join(root, "first-bundle/examples/player-show-complete-cue-tracks/player.be")); assert.deepEqual(bundledPlugin, sourcePlugin); assert.match(bundledPlugin.toString("utf8"), /timeline[.]at/u); assert.doesNotMatch(bundledPlugin.toString("utf8"), /timeline[.]toMillis/u); const sourceLock = JSON.parse(await readFile(path.join(root, "first-bundle/source-lock.json"), "utf8")); - const pluginLock = sourceLock.files.find((file) => file.sourcePath.endsWith("/player-bundle.be")); + const pluginLock = sourceLock.files.find((file) => file.sourcePath.endsWith("/player.be")); assert.equal(pluginLock.sourceSha256, sha256(sourcePlugin)); assert.equal(pluginLock.bundleSha256, sha256(bundledPlugin)); assert.equal(await readFile(first.archive.archivePath).then((value) => value.length), await readFile(second.archive.archivePath).then((value) => value.length)); diff --git a/scripts/creator-kit/validate.mjs b/scripts/creator-kit/validate.mjs index c1cc31e..2c491a6 100644 --- a/scripts/creator-kit/validate.mjs +++ b/scripts/creator-kit/validate.mjs @@ -156,14 +156,14 @@ export async function validateBundle(bundleRoot = DEFAULT_BUNDLE) { } } const example = manifest.examples[0]; - check(example.id === "player-bundle-ab-complete-cue-tracks" && example.license === "MIT", "Public example identity/license is invalid"); + check(example.id === "player-show-complete-cue-tracks" && example.license === "MIT", "Public example identity/license is invalid"); check(example.compatibility?.firmware === "0.12.11" && example.compatibility?.wasm === "DEBUG_UNIVERSAL_0.12.11_20260808", "Public example compatibility is invalid"); check(Array.isArray(example.files) && example.files.length === 3, "Public example must contain exactly the reviewed three files"); check( example.files.map((file) => file.path).sort().join("\n") === [ - "examples/player-bundle-ab-complete-cue-tracks/README.md", - "examples/player-bundle-ab-complete-cue-tracks/example.yaml", - "examples/player-bundle-ab-complete-cue-tracks/player-bundle.be", + "examples/player-show-complete-cue-tracks/README.md", + "examples/player-show-complete-cue-tracks/example.yaml", + "examples/player-show-complete-cue-tracks/player.be", ].sort().join("\n"), "Public example file selection is invalid", ); @@ -174,10 +174,10 @@ export async function validateBundle(bundleRoot = DEFAULT_BUNDLE) { const lock = sourceLockByPath.get(file.sourcePath); check(lock?.sourceSha256 === file.sourceSha256 && lock?.bundleSha256 === file.sha256 && lock?.license === "MIT", `${file.sourcePath} source lock is invalid`); } - const plugin = await readFile(path.join(root, "examples/player-bundle-ab-complete-cue-tracks/player-bundle.be"), "utf8"); + const plugin = await readFile(path.join(root, "examples/player-show-complete-cue-tracks/player.be"), "utf8"); check(plugin.includes("timeline.at") && !plugin.includes("timeline.toMillis"), "Bundled Player plugin does not use the final timeline.at API"); check(Array.isArray(selection.selectedExamples) && selection.selectedExamples.length === 1, "Selection does not expose the public example"); - const selectionLock = sourceLockByPath.get("data/v2/examples/player-bundle-ab-complete-cue-tracks/creator-kit.json"); + const selectionLock = sourceLockByPath.get("data/v2/examples/player-show-complete-cue-tracks/creator-kit.json"); check(selectionLock?.role === "selection" && selectionLock.sourceSha256 === selectionLock.bundleSha256 && selectionLock.license === "MIT", "Public example selection lock is invalid"); const expectedChecksums = (await readFile(path.join(root, "checksums.sha256"), "utf8")).trim(); const actualChecksums = (await createChecksums(root)).trim(); From c9d976f9232665475a0ef57f3f7bdf0bac68e3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sat, 8 Aug 2026 18:37:24 +0200 Subject: [PATCH 08/10] Creator Kit: Regenerate the static Event Player copy Regenerate the reviewed candidate from exact source commit 40a4ada so its manifest, source lock, checksums and agent instructions contain the canonical player.show plugin byte-for-byte. The candidate remains unpublished and the human release gate is unchanged. --- creator-kit/AGENTS.md | 4 +- creator-kit/README.md | 6 +- creator-kit/checksums.sha256 | 16 +- .../README.md | 157 ---- .../example.yaml | 44 - .../player-bundle.be | 774 ------------------ .../player-show-complete-cue-tracks/README.md | 62 ++ .../example.yaml | 35 + .../player-show-complete-cue-tracks/player.be | 179 ++++ creator-kit/manifest.json | 32 +- creator-kit/selection.json | 10 +- creator-kit/source-lock.json | 26 +- 12 files changed, 323 insertions(+), 1022 deletions(-) delete mode 100644 creator-kit/examples/player-bundle-ab-complete-cue-tracks/README.md delete mode 100644 creator-kit/examples/player-bundle-ab-complete-cue-tracks/example.yaml delete mode 100644 creator-kit/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be create mode 100644 creator-kit/examples/player-show-complete-cue-tracks/README.md create mode 100644 creator-kit/examples/player-show-complete-cue-tracks/example.yaml create mode 100644 creator-kit/examples/player-show-complete-cue-tracks/player.be diff --git a/creator-kit/AGENTS.md b/creator-kit/AGENTS.md index 740b32d..d6f755b 100644 --- a/creator-kit/AGENTS.md +++ b/creator-kit/AGENTS.md @@ -21,6 +21,6 @@ hosted RAG, a central MCP gateway and a writable Documentation API are not part of Creator Kit v1. The canonical copy-ready firmware 0.12.11 Event Player integration is exposed -at `examples/player-bundle-ab-complete-cue-tracks/`. Use its README for setup -and its `player-bundle.be` as the Project Berry source. Do not rewrite or +at `examples/player-show-complete-cue-tracks/`. Use its README for setup and +its `player.be` as the Project Berry source. Do not rewrite or silently modernize that locked example. diff --git a/creator-kit/README.md b/creator-kit/README.md index 357363b..c289244 100644 --- a/creator-kit/README.md +++ b/creator-kit/README.md @@ -13,7 +13,7 @@ read-only transport descriptor; its current state is `unpublished`. The Documentation repository remains the private document authority. The copy-ready Event Player plugin under -`examples/player-bundle-ab-complete-cue-tracks/` is copied byte-for-byte from +`examples/player-show-complete-cue-tracks/` is copied byte-for-byte from its canonical `Spectoda/examples` source at the locked commit. Partner-local product knowledge belongs in a separate sibling directory and is never overwritten by Creator Kit updates or rollback. @@ -27,8 +27,8 @@ time-sensitive information. The bundle has no write path to Documentation or to an Organization. For the firmware 0.12.11 Event Player setup, cite and follow -`examples/player-bundle-ab-complete-cue-tracks/README.md`. Add the accompanying -`player-bundle.be` to each Controller Project; uploading a Player bundle does +`examples/player-show-complete-cue-tracks/README.md`. Add the accompanying +`player.be` to each Controller Project; uploading Event Player files does not install the Project Berry plugin. ## Supported transport diff --git a/creator-kit/checksums.sha256 b/creator-kit/checksums.sha256 index 7b59842..5d54a2c 100644 --- a/creator-kit/checksums.sha256 +++ b/creator-kit/checksums.sha256 @@ -1,21 +1,21 @@ +111e3ca1bf21b76dd262de5b21e98bfd0eba274128337b6d73e07c981cd0c989 manifest.json 1d5899938769871ada19da20f42f26d94cecb6161eff5fb33660d8e9f088bbc4 schemas/creator-kit-compatibility.v1.schema.json -1ee047eb0cbf291d6ccf7d61342f5da6af64119dab2b8e41c337b350b70bc732 selection.json 25090f867683901852fb23075829dff154741a7427acd8592266dd79765d7082 indexes/documents.json 29be85035a878a415590ea1578068337f552e0b8eae729745e2a3427ed7ce4a7 schemas/creator-kit-licenses.v1.schema.json -2a5ba31e24c366e1ebd5e81f7c73c95d57220d354464cd34074f0301973b10e0 examples/player-bundle-ab-complete-cue-tracks/player-bundle.be -2e59802f09261f6ae78d5e275798680ee76237ff5dc4fd68cbe2f4378837234a source-lock.json +2c3eb54b56fcdc3c421e9ff1863647167753b7e42b0fb58125ffab20b3557f5f examples/player-show-complete-cue-tracks/README.md +2d0c9988d38e44776f7655a5286ccc23469d16910ba5183686ee7071fdaab554 selection.json +2f6c95f41cf238c6dee1967964f1d2a0a641ae779d2a009be930a8a580926a8a examples/player-show-complete-cue-tracks/player.be 3488e911f2e41d29518e32ff67485ce4dd310f981133028326b8e77a60254822 schemas/creator-kit-manifest.v1.schema.json 41f576a534c5277269c3c969eb30d6f3e9b8f8112f04eb2ea28c0875a3a0aeb2 stable-channel.json -4d19b2085b9823627e9b010cbb33683d33116b306a7aa96ca4b4e7bfbec622f4 examples/player-bundle-ab-complete-cue-tracks/example.yaml +458b574c6fd6e8cd1e515382f35a6bf20d180541a9c2e69d83c3b084d85e9c8d examples/player-show-complete-cue-tracks/example.yaml 518226851b88ae01e595de3f28f6aedc32450bf2bec8f6036dcde95c1972878d RELEASE_NOTES.md +530c81158f70a04d23ce5bf14fd2aab497081cca4687c3f615c0faf595ddefcd README.md +561f3af13708961c43257d0d4a5008fa0848c9d08d1970dabc0299138fdaad36 AGENTS.md 62c1d40c752ef23c11eb7603945c3e3c64fdde950833a92e2553a0804d3c2fd3 licenses.json -63ab2aa1cc0f51f58adbc5143af432d49e0c57944269033b213199d22b69cb26 manifest.json -78fd74f9223f9afd727c3a220eeaafca2fa63e04183784cf5a4147bae1c001dc AGENTS.md 7a2583955a991b5d5bdebcc505ab8e783831310f308be01b976cd77a46e8af6e indexes/terms.json 7c09ed10e2904a7b01cd82da7c4c7c2cc4e7840a3f9571e28de31ce135ec180d compatibility.json -8687920a58f1e19f132913a327da1ef287a9b291429e7fc781fc54f2ab15ef4a examples/player-bundle-ab-complete-cue-tracks/README.md -86eb826e97162c2f76f47c4cf7e92f42bf9401f5a9c58cc606145b6a64f3423d README.md bae96c2102fcbfc9ff5afc62605232193a308761d5bdb62df1995c03aea68fb8 documents/en/synthetic-creator.mdx +c06f7b55b11c281c8b8f8997b5c40c2d17074a2008030c9ad15779c85bd1c7dc source-lock.json c0bc32a6efdea1a865f5dbba4f6bc94cf115b2350ab065cd031226a7b6e75cb8 bundle.json c79a442b75413ee3f2c5db22ddb5b6bda6332db0c1f5e73531ef80c9092ee6ad documents/en/synthetic-creator.md e5d14caad4ff80aff41ab60b7b2a06ca5adec236d3820083d1fa780ed10ebc7b schemas/creator-kit-bundle.v1.schema.json diff --git a/creator-kit/examples/player-bundle-ab-complete-cue-tracks/README.md b/creator-kit/examples/player-bundle-ab-complete-cue-tracks/README.md deleted file mode 100644 index 09ef74e..0000000 --- a/creator-kit/examples/player-bundle-ab-complete-cue-tracks/README.md +++ /dev/null @@ -1,157 +0,0 @@ -# A/B Complete-Cue Track Player from NetworkStorage - -This copy-ready Project Berry example plays the final SPM/SPT v1 bundle format -on firmware **0.12.11**. The shared Spectoda timeline is the only transport: -play, pause, seek, rewind, and loop require no separate Player events. - -Each SPT is one Track for one Spectoda ID; the Track itself has no event label. -A Track defines stable typed tracked events, and every Cue supplies a complete -value for each of them. The plugin automatically includes a global ID255 Track -when the selected generation contains one, then adds only the local IDs -configured for this Controller. - -```berry -PlayerBundle({"namespace": "demo", "ids": [1], "debug": false}) -``` - -## Canonical source - -`player-bundle.be` in this directory is the single editable, copy-ready source -for Studio, Documentation, and the generated Spectoda Creator Kit. Keep this -public path stable. Creator Kit releases copy its exact bytes from a locked -Examples commit according to the fail-closed `creator-kit.json` selection; the -generated bundle is never a second authoring copy. - -Configure local IDs only in the range 0–254. Never put `255` in `ids`; global -ID255 is discovered automatically. A Controller which owns IDs 1 and 7 uses -`{"ids": [1, 7]}`. Another Controller may use `{"ids": [2]}` while all of the -same NetworkStorage files still synchronize to both Controllers. - -## Files and publication order - -For namespace `demo`, the manifest is `demo.spm`; Track files are named -`demo..<000-255>.spt`. `player-bundle-artifacts.json` contains a synthetic -two-generation show as exact binary hex: - -- slot A is the fallback generation; -- slot B is the preferred generation; -- both generations contain ID255 and ID1; -- the ID1 brightness Cue values change in generation B. - -Convert each `hex` value to exact bytes and upload it with the listed filename -and `version`. A real Studio deployment writes every changed inactive-slot SPT, -reads back its exact bytes/fingerprint, and writes the SPM **last**. Do not hand -edit revisions or fingerprints. - -Studio authors Tracks, tracked events, and complete Cues. **Capture** is the -native WASM operation which reads current EventStore values into a complete Cue; -this Berry plugin does not implement Capture. SEB exists here only as compiled -binary segments inside SPT files, not as an authoring concept. - -The plugin watches only the active manifest and relevant active Track files. -Inactive-slot uploads therefore do not interrupt a running show. Once SPM -changes, playback halts. The new preferred generation is selected only after -ID255 (when present) and every configured local SPT match their manifest ID, -artifact revision, exact size, and NetworkStorage fingerprint. An incomplete -preferred generation keeps a complete fallback. Reconciliation after an -artifact change deliberately waits for the shared timeline to be paused. - -## Runtime behavior - -The plugin range-reads the manifest, SPT headers/directories, and individual -SEB segments. It never loads a whole large SPT into one Berry `bytes()` value. -Every embedded SEB receives native fail-closed validation before a generation -can activate. Seek, rewind, and loop binary-search the last Cue at or before the -timeline target and land that complete Cue at the transport's causal local -millis. Forward playback keeps the exact `(SEB bytes, at, cursor)` tuple until -the segment changes. One local timeline-zero mapping is frozen per -epoch/discontinuity and refreshed when a paused timeline resumes. Equal Cue -times in separate Track files therefore receive the same causal timestamp even -when those Tracks execute on different Plugin turns, while time spent paused is -still reflected in later Cue clocks. - -At most one `SEB.land` execution occurs per Plugin turn. A complete Cue can -therefore use the full firmware 0.12.11 limit of **340 event values** without -competing with another Track in the same transient queue. A Cue is never split. -Future records remain outside EventStore until their timeline offsets are due. - -## Requirements and limits - -- firmware exactly 0.12.11 with final SEB v1 and SPM/SPT v1 support; -- every enabled Track starts with a complete Cue at timeline `0 ms`; -- one SEB segment is at most 4,092 bytes, 340 records, and 65,535 ms; -- one SPT is at most 65,535 bytes, but Studio's measured - `maxTrackPayloadBytes`, bundle, metadata, and free-flash gates may be lower; -- NetworkStorage fingerprints are HMAC-SHA256 with UTF-8 key `fingerprint`; -- this distributed Player uses `SEB.land`; do not replace it with `SEB.emit` - on every Controller. - -Turn `debug` on only while commissioning. The example contains no network key, -MAC address, customer name, or other private installation data. - -## Isolated DEVKIT end-to-end runner - -`player-bundle-devkit-smoke.mts` uses the real NodeSerial connector and App -Controller API. It refuses all writes unless you pass an explicit serial port, -the exact monorepo checkout, and `--confirm-isolated-devkit`: - -```bash -cd /path/to/Spectoda-monorepo -SPECTODA_WASM_VERSION=DEBUG_UNIVERSAL_0.12.11_20260808 \ -node_modules/.bin/tsx \ - /path/to/examples/data/v2/examples/player-bundle-ab-complete-cue-tracks/player-bundle-devkit-smoke.mts \ - --monorepo=/path/to/Spectoda-monorepo \ - --path=/dev/cu.usbserial-XXXX \ - --confirm-isolated-devkit \ - --evidence=/tmp/player-bundle-smoke.json -``` - -Equivalent environment options are `SPECTODA_MONOREPO`, -`SPECTODA_SERIAL_PATH`, and `SPECTODA_WASM_VERSION`. Optional CLI settings are -`--baudrate`, `--scan-timeout`, `--poll-timeout`, `--namespace`, -`--skip-capacity-probe`, and `--evidence`. - -For a `DEBUG_DEV_*` version, copy that exact local build's `.js` and `.wasm` -files into `.webassembly/` below the runner's current directory. The runner -fails before any Controller write when either file is absent; this prevents the -JS loader from silently using an older universal WASM without the Player APIs. - -The runner derives fresh unsigned-48 revisions above every observed namespace -version. It first writes slot A SPTs with exact readback, then writes the A-only -SPM last. It removes copy-only comment lines and indentation, matching Studio's -Project preprocessing, uploads the Berry as `BERRY(...)` through `writeTngl`, -then resumes automatically in a fresh Node/WASM process after the expected -Controller serial restart. The resumed phase physically reads the TNGL bytecode -into a fresh WASM runtime and requires its fingerprint to equal the pre-restart -compiler fingerprint. It then checks global ID255 and local ID1 values plus -identical causal clocks across pause, external-authority, play, seek, rewind, -and a running-rewind loop. It then writes slot B -SPTs, proves that inactive files did not alter playback, writes the new SPM -last, verifies the preferred switch without mixed Cue state, and repeats the -transport checks. - -By default the same isolated Controller also gets a deterministic 60,000-byte -NetworkStorage transport/readback probe. The deploy gate accepts 60,000 and -rejects 60,001 before the write function can run. Use -`--skip-capacity-probe` when flash headroom has already been measured elsewhere. -After the playback checks, the runner retires the probe with a higher-version -zero-byte marker so that its payload does not remain on flash. -The runner prints and optionally saves revisions, exact sizes, fingerprints, -the final namespace listing, firmware version, serial port, monorepo head, and -all observed causal clocks. It leaves the example Project Berry and named -NetworkStorage files installed; restore the intended Project after testing. - -## Host Berry regression - -From the Examples repository root, run the deterministic lifecycle/failure-path -test with the Berry host interpreter built by the matching firmware checkout: - -```bash -/path/to/firmware/components/spectoda-scripting/berry/berry \ - tests/player-bundle-stub.be -``` - -The test reads this example's fixture corpus and covers preferred/fallback -selection, bounded range reads, transient metadata and timeline-projection failures, -pause/resume clocks, seek/loop reconstruction, and local-ID validation without -writing to a Controller. diff --git a/creator-kit/examples/player-bundle-ab-complete-cue-tracks/example.yaml b/creator-kit/examples/player-bundle-ab-complete-cue-tracks/example.yaml deleted file mode 100644 index b0805c8..0000000 --- a/creator-kit/examples/player-bundle-ab-complete-cue-tracks/example.yaml +++ /dev/null @@ -1,44 +0,0 @@ -schemaVersion: example.v2 -slug: player-bundle-ab-complete-cue-tracks -title: A/B Complete-Cue Track Event Player -category: controller -summary: >- - Copy-ready firmware 0.12.11 Project Berry player for SPM/SPT v1 bundles. It - selects a complete preferred or fallback generation, automatically lands the - global ID255 Track plus configured local-ID Tracks, and follows the shared - timeline through play, pause, seek, rewind, and loop. Each Track defines - stable typed tracked events, and every Cue supplies all their values. -tags: - - berry - - event-player - - network-storage - - seb - - spm - - spt - - timeline - - ab-deployment -hardware: >- - Requires exactly firmware 0.12.11 with final SEB v1 and SPM/SPT v1 support. - Configure Project Berry with only this Controller's local Spectoda IDs 0-254; - the global ID255 Track is automatic. Upload the SPM/SPT bundle to network-wide - NetworkStorage. The player-bundle.be path is a stable public integration - contract used by Studio, Documentation, and Spectoda Creator Kit releases. -files: - - path: README.md - role: Setup, publication, validation, and runtime behavior - language: markdown - - path: creator-kit.json - role: Explicit fail-closed Spectoda Creator Kit export selection - language: json - - path: player-bundle.be - role: Copy-ready Project Berry SPM/SPT Player - language: berry - - path: player-bundle-artifacts.json - role: Synthetic two-generation A/B NetworkStorage artifact corpus - language: json - - path: player-bundle-devkit-smoke.mts - role: Explicit isolated-DEVKIT NodeSerial/App Controller end-to-end runner - language: typescript -related: - - seb-causal-scene-recall - - seb-segmented-id-track-player diff --git a/creator-kit/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be b/creator-kit/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be deleted file mode 100644 index eecce25..0000000 --- a/creator-kit/examples/player-bundle-ab-complete-cue-tracks/player-bundle.be +++ /dev/null @@ -1,774 +0,0 @@ -# Project Berry Player for firmware 0.12.11 and SPM/SPT v1. -# -# Public API: -# PlayerBundle({"namespace": "demo", "ids": [1], "debug": false}) -# -# ID255 is discovered automatically when it exists in a generation. Configure -# only Controller-local IDs 0..254. NetworkStorage remains a global union: this -# Controller validates and opens only the global Track and its configured local -# Tracks. -def PlayerBundle(S) - import string - - var ns = S.find("namespace", "player") - var ids = S.find("ids", []) - var dbg = S.find("debug", false) - var integrity_polls = S.find("integrityPolls", 25) - var retry_polls = S.find("retryPolls", 100) - - # The namespace is also part of every NetworkStorage filename. Keeping the - # validation here makes an invalid Project fail before registering Plugin. - if type(ns) != "string" || size(ns) < 1 || size(ns) > 14 - raise "value_error", "Player namespace must contain 1-14 ASCII characters" - end - var allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-" - for i : 0..size(ns) - 1 - if string.find(allowed, ns[i]) < 0 - raise "value_error", "Player namespace contains an invalid character" - end - end - var manifest_file = ns + ".spm" - if type(ids) != "instance" || classname(ids) != "list" || - type(integrity_polls) != "int" || type(retry_polls) != "int" || - integrity_polls < 1 || retry_polls < 1 - raise "value_error", "Player ids/poll configuration is invalid" - end - - # local_ids is sorted once so activation and playback stay deterministic. - var local_ids = [] - for id : ids - if type(id) != "int" || id < 0 || id > 254 - raise "value_error", "Player local IDs must be integers in 0..254" - end - var at = 0 - while at < local_ids.size() && local_ids[at] < id - at += 1 - end - if at < local_ids.size() && local_ids[at] == id - raise "value_error", "Player local IDs must be unique" - end - local_ids.insert(at, id) - end - - # Mutable state lives in one map because Berry closures capture scalar - # upvalues by value. active is kept when a newer upload is incomplete. - var R = { - "active": nil, - "armed": false, - "storage_dirty": false, - "reconcile": nil, - "last_time": 0, - "last_epoch": 0, - "last_paused": true, - "timeline_zero": nil, - "poll": 0, - "round": 0, - "error": nil - } - - def log_error(message) - if message != R["error"] - R["error"] = message - if dbg - print("PlayerBundle", message) - end - end - end - - def track_name(slot, id) - var digits = str(id) - if id < 10 - digits = "00" + digits - elif id < 100 - digits = "0" + digits - end - return ns + "." + slot + "." + digits + ".spt" - end - - # getNetworkStorageData(name, offset, length) is bounded to 8192 bytes. - # Every read in this player is a header, directory entry, or <=4092B SEB. - def read_exact(name, offset, length) - var b = spectoda.getNetworkStorageData(name, offset, length) - if b == nil || b.size() != length - return nil - end - return b - end - - def exact_size(name, size) - var tail = spectoda.getNetworkStorageData(name, size, 1) - return tail != nil && tail.size() == 0 - end - - def bytes_zero(b, first, last) - for i : first..last - if b.get(i) != 0 - return false - end - end - return true - end - - def bytes_equal(a, ao, b, bo, count) - for i : 0..count - 1 - if a.get(ao + i) != b.get(bo + i) - return false - end - end - return true - end - - def fingerprint_hex(b, offset) - var h = "0123456789abcdef" - var result = "" - for i : 0..31 - var v = b.get(offset + i) - result += h[(v >> 4) & 15] + h[v & 15] - end - return result - end - - def metadata() - var result = {} - var entries = spectoda.listNetworkStorageData() - if entries == nil - return nil - end - for entry : entries - var name = entry.find("name", nil) - var fingerprint = entry.find("fingerprint", nil) - if name != nil && fingerprint != nil - result[name] = fingerprint - end - end - return result - end - - def generation(header, descriptor_offset, slot, expected_table) - var count = header.get(descriptor_offset + 6, 2) - var duration = header.get(descriptor_offset + 8, 4) - var table = header.get(descriptor_offset + 12, 2) - var table_size = header.get(descriptor_offset + 14, 2) - if duration < 0 || table != expected_table || table_size != count * 44 - return nil - end - return { - "slot": slot, - "duration": duration, - "count": count, - "table": table, - "table_size": table_size, - "tracks": {} - } - end - - # Parse both generation tables so gaps, overlap, duplicates, unsorted IDs, - # reserved fields, and trailing bytes fail before any artifact is opened. - def parse_manifest(meta) - var name = manifest_file - var fp = meta.find(name, nil) - var h = read_exact(name, 0, 112) - if fp == nil || h == nil || h.get(0) != 83 || h.get(1) != 80 || h.get(2) != 77 - return nil - end - var file_size = h.get(6, 2) - var ps = h.get(8) - var fs = h.get(9) - if h.get(3) != 1 || h.get(4) != 16 || h.get(5) != 0 || - (ps != 0 && ps != 1) || (fs != 0 && fs != 1 && fs != 255) || - fs == ps || h.get(10) != 48 || h.get(11) != 44 || - !bytes_zero(h, 12, 15) || file_size < 112 || !exact_size(name, file_size) - return nil - end - - var preferred = generation(h, 16, ps == 0 ? "a" : "b", 112) - if preferred == nil - return nil - end - var expected = 112 + preferred["table_size"] - var fallback = nil - if fs == 255 - if !bytes_zero(h, 64, 111) - return nil - end - else - fallback = generation(h, 64, fs == 0 ? "a" : "b", expected) - if fallback == nil - return nil - end - expected += fallback["table_size"] - end - if expected != file_size - return nil - end - - var generations = [preferred] - if fallback != nil - generations.push(fallback) - end - for g : generations - var previous_id = -1 - for i : 0..g["count"] - 1 - var entry = read_exact(name, g["table"] + i * 44, 44) - if entry == nil - return nil - end - var id = entry.get(0) - if id <= previous_id || entry.get(1) != 0 || entry.get(10, 2) != 0 - return nil - end - previous_id = id - g["tracks"][id] = { - "id": id, - "size": entry.get(2, 2), - "revision": entry[4..9], - "fingerprint": fingerprint_hex(entry, 12) - } - end - end - return {"fingerprint": fp, "preferred": preferred, "fallback": fallback} - end - - def read_segment(track, index) - if index < 0 || index >= track["segment_count"] - return nil - end - var b = read_exact(track["name"], track["segment_offset"] + index * 12, 12) - if b == nil - return nil - end - return { - "origin": b.get(0, 4), - "first_cue": b.get(4, 2), - "cue_count": b.get(6, 2), - "seb_offset": b.get(8, 2), - "seb_size": b.get(10, 2) - } - end - - def read_cue(track, index) - if index < 0 || index >= track["cue_count"] - return nil - end - var b = read_exact(track["name"], track["cue_offset"] + index * 12, 12) - if b == nil - return nil - end - return { - "time": b.get(0, 4), - "segment": b.get(4, 2), - "cursor": b.get(6, 2), - "count": b.get(8, 2), - "offset": b.get(10, 2) - } - end - - # Validate the full SPT directory and each embedded SEB without retaining - # a whole SPT in Berry RAM. SEB.land at cursor=record_count is a native, - # mutation-free full-blob validation call. - def validate_track(descriptor, slot, duration, meta) - var name = track_name(slot, descriptor["id"]) - if descriptor["size"] < 36 || descriptor["size"] > 65535 || - meta.find(name, nil) != descriptor["fingerprint"] || - !exact_size(name, descriptor["size"]) - return nil - end - var h = read_exact(name, 0, 36) - if h == nil || h.get(0) != 83 || h.get(1) != 80 || h.get(2) != 84 || - h.get(3) != 1 || h.get(4) != 36 || h.get(5) != 0 || - h.get(6) != descriptor["id"] || h.get(7) != 0 || - !bytes_equal(h, 8, descriptor["revision"], 0, 6) || - h.get(14) != 12 || h.get(15) != 12 || h.get(16, 4) != duration || - h.get(20, 2) < 1 || h.get(20, 2) > 340 || h.get(22, 2) < 1 || - h.get(24, 2) < 1 || h.get(26, 2) != 36 || h.get(34, 2) != 0 || - h.get(32, 2) != descriptor["size"] - return nil - end - var cue_count = h.get(22, 2) - var segment_count = h.get(24, 2) - var segment_offset = h.get(28, 2) - var data_offset = h.get(30, 2) - if segment_offset != 36 + cue_count * 12 || - data_offset != segment_offset + segment_count * 12 || - data_offset > descriptor["size"] - return nil - end - - var track = { - "id": descriptor["id"], - "name": name, - "fingerprint": descriptor["fingerprint"], - "size": descriptor["size"], - "duration": duration, - "event_count": h.get(20, 2), - "cue_count": cue_count, - "segment_count": segment_count, - "cue_offset": 36, - "segment_offset": segment_offset, - "data_offset": data_offset, - "play_segment": 0, - "cursor": 0, - "bytes": nil, - "at": nil - } - - var expected_cue = 0 - var expected_data = data_offset - var previous_time = -1 - for segment_index : 0..segment_count - 1 - var segment = read_segment(track, segment_index) - if segment == nil || segment["origin"] < 0 || - segment["first_cue"] != expected_cue || segment["cue_count"] < 1 || - segment["first_cue"] + segment["cue_count"] > cue_count || - segment["seb_offset"] != expected_data || segment["seb_size"] < 12 || - segment["seb_size"] > 4092 || - segment["seb_offset"] + segment["seb_size"] > descriptor["size"] - return nil - end - var seb = read_exact(name, segment["seb_offset"], segment["seb_size"]) - if seb == nil || seb.get(0) != 83 || seb.get(1) != 69 || seb.get(2) != 66 || - seb.get(3) != 1 || seb.get(4) != 12 || seb.get(5) != 0 || - seb.get(10) != 12 || seb.get(11) != 0 - return nil - end - var records = seb.get(6, 2) - var seb_duration = seb.get(8, 2) - if records < 1 || records > 340 || segment["seb_size"] != 12 + records * 12 || - records != segment["cue_count"] * track["event_count"] - return nil - end - var inspected = SEB.land(seb, { - "at": controller.millis(), - "cursor": records, - "until": 0 - }) - if !inspected["ok"] || !inspected["done"] || inspected["cursor"] != records || - inspected["consumed"] != 0 - return nil - end - - var expected_record = 0 - for local_cue : 0..segment["cue_count"] - 1 - var cue_index = segment["first_cue"] + local_cue - var cue = read_cue(track, cue_index) - if cue == nil || cue["time"] <= previous_time || cue["time"] > duration || - cue["segment"] != segment_index || cue["cursor"] != expected_record || - cue["count"] != track["event_count"] || - cue["offset"] != cue["time"] - segment["origin"] || - cue["offset"] > seb_duration || - (local_cue == 0 && cue["time"] != segment["origin"]) - return nil - end - if cue_index == 0 && cue["time"] != 0 - return nil - end - for record : 0..cue["count"] - 1 - var record_offset = 12 + (cue["cursor"] + record) * 12 - if seb.get(record_offset + 8, 2) != cue["offset"] || - seb.get(record_offset + 10) != track["id"] - return nil - end - end - expected_record += cue["count"] - previous_time = cue["time"] - end - if expected_record != records - return nil - end - expected_cue += segment["cue_count"] - expected_data += segment["seb_size"] - end - if expected_cue != cue_count || expected_data != descriptor["size"] - return nil - end - return track - end - - def validate_generation(manifest, generation, meta) - if generation == nil - return nil - end - var requested = [] - if generation["tracks"].contains(255) - requested.push(255) - end - for id : local_ids - if !generation["tracks"].contains(id) - return nil - end - requested.push(id) - end - - var tracks = [] - for id : requested - var track = validate_track( - generation["tracks"][id], generation["slot"], generation["duration"], meta) - if track == nil - return nil - end - tracks.push(track) - end - return { - "slot": generation["slot"], - "duration": generation["duration"], - "manifest_fingerprint": manifest["fingerprint"], - "tracks": tracks - } - end - - def active_matches(active, meta) - if meta == nil - return nil - end - if active == nil || - meta.find(manifest_file, nil) != active["manifest_fingerprint"] - return false - end - for track : active["tracks"] - if meta.find(track["name"], nil) != track["fingerprint"] - return false - end - end - return true - end - - # Preferred is tried first. A failed preferred never replaces the current - # generation; a fully valid fallback is the only startup/recovery choice. - def load_bundle() - var before = metadata() - if before == nil - return nil - end - var manifest = parse_manifest(before) - if manifest == nil - return nil - end - var candidate = validate_generation(manifest, manifest["preferred"], before) - if candidate == nil - candidate = validate_generation(manifest, manifest["fallback"], before) - end - if candidate == nil - return nil - end - var after = metadata() - if !active_matches(candidate, after) - return nil - end - return candidate - end - - def subtract_local_millis(local_millis, offset) - # Avoid relying on signed-overflow behavior at the 32-bit millis wrap. - var minimum = -2147483648 - if local_millis >= minimum + offset - return local_millis - offset - end - var underflow = offset - (local_millis - minimum) - return 2147483647 - underflow + 1 - end - - def add_local_millis(local_millis, offset) - # Mirror subtract_local_millis while keeping the result in signed - # 32-bit millis space across controller.millis() wrap. - var maximum = 2147483647 - var room = maximum - local_millis - if offset <= room - return local_millis + offset - end - return -2147483648 + (offset - room - 1) - end - - def find_cue(track, target) - var low = 0 - var high = track["cue_count"] - 1 - var found = nil - while low <= high - var middle = (low + high) >> 1 - var cue = read_cue(track, middle) - if cue == nil - return nil - end - if cue["time"] <= target - found = cue - low = middle + 1 - else - high = middle - 1 - end - end - return found - end - - def prepare_seek(track, target, local_now) - var cue = find_cue(track, target) - if cue == nil - return nil - end - var segment = read_segment(track, cue["segment"]) - if segment == nil - return nil - end - return { - "track": track, - "cue": cue, - "segment": segment, - "at": subtract_local_millis(local_now, cue["offset"]) - } - end - - def begin_reconcile(state) - var active = R["active"] - if active == nil || state["time"] < 0 || state["time"] > active["duration"] - R["armed"] = false - R["reconcile"] = nil - log_error("timeline is outside the active show") - return false - end - # Keep the discontinuity trigger armed until reconciliation completes, - # so a transient projection/directory read failure retries next turn. - R["last_epoch"] = state["epoch"] - 1 - var local_now = timeline.at(state["time"]) - if local_now == nil - R["armed"] = false - log_error("timeline cannot project local millis") - return false - end - R["timeline_zero"] = subtract_local_millis(local_now, state["time"]) - # Freeze one mapping per timeline epoch/discontinuity. Calling - # timeline.at(segment origin) separately for each Track can - # quantize equal Cues one millisecond apart on real Controllers. - # Resolve every Cue and segment directory before the first Cue value is - # landed. SEB bytes are loaded one Track at a time so a large - # multi-ID bundle does not remain resident in Berry RAM. - var operations = [] - for track : active["tracks"] - var operation = prepare_seek(track, state["time"], local_now) - if operation == nil - R["armed"] = false - log_error("failed to prepare Cue") - return false - end - operations.push(operation) - end - R["armed"] = false - R["reconcile"] = { - "target": state["time"], - "epoch": state["epoch"], - "operations": operations, - "index": 0 - } - return true - end - - def reconcile_step(state) - var reconciliation = R["reconcile"] - if reconciliation == nil - return false - end - if state["epoch"] != reconciliation["epoch"] - R["reconcile"] = nil - return begin_reconcile(state) - end - if reconciliation["index"] < reconciliation["operations"].size() - var operation = reconciliation["operations"][reconciliation["index"]] - var matches = active_matches(R["active"], metadata()) - if matches == nil - return false - end - if !matches - R["storage_dirty"] = true - R["reconcile"] = nil - log_error("Player artifacts changed") - return false - end - var cue = operation["cue"] - var segment = operation["segment"] - var seb = read_exact(operation["track"]["name"], - segment["seb_offset"], segment["seb_size"]) - if seb == nil - log_error("failed to range-read Cue SEB") - return false - end - var result = SEB.land(seb, { - "at": operation["at"], - "cursor": cue["cursor"], - "until": cue["offset"] - }) - if !result["ok"] || result["consumed"] != cue["count"] - log_error("Complete Cue failed: " + str(result["error"])) - return false - end - var track = operation["track"] - var next_cursor = result["cursor"] - if result["done"] - track["play_segment"] = cue["segment"] + 1 - track["cursor"] = 0 - else - track["play_segment"] = cue["segment"] - track["cursor"] = next_cursor - end - track["bytes"] = nil - track["at"] = nil - reconciliation["index"] += 1 - return true - end - - R["reconcile"] = nil - R["armed"] = true - R["last_time"] = state["time"] - R["last_epoch"] = state["epoch"] - R["last_paused"] = state["paused"] - R["error"] = nil - return true - end - - # Return 1 after one SEB call, 0 when this Track has nothing due, and -1 on - # a read/projection failure. Cursor advances only from a successful native call. - def process_track(track, target) - if track["play_segment"] >= track["segment_count"] - return 0 - end - var segment = read_segment(track, track["play_segment"]) - if segment == nil || target < segment["origin"] - return segment == nil ? -1 : 0 - end - if track["bytes"] == nil - track["bytes"] = read_exact(track["name"], segment["seb_offset"], segment["seb_size"]) - if R["timeline_zero"] != nil - track["at"] = add_local_millis(R["timeline_zero"], segment["origin"]) - end - if track["bytes"] == nil || track["at"] == nil - track["bytes"] = nil - track["at"] = nil - return -1 - end - end - var until = target - segment["origin"] - var duration = track["bytes"].get(8, 2) - if until > duration - until = duration - end - var result = SEB.land(track["bytes"], { - "at": track["at"], - "cursor": track["cursor"], - "until": until - }) - if !result["ok"] - log_error("forward SEB failed: " + str(result["error"])) - return 1 - end - track["cursor"] = result["cursor"] - if result["done"] - track["play_segment"] += 1 - track["cursor"] = 0 - track["bytes"] = nil - track["at"] = nil - end - R["error"] = nil - return 1 - end - - # At most one SEB call is issued per Plugin turn. This makes the native - # 340-value atomic queue available to one complete Cue even with many IDs. - def process_forward(target) - var tracks = R["active"]["tracks"] - if tracks.size() == 0 - return - end - for checked : 0..tracks.size() - 1 - var index = (R["round"] + checked) % tracks.size() - var status = process_track(tracks[index], target) - if status != 0 - R["round"] = (index + 1) % tracks.size() - if status < 0 - log_error("failed to range-read active SPT") - end - return - end - end - end - - return Plugin(def() - var state = timeline.getState() - R["poll"] += 1 - - # Integrity polling watches only the manifest and currently active - # relevant SPTs. Writes to the inactive slot do not interrupt playback; - # the SPM-last publication is the change that arms reconciliation. - if R["active"] != nil && R["poll"] % integrity_polls == 0 - if active_matches(R["active"], metadata()) == false - R["storage_dirty"] = true - R["armed"] = false - R["reconcile"] = nil - log_error("Player artifacts changed; pause") - end - end - - if R["storage_dirty"] - if state["paused"] && R["poll"] % retry_polls == 0 - var candidate = load_bundle() - if candidate != nil - R["active"] = candidate - R["storage_dirty"] = false - begin_reconcile(state) - else - log_error("Player generation incomplete") - end - end - return - end - - if R["active"] == nil - if R["poll"] == 1 || R["poll"] % retry_polls == 0 - var candidate = load_bundle() - if candidate != nil - R["active"] = candidate - begin_reconcile(state) - else - log_error("no complete Player generation") - end - end - return - end - - if R["reconcile"] != nil - reconcile_step(state) - return - end - - # A paused timeline stops advancing while the local/network clock does - # not. Refresh the mapping on resume without landing the current Cue - # again: an external event remains authoritative until the next Cue, - # whose causal clock must still reflect the time spent paused. - if R["last_paused"] && !state["paused"] - var local_now = timeline.at(state["time"]) - if local_now == nil - log_error("timeline resume has no local projection") - return - end - R["timeline_zero"] = subtract_local_millis(local_now, state["time"]) - for track : R["active"]["tracks"] - track["bytes"] = nil - track["at"] = nil - end - end - - # Rewind, seek, and timeline loop are all reconstructed from the last - # complete Cue at/before the new shared timeline position. - if state["epoch"] != R["last_epoch"] || state["time"] < R["last_time"] - if begin_reconcile(state) - reconcile_step(state) - end - return - end - - if !state["paused"] && R["armed"] - process_forward(state["time"]) - end - R["last_time"] = state["time"] - R["last_epoch"] = state["epoch"] - R["last_paused"] = state["paused"] - end) -end - -# Copy this call into each Controller's Project and configure only its local IDs. -PlayerBundle({"namespace": "demo", "ids": [1], "debug": false}) diff --git a/creator-kit/examples/player-show-complete-cue-tracks/README.md b/creator-kit/examples/player-show-complete-cue-tracks/README.md new file mode 100644 index 0000000..338de25 --- /dev/null +++ b/creator-kit/examples/player-show-complete-cue-tracks/README.md @@ -0,0 +1,62 @@ +# 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,789-byte TNGL BERRY payload +(3,785 source bytes plus framing), 307 bytes below the 4 KiB hard limit. The +1 KiB target remains a stretch measurement, not a correctness promise. + +`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 new file mode 100644 index 0000000..f61e32a --- /dev/null +++ b/creator-kit/examples/player-show-complete-cue-tracks/example.yaml @@ -0,0 +1,35 @@ +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 new file mode 100644 index 0000000..58136b6 --- /dev/null +++ b/creator-kit/examples/player-show-complete-cue-tracks/player.be @@ -0,0 +1,179 @@ + + + +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 +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 !=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 +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"]|| q["cursor"]!=cur+t[1] +if d print("Player reconcile",t[0],q["error"])end +return false +end +t[3]=bs t[4]=bc advance(t,bs,bc)R[3]+=1 +return true +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"]|| q["cursor"]<=cur || q["cursor"]%pick[1]!=0 +if d print("Player forward",pick[0],q["error"])end +return false +end +var n=int(q["cursor"]/ pick[1]) +if n>=seg[1].size()pick[3]=si+1 pick[4]=0 +else pick[4]=n end +return true +end + +return Plugin(def() +var s=timeline.getState() +var x=s["time"] +var e=s["epoch"] +if !R[0]|| e !=R[6]|| x Date: Sat, 8 Aug 2026 20:03:33 +0200 Subject: [PATCH 09/10] Creator Kit: lock hardened static Player --- creator-kit/checksums.sha256 | 8 +++--- .../player-show-complete-cue-tracks/README.md | 16 ++++++++--- .../player-show-complete-cue-tracks/player.be | 27 ++++++++++++------- creator-kit/manifest.json | 10 +++---- creator-kit/source-lock.json | 10 +++---- 5 files changed, 44 insertions(+), 27 deletions(-) diff --git a/creator-kit/checksums.sha256 b/creator-kit/checksums.sha256 index 5d54a2c..812b9ca 100644 --- a/creator-kit/checksums.sha256 +++ b/creator-kit/checksums.sha256 @@ -1,10 +1,7 @@ -111e3ca1bf21b76dd262de5b21e98bfd0eba274128337b6d73e07c981cd0c989 manifest.json 1d5899938769871ada19da20f42f26d94cecb6161eff5fb33660d8e9f088bbc4 schemas/creator-kit-compatibility.v1.schema.json 25090f867683901852fb23075829dff154741a7427acd8592266dd79765d7082 indexes/documents.json 29be85035a878a415590ea1578068337f552e0b8eae729745e2a3427ed7ce4a7 schemas/creator-kit-licenses.v1.schema.json -2c3eb54b56fcdc3c421e9ff1863647167753b7e42b0fb58125ffab20b3557f5f examples/player-show-complete-cue-tracks/README.md 2d0c9988d38e44776f7655a5286ccc23469d16910ba5183686ee7071fdaab554 selection.json -2f6c95f41cf238c6dee1967964f1d2a0a641ae779d2a009be930a8a580926a8a examples/player-show-complete-cue-tracks/player.be 3488e911f2e41d29518e32ff67485ce4dd310f981133028326b8e77a60254822 schemas/creator-kit-manifest.v1.schema.json 41f576a534c5277269c3c969eb30d6f3e9b8f8112f04eb2ea28c0875a3a0aeb2 stable-channel.json 458b574c6fd6e8cd1e515382f35a6bf20d180541a9c2e69d83c3b084d85e9c8d examples/player-show-complete-cue-tracks/example.yaml @@ -14,9 +11,12 @@ 62c1d40c752ef23c11eb7603945c3e3c64fdde950833a92e2553a0804d3c2fd3 licenses.json 7a2583955a991b5d5bdebcc505ab8e783831310f308be01b976cd77a46e8af6e indexes/terms.json 7c09ed10e2904a7b01cd82da7c4c7c2cc4e7840a3f9571e28de31ce135ec180d compatibility.json +8f0b7f2d735edb7505aeee1408e57eda385d093c228ae5be84662faa09ee5399 source-lock.json +a537ea02342dc335f4d5b7b478a1217b6d35ca72ad6aeff638e46aa105e44948 examples/player-show-complete-cue-tracks/player.be +af580f8f045e1133773f57a8d24d17a2b4072eca8c3d9cf2d1c515146d0321de examples/player-show-complete-cue-tracks/README.md bae96c2102fcbfc9ff5afc62605232193a308761d5bdb62df1995c03aea68fb8 documents/en/synthetic-creator.mdx -c06f7b55b11c281c8b8f8997b5c40c2d17074a2008030c9ad15779c85bd1c7dc source-lock.json c0bc32a6efdea1a865f5dbba4f6bc94cf115b2350ab065cd031226a7b6e75cb8 bundle.json c79a442b75413ee3f2c5db22ddb5b6bda6332db0c1f5e73531ef80c9092ee6ad documents/en/synthetic-creator.md +d95dec71686c62e2560906d8777b0ff9d144bf11bac59b57d72988e1a334cad6 manifest.json e5d14caad4ff80aff41ab60b7b2a06ca5adec236d3820083d1fa780ed10ebc7b schemas/creator-kit-bundle.v1.schema.json fe3c496bf794676fe9d8c4854f1a29272c6cf6a22d2dd7cc1422b17caf7e33cb schemas/creator-kit-source-lock.v1.schema.json diff --git a/creator-kit/examples/player-show-complete-cue-tracks/README.md b/creator-kit/examples/player-show-complete-cue-tracks/README.md index 338de25..6615f1f 100644 --- a/creator-kit/examples/player-show-complete-cue-tracks/README.md +++ b/creator-kit/examples/player-show-complete-cue-tracks/README.md @@ -49,9 +49,19 @@ read and atomically landed by the native `SEB.land(filename, - 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,789-byte TNGL BERRY payload -(3,785 source bytes plus framing), 307 bytes below the 4 KiB hard limit. The -1 KiB target remains a stretch measurement, not a correctness promise. +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 diff --git a/creator-kit/examples/player-show-complete-cue-tracks/player.be b/creator-kit/examples/player-show-complete-cue-tracks/player.be index 58136b6..4157ba0 100644 --- a/creator-kit/examples/player-show-complete-cue-tracks/player.be +++ b/creator-kit/examples/player-show-complete-cue-tracks/player.be @@ -1,6 +1,5 @@ - - +# 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",[]) @@ -61,12 +60,14 @@ 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 @@ -85,6 +86,7 @@ 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 @@ -122,13 +124,14 @@ 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"]|| q["cursor"]!=cur+t[1] +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 -t[3]=bs t[4]=bc advance(t,bs,bc)R[3]+=1 -return true -end def forward(x) var pick=nil @@ -150,15 +153,19 @@ if until>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"]|| q["cursor"]<=cur || q["cursor"]%pick[1]!=0 -if d print("Player forward",pick[0],q["error"])end -return false -end +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() diff --git a/creator-kit/manifest.json b/creator-kit/manifest.json index c0b2378..4c8db34 100644 --- a/creator-kit/manifest.json +++ b/creator-kit/manifest.json @@ -6,7 +6,7 @@ "contentScope": "synthetic-fixture-with-public-examples", "source": { "repository": "Spectoda/examples", - "commit": "40a4ada016884a07e7ba8f6ca350a203311f193e" + "commit": "71698144f34573b28e81a0a39031147ee0c683b6" }, "documents": [ { @@ -67,15 +67,15 @@ "sourcePath": "data/v2/examples/player-show-complete-cue-tracks/player.be", "path": "examples/player-show-complete-cue-tracks/player.be", "format": "be", - "sourceSha256": "2f6c95f41cf238c6dee1967964f1d2a0a641ae779d2a009be930a8a580926a8a", - "sha256": "2f6c95f41cf238c6dee1967964f1d2a0a641ae779d2a009be930a8a580926a8a" + "sourceSha256": "a537ea02342dc335f4d5b7b478a1217b6d35ca72ad6aeff638e46aa105e44948", + "sha256": "a537ea02342dc335f4d5b7b478a1217b6d35ca72ad6aeff638e46aa105e44948" }, { "sourcePath": "data/v2/examples/player-show-complete-cue-tracks/README.md", "path": "examples/player-show-complete-cue-tracks/README.md", "format": "md", - "sourceSha256": "2c3eb54b56fcdc3c421e9ff1863647167753b7e42b0fb58125ffab20b3557f5f", - "sha256": "2c3eb54b56fcdc3c421e9ff1863647167753b7e42b0fb58125ffab20b3557f5f" + "sourceSha256": "af580f8f045e1133773f57a8d24d17a2b4072eca8c3d9cf2d1c515146d0321de", + "sha256": "af580f8f045e1133773f57a8d24d17a2b4072eca8c3d9cf2d1c515146d0321de" } ] } diff --git a/creator-kit/source-lock.json b/creator-kit/source-lock.json index c4e264f..53dc27a 100644 --- a/creator-kit/source-lock.json +++ b/creator-kit/source-lock.json @@ -1,7 +1,7 @@ { "schemaVersion": "creator-kit-source-lock.v1", "repository": "Spectoda/examples", - "commit": "40a4ada016884a07e7ba8f6ca350a203311f193e", + "commit": "71698144f34573b28e81a0a39031147ee0c683b6", "files": [ { "sourcePath": "data/v2/examples/player-show-complete-cue-tracks/creator-kit.json", @@ -18,14 +18,14 @@ }, { "sourcePath": "data/v2/examples/player-show-complete-cue-tracks/player.be", - "sourceSha256": "2f6c95f41cf238c6dee1967964f1d2a0a641ae779d2a009be930a8a580926a8a", - "bundleSha256": "2f6c95f41cf238c6dee1967964f1d2a0a641ae779d2a009be930a8a580926a8a", + "sourceSha256": "a537ea02342dc335f4d5b7b478a1217b6d35ca72ad6aeff638e46aa105e44948", + "bundleSha256": "a537ea02342dc335f4d5b7b478a1217b6d35ca72ad6aeff638e46aa105e44948", "license": "MIT" }, { "sourcePath": "data/v2/examples/player-show-complete-cue-tracks/README.md", - "sourceSha256": "2c3eb54b56fcdc3c421e9ff1863647167753b7e42b0fb58125ffab20b3557f5f", - "bundleSha256": "2c3eb54b56fcdc3c421e9ff1863647167753b7e42b0fb58125ffab20b3557f5f", + "sourceSha256": "af580f8f045e1133773f57a8d24d17a2b4072eca8c3d9cf2d1c515146d0321de", + "bundleSha256": "af580f8f045e1133773f57a8d24d17a2b4072eca8c3d9cf2d1c515146d0321de", "license": "MIT" }, { From 1fc8f8809518ad103b0fe43c0fa367072b355fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Sat, 8 Aug 2026 21:17:35 +0200 Subject: [PATCH 10/10] Creator Kit: Refresh source provenance after stack merge Point the candidate at the rebased exact source commit that contains the published player.show example and the reviewed synthetic fixture sources. The bundle content remains unchanged; only provenance and deterministic checksums are refreshed so validation remains fail-closed after the Examples stack reaches main. --- creator-kit/checksums.sha256 | 4 ++-- creator-kit/manifest.json | 2 +- creator-kit/source-lock.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/creator-kit/checksums.sha256 b/creator-kit/checksums.sha256 index 812b9ca..90a36b3 100644 --- a/creator-kit/checksums.sha256 +++ b/creator-kit/checksums.sha256 @@ -1,6 +1,7 @@ 1d5899938769871ada19da20f42f26d94cecb6161eff5fb33660d8e9f088bbc4 schemas/creator-kit-compatibility.v1.schema.json 25090f867683901852fb23075829dff154741a7427acd8592266dd79765d7082 indexes/documents.json 29be85035a878a415590ea1578068337f552e0b8eae729745e2a3427ed7ce4a7 schemas/creator-kit-licenses.v1.schema.json +2b0a557e071f8118a29f6a9e2188c164861b55e86e2dc8db6f77142bbfd62ee7 manifest.json 2d0c9988d38e44776f7655a5286ccc23469d16910ba5183686ee7071fdaab554 selection.json 3488e911f2e41d29518e32ff67485ce4dd310f981133028326b8e77a60254822 schemas/creator-kit-manifest.v1.schema.json 41f576a534c5277269c3c969eb30d6f3e9b8f8112f04eb2ea28c0875a3a0aeb2 stable-channel.json @@ -9,14 +10,13 @@ 530c81158f70a04d23ce5bf14fd2aab497081cca4687c3f615c0faf595ddefcd README.md 561f3af13708961c43257d0d4a5008fa0848c9d08d1970dabc0299138fdaad36 AGENTS.md 62c1d40c752ef23c11eb7603945c3e3c64fdde950833a92e2553a0804d3c2fd3 licenses.json +640e72428c7fb46fdb1f18534ceb373cb3f7758c14027abbddab92832b130cef source-lock.json 7a2583955a991b5d5bdebcc505ab8e783831310f308be01b976cd77a46e8af6e indexes/terms.json 7c09ed10e2904a7b01cd82da7c4c7c2cc4e7840a3f9571e28de31ce135ec180d compatibility.json -8f0b7f2d735edb7505aeee1408e57eda385d093c228ae5be84662faa09ee5399 source-lock.json a537ea02342dc335f4d5b7b478a1217b6d35ca72ad6aeff638e46aa105e44948 examples/player-show-complete-cue-tracks/player.be af580f8f045e1133773f57a8d24d17a2b4072eca8c3d9cf2d1c515146d0321de examples/player-show-complete-cue-tracks/README.md bae96c2102fcbfc9ff5afc62605232193a308761d5bdb62df1995c03aea68fb8 documents/en/synthetic-creator.mdx c0bc32a6efdea1a865f5dbba4f6bc94cf115b2350ab065cd031226a7b6e75cb8 bundle.json c79a442b75413ee3f2c5db22ddb5b6bda6332db0c1f5e73531ef80c9092ee6ad documents/en/synthetic-creator.md -d95dec71686c62e2560906d8777b0ff9d144bf11bac59b57d72988e1a334cad6 manifest.json e5d14caad4ff80aff41ab60b7b2a06ca5adec236d3820083d1fa780ed10ebc7b schemas/creator-kit-bundle.v1.schema.json fe3c496bf794676fe9d8c4854f1a29272c6cf6a22d2dd7cc1422b17caf7e33cb schemas/creator-kit-source-lock.v1.schema.json diff --git a/creator-kit/manifest.json b/creator-kit/manifest.json index 4c8db34..99d107a 100644 --- a/creator-kit/manifest.json +++ b/creator-kit/manifest.json @@ -6,7 +6,7 @@ "contentScope": "synthetic-fixture-with-public-examples", "source": { "repository": "Spectoda/examples", - "commit": "71698144f34573b28e81a0a39031147ee0c683b6" + "commit": "8c33c51021701dfd8fedf945656792e6a3721c2d" }, "documents": [ { diff --git a/creator-kit/source-lock.json b/creator-kit/source-lock.json index 53dc27a..8dcb0ed 100644 --- a/creator-kit/source-lock.json +++ b/creator-kit/source-lock.json @@ -1,7 +1,7 @@ { "schemaVersion": "creator-kit-source-lock.v1", "repository": "Spectoda/examples", - "commit": "71698144f34573b28e81a0a39031147ee0c683b6", + "commit": "8c33c51021701dfd8fedf945656792e6a3721c2d", "files": [ { "sourcePath": "data/v2/examples/player-show-complete-cue-tracks/creator-kit.json",