diff --git a/.github/workflows/creator-kit-candidate.yml b/.github/workflows/creator-kit-candidate.yml new file mode 100644 index 0000000..d0ac83b --- /dev/null +++ b/.github/workflows/creator-kit-candidate.yml @@ -0,0 +1,45 @@ +name: Creator Kit candidate + +on: + pull_request: + paths: + - "creator-kit-fixtures/**" + - "creator-kit-negative-fixtures/**" + - "creator-kit-templates/**" + - "scripts/creator-kit/**" + - "creator-kit/**" + - "data/v2/examples/player-show-complete-cue-tracks/**" + - "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: + fetch-depth: 0 + 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..70f8295 --- /dev/null +++ b/.github/workflows/creator-kit-release.yml @@ -0,0 +1,97 @@ +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: 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 }} + 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 }} + 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..7d52dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,25 @@ 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` +- Candidate CI fetches full history before checking the committed source lock, + so ancestry validation remains fail-closed on a fresh GitHub Actions checkout. + ## 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..944eab2 --- /dev/null +++ b/creator-kit-evaluations/questions.json @@ -0,0 +1,32 @@ +[ + { + "id": "event-player-plugin", + "prompt": "Where is the Berry plugin required by the Studio Event Player?", + "expected": { + "abstain": false, + "citation": "examples/player-show-complete-cue-tracks/README.md" + } + }, + { + "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..f6cd5cd --- /dev/null +++ b/creator-kit-evaluations/responses/claude.json @@ -0,0 +1,29 @@ +{ + "agent": "claude", + "responses": [ + { + "id": "event-player-plugin", + "answer": "The required Project Berry source is player.be in the locked Event Player example; it must be added to each Controller Project.", + "citations": ["examples/player-show-complete-cue-tracks/README.md"], + "abstained": false + }, + { + "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..f1a1c08 --- /dev/null +++ b/creator-kit-evaluations/responses/codex.json @@ -0,0 +1,29 @@ +{ + "agent": "codex", + "responses": [ + { + "id": "event-player-plugin", + "answer": "Add the locked player.be Project Berry source to every Controller Project; Studio Event Player upload does not install it.", + "citations": ["examples/player-show-complete-cue-tracks/README.md"], + "abstained": false + }, + { + "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..d6f755b --- /dev/null +++ b/creator-kit-templates/AGENTS.md @@ -0,0 +1,26 @@ +# 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/...` 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 + 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. + +The canonical copy-ready firmware 0.12.11 Event Player integration is exposed +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 new file mode 100644 index 0000000..c289244 --- /dev/null +++ b/creator-kit-templates/README.md @@ -0,0 +1,39 @@ +# 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 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. + +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 document authority. The +copy-ready Event Player plugin under +`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. + +## 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. + +For the firmware 0.12.11 Event Player setup, cite and follow +`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 + +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..68c5123 --- /dev/null +++ b/creator-kit-templates/RELEASE_NOTES.md @@ -0,0 +1,11 @@ +# Spectoda Creator Kit candidate + +This is a review candidate, not a release. It contains two synthetic English +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 +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..920117f --- /dev/null +++ b/creator-kit-templates/schemas/creator-kit-bundle.v1.schema.json @@ -0,0 +1,26 @@ +{ + "$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-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 new file mode 100644 index 0000000..b1ba10d --- /dev/null +++ b/creator-kit-templates/schemas/creator-kit-compatibility.v1.schema.json @@ -0,0 +1,19 @@ +{ + "$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", "firmware", "wasm"], + "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" }, + "firmware": { "const": "0.12.11" }, + "wasm": { "const": "DEBUG_UNIVERSAL_0.12.11_20260808" } + }, + "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..5c5d7a4 --- /dev/null +++ b/creator-kit-templates/schemas/creator-kit-manifest.v1.schema.json @@ -0,0 +1,94 @@ +{ + "$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", "examples"], + "properties": { + "schemaVersion": { "const": "creator-kit-manifest.v1" }, + "bundleName": { "const": "Spectoda Creator Kit" }, + "bundleVersion": { "type": "string" }, + "locale": { "const": "en" }, + "contentScope": { "enum": ["synthetic-fixture-with-public-examples", "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 + } + }, + "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 new file mode 100644 index 0000000..4b7ed8e --- /dev/null +++ b/creator-kit-templates/schemas/creator-kit-source-lock.v1.schema.json @@ -0,0 +1,28 @@ +{ + "$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", + "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-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/creator-kit/AGENTS.md b/creator-kit/AGENTS.md new file mode 100644 index 0000000..d6f755b --- /dev/null +++ b/creator-kit/AGENTS.md @@ -0,0 +1,26 @@ +# 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/...` 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 + 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. + +The canonical copy-ready firmware 0.12.11 Event Player integration is exposed +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 new file mode 100644 index 0000000..c289244 --- /dev/null +++ b/creator-kit/README.md @@ -0,0 +1,39 @@ +# 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 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. + +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 document authority. The +copy-ready Event Player plugin under +`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. + +## 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. + +For the firmware 0.12.11 Event Player setup, cite and follow +`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 + +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..68c5123 --- /dev/null +++ b/creator-kit/RELEASE_NOTES.md @@ -0,0 +1,11 @@ +# Spectoda Creator Kit candidate + +This is a review candidate, not a release. It contains two synthetic English +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 +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..33eaac7 --- /dev/null +++ b/creator-kit/bundle.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": "creator-kit-bundle.v1", + "name": "Spectoda Creator Kit", + "version": "0.1.0-rc.2", + "status": "candidate", + "locale": "en", + "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 new file mode 100644 index 0000000..90a36b3 --- /dev/null +++ b/creator-kit/checksums.sha256 @@ -0,0 +1,22 @@ +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 +458b574c6fd6e8cd1e515382f35a6bf20d180541a9c2e69d83c3b084d85e9c8d examples/player-show-complete-cue-tracks/example.yaml +518226851b88ae01e595de3f28f6aedc32450bf2bec8f6036dcde95c1972878d RELEASE_NOTES.md +530c81158f70a04d23ce5bf14fd2aab497081cca4687c3f615c0faf595ddefcd README.md +561f3af13708961c43257d0d4a5008fa0848c9d08d1970dabc0299138fdaad36 AGENTS.md +62c1d40c752ef23c11eb7603945c3e3c64fdde950833a92e2553a0804d3c2fd3 licenses.json +640e72428c7fb46fdb1f18534ceb373cb3f7758c14027abbddab92832b130cef source-lock.json +7a2583955a991b5d5bdebcc505ab8e783831310f308be01b976cd77a46e8af6e indexes/terms.json +7c09ed10e2904a7b01cd82da7c4c7c2cc4e7840a3f9571e28de31ce135ec180d compatibility.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 +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 new file mode 100644 index 0000000..9bf5b11 --- /dev/null +++ b/creator-kit/compatibility.json @@ -0,0 +1,14 @@ +{ + "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", + "firmware": "0.12.11", + "wasm": "DEBUG_UNIVERSAL_0.12.11_20260808" +} 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/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..6615f1f --- /dev/null +++ b/creator-kit/examples/player-show-complete-cue-tracks/README.md @@ -0,0 +1,72 @@ +# Complete-Cue Track Player from `player.show` + +This is the canonical copy-ready Project Berry Player for firmware **0.12.11**. +Studio compiles one static `.show` index and deterministic per-ID SEB +chains. The plugin follows the shared timeline; it owns no start, seek or loop +clock of its own. + +```berry +Player({"base": "player", "ids": [1], "debug": false}) +``` + +Configure `ids` with only the local Spectoda IDs 0–254 owned by this Controller. +ID255 is selected automatically when the Show Index contains it. NetworkStorage +still synchronizes every file to every Controller; the plugin opens only ID255 +and its configured local IDs. + +## Authoring and upload + +One Track owns one Spectoda ID. Its Event definitions are stable across the +Track, and every Cue supplies one complete type-correct value for every enabled +Event. SEB files contain those encoded Event values/records; landing them +produces normal EventStore updates. + +Use Studio's Event Player to Capture and edit Cues, then follow this workflow: + +1. pause the shared timeline; +2. upload the changed SEB files and `.show`; +3. wait for ordinary NetworkStorage propagation — App Controller readback does + not prove the whole mesh is ready; +4. rewind the shared timeline; +5. play. + +Hot update and simultaneous writers for the same base are unsupported in v1. +Old deterministic files no longer referenced by `.show` are ignored. + +## Runtime behavior and limits + +The plugin reads only the bounded Show Index into Berry. Each SEB payload is +read and atomically landed by the native `SEB.land(filename, +{"source":"networkStorage", ...})` path, without a Berry payload buffer. + +- one complete Cue contains 1–340 Event values; +- one SEB file is at most 4,092 bytes and spans at most 65,535 ms; +- `.show` is exact-EOF validated and at most 4,096 bytes; +- the base is 1–11 safe ASCII characters and generated names are at most 23 + bytes; +- one plugin turn performs at most one SEB contribution; +- pause performs no landing; resume derives a fresh timeline projection; +- seek, rewind, loop and the 24-hour timeline wrap reconcile the last complete + Cue at or before the target. + +The canonical compact source compiles to a 3,999-byte TNGL BERRY payload +(3,995 source bytes plus framing). Including the copy-ready `return Player(...)` +invocation produces 4,056 bytes, 40 bytes below the 4 KiB hard limit. The 1 KiB +target remains a stretch measurement, not a correctness promise. + +The short local names and positional runtime arrays in `player.be` are +intentional consequences of that hard limit. `R` holds loaded state, selected +Tracks, reconciliation state, the next Track, seek target, last timeline +position, and epoch. Each selected Track stores ID, records per complete Cue, +ordered segments, next segment, and next Cue. The parser verifies those tuple +shapes before playback, rejects chronological segment overlap, and stops a +Track after an already-committed native result disagrees with the Show Index; +it never retries that committed contribution. + +`player-show-artifacts.json` is a public synthetic three-Track corpus used by +the Berry smoke. It is illustrative compiled output; Project data remains the +authoring source of truth. + +For causal scene recall, use the separate `seb-causal-scene-recall` example. A +scene is an ordinary SEB file landed at its trigger Event time and is independent +of the timeline Player. diff --git a/creator-kit/examples/player-show-complete-cue-tracks/example.yaml b/creator-kit/examples/player-show-complete-cue-tracks/example.yaml 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..4157ba0 --- /dev/null +++ b/creator-kit/examples/player-show-complete-cue-tracks/player.be @@ -0,0 +1,186 @@ + +# Compact by design: compiled Project Player must fit one 4 KiB TNGL block. +def Player(s) +var b=s.find("base","player") +var ids=s.find("ids",[]) +var d=s.find("debug",false) +if type(b)!="string" || size(b)<1 || size(b)>11 || +type(ids)!="instance" || classname(ids)!="list" +raise "value_error","invalid Player config" +end +for j:0..size(b)-1 +var c=b[j] +if !((c>="A"&&c<="Z")||(c>="a"&&c<="z")|| +(c>="0"&&c<="9")||c=="_"||c=="-") +raise "value_error","invalid Player base" +end +end +var w={} +for i:ids +if type(i)!="int" || i<0 || i>254 || w.find(i,false) +raise "value_error","local IDs must be unique 0..254" +end +w[i]=true +end +var R=[false,[],false,0,0,0,-1] +def wanted(i)return i==255 || w.find(i,false)end +def n3(n) +if n<10 return "00"+str(n)end +if n<100 return "0"+str(n)end +return str(n) +end +def file(i,n)return b+"."+n3(i)+"."+n3(n)+".seb" end + +def load(x) +var z=spectoda.getNetworkStorageData(b+".show") +if z==nil || z.size()<16 || z.size()>4096 || +z.get(0)!=80 || z.get(1)!=83 || z.get(2)!=72 || +z.get(3)!=1 || z.get(4)!=16 || z.get(5)!=0 || +z.get(12,4)!=0 +return false +end +var c=z.get(6,2) +var duration=z.get(8,4) +if c<1 || c>256 || duration<1 || duration>86400000 +return false +end +var tracks=[] +var p=16 +var previous_id=-1 +for ti:0..c - 1 +if p+8>z.size()return false end +var id=z.get(p) +var rpc=z.get(p+2,2) +var sc=z.get(p+4,2) +if z.get(p+1)!=0 || z.get(p+6,2)!=0 || +id<=previous_id || rpc<1 || rpc>340 || sc<1 +return false +end +previous_id=id +p+=8 +var segments=[] +var previous_base=-1 +var previous_end=-1 +for si:0..sc - 1 +if p+8>z.size()return false end +var base=z.get(p,4) +var cc=z.get(p+4,2) +if z.get(p+6,2)!=0 || cc<1 || rpc*cc>340 || +base<=previous_base || base>=duration || +(si>0 && base<=previous_end)|| +(si==0 && base !=0)|| p+8+cc*2>z.size() +return false +end +p+=8 +previous_base=base +var offsets=[] +var previous_offset=-1 +for ci:0..cc - 1 +var offset=z.get(p,2) +p+=2 +if(ci==0 && offset !=0)|| +(ci>0 && offset<=previous_offset)|| +base+offset>=duration +return false +end +previous_offset=offset +offsets.push(offset) +end +previous_end=base+previous_offset +segments.push([base,offsets]) +end +if wanted(id)tracks.push([id,rpc,segments,0,0])end +end +if p !=z.size()return false end +R[0]=true R[1]=tracks R[2]=false R[3]=0 R[4]=x +return true +end + +def advance(t,si,ci) +ci+=1 +if ci>=t[2][si][1].size()si+=1 ci=0 end +t[3]=si +t[4]=ci +end + +def reconcile() +if R[3]>=R[1].size()R[2]=true return true end +var t=R[1][R[3]] +var bs=0 +var bc=0 +var found=false +for si:0..t[2].size()- 1 +var seg=t[2][si] +for ci:0..seg[1].size()- 1 +if seg[0]+seg[1][ci]<=R[4] +bs=si bc=ci found=true +end +end +end +if !found return false end +var o=t[2][bs][1][bc] +var at=timeline.at(R[4]- o) +if at==nil return false end +var cur=bc*t[1] +var q=SEB.land(file(t[0],bs),{"source":"networkStorage", +"at":at,"cursor":cur,"until":o}) +if q["ok"] +if q["cursor"]==cur+t[1]advance(t,bs,bc) +else t[3]=t[2].size()end +R[3]+=1 return true +end +if d print("Player reconcile",t[0],q["error"])end +return false +end + +def forward(x) +var pick=nil +var due=nil +for t:R[1] +if t[3]65535 until=65535 end +var cur=pick[4]*pick[1] +var q=SEB.land(file(pick[0],si),{"source":"networkStorage", +"at":at,"cursor":cur,"until":until}) +if q["ok"] +if q["cursor"]<=cur || q["cursor"]%pick[1]!=0 +pick[3]=pick[2].size() pick[4]=0 +else +var n=int(q["cursor"]/ pick[1]) +if n>=seg[1].size()pick[3]=si+1 pick[4]=0 +else pick[4]=n end +end +return true +end +if d print("Player forward",pick[0],q["error"])end +return false +end + +return Plugin(def() +var s=timeline.getState() +var x=s["time"] +var e=s["epoch"] +if !R[0]|| e !=R[6]|| x 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.2", + sourceCommit = gitHead(), +} = {}) { + const build = await buildCreatorKitCandidate({ 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: "Spectoda/examples", + commit: sourceCommit, + generatorCommit: 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.2"), + 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..b2c99e0 --- /dev/null +++ b/scripts/creator-kit/build.mjs @@ -0,0 +1,604 @@ +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"; +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-show-complete-cue-tracks"; +const EXAMPLES_REPOSITORY = "Spectoda/examples"; + +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}`); +} + +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-show-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.2", + sourceCommit = gitHead(), + sourceRepository = EXAMPLES_REPOSITORY, + includePublicExamples = true, +} = {}) { + 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); + 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)); + const publicExamples = includePublicExamples ? await loadPublicExamples(sourceCommit) : { selection: null, example: null, files: [] }; + 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"); + } + 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-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, 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", + name: BUNDLE_NAME, + version, + status: "candidate", + locale: "en", + 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); + 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-and-public-examples", + entries: ["spectoda-creator-kit-synthetic", "MIT"], + }); + 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", + 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, + 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", + 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, + exampleCount: publicExamples.example ? 1 : 0, + 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 buildCreatorKitCandidate({ + sourceRoot: options.source ?? DEFAULT_SOURCE, + outputDir: options.output ?? DEFAULT_OUTPUT, + version: options.version ?? "0.1.0-rc.2", + 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..30a3d56 --- /dev/null +++ b/scripts/creator-kit/creator-kit.test.mjs @@ -0,0 +1,146 @@ +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, buildCreatorKitCandidate, sha256 } 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(first.validation.exampleCount, 1); + 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.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 }); + } +}); + +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( + buildCreatorKitCandidate({ sourceRoot: ROOT, outputDir: path.join(root, "out"), includePublicExamples: false }), + (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( + buildCreatorKitCandidate({ sourceRoot: FIXTURE_ROOT, outputDir: ROOT, includePublicExamples: false }), + (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( + buildCreatorKitCandidate({ sourceRoot: path.join(NEGATIVE_ROOT, name), outputDir: path.join(root, name), includePublicExamples: false }), + (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..2c491a6 --- /dev/null +++ b/scripts/creator-kit/validate.mjs @@ -0,0 +1,209 @@ +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-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 === "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"); + 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(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`); + 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?.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`); + 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 example = manifest.examples[0]; + 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-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", + ); + 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-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-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(); + 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, + exampleCount: manifest.examples.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; + }); +}