@seriousgaming/sdk-tools 0.0.0-stage → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +93 -0
- package/README.md +36 -2
- package/dist/bundling/logic-exports.d.ts +34 -0
- package/dist/bundling/node-builtins.d.ts +21 -0
- package/dist/bundling/transpiler.d.ts +7 -0
- package/dist/cli.js +27368 -0
- package/dist/client/dev-server.d.ts +59 -0
- package/dist/client/index.d.ts +28 -0
- package/dist/client/stub-services.d.ts +32 -0
- package/dist/client/test-session.d.ts +195 -0
- package/dist/client.js +16884 -0
- package/dist/core/asset-reference-translator.d.ts +46 -0
- package/dist/core/clock.d.ts +57 -0
- package/dist/core/faults.d.ts +65 -0
- package/dist/core/game-settings.d.ts +65 -0
- package/dist/core/history.d.ts +89 -0
- package/dist/core/listen-with-fallback.d.ts +30 -0
- package/dist/core/mutation-queue.d.ts +61 -0
- package/dist/core/package-scenario.d.ts +89 -0
- package/dist/core/participant-registry.d.ts +109 -0
- package/dist/core/pending-changes.d.ts +14 -0
- package/dist/core/protocol.d.ts +83 -0
- package/dist/core/scenario-drafts.d.ts +32 -0
- package/dist/core/session-endpoints.d.ts +108 -0
- package/dist/core/session-gates.d.ts +75 -0
- package/dist/core/session-host.d.ts +94 -0
- package/dist/core/session-refusals.d.ts +56 -0
- package/dist/core/session-rewind.d.ts +32 -0
- package/dist/core/session-roster.d.ts +128 -0
- package/dist/core/session-surfaces.d.ts +32 -0
- package/dist/core/session-ticks.d.ts +18 -0
- package/dist/core/session-timers.d.ts +84 -0
- package/dist/core/session-writes.d.ts +40 -0
- package/dist/core/session.d.ts +385 -0
- package/dist/core/settings-locks.d.ts +22 -0
- package/dist/core/team-names.d.ts +52 -0
- package/dist/core/team-state-store.d.ts +209 -0
- package/dist/core/tick-timer.d.ts +75 -0
- package/dist/dev/blob-links.d.ts +10 -0
- package/dist/dev/console/assets/index-DgFglsnA.js +204 -0
- package/dist/dev/console/index.html +12 -0
- package/dist/dev/console-path.d.ts +19 -0
- package/dist/dev/lockfile.d.ts +124 -0
- package/dist/dev/package-watcher.d.ts +21 -0
- package/dist/dev/routes.d.ts +67 -0
- package/dist/dev/shell.d.ts +260 -0
- package/dist/dev/surfaces.d.ts +41 -0
- package/dist/dev/tools-identity.d.ts +23 -0
- package/dist/dev/view/batch.d.ts +172 -0
- package/dist/dev/view/commands.d.ts +75 -0
- package/dist/dev/view/route.d.ts +58 -0
- package/dist/dev/view/view.d.ts +78 -0
- package/dist/dev/view/viewer.d.ts +32 -0
- package/dist/discovery/config.d.ts +9 -0
- package/dist/discovery/manifest-paths.d.ts +21 -0
- package/dist/discovery/removed-fields.d.ts +90 -0
- package/dist/docs/api/deck.md +386 -0
- package/dist/docs/api/index.md +67 -0
- package/dist/docs/api/sdk-commands.md +286 -0
- package/dist/docs/api/sdk-e2e.md +433 -0
- package/dist/docs/api/sdk-projection.md +207 -0
- package/dist/docs/api/sdk-testing.md +214 -0
- package/dist/docs/api/sdk-tools-client.md +560 -0
- package/dist/docs/api/sdk-ui-testing.md +231 -0
- package/dist/docs/api/sdk-ui.md +1269 -0
- package/dist/docs/api/sdk.md +2140 -0
- package/dist/docs/changelog-0.x.md +1156 -0
- package/dist/docs/changelog.md +236 -0
- package/dist/docs/cli-and-dev-server.md +217 -0
- package/dist/docs/cli-error-codes.md +154 -0
- package/dist/docs/cli-reference.md +2488 -0
- package/dist/docs/cli-simulation-testing.md +222 -0
- package/dist/docs/concepts/index.md +46 -0
- package/dist/docs/concepts/terminology.md +322 -0
- package/dist/docs/environment-variables.md +102 -0
- package/dist/docs/getting-started.md +145 -0
- package/dist/docs/guides/ai-facilitation.md +220 -0
- package/dist/docs/guides/capabilities.md +460 -0
- package/dist/docs/guides/endpoints.md +335 -0
- package/dist/docs/guides/engine-testing.md +477 -0
- package/dist/docs/guides/engine.md +527 -0
- package/dist/docs/guides/file-handling.md +405 -0
- package/dist/docs/guides/games.md +176 -0
- package/dist/docs/guides/global-settings.md +94 -0
- package/dist/docs/guides/guide.md +123 -0
- package/dist/docs/guides/hooks.md +183 -0
- package/dist/docs/guides/index.md +51 -0
- package/dist/docs/guides/internationalization.md +288 -0
- package/dist/docs/guides/llm-integration.md +635 -0
- package/dist/docs/guides/multi-team-games.md +586 -0
- package/dist/docs/guides/patterns.md +391 -0
- package/dist/docs/guides/presentation.md +196 -0
- package/dist/docs/guides/production-constraints.md +130 -0
- package/dist/docs/guides/scenario-editor.md +143 -0
- package/dist/docs/guides/scenarios.md +515 -0
- package/dist/docs/guides/session.md +972 -0
- package/dist/docs/guides/settings.md +458 -0
- package/dist/docs/guides/setup.md +193 -0
- package/dist/docs/guides/speech-generation.md +233 -0
- package/dist/docs/guides/starter-game.md +164 -0
- package/dist/docs/guides/testing.md +575 -0
- package/dist/docs/guides/tick-controller.md +569 -0
- package/dist/docs/guides/type-safety.md +516 -0
- package/dist/docs/guides/ui.md +1389 -0
- package/dist/docs/guides/upgrading.md +636 -0
- package/dist/docs/guides/validators.md +217 -0
- package/dist/docs/guides/waiting-room.md +244 -0
- package/dist/docs/index.md +41 -0
- package/dist/docs/recipes/act-on-the-session-from-an-endpoint.md +95 -0
- package/dist/docs/recipes/adapt-a-game-for-walkthroughs.md +133 -0
- package/dist/docs/recipes/adapt-ui-for-observers.md +74 -0
- package/dist/docs/recipes/call-endpoint-from-ui.md +109 -0
- package/dist/docs/recipes/check-a-package-from-a-script.md +117 -0
- package/dist/docs/recipes/create-capability.md +66 -0
- package/dist/docs/recipes/create-endpoint.md +127 -0
- package/dist/docs/recipes/create-engine.md +111 -0
- package/dist/docs/recipes/create-game.md +70 -0
- package/dist/docs/recipes/create-guide.md +75 -0
- package/dist/docs/recipes/create-hook.md +80 -0
- package/dist/docs/recipes/create-presentation.md +142 -0
- package/dist/docs/recipes/create-scenario-editor.md +102 -0
- package/dist/docs/recipes/create-scenario.md +98 -0
- package/dist/docs/recipes/create-session-ui.md +116 -0
- package/dist/docs/recipes/create-settings.md +88 -0
- package/dist/docs/recipes/create-setup.md +115 -0
- package/dist/docs/recipes/create-tick-controller.md +65 -0
- package/dist/docs/recipes/create-user-interface.md +96 -0
- package/dist/docs/recipes/create-validator.md +67 -0
- package/dist/docs/recipes/create-waiting-room-ui.md +109 -0
- package/dist/docs/recipes/declare-api-uses.md +67 -0
- package/dist/docs/recipes/design-state-for-concurrent-writes.md +96 -0
- package/dist/docs/recipes/distribute-package.md +80 -0
- package/dist/docs/recipes/e2e-testing.md +116 -0
- package/dist/docs/recipes/end-game-on-condition.md +154 -0
- package/dist/docs/recipes/fix-duplicate-packages.md +101 -0
- package/dist/docs/recipes/index.md +86 -0
- package/dist/docs/recipes/make-an-endpoint-external.md +101 -0
- package/dist/docs/recipes/negotiate-across-teams.md +164 -0
- package/dist/docs/recipes/observe-other-teams-state.md +119 -0
- package/dist/docs/recipes/publish-a-package.md +139 -0
- package/dist/docs/recipes/remember-a-viewers-choice.md +55 -0
- package/dist/docs/recipes/review-a-past-day.md +73 -0
- package/dist/docs/recipes/share-assets-across-scenarios.md +120 -0
- package/dist/docs/recipes/share-logic-between-engine-and-ui.md +130 -0
- package/dist/docs/recipes/show-all-teams-participants.md +109 -0
- package/dist/docs/recipes/start-from-an-empty-package.md +169 -0
- package/dist/docs/recipes/take-a-file-in-an-endpoint.md +82 -0
- package/dist/docs/recipes/test-a-multi-role-game.md +51 -0
- package/dist/docs/recipes/test-user-interface.md +95 -0
- package/dist/docs/recipes/test-with-engine.md +115 -0
- package/dist/docs/recipes/upload-and-display-files.md +148 -0
- package/dist/docs/recipes/use-request-play.md +65 -0
- package/dist/docs/recipes/use-request-tick.md +75 -0
- package/dist/docs/troubleshooting.md +816 -0
- package/dist/execution/blob-inputs.d.ts +57 -0
- package/dist/execution/bundle-reload.d.ts +42 -0
- package/dist/execution/config-resolver.d.ts +127 -0
- package/dist/execution/configuration-builder.d.ts +66 -0
- package/dist/execution/context-composition.d.ts +161 -0
- package/dist/execution/endpoint-actions.d.ts +9 -0
- package/dist/execution/endpoint-payloads.d.ts +32 -0
- package/dist/execution/endpoint-runner.d.ts +129 -0
- package/dist/execution/engine-state.d.ts +40 -0
- package/dist/execution/executor-callbacks.d.ts +56 -0
- package/dist/execution/executor.d.ts +436 -0
- package/dist/execution/gates.d.ts +45 -0
- package/dist/execution/hook-chain.d.ts +25 -0
- package/dist/execution/logic-calls.d.ts +190 -0
- package/dist/execution/logic-failure.d.ts +84 -0
- package/dist/execution/logic-loader.d.ts +54 -0
- package/dist/execution/logic-result.d.ts +76 -0
- package/dist/execution/tick-controller-call.d.ts +32 -0
- package/dist/execution/types.d.ts +48 -0
- package/dist/generation/ts-type.d.ts +39 -0
- package/dist/guide/locale.d.ts +12 -0
- package/dist/guide/video-registry.d.ts +42 -0
- package/dist/help-examples.d.ts +125 -0
- package/dist/host/adapter.d.ts +67 -0
- package/dist/host/host-channel.d.ts +222 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/protocol.d.ts +783 -0
- package/dist/host/surfaces.d.ts +42 -0
- package/dist/host/theme.d.ts +55 -0
- package/dist/host.js +583 -0
- package/dist/prompts/index.d.ts +7 -0
- package/dist/prompts/schema-builder.d.ts +29 -0
- package/dist/prompts/types.d.ts +108 -0
- package/dist/prompts/validators.d.ts +32 -0
- package/dist/runtime/capability-scope.d.ts +6 -0
- package/dist/runtime/context-merge.d.ts +29 -0
- package/dist/runtime/context-projection.d.ts +41 -0
- package/dist/runtime/guarded-callbacks.d.ts +15 -0
- package/dist/runtime/index.d.ts +27 -0
- package/dist/runtime/manifest-version.d.ts +55 -0
- package/dist/runtime/tick-validation.d.ts +25 -0
- package/dist/runtime.js +346 -0
- package/dist/schema.json +1546 -0
- package/dist/server/vite-plugin.d.ts +46 -0
- package/dist/services/tts/client.d.ts +51 -0
- package/dist/services/tts/elevenlabs.d.ts +24 -0
- package/dist/services/tts/types.d.ts +44 -0
- package/dist/storage/asset-resolver.d.ts +49 -0
- package/dist/storage/blob-store.d.ts +41 -0
- package/dist/templates/endpoint/logic.test.ts.liquid +61 -0
- package/dist/templates/endpoint/logic.ts.liquid +25 -0
- package/dist/templates/engine/init.json.liquid +7 -0
- package/dist/templates/engine/logic-aggregate.test.ts.liquid +90 -0
- package/dist/templates/engine/logic-aggregate.ts.liquid +46 -0
- package/dist/templates/engine/logic.test.ts.liquid +61 -0
- package/dist/templates/engine/logic.ts.liquid +32 -0
- package/dist/templates/engine/run.json.liquid +7 -0
- package/dist/templates/guide/01-at-a-glance.md.liquid +19 -0
- package/dist/templates/guide/02-preparation.md.liquid +18 -0
- package/dist/templates/guide/03-running-the-session.md.liquid +37 -0
- package/dist/templates/hook/logic.test.ts.liquid +46 -0
- package/dist/templates/hook/logic.ts.liquid +12 -0
- package/dist/templates/package/CLAUDE.md.template +338 -0
- package/dist/templates/package/DESIGN.md.liquid +144 -0
- package/dist/templates/package/README.md.liquid +143 -0
- package/dist/templates/package/claude/rules/workflow-create-capability.md +26 -0
- package/dist/templates/package/claude/rules/workflow-create-endpoint.md +35 -0
- package/dist/templates/package/claude/rules/workflow-create-engine.md +35 -0
- package/dist/templates/package/claude/rules/workflow-create-guide.md +40 -0
- package/dist/templates/package/claude/rules/workflow-create-presentation.md +37 -0
- package/dist/templates/package/claude/rules/workflow-create-user-interface.md +31 -0
- package/dist/templates/package/claude/rules/workflow-publish-package.md +54 -0
- package/dist/templates/package/claude/rules/workflow-test-engine-or-ui.md +30 -0
- package/dist/templates/package/claude/rules/workflow-update-sdk-version.md +43 -0
- package/dist/templates/package/claude/settings.json.template +16 -0
- package/dist/templates/package/gitignore.template +15 -0
- package/dist/templates/package/global-setup.ts.template +21 -0
- package/dist/templates/package/globals.d.ts.template +2 -0
- package/dist/templates/package/oxfmtrc.json.template +14 -0
- package/dist/templates/package/oxlintrc.json.template +238 -0
- package/dist/templates/package/package.json.liquid +65 -0
- package/dist/templates/package/playwright.config.ts.liquid +19 -0
- package/dist/templates/package/pnpm-workspace.yaml.template +27 -0
- package/dist/templates/package/test-setup.ts.liquid +7 -0
- package/dist/templates/package/tsconfig.json.liquid +15 -0
- package/dist/templates/package/vite.config.ts.liquid +19 -0
- package/dist/templates/package/vitest.config.ts.liquid +25 -0
- package/dist/templates/package/vitest.engine.config.ts.liquid +12 -0
- package/dist/templates/presentation/index.html.liquid +20 -0
- package/dist/templates/presentation/src/App.tsx.liquid +49 -0
- package/dist/templates/presentation/src/Slides.tsx.liquid +81 -0
- package/dist/templates/presentation/src/i18n.ts.liquid +54 -0
- package/dist/templates/presentation/src/main.tsx.liquid +14 -0
- package/dist/templates/scenario_editor/src/App.test.tsx.liquid +30 -0
- package/dist/templates/scenario_editor/src/App.tsx.liquid +95 -0
- package/dist/templates/scenario_editor/src/i18n.ts.liquid +44 -0
- package/dist/templates/session/src/App.test.tsx.liquid +60 -0
- package/dist/templates/session/src/App.tsx.liquid +128 -0
- package/dist/templates/session/src/i18n.ts.liquid +53 -0
- package/dist/templates/session/src/main.tsx.liquid +48 -0
- package/dist/templates/session/src/theme.ts.liquid +46 -0
- package/dist/templates/settings-editor/index.html.liquid +12 -0
- package/dist/templates/settings-editor/src/main.tsx.liquid +48 -0
- package/dist/templates/settings-editor/src/theme.ts.liquid +24 -0
- package/dist/templates/setup/src/App.test.tsx.liquid +39 -0
- package/dist/templates/setup/src/App.tsx.liquid +102 -0
- package/dist/templates/setup/src/i18n.ts.liquid +47 -0
- package/dist/templates/starter-game/capabilities/briefing/capability.sdk.json +18 -0
- package/dist/templates/starter-game/capabilities/plan/capability.sdk.json +17 -0
- package/dist/templates/starter-game/capabilities/results/capability.sdk.json +61 -0
- package/dist/templates/starter-game/capabilities/roles/capability.sdk.json +17 -0
- package/dist/templates/starter-game/capabilities/weather/capability.sdk.json +23 -0
- package/dist/templates/starter-game/engine/defaults.ts +81 -0
- package/dist/templates/starter-game/engine/engine.sdk.json +12 -0
- package/dist/templates/starter-game/engine/logic.test.ts +221 -0
- package/dist/templates/starter-game/engine/logic.ts +149 -0
- package/dist/templates/starter-game/engine/test-helpers.ts +74 -0
- package/dist/templates/starter-game/engine/types.ts +139 -0
- package/dist/templates/starter-game/guide/content/en/01-at-a-glance.md +51 -0
- package/dist/templates/starter-game/guide/content/en/02-preparation.md +35 -0
- package/dist/templates/starter-game/guide/content/en/03-running-the-day.md +58 -0
- package/dist/templates/starter-game/guide/guide.sdk.json +10 -0
- package/dist/templates/starter-game/guide/guide.test.ts +108 -0
- package/dist/templates/starter-game/hooks/assignproducts/defaults.ts +37 -0
- package/dist/templates/starter-game/hooks/assignproducts/hook.sdk.json +13 -0
- package/dist/templates/starter-game/hooks/assignproducts/logic/logic.test.ts +77 -0
- package/dist/templates/starter-game/hooks/assignproducts/logic/logic.ts +47 -0
- package/dist/templates/starter-game/hooks/assignproducts/types.ts +55 -0
- package/dist/templates/starter-game/package.json +68 -0
- package/dist/templates/starter-game/playwright.config.ts +19 -0
- package/dist/templates/starter-game/scenarios/heatwave/scenario.sdk.json +20 -0
- package/dist/templates/starter-game/scenarios/ordinary_week/scenario.sdk.json +12 -0
- package/dist/templates/starter-game/seriousgaming.sdk.json +23 -0
- package/dist/templates/starter-game/session/defaults.ts +80 -0
- package/dist/templates/starter-game/session/index.html +12 -0
- package/dist/templates/starter-game/session/session.sdk.json +11 -0
- package/dist/templates/starter-game/session/src/App.test.tsx +307 -0
- package/dist/templates/starter-game/session/src/App.tsx +309 -0
- package/dist/templates/starter-game/session/src/Section.tsx +37 -0
- package/dist/templates/starter-game/session/src/TeamDoor.tsx +96 -0
- package/dist/templates/starter-game/session/src/format.ts +8 -0
- package/dist/templates/starter-game/session/src/i18n.ts +51 -0
- package/dist/templates/starter-game/session/src/main.tsx +48 -0
- package/dist/templates/starter-game/session/src/theme.ts +45 -0
- package/dist/templates/starter-game/session/types.ts +135 -0
- package/dist/templates/starter-game/settings/engine/settings.sdk.json +50 -0
- package/dist/templates/starter-game/settings/menu/settings.sdk.json +78 -0
- package/dist/templates/starter-game/settings/session/settings.sdk.json +23 -0
- package/dist/templates/starter-game/settings/ui/settings.sdk.json +25 -0
- package/dist/templates/starter-game/test-setup.ts +7 -0
- package/dist/templates/starter-game/testing/e2e/game.spec.ts +139 -0
- package/dist/templates/starter-game/testing/e2e/global-setup.ts +21 -0
- package/dist/templates/starter-game/testing/engine/game.engine.test.ts +171 -0
- package/dist/templates/starter-game/tsconfig.json +19 -0
- package/dist/templates/starter-game/ui/defaults.ts +60 -0
- package/dist/templates/starter-game/ui/index.html +12 -0
- package/dist/templates/starter-game/ui/src/App.test.tsx +138 -0
- package/dist/templates/starter-game/ui/src/App.tsx +175 -0
- package/dist/templates/starter-game/ui/src/DayBar.test.tsx +78 -0
- package/dist/templates/starter-game/ui/src/DayBar.tsx +72 -0
- package/dist/templates/starter-game/ui/src/HistoryPanel.test.tsx +74 -0
- package/dist/templates/starter-game/ui/src/HistoryPanel.tsx +120 -0
- package/dist/templates/starter-game/ui/src/MakeRow.tsx +129 -0
- package/dist/templates/starter-game/ui/src/Section.tsx +37 -0
- package/dist/templates/starter-game/ui/src/ThemeToggle.tsx +49 -0
- package/dist/templates/starter-game/ui/src/TodayPanel.test.tsx +179 -0
- package/dist/templates/starter-game/ui/src/TodayPanel.tsx +92 -0
- package/dist/templates/starter-game/ui/src/YesterdayPanel.tsx +142 -0
- package/dist/templates/starter-game/ui/src/format.ts +12 -0
- package/dist/templates/starter-game/ui/src/i18n.ts +95 -0
- package/dist/templates/starter-game/ui/src/main.tsx +48 -0
- package/dist/templates/starter-game/ui/src/menu.ts +18 -0
- package/dist/templates/starter-game/ui/src/test-helpers.ts +58 -0
- package/dist/templates/starter-game/ui/src/theme.ts +45 -0
- package/dist/templates/starter-game/ui/src/writes.ts +43 -0
- package/dist/templates/starter-game/ui/types.ts +137 -0
- package/dist/templates/starter-game/ui/ui.sdk.json +11 -0
- package/dist/templates/starter-game/validators/capacity/defaults.ts +37 -0
- package/dist/templates/starter-game/validators/capacity/logic/logic.test.ts +96 -0
- package/dist/templates/starter-game/validators/capacity/logic/logic.ts +45 -0
- package/dist/templates/starter-game/validators/capacity/types.ts +55 -0
- package/dist/templates/starter-game/validators/capacity/validator.sdk.json +11 -0
- package/dist/templates/starter-game/vite.config.ts +19 -0
- package/dist/templates/starter-game/vitest.config.ts +15 -0
- package/dist/templates/starter-game/vitest.engine.config.ts +19 -0
- package/dist/templates/starter-game/waiting_room/defaults.ts +40 -0
- package/dist/templates/starter-game/waiting_room/index.html +12 -0
- package/dist/templates/starter-game/waiting_room/src/App.test.tsx +102 -0
- package/dist/templates/starter-game/waiting_room/src/App.tsx +194 -0
- package/dist/templates/starter-game/waiting_room/src/i18n.ts +33 -0
- package/dist/templates/starter-game/waiting_room/src/main.tsx +48 -0
- package/dist/templates/starter-game/waiting_room/src/theme.ts +45 -0
- package/dist/templates/starter-game/waiting_room/types.ts +59 -0
- package/dist/templates/starter-game/waiting_room/waiting_room.sdk.json +10 -0
- package/dist/templates/tick-controller/logic.ts.liquid +8 -0
- package/dist/templates/ui/index.html.liquid +12 -0
- package/dist/templates/ui/src/App.test.tsx.liquid +41 -0
- package/dist/templates/ui/src/App.tsx.liquid +103 -0
- package/dist/templates/ui/src/i18n.ts.liquid +47 -0
- package/dist/templates/ui/src/main.tsx.liquid +48 -0
- package/dist/templates/ui/src/theme.ts.liquid +46 -0
- package/dist/templates/validator/logic.test.ts.liquid +42 -0
- package/dist/templates/validator/logic.ts.liquid +21 -0
- package/dist/templates/waiting-room/index.html.liquid +12 -0
- package/dist/templates/waiting-room/src/App.test.tsx.liquid +57 -0
- package/dist/templates/waiting-room/src/App.tsx.liquid +118 -0
- package/dist/templates/waiting-room/src/i18n.ts.liquid +44 -0
- package/dist/templates/waiting-room/src/main.tsx.liquid +48 -0
- package/dist/templates/waiting-room/src/theme.ts.liquid +46 -0
- package/dist/templating/resource-metadata.d.ts +131 -0
- package/dist/types.d.ts +14 -0
- package/dist/utils/atomic-write.d.ts +40 -0
- package/dist/utils/cli-error.d.ts +64 -0
- package/dist/utils/document-parser-client.d.ts +20 -0
- package/dist/utils/env-loader.d.ts +40 -0
- package/dist/utils/error-codes.d.ts +335 -0
- package/dist/utils/errors.d.ts +19 -0
- package/dist/utils/global-settings.d.ts +65 -0
- package/dist/utils/llm-client.d.ts +34 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/output.d.ts +109 -0
- package/dist/utils/platform.d.ts +14 -0
- package/dist/utils/prompt.d.ts +36 -0
- package/dist/utils/real-path.d.ts +6 -0
- package/dist/utils/schema-properties.d.ts +35 -0
- package/dist/validation/ajv.d.ts +17 -0
- package/dist/validation/facilitator-guide-validator.d.ts +27 -0
- package/dist/validation/findings.d.ts +40 -0
- package/dist/validation/game-constraints.d.ts +10 -0
- package/dist/validation/game-validator.d.ts +34 -0
- package/dist/validation/package-validation.d.ts +41 -0
- package/dist/validation/package-validator.d.ts +19 -0
- package/dist/validation/resource-validator.d.ts +79 -0
- package/dist/validation/schema-defaults-validator.d.ts +29 -0
- package/dist/validation/schema-validation.d.ts +115 -0
- package/dist/validation/semver.d.ts +9 -0
- package/package.json +122 -3
|
@@ -0,0 +1,1156 @@
|
|
|
1
|
+
# Changelog for 0.x
|
|
2
|
+
|
|
3
|
+
Every release before 1.0, from 0.0.1 to 0.6.0, kept as history. The current changelog, from 1.0 on, is [changelog.md](changelog.md).
|
|
4
|
+
|
|
5
|
+
To bring a game from 0.6 to 1.0, follow the [upgrading guide](guides/upgrading.md). Entries below refer to upgrade steps, recipes and sections as they stood at that release; where those have since been removed, the reference is left as plain text, and the upgrading guide shipped inside each release (under `dist/docs/` in its SDK tools package) has the steps it names.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [0.6.0] — 2026-09-24
|
|
10
|
+
|
|
11
|
+
### Breaking Changes
|
|
12
|
+
|
|
13
|
+
- **`sdk build` refuses `waitingRoom.openOnStart: true`, from the game or a scenario, when the package has no waiting room UI.** A held start holds participants in the waiting room, and without one there was nowhere to hold them. **To migrate:** add one with `sdk new waiting_room`, or remove `openOnStart`
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`--participants` on `sdk dev`, `sdk init`, `sdk tick` and `sdk team add` chooses how many participants each team starts with.** One count for every team, or one per team such as `2,0,1`. Zero is allowed, so you can test a team before anybody joins it. Without it every team still starts with one
|
|
18
|
+
- **`sdk play` leaves a game's configuration step**, as the console's Play does, so a session started with `sdk dev` can be started from a script
|
|
19
|
+
- **`sdk patch --shared` writes the shared store**, as a session UI's shared write does, so a script can change a shared capability in a multi-team game. A team-addressed patch to one is refused there
|
|
20
|
+
- **`sdk view --tick N` prints the state at a past tick**, and `--to M` a range of them, from the same snapshots a UI reads with `getPastState`
|
|
21
|
+
- **`createTestSession` in `@businesssimulations/seriousgaming-sdk-tools/client` tests the game around your engine, in-process.** It runs the session the dev server runs, so hooks fire, validators refuse, endpoints answer, independent teams tick on their own, undo works and a `FakeClock` drives automatic ticks, none of which `GameRunner` reaches. See [Engine testing](./guides/engine-testing.md#what-is-createtestsession)
|
|
22
|
+
- **New Playwright helpers in `@businesssimulations/seriousgaming-sdk/e2e`:** `reinitialiseSession` starts the session again from the package before a test, pressing Play and disarming faults, where `resetSession` kept a team or scenario an earlier test left; `waitForState` waits for something that happens after its command answers; `advanceClock` drives a `--fake-clock` server; and a `run: 'walkthrough'` option addresses the walkthrough
|
|
23
|
+
- **`sdk dev --fake-clock` and `sdk clock advance <seconds>` test automatic ticks without waiting for them.** Time stands still until you move it, and every tick that falls due on the way runs. Playwright suites post `advanceClock` to `/api/command`
|
|
24
|
+
- **`sdk timer` pauses, resumes and extends the automatic tick timer, and reports it**, so what a session UI does to the timer can be done from a script. `sdk timer extend <seconds>` moves the scheduled tick only, and `--team` addresses one team's timer in an independent game. The dev drawer's Session tab has the same controls
|
|
25
|
+
- **Conformance checks for what a surface is sent**, against a new `probe` fixture whose UIs log every message they receive: the initial batches, cross-team data, round processing, bounded `getPastState`, the waiting room, hook failures, and sentinel searches of everything a participant's device receives for another team's data, private capabilities and other teams' links
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **New packages' engine tests, `CLAUDE.md.template` and the `workflow-test-engine-or-ui.md` rule use `createTestSession`.** The starter game's `testing/engine/game.engine.test.ts` is written against it, and the rule no longer says `GameRunner` covers hooks and validators, which it never did. To adopt it in an existing package, merge the two templates from `dist/templates/package/` in the installed `@businesssimulations/seriousgaming-sdk-tools`
|
|
30
|
+
- **`runCommand` is typed against the command set the dev server accepts**, published as `Command`, `MutationCommand` and `QueryCommand` from `@businesssimulations/seriousgaming-sdk/commands`. A misspelled command or field that the server would have refused with a 400 now fails to compile, and a read command's result is typed as `QueryResult`
|
|
31
|
+
- **`sdk configure` restarts a running session on the new values**, at tick 0 and keeping its team count, scenario and locale, rather than telling you to run `sdk init`. A session waiting at its configuration step is started
|
|
32
|
+
- **The starter game reads its history back with `getPastState` instead of keeping it in context.** New packages no longer carry a `history` capability that grew every day; the history table asks the host for the earlier days and adds today's from live context. Over 1,000 ticks with five teams this takes the dev server from about 414MB to 44MB and keeps each tick under a millisecond
|
|
33
|
+
- **A team count outside the game's `teams.min` or `teams.max` is refused rather than adjusted.** `sdk dev --teams` and `sdk init --teams` used to move the count inside the bounds without saying so. The runtime contract requires a runtime to reject a session seeded outside them, so the dev server now does. Once a session is running, a team can be removed down to one: `teams.min` applies only when the session starts
|
|
34
|
+
- **The specification no longer implies how joining is secured.** A runtime chooses its own join authentication, whether a PIN, a token, a sign-in or none, and a join URL need not be secret. `joinPin` is whatever code the runtime asks participants for, so a session UI should render it as an opaque string and expect `null` on runtimes that never set one
|
|
35
|
+
- **The runtime contract describes what a restart must preserve, not how to store it.** The snapshot shape is now an informative example, the restore rules are stated as outcomes, and it says plainly that its history and undo rules apply to every runtime while only the restart rules are production-only. The list of what must survive a restart now includes participants and the waiting-room flag, which other sections already required to last the whole session
|
|
36
|
+
|
|
37
|
+
### Deprecated
|
|
38
|
+
|
|
39
|
+
- **`GameRunner` and `MultiTeamGameRunner` are deprecated in favour of `createTestSession`**, and will be removed in a later release. They drive the engine alone, so a test written with them never sees a hook, a validator, an endpoint, a participant, undo, a per-team tick or a timer. They keep working until then. **To migrate:** follow the table in [Moving from GameRunner](./guides/engine-testing.md#moving-from-gamerunner). A tick past the limit comes back as `{ ok: false }` rather than throwing, and a multi-team game runs in its declared interaction mode
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **A session at its configuration step refuses a tick, an undo, a reset and a context write** until somebody presses Play. It ran `Run` over a context `Init` had never built, with nobody on the team, and the console went on showing the configuration page. The refusal says to press Play or run `sdk play`
|
|
44
|
+
- **A session a CLI command starts for itself presses Play** on a game with a configuration step, as `--no-server` already did, so `sdk tick` on a fresh package ticks a session that has started
|
|
45
|
+
- **`sdk reset --walkthrough` starts the walkthrough on the session's current scenario, locale and configuration.** It rewound the walkthrough it had, so after you changed the session's scenario it stayed on the old one, unlike the console's reset
|
|
46
|
+
- **`sdk dev` reports who is connected.** Every participant read `connected: false` whatever was open, so a roster that showed who was in the room showed nobody. A participant is now connected while a page addressed to them is open, and the change reaches every surface as it happens
|
|
47
|
+
- **Editing logic or a bundle file under `sdk dev` takes effect.** The dev server stopped watching the package, so an edited `logic.ts` kept running its old code until a full reset, and an edited `*.sdk.json` was ignored. A logic edit now reaches the next run and a bundle edit re-resolves configuration, both without resetting the session
|
|
48
|
+
- **A new package's `CLAUDE.md`, `.claude/rules`, README and design notes describe the game it actually contains, and commands that exist.** They documented `sdk game init`, `game tick` and `game view`, which were removed, described an older starter game, and told an agent endpoints cannot write. The scaffolded `.claude/settings.json` no longer pre-approves `pnpm start`, which the same file tells an agent not to run
|
|
49
|
+
- **The dev server starts Vite the first time a surface is opened**, not at start-up, so a session `sdk tick` starts in the background never pays for one. A package whose dependencies are not installed now reports that it cannot serve surfaces when a surface is first asked for
|
|
50
|
+
- **The dev server sends a viewer's device its view only when that view has changed.** Every event re-sent the whole batch to every device, so one team's write reached every other team unchanged and a tick sent each device three copies. Each event is also projected once rather than once per device
|
|
51
|
+
- **A game that declares no `teams.mode` runs as independent under `sdk dev`, as the specification and the schema always said.** The dev server ran it as shared-state, so its teams ticked together and a team tick fanned out across the cohort. If your game relied on that, declare `"mode": "shared-state"`
|
|
52
|
+
- **`sdk new endpoint` keeps `scope`, `agentCallable` and `writes`.** It dropped all three from the JSON it was given and still reported success, and it scaffolded a handler for an endpoint that cannot write. An endpoint that declares `writes` is now scaffolded returning `{ response, context }`
|
|
53
|
+
- **The CLI calls the engine the engine.** `sdk new engine --help`, its success and error messages, and runtime and validation errors said 'module', which is no longer a word the SDK uses. Adding an engine to a game that already has one now says so and points at `update engine`
|
|
54
|
+
- **A writing endpoint that returns a bare value is refused.** One that declares `writes` must return `{ response, context }`; a bare value used to be read as an empty answer that wrote nothing, with no error
|
|
55
|
+
- **An edited endpoint schema takes effect under `sdk dev`.** Compiled request and response validators were cached by endpoint name for the life of the process, so the old schema went on being enforced
|
|
56
|
+
- **`openHostOverlay` is refused for an overlay the host did not offer**, such as the guide for a game without one, and for print sheets naming a team that is not in the session. Both used to open a page anyway
|
|
57
|
+
- **`setWaitingRoom` and `renameOwnTeam` refuse a `teamId` from the surface** rather than silently ignoring it, since the host decides which team each applies to
|
|
58
|
+
- **A session UI can no longer write a shared capability it does not declare in `writes`.** The dev server checked only that the capability was shared, so any session surface could change any shared value
|
|
59
|
+
- **A writing endpoint on a lagging independent team is refused when that team ticks while it runs.** The check compared the session's tick, which is the furthest team's, so a team behind it could tick under a running endpoint and have the stale patch applied over the new round
|
|
60
|
+
- **`MultiTeamGameRunner.update()` settles shared values as a live session does.** It refused any patch naming a shared capability, including one that left it unchanged, and refused a shared write in a one-team game, which a session takes. It now refuses only a change in a multi-team game
|
|
61
|
+
- **Automatic ticking works under `sdk dev` again.** The dev server ignored a tick controller's schedule, so a timed game never advanced on its own, and `pauseTimer`, `resumeTimer` and `extendTick` were accepted and did nothing. Game UIs are now sent `setTimer`, and a session UI reads each team's real timer from `SessionData`. In an independent game each team has its own timer
|
|
62
|
+
- **A tick controller in a shared-state game is handed every team in `teams`**, and `meta.teamId` and `meta.index` name the team whose context it received rather than always `team-1`
|
|
63
|
+
- **A tick controller result outside the 30-second to 7-day bounds is clamped and reported under `sdk dev`**, as a production runtime does, rather than honoured
|
|
64
|
+
- **An engine or tick controller that never returns no longer stalls the dev server.** Every later tick and write waited on it for ever. Each call into an engine or tick controller is now abandoned after two minutes, and the tick fails with an error that names the call
|
|
65
|
+
- **A configuration UI no longer stores every value it was shown as a facilitator override.** `setConfig` sent the whole merged configuration, so spreading `config` into an update pinned the scenario's values in the session-override layer. It now sends only what changed, the dev server drops any value equal to the one already in effect, and a partial update (`setConfig({ engine: { Increment: 20 } })`) keeps the rest of the slot in the form's local state
|
|
66
|
+
- **A session started in the background by `sdk tick` or `sdk init` no longer dies the first time it logs something**, such as a warning or a reload notice
|
|
67
|
+
- **A participant's device is sent only what their surface is owed.** Every page used to receive the whole session over one socket: every team's context, private capabilities included, whatever the page showed. A `?bare` page, which is what a join link now opens, gets a feed the server builds for that viewer alone. The operator console, without `?bare`, still sees everything
|
|
68
|
+
- **A UI can no longer call the dev server's API directly.** UIs are now served from their own origin (`--asset-port`), as production serves them from its asset host, so code that fetched `/api/...` from inside a UI stops working here as it would have in production. The same change stops a UI reaching into the console page around it
|
|
69
|
+
- **A surface's `getPastState` returns only what the surface declared.** Omitting `capabilityIds`, or naming a private or undeclared capability, used to return it
|
|
70
|
+
- **`getAllTeamsParticipants` returns an empty roster to a UI that does not declare `crossTeamParticipants`**, rather than every team's
|
|
71
|
+
- **A game UI declaring `crossTeamReads` now receives `setCrossTeamData`**, which the dev server never sent
|
|
72
|
+
- **A session UI now receives `hookFailed`** when a hook throws
|
|
73
|
+
- **`setRoundProcessing` now tells a game UI when a round starts**, not only that none is running
|
|
74
|
+
- **A player page follows its participant when a facilitator moves them**, rather than showing whoever takes their old seat
|
|
75
|
+
- **A UI can no longer send a facilitator's control**, such as `createTeam`, from a player surface
|
|
76
|
+
- **A `participantMove` hook now receives `phase`, `fromTeamId` and `toTeamId`.** The dev server dispatched both calls with only the participant, so a hook could not tell its departure call from its arrival call, and a game branching on `phase` ran neither branch
|
|
77
|
+
- **The dev server now refuses an endpoint call from a UI that does not list that endpoint in its `endpoints`.** The runtime contract requires it, and the build already checked the list against the game's endpoints, but at run time any UI could call any endpoint. A UI that worked locally by calling an endpoint it never declared would have failed on a production runtime
|
|
78
|
+
|
|
79
|
+
## [0.5.1] — 2026-09-20
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **Take this release rather than 0.5.0, which published only half of itself.** The release workflow bumps two package versions in a row, and pnpm 11 refuses the second: it reimplemented `pnpm version` to check that the working tree is clean even when told not to touch git, and the first bump is what dirties it. `@businesssimulations/seriousgaming-sdk@0.5.0` reached the registry, `@businesssimulations/seriousgaming-sdk-tools@0.5.0` never did, and the two are no use apart. 0.5.1 carries exactly what 0.5.0 was meant to and nothing else, so read the 0.5.0 entries below as this release's own
|
|
84
|
+
|
|
85
|
+
## [0.5.0] — 2026-09-20
|
|
86
|
+
|
|
87
|
+
> **Do not use.** Only the SDK published; the CLI did not, so the pair cannot be installed. Everything listed here ships in 0.5.1 above.
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- **A scenario can declare that it is restricted.** Set `restricted: true` on a scenario built for one client rather than every client the package is shared with. A production runtime must not offer it for selection, list it, resolve it by identifier, or serve its assets to a caller it has not granted it to. The package never names that audience: organisations belong to the runtime, which is what keeps the package portable. The dev server has no organisations, so it offers restricted scenarios normally and the restriction cannot be tested locally. A restricted scenario cannot also be the default, and the build refuses one that sets both. The flag does not cover `shared:` assets, which resolve whatever scenario is active, so files belonging to one client go under that scenario's own `assets/`. `examples/factory` ships one
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- **`new scenario` no longer discards most of the definition you give it.** Creating a scenario from JSON kept `locales` and `configuration` and dropped `default`, `ticks`, `demo`, `waitingRoom` and `teams` on the way to disk, so the command validated your input, reported success and wrote a bundle without them. If you worked around this by patching the fields back in with `update scenario`, that is no longer necessary
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
|
|
99
|
+
- **The scaffolded game ships on a newer toolchain.** `pnpm run sdk new package` now writes React 19.3, MUI 9.4, Vitest 5, Playwright 1.63, i18next 26.4, oxlint 1.82 and oxfmt 0.67. Existing packages do not pick up template changes automatically: compare your own `package.json` against a freshly scaffolded one and bump what you want. Vitest 5 is a major, so read [its migration notes](https://vitest.dev/guide/migration) before moving an existing suite
|
|
100
|
+
- **The scaffold pins `@types/node` to 24, not the newest published.** It had drifted to 26 while the SDK's supported floor, and the Node the dev server runs on, stayed 24, which lets an editor and `tsc` accept an API that is not there at runtime. If your own game pins 26, drop it back to `^24` unless you have deliberately raised your own floor
|
|
101
|
+
- **Installing the SDK pulls down far less.** `@lancedb/lancedb` was a declared dependency of the tooling that nothing imported: the `ask` command keeps its embeddings in a JSON file, not a vector database. Removing it takes `@huggingface/transformers` and its native image codecs with it, and the workspace resolves 190 fewer packages. `iso-639-1`, also unused, went the same way
|
|
102
|
+
- **The scaffold no longer installs `globals` or `dotenv`.** Neither was used by anything the scaffold writes. If your own game imports either, add it back to your `package.json`; nothing else changes
|
|
103
|
+
- **The scaffold's `sharp` override moves to `^0.35.4`.** `^0.35.0` still reached a build carrying the unpatched libheif (GHSA-rgj7-g3m4-5g8c). The override is in `pnpm-workspace.yaml`, not `package.json`; if you scaffolded before this, edit it there
|
|
104
|
+
- **A scaffolded game now declares which install scripts may run.** `new package` writes an `allowBuilds` block to `pnpm-workspace.yaml`, denying esbuild's install script. This matters from pnpm 11 onwards, where an install script that no `allowBuilds` entry speaks for stops the install with `ERR_PNPM_IGNORED_BUILDS` instead of printing the warning pnpm 10 printed. **A game scaffolded before this will not install on pnpm 11 or 12 until you add the block**, which you can copy from a freshly scaffolded game or let `pnpm approve-builds` write for you. esbuild is denied rather than allowed because its executable arrives in the `@esbuild/<platform>` optional dependency and the script only re-checks that it matches
|
|
105
|
+
- **The SDK is built and tested on pnpm 12.** The workspace pin moves from 10.34.4, crossing two majors. Your own game does not have to follow: nothing the SDK publishes depends on which pnpm you run it with. If you do follow, the changes most likely to catch a game package out are the `allowBuilds` one above, that pnpm no longer reads settings from a `pnpm` key in `package.json` or from `npm_config_*` environment variables, and that from pnpm 12 an unrecognised key in `pnpm-workspace.yaml` fails the command rather than being ignored
|
|
106
|
+
|
|
107
|
+
## [0.4.0] — 2026-09-18
|
|
108
|
+
|
|
109
|
+
### Breaking Changes
|
|
110
|
+
|
|
111
|
+
Work through step 7 of the upgrade guide.
|
|
112
|
+
|
|
113
|
+
- **A validator's `actor` is now a union, and an observer's writes are validated.** `actor` was `Participant`, which could only ever describe one kind of caller, so an observer's writes bypassed validators entirely and a game had no way to notice or to decide otherwise. An observer is a facilitator watching one team, and the runtime contract renders them **the same play surface a participant sees, with the same controls on it**, so every one of those controls wrote past the rules the game had stated. That is the same shape as the phantom-participant bug below, sanctioned rather than accidental. Validators now run on writes from a play surface under either viewer role, and `actor` is `{ role: 'participant', participant }` or `{ role: 'observer', teamId }`. **To migrate:** a validator that never reads `actor` needs no change. One that does must narrow on `actor.role` before reaching `actor.participant`, which your compiler names for you, and decide what an observer means for that rule. A game that wants a facilitator to be able to do what a participant cannot now says so itself, in one line, rather than every game being decided at once. `examples/factory` ships the exemption with the reasoning beside it
|
|
114
|
+
|
|
115
|
+
### Changed
|
|
116
|
+
|
|
117
|
+
- **The `validator` scaffold shows the caller.** `pnpm run sdk new validator` writes a `logic.ts` naming the `actor.role` narrowing and a `logic.test.ts` whose actor fixture is the union. Existing packages do not pick up template changes automatically: diff your own against `dist/templates/validator/` after upgrading
|
|
118
|
+
- **Nothing else an observer does is gated.** `requestTick`, `undoTick` and `resetGame` are not validated for a participant either, so they are not validated for an observer. The facilitator's deliberately unvalidated route is still the session surface, whose writes carry no participant and stay system-trusted
|
|
119
|
+
|
|
120
|
+
### Fixed
|
|
121
|
+
|
|
122
|
+
- **A player URL naming somebody who is not in the session no longer writes past your validators.** `/team/1/player/3`, on a team holding two participants, served a complete and apparently working player surface. Because the console could resolve no participant for that seat, its writes reached the team's real context carrying nobody, and the runtime reads 'nobody' as 'the system': every validator was skipped, silently, with nothing in the log and nothing on screen to distinguish it from a legitimate client. The same patch a real participant was refused for was accepted from the phantom one. A second browser tab being the obvious way to try a multi-participant rule, this was reachable by anyone testing a validator before adding a participant, and the likeliest way to notice was a game rule quietly ceasing to hold. The dev server now refuses the URL and says which seats the team has, offering **Add participant**; and the session refuses any write it cannot attribute rather than treating it as its own. A write that legitimately carries no participant — the CLI, a session surface — is unaffected and still bypasses validators, which is what the runtime contract requires of it.
|
|
123
|
+
- **An endpoint called from a player surface knows who called it.** The call carried no identity at all, so `caller` arrived stamped `role: 'facilitator'` with no `playerId`, and a team-scoped endpoint read whichever board the runtime last held rather than the caller's team. An endpoint gating on the caller's role or reading its team was gating on, and reading, the wrong thing. Calls now carry the viewer that made them, and an observer's call is stamped `role: 'observer'` as the runtime contract has always required
|
|
124
|
+
|
|
125
|
+
## [0.3.0] — 2026-09-18
|
|
126
|
+
|
|
127
|
+
### Breaking Changes
|
|
128
|
+
|
|
129
|
+
Work through step 6 of the upgrade guide.
|
|
130
|
+
|
|
131
|
+
- **`getPastState` is now on the callbacks every logic-bearing resource receives, and it is required.** Any object you built by hand to stand in for those callbacks in a test needs the extra member; production code is unaffected, because the runtime supplies it and the generated `Callbacks` type carries it. Required rather than optional deliberately: `extractText`, `generateSpeech` and `writeBlob` are optional only because they were added after the interface was first published, and an undeclared gated callback is replaced by a thrower rather than omitted, so nothing on this interface is ever actually absent. **To migrate:** add `getPastState: async () => []` to each test double, which is what a session with no committed rounds answers. Your compiler names every one, and they are all in test files
|
|
132
|
+
|
|
133
|
+
### Added
|
|
134
|
+
|
|
135
|
+
- **Logic can read past ticks.** `getPastState` was a user interface callback only, so an engine, hook, tick controller or endpoint had no way to see a round that had already closed. A surface could fetch the rounds it needed and hand them to an endpoint in its request, but the caller endpoints exist for may be a facilitator's AI agent, which has no surface — so 'how has this team's profit moved over the last five rounds' was unanswerable. It is now on the callbacks every logic-bearing resource receives, answering by the same rules as the surface callback and ungated by `uses`, since it reads state the resource already declared one tick further back. `'own'` is the team the call is scoped to, and a session-scoped endpoint has none, so `'own'` there is every team. An engine reaches every round strictly before the one its tick is closing, which is also what lets it stop carrying a round-keyed ledger capability purely to get a value to the next round. `examples/factory` ships `profittrend`
|
|
136
|
+
- **A session-scoped endpoint can change the whole session, and read every team.** `scope: 'session'` named only the caller's side of the contract, so both halves of what the handler itself could do were undefined: a write went down the team-addressed path and landed on the first team alone, and a read of a team capability was answered from whichever board the runtime happened to hold. A session-scoped endpoint now writes shared capabilities to the shared store, broadcast to every team the way a facilitator's shared write is, with its own `writes` array as the authorisation. Its reads split by capability scope: a `shared` one arrives on `context` as the single value it is, and a `team`, `team-readable` or `private` one arrives on `teams`, an entry per team carrying `teamId`, `index`, `name` and that team's values in stable index order. This is the path an AI facilitator needs, since an agent has no surface to borrow authority from and no bundle to declare reads in. `examples/factory` ships `announce`, which posts a notice to every team and reports back what each is charging
|
|
137
|
+
|
|
138
|
+
### Fixed
|
|
139
|
+
|
|
140
|
+
- **A surface you break and then fix comes back on its own.** Hot reload could not recover a surface that died on its way up. A module that throws while it is being evaluated never reaches the `import.meta.hot.accept` call Fast Refresh appends to it, and a component that throws while rendering leaves React with no mounted tree to patch, so the update the dev server sent when you fixed the file was dropped without a word. The frame stayed blank and the console stayed on 'Loading' through every later edit, and the only way out was to reload the page by hand. A dev-only guard in each served surface now reloads a frame that is both blank and has thrown, on the next change to any file it uses
|
|
141
|
+
- **A surface that reloads is told the state again.** Reconnection re-opened the channel but re-sent nothing, so a frame that reloaded for any reason, having once connected, blocked its first paint for good: the runtime contract requires the initial state messages to go out again, and only a change on the host's side happened to trigger them. The host now keeps the last value of each state message and replays it on every handshake. One-shot messages, such as a validator's refusal, are not replayed, so a fresh frame is never shown a rejection of a submission it did not make
|
|
142
|
+
- **A team write can no longer change a shared capability for the whole session.** A shared capability holds one value for every team, and a team-addressed write carrying a different one was accepted rather than refused. Worse, a write addressed to the _first_ team was adopted into the shared store on its way through, while only that team's view was re-composed: the store and every other team then disagreed about a value whose entire contract is that they cannot. Such a write is now refused with a `denied` outcome naming the capability, and a payload echoing the store unchanged is still accepted, so a full-context sender returning the view it was handed is unaffected. A single-team session is the one exception and keeps taking the write, which now goes to the store rather than sitting on the team where composition would mask it
|
|
143
|
+
- **A team added mid-session now reads the same shared values as everyone else.** Both interaction modes had a way of breaking the one-value-for-all-teams promise at the moment a team joins. A shared-state addition rebuilds the session so the engine sees the enlarged cohort, and that rebuild re-seeded the shared store from the capability defaults, discarding any shared write already made — for every team, not only the newcomer. An independent addition left the store alone but seeded the joining team from the capability defaults, so that one team read a different value from everybody else. The store is now captured and restored across the rebuild, before the initial state a reset returns to is derived from it, and an independent newcomer is composed with the store it is joining. A full reset still re-seeds the store, which is what a reset is for
|
|
144
|
+
- **A hook or waiting room interface that writes a shared capability now fails the build.** Only an aggregate engine, a session interface and a session-scoped endpoint may write one. The check existed but looked at the participant interface alone, so a hook declaring a shared write built and ran: a hook fires once per team, so a cohort would race on a single value. The error names the resource and the capability
|
|
145
|
+
- **A team-scoped endpoint that writes a shared capability now fails the build.** It used to build, run, answer the caller with the new value, and drop the write: every read afterwards returned the old one. The error names the endpoint and the capability, and points at `"scope": "session"`. A session-scoped endpoint writing a team-scoped capability fails the same way, for the mirror reason
|
|
146
|
+
|
|
147
|
+
### Changed
|
|
148
|
+
|
|
149
|
+
- **A generated `Callbacks` type now carries the callbacks `uses` does not gate.** It was `Pick<SignatureCallbacks, ...your uses>`, which left out the ungated ones entirely: the runtime passed `readAsset` to every engine and no generated type mentioned it, so it was reachable only through a cast, which is why nothing used it. The type is now that `Pick` intersected with `UngatedSignatureCallbacks`, so `readAsset` and `getPastState` are there without one. Nothing you have written stops compiling; a call you previously cast to reach can drop the cast
|
|
150
|
+
|
|
151
|
+
## [0.2.0] — 2026-09-17
|
|
152
|
+
|
|
153
|
+
### Breaking Changes
|
|
154
|
+
|
|
155
|
+
Work through step 5 of the upgrade guide: it gives the five in order, with the edit each one needs.
|
|
156
|
+
|
|
157
|
+
- **An endpoint now declares whether it acts on a team or on the session, and a teamless surface can no longer call a team-scoped one.** `scope` is `'team'` when absent, which is what every endpoint written so far meant, so a game whose endpoints are called from its play surface needs no change. What was never checked is a session or configuration UI calling one: those surfaces have no team of their own, so the handler received whichever board the runtime happened to hold, and nothing said so. The build now fails, naming the surface and the endpoint. **To migrate:** add `"scope": "session"` to any endpoint your session UI or configuration UI calls. A session-scoped endpoint's `caller` carries no `teamId`, which is what it was already receiving
|
|
158
|
+
- **A tick snapshot is now the round it closed, not the board that round opened with.** `getPastState` used to answer with the state _after_ an engine run, so anything the run cleared, reset or overwrote was observable in no snapshot at all: a game that deals a fresh hand or wipes pending decisions each round could never review what its teams actually submitted. The snapshot is now the board the tick was **submitted with**, which opened with the previous tick's outcomes and closed with everything the teams then did. The tick numbers do not move, so `{ fromTick: N }` still asks for the same round, and the reviewable range is unchanged. What changes is what comes back: a decision your engine clears is now in `entry.context`, and a value a UI overwrote during the round reads as the team left it rather than as the engine produced it. **To migrate:** nothing is required, and a round-keyed ledger capability you wrote to work around this keeps working. If you have one, you can now read those values straight off the snapshot and stop carrying the ledger in context, where it rides every push (see [Review a past day](recipes/review-a-past-day.md)). Two smaller shifts come with it: tick 0 has no snapshot, since it closed no round, so `{ fromTick: 0 }` reaches nothing until the first tick has been taken; and undo now leaves a team's decisions on the board rather than clearing them, which is what makes it useful for a tick somebody triggered by accident
|
|
159
|
+
- **Hooks, endpoints and validators receive a `meta` object instead of a bare `locale`.** They carried the session locale as a loose `locale: string` (validators carried nothing at all), which left nowhere to put the next run-wide fact the runtime needed to tell them about. They now take `meta`, carrying `locale`, `seed` and `runKind`, matching the `meta` an engine already receives minus `currentTick`: none of the three runs as part of a tick, so there is no tick being executed to name. **To migrate:** change `({ context, locale })` to `({ context, meta })` and read `meta.locale`; a validator that wants to phrase a rejection in the session's language can now do so. The scaffolded test files for all three state a `meta` fixture, so take them from the shipped templates if you want your scaffold in step
|
|
160
|
+
- **Every capability variable must declare a `default`.** The runtime has always seeded an undeclared one to `null` while the generated `Context` typed it by its declared type, so the two disagreed and nothing said so. Generating the starting values as a typed fixture is what forces the question, and the build now names the capability and the variable rather than letting a fixture fail to compile somewhere downstream. **To migrate:** add a `default` to any variable that lacks one; `pnpm run sdk build` lists them. A runtime still has to handle a manifest that declares none, so the runtime contract is unchanged for runtime implementers
|
|
161
|
+
- **An engine is aggregate because of what it exports, and `aggregate` is no longer authored.** An engine whose logic exports `InitAggregate` and `RunAggregate` runs once across every team; the build reads that from the code and records it in the package it produces. An `aggregate` in `engine.sdk.json` now fails the build. The flag said nothing the exports did not, and it could disagree with them: an engine that lost the declaration kept aggregating in the dev server while the hosted runtime skipped the step entirely, and an interactive `sdk update engine` dropped the flag without saying so. **To migrate:** delete `aggregate` from `engine.sdk.json` (`pnpm run sdk update engine -- --patch '[{ "op": "remove", "path": "/definition/aggregate" }]'` does it), then make sure the engine's entrypoint exports what you meant. A re-export counts, so aggregate functions living in another file need only `export { InitAggregate, RunAggregate } from './market'`. Exporting one of the pair without the other is now an error naming the missing one, where before it was an engine that quietly did half its job. `sdk new engine --aggregate` replaces passing `"aggregate": true` in the definition, and scaffolds the three exports as before
|
|
162
|
+
|
|
163
|
+
### Added
|
|
164
|
+
|
|
165
|
+
- **Endpoints can change state, by declaring `writes`.** An endpoint used to compute and answer; one that declares the capabilities it may change now returns `{ response, context }`, and the runtime applies that patch to the team it was called against, through the same queue as every other write. It is how a facilitator operation with rules behind it gets one sanctioned path: a validator can refuse a change outright while the endpoint remains the only way to make it. The patch is held to the declaration, so returning a capability outside `writes` is a fault rather than a silent drop, and it is refused outright if a tick lands while the handler is still running, since the board it was computed against has gone. Endpoint writes are not put to validators whoever called them: a validator gates what a participant submits directly, and an endpoint is your own code, which checks its own request. An endpoint that declares no `writes` behaves exactly as before. `examples/factory` ships `setprice`, which sets a team's unit price
|
|
166
|
+
- **An endpoint can be marked `agentCallable`, offering it to a facilitator's AI agent.** Off by default, so an endpoint you wrote for your own screens never becomes part of an agent's vocabulary until you say so. A marked endpoint must carry a `description`: everywhere else that is author-time convenience, but here it is what the agent reads to decide whether to call the endpoint, so the build fails without one. The usual 'no user interface requests it' warning is suppressed for these, since their caller is one no bundle names
|
|
167
|
+
- **`pnpm run sdk endpoint list` prints every endpoint and what it may do.** Its scope, whether it writes, whether an agent may call it, and its description, with `--agent` to narrow to the agent-callable ones and `--json` to include the request and response schemas. It reads the manifest, so it answers without starting a session. Reading your descriptions side by side is the point: on an agent-callable endpoint the description is what a model reads to decide whether to call it, and three that say much the same thing is a wrong call every session
|
|
168
|
+
- **`pnpm run sdk endpoint call <identifier>` runs an endpoint from the terminal.** Supply the request with `--request` (or `--file`, or on stdin) and the team with `--team`, and the response prints as JSON. It exists so an endpoint can be exercised without a model in the loop, which is a slow and unrepeatable way to find out that a request schema is wrong
|
|
169
|
+
- **Walkthrough runs, for demonstrating a game before you play it.** A walkthrough is a disposable second run of the same game beside the session: one team, one participant, always from tick 0, and isolated from the session in both directions, so driving it moves nothing you were about to play. It is what a facilitator puts on a projector while talking a room through the game. In `sdk dev` it lives at `/walkthrough`, listed in the drawer's Surfaces tab and built the first time you open it; from the command line, `sdk tick --walkthrough` and the same flag on `init`, `view`, `patch`, `undo` and `reset` drive it instead of the session. It inherits the session's configuration, scenario and locale, respects the game's `ticks` limit, never shows the waiting room, and resets to tick 0 rather than offering undo. A shared-state game runs solo there whatever its `teams.min` says: that number is what the game needs to be _played_, and a walkthrough is not play
|
|
170
|
+
- **`meta.runKind` tells your logic which kind of run it is in, and `useRunKind()` tells your surfaces.** It is `'session'` or `'walkthrough'`, host-supplied, identical everywhere within one run and fixed for its lifetime, so a decision your engine takes on it and one your interface takes on it cannot disagree. A game that ignores it behaves exactly as it always has, since `'session'` is what a host offering no walkthrough reports. Use it to fill in what a solo run genuinely lacks (rival teams on a leaderboard, teammates in a presence list), and to suppress spoilers and calls that reach outside. Do not use it to change how the game plays: a walkthrough that behaves differently demonstrates a different game. `examples/factory` shows the pattern, skipping the summary step's two model calls and writing the report itself while every figure on the screen stays the one the round produced
|
|
171
|
+
- **A session surface can offer the walkthrough.** `useSessionUserInterfaceWalkthroughLink()` returns where to open it, or `null` on a host that offers none, and `callbacks.resetWalkthrough()` throws it away and starts it again at tick 0. Gate the control on the link, the way a guide button gates on `useSessionUserInterfaceHostOverlays()`
|
|
172
|
+
- **`SERIOUSGAMING_DEV_PORT` sets where a dev server starts looking for a port.** A session command starts a server when none is running, and until now that server always began its search at 3000, with no flag or variable able to move it: the command has no `--port` of its own to hand on. Set the variable in your environment or `.env` and both `sdk dev` and an auto-started server read it. It is still a starting point rather than a binding, so the server steps up to the first free port and prints the one it landed on
|
|
173
|
+
- **`defaultContext` and `defaultConfig` are generated beside every resource's `types.ts`, in a `defaults.ts`.** They hold the values a session opens with, taken from the same schemas the types are compiled from, and they exist for test fixtures. `Context` is the union of every capability a resource declares and TypeScript requires all of them, so a step test about two capabilities had to write out twenty, or reach for `as unknown as Context` and lose shape checking for the whole fixture. Spreading the default and naming what the step reads keeps both, and a capability added later arrives in every fixture already populated: `const context: Context = { ...defaultContext, price: { Price: 5 } }`. A configuration UI gets `defaultConfig` alone, keyed by the slots it configures, so a screen's fixture no longer restates the engine's schema defaults by hand where they rot; an endpoint gets `defaultContext` alone. `types.ts` stays types-only, so every `import type` that reaches it is still telling the truth. The engine's scaffolded `logic.test.ts` and `logic-aggregate.test.ts` now open from the defaults rather than an empty cast; take them from the shipped templates if you want your scaffold in step
|
|
174
|
+
|
|
175
|
+
### Fixed
|
|
176
|
+
|
|
177
|
+
- **Endpoint requests and responses are validated against their declared schemas.** `requestSchema` and `responseSchema` were compiled into the types you write against and then checked by nothing at run time, so a caller could send a value the schema forbids and a handler could return one. The runtime now refuses both, naming the property and the rule: a bad request is refused before the handler runs, and a bad response is refused rather than delivered. The runtime contract has required this throughout. It was survivable while every caller was a screen built from those same generated types, and it stops being survivable once an endpoint is agent-callable, because then the requests come from a model and the schema is the boundary rather than a description of one
|
|
178
|
+
- **A single team can no longer be undone or reset out of lockstep in a shared-state game.** `undo --team N` and `reset --team N` accepted a team in any mode, and nothing put that team back in step: it landed a tick ahead of where it had been on the next tick, never having evolved through the one in between, while the aggregate engine went on treating the cohort as synchronous. Both now refuse outside independent mode, naming the reason, and the dev server's per-team tick, undo and reset controls no longer appear on a shared-state game
|
|
179
|
+
- **A per-team undo no longer rolls that team back off a facilitator's session-wide write.** A snapshot carries the shared store as it stood when it was taken, so restoring one wholesale took the team back to an older value of every shared capability while the rest of the session kept the current one. The restored board is now re-composed with the live store, as a per-team reset already was, and as the runtime contract requires
|
|
180
|
+
- A session command that starts a dev server now says so, naming the URL and how to stop it. It started one silently, and the server outlives the command, so a test fixture that reached a session command at collection time (a Vitest `describe` body runs before any `beforeAll`) found a session already running and failed somewhere else entirely, with nothing linking the two
|
|
181
|
+
- `sdk dev` no longer offers a second server on another port when one is already running. A package holds one session and the lock file holds one address, so the guard refuses whatever port was asked for; the message used to recommend `--port <n>`, which was the flag the caller had just passed and watched being ignored. It now says a package holds one session, and points at `sdk dev stop`
|
|
182
|
+
- `sdk dev --port` rejects a value that is not a port, rather than reading the leading digits off it or binding a random one. `--port 3310abc` bound 3310; `--port abc` passed `NaN` to `listen`, which binds whatever the operating system offers
|
|
183
|
+
- A session that holds one team now reports the mode its game declared. `session.mode` in the state projection carried a third value, `single`, whenever the team count was one, which the specification's state projection does not admit, and which decided how that session ticked, undid and answered `getPastState`. An independent game that opens with one team now runs per team from the start, as it does with any other number of them. `SessionMode` narrows to `'independent' | 'shared-state'` to match
|
|
184
|
+
- A shared-state session no longer accepts a team once it has ticked. Its team count is fixed at creation, but the guard read the mode as reported rather than as declared, so a session that opened with one team let a team in at any point, including after ticks had run
|
|
185
|
+
- Adding a team to a shared-state session **before** the first tick now does what it has always been documented to do: the session re-initialises, so `InitAggregate` observes the enlarged cohort and every team's starting state is rebuilt for the new team count. It used to slot the team in beside the others, leaving the aggregate's view of the cohort as it was at creation. Re-initialising discards anything written into the board beforehand; the configuration survives, being an input to `Init` rather than state. A production runtime refuses the add throughout, so this stays an authoring convenience a game must not depend on
|
|
186
|
+
- `meta.currentTick` is now documented as what it has always been: the tick **being executed**, 1-based in `Run`, so the first `Run` of a session sees `1` and the last `Run` of a `ticks: 10` game sees `10`. The type's doc comment and the runtime specification both described it as a count that increments after `Run`, which would put the last round at `9` and silently skip anything an author scheduled for the final tick. No behaviour changed; the description did. Note that the `currentTick` a user interface reads is deliberately one lower, being the head tick (rounds completed)
|
|
187
|
+
- `pnpm run sdk config set` no longer wipes your other global settings when `~/.seriousgaming-sdk/config.json` cannot be parsed. It used to treat an unparseable file as empty and write back only the key you were setting, so a single `config set` over a malformed or truncated file took your API key with it, and still reported success. It now refuses to write and tells you to fix or remove the file. The file is also written atomically, via a temporary file renamed into place, so an interrupted write can no longer leave the truncated file that caused this, and owner-only permissions are re-applied on every write rather than only when the file is created. `config set` now says when it replaced an existing value, so an accidental overwrite is visible
|
|
188
|
+
|
|
189
|
+
## [0.1.0] — 2026-09-15
|
|
190
|
+
|
|
191
|
+
### Breaking Changes
|
|
192
|
+
|
|
193
|
+
- **Logic now receives only the capabilities it declared.** The engine, hooks, validators and the tick controller were handed the whole context while the generated `Context` type showed them their `reads` and `writes` alone, so an author held a type that was permissive in the wrong direction. What a logic function returns is now folded back as a patch, so a capability it never saw survives untouched. **To migrate:** declare every capability your logic reads or writes. The generated types already describe the narrower set, so TypeScript will point at anything missing.
|
|
194
|
+
- **`writes` is enforced.** A resource that changes a capability it declared only in `reads` now fails the tick, naming both. Returning it unchanged — which the usual `return { ...context }` does — is unaffected.
|
|
195
|
+
- **A validator runs only when a write changes something it reads.** One declaring no `reads` still runs on every write. The same rule the tick controller already followed, and the reason validators are cheap enough to keep on the write path.
|
|
196
|
+
- **Validators no longer carry a `uses` field.** It was reserved and never honoured. A rule that needs an LLM or a document belongs in an endpoint, not between a participant pressing submit and their write landing.
|
|
197
|
+
- **The `shell` block is gone.** `shell.configuration.playButton.hidden` was the last field in it. The host always renders the Play control on the configuration stage now, so a game cannot hide the only way out of it. `requestPlay` still works and now adds a control rather than replacing one.
|
|
198
|
+
- **Every `configurationSchema` property must declare a `default`.** Every override layer above the defaults is partial, so an incomplete first layer can never be reliably completed. **To migrate:** add a `default` to any property that lacks one; the build names each.
|
|
199
|
+
- **One participant shape everywhere.** Hook payloads and a validator's `actor` carried `{ id, teamId, name }` while every surface saw `{ id, name, index, connected, teamId }`, and both were called `Participant`. A hook seeding per-participant state could not reach `index` — the natural key for 'the first to join leads'. Hooks and validators now receive the full record; existing code reading `id`, `name` or `teamId` is unaffected.
|
|
200
|
+
- **A configuration UI is seeded by its own message, `setResolvedConfiguration`.** `setConfig` carried two incompatible shapes under one name — a game or session surface's own settings, and the whole slot-keyed map — so the protocol typed the payload as `unknown` and the spec typed the configuration UI's seed as the sparse override layer, which for a fresh session is `{}`. Configuration UI bundles pick this up with the next SDK release; nothing else changes.
|
|
201
|
+
- **`CentralizedConfiguration` is now `SessionConfigurationOverrides`, beside a new `ResolvedConfiguration`.** One name covered both the sparse layer a facilitator writes and the complete map everything reads. The layer was called 'centralized configuration' and 'the session-override layer' in adjacent sentences; it is the session overrides throughout now.
|
|
202
|
+
- **A scenario can vary game-level settings, not just configuration.** `ticks`, `demo.maxTicks`, `waitingRoom.openOnStart`, `teams.min` and `teams.max` now resolve with the active scenario winning, so a 'short edition' or a 'facilitated edition' is expressible directly rather than through a tick controller reading its own config. `teams.mode` stays fixed at authoring time.
|
|
203
|
+
- **The waiting room is a configuration slot like every other resource.** Its overrides move from `waitingRoom.configuration` on the game to the `waiting_room` slot, so a scenario can vary it and a `quick-config` surface can expose it — neither of which was previously possible for the surface participants see before a session starts. `game.waitingRoom` now holds only `openOnStart`. **To migrate:** move the block from `waitingRoom.configuration` to `configuration.waiting_room`.
|
|
204
|
+
- **A blob's `scope` is now its `retention`.** The field only ever decided when the blob was deleted, and `scope` already means capability visibility elsewhere. Rename it in any `writeBlob` call.
|
|
205
|
+
- **`supportedLocales` and `locales` must describe the same set.** A mismatch in either direction now fails the build naming the codes. A named locale that nothing lists was previously unreachable in silence.
|
|
206
|
+
- **`updateData` no longer carries `composedAtTick`.** The surface always knew which round it composed against and compares locally; the host was forbidden from acting on the value. `committedAtTick` on the response is unchanged.
|
|
207
|
+
- **A `configurationSchema` may no longer declare a top-level `required`.** Every property must declare a `default` and every layer above the defaults is a partial override, so resolved configuration carries every property and the list could reject nothing. Nothing read it, so an author writing one was describing a constraint that was already unconditionally true. **To migrate:** delete the `required` array. For a property that may genuinely have no value, widen its type instead — `"type": ["string", "null"]` with `"default": null` says 'or nothing' outright, where an absent property used to say it by implication. A `required` inside a property's own subschema is untouched: that one still governs what a default for the property must contain.
|
|
208
|
+
- **A user interface's bundle no longer records `ui.directory`.** Its web files sit in the resource's own directory, so the field carried no information and the value was already fixed by the CLI. Worse, it was honoured unevenly: the build and the validator read it and the dev server did not, so a package recording `"directory": "ui"` validated, built, ran on a production runtime, and then rendered an empty frame on localhost with nothing outside the browser console to say why. This retracts the compatibility promise made when the configuration UI scaffold flattened: a bundle still recording a directory now fails validation naming the field, rather than rendering nothing. **To migrate:** delete `"directory"` from the `ui` block, and if your files really do sit in a subdirectory, move them up beside the bundle. `logic.directory` is unaffected and still required.
|
|
209
|
+
- **`teamId` is opaque, and the specification no longer says otherwise.** The specification said in one place that a `teamId` is an opaque runtime-assigned string a game must not parse, and in another that it is always `team-${index + 1}`. The first is normative. The dev server still uses the reference derivation, so nothing changes for a game that was already treating ids as opaque; a game extracting `N` from `team-N` was relying on something the specification never promised and will break on a runtime using its own handles. `index` is the ordinal contract.
|
|
210
|
+
|
|
211
|
+
- **The six resources a game holds one of answer to one name each: `engine`, `ui`, `session`, `waiting_room`, `tick_controller` and `guide`.** The slot said what the thing was and the type name said what kind it was, so `sdk new module` made `engine/`, `sdk new session-user-interface` made `session/`, and `sdk new facilitator-guide` made `guide/`. Two words for one thing is what kept generating documentation that could not be followed: an audit found a guide teaching a second module, which fails with `Module directory already exists`, and another recommending 'one module for inventory, one for pricing', which a game cannot have. The CLI verb, the directory, the bundle filename, the identifier, the `configuration` key and the manifest key are now the same word, with no translation between them. **To migrate:** rename the bundle file in each slot you fill, and change the verb you type. `engine/module.sdk.json` becomes `engine/engine.sdk.json`, `ui/user-interface.sdk.json` becomes `ui/ui.sdk.json`, and likewise `session/session.sdk.json`, `waiting_room/waiting_room.sdk.json`, `tick_controller/tick_controller.sdk.json` and `guide/guide.sdk.json`. Nothing inside the files changes. A slot still holding its old filename fails to load, naming the rename. `sdk new module` becomes `sdk new engine`, `sdk new user-interface` becomes `sdk new ui`, and so on for `update` and `delete`; `sdk build module` becomes `sdk build engine`
|
|
212
|
+
|
|
213
|
+
- **The two multi-word verbs take underscores: `sdk new waiting_room` and `sdk new tick_controller`.** A slot is the resource's identifier and a `configuration` key that gets dot-accessed, so it cannot contain a hyphen. Given the choice between an underscore in a command name and a translation layer between the verb and the directory it makes, the underscore is the smaller cost: `waiting_room` is the one spelling, everywhere
|
|
214
|
+
|
|
215
|
+
- **`module` is gone as a word for a game's engine.** Nothing about it was modular once a game held exactly one, `moduleOrder` and `requiresDownstreamWrite` were removed, and a simulation with several steps calls them as plain functions from its one `Run`. `engine` says what the thing is: the code that computes the game. **To migrate:** the type renames are `SDKModule` to `SDKEngine`, `ModuleDefinition` to `EngineDefinition`, and `ModuleLogicConfiguration` to `LogicConfiguration` (which the engine, tick controllers, hooks, validators and endpoints all share, so `Module` never belonged in it). `RunFunction` and `InitializationFunction` are unchanged
|
|
216
|
+
|
|
217
|
+
- **`ModuleMeta` and `TeamModuleMeta` are `RunMeta` and `TeamRunMeta`.** They are passed to tick controllers as well as the engine, so they were never module-specific. **To migrate:** change the import. Nothing in the shape changes, so a `meta` literal in an engine or tick-controller test compiles as soon as the type name is updated
|
|
218
|
+
|
|
219
|
+
- **The built manifest keys the six slots by their slot.** `module`, `userInterface`, `sessionUserInterface`, `waitingRoomUserInterface`, `tickController` and `facilitatorGuide` become `engine`, `ui`, `session`, `waiting_room`, `tick_controller` and `guide`. Keys that _are_ identifiers now spell themselves as identifiers, which is what the `configuration` block already did. **To migrate:** nothing, unless you read `dist/seriousgaming.json` or `sdk resolve` output from a script. Note that `waitingRoom` is untouched and still means the room's _settings_ on the game: the interface is `waiting_room`, so the two are no longer one word with two meanings
|
|
220
|
+
|
|
221
|
+
- **One set of resource bundle types, not two.** `schema.json` carried two families over the same fourteen resources: `ModuleBundle`, `CapabilityBundle`, `UserInterfaceBundle` and the rest alongside `SDKModule`, `SDKCapability`, `SDKUserInterface`. Eleven of the fourteen pairs were semantically identical and the other three differed only in whether they carried a `rootDirectory`. **To migrate:** import the `SDK*` name. `ModuleBundle` becomes `SDKModule`, `CapabilityBundle` becomes `SDKCapability`, and so on for all fourteen; `PackageBundle` becomes `SDKPackage`. `SDKConfigurationResolved` is now `Configuration`, which is the one shape both the loader and the built manifest use
|
|
222
|
+
|
|
223
|
+
- **`sdk resolve` prints the singular shape the manifest already had.** The loader held `modules`, `userInterfaces`, `games` and the rest as arrays of at most one, and the built manifest has carried `module`, `userInterface` and `game` since `manifestVersion: 2`. The loader now agrees with it. **To migrate:** if you read `sdk resolve` output from a script, `config.modules[0]` becomes `config.module`, `config.games[0]` becomes `config.game`, and likewise for `userInterface`, `sessionUserInterface`, `waitingRoomUserInterface`, `tickController` and `facilitatorGuide`. A slot the package does not fill is absent rather than an empty array. `capabilities`, `configurationUserInterfaces`, `scenarios`, `hooks`, `validators` and `endpoints` are unchanged, because a package may hold several of each
|
|
224
|
+
|
|
225
|
+
- **`sdk build module` takes no argument.** It searched for a module by identifier, and a game holds one, in the `engine` slot, so `engine` was the only value that ever resolved. **To migrate:** drop the argument. `sdk build module engine` becomes `sdk build module`. With an empty slot it now says so, rather than reporting a module it could not find
|
|
226
|
+
|
|
227
|
+
- **`reads` and `writes` are arrays of capability identifiers.** A reference used to be an object, `{"identifier": "revenue"}`, and a write could never carry anything else. **To migrate:** replace each object with the identifier itself, so `"reads": [{"identifier": "revenue", "optional": false}]` becomes `"reads": ["revenue"]`. A bundle that still holds the object form fails to load, and the message names the identifier to put in its place. If you patch bundles from scripts, the patch value is now a string: `{"op": "add", "path": "/definition/reads/-", "value": "health"}`. `CapabilityRead` and `CapabilityWrite` are no longer exported from `@businesssimulations/seriousgaming-sdk`, and neither is `CapabilityDeclaration`
|
|
228
|
+
|
|
229
|
+
- **`optional` on a read is removed.** It said the capability might be missing from the game, which is a state that can no longer occur: a game wires whatever its package declares, and the runtime seeds every capability any resource references from its schema defaults, so a declared read is always satisfied. What the flag actually did was drop the capability from the generated `Context`'s required set, which bought `?.` guards through a screen and protected against nothing. **To migrate:** delete the field. Your `Context` now types every declared capability as present, so the guards that existed only because of it can go too. Where you were treating an optional read as 'the author may not have uploaded anything', that absence is the capability holding its schema default, so branch on the value (`DocumentBlobId === ''`) rather than on the key
|
|
230
|
+
|
|
231
|
+
- **A `crossTeam` read moves to its own `crossTeamReads` array.** It was a flag on a read, but was legal only on the participant interface and the waiting room, against a capability scoped `team-readable`, in a shared-state game. A field six other resource types could write and none could use needed a build check to reject them; a field only the two player surfaces carry cannot be written there at all. **To migrate:** list the capability in both arrays, `"reads": ["decisions"], "crossTeamReads": ["decisions"]`. `reads` stays the single answer to what a surface is delivered, and `crossTeamReads` narrows it to the entries that arrive for every team; the build fails on an entry missing from `reads`. `CrossTeamContext` is generated from the new array and is otherwise unchanged
|
|
232
|
+
|
|
233
|
+
- **`new` and `update` refuse the fields they used to accept and throw away.** Three of them: an `identifier` on a resource a game holds one of, a `locales` block on a singular user interface, and a `logic` / `ui` block naming a directory or entrypoint the CLI does not write. Each was validated and then overwritten, so the command exited 0 with correct output and the author kept a wrong belief — that a module has a name, that the participant screen is labelled, that logic can live where you put it. **To migrate:** delete the field. `sdk new module '{"definition":{"identifier":"warehouse"},...}'` becomes `sdk new module '{"definition":{},...}'`; the module lives in the `engine` slot and the slot is the name everything downstream reads. A module's and a tick controller's logic sits directly in its slot, so `"logic": {"directory": "logic", ...}` becomes `"logic": {"directory": ".", ...}`; a hook, validator or endpoint keeps `"logic"`. Every rejection names the field, says why it has no meaning there, and prints the shape that does work. The message an author saw for a dotted identifier on a slot resource was doubly wrong — it named a short replacement for a resource that takes no name at all — and is replaced; identified resources (capabilities, scenarios, hooks, validators, endpoints, configuration UIs) still get the short-form advice, which is right for them
|
|
234
|
+
|
|
235
|
+
- **Two interactive prompts that discarded their answer are gone.** `update module` opened by asking for a module name and then threw it away, and `update tick-controller`'s whole interactive mode was that one prompt, so it wrote the bundle back unchanged and reported success. A game holds one of each, so there was never a name to change. **To migrate:** `update module` simply no longer asks. `update tick-controller` now prints the two invocations that do something — a definition or a `--patch` — as the singular interfaces already did.
|
|
236
|
+
|
|
237
|
+
- **`init`, `tick`, `view` and `patch` no longer accept `-j, --json`.** All four have only ever printed JSON, so the flag promised a choice that did not exist and the documentation described one. **To migrate:** drop the flag; the output is unchanged. `configure`, `blob upload`, the `new` / `update` / `delete` commands and `package bundle` / `version` / `publish` all still take it, and all still honour it
|
|
238
|
+
|
|
239
|
+
- **The CLI's binary is `seriousgaming-sdk`.** npm derived it from the package name, so it installed as `seriousgaming-sdk-tools` while every example the CLI printed in its own help (and every line of the documentation) said `seriousgaming-sdk`. The name a reader was taught was one nobody had on their path. The binary now carries the name it was always called, and the scaffold's `sdk` script points at it. **To migrate:** change `"sdk": "seriousgaming-sdk-tools"` to `"sdk": "seriousgaming-sdk"` in your `package.json` scripts, or re-merge the block from the shipped template. Nothing else refers to the binary by name: the two package names are unchanged, and `pnpm run sdk` is what the documentation tells you to type either way
|
|
240
|
+
- **A configuration surface names its UI by identifier, not by an object holding one.** `"quick-config": { "identifier": "config" }` becomes `"quick-config": "config"` in `seriousgaming.sdk.json`. The wrapper dated from when a reference carried its own `configuration` block beside the identifier; that configuration moved into the `configuration` map two tranches ago, leaving an object around a single string while every other identifier reference in the manifest (`configures`, `endpoints`, a module's `reads`) is already the string itself. **To migrate:** replace each surface's object with its identifier. A manifest that still has the object fails to load with a message naming the surface and the value to use. The `GameConfigurationUserInterfaceReference` type is gone from `@businesssimulations/seriousgaming-sdk`, along with the `GameConfigurationUserInterfaceReference1` and `GameConfigurationUserInterfaceReference2` duplicates the generator emitted beside it; `GameConfigurationUserInterfaces` stays and is now a map of surface to identifier. Renaming or deleting a configuration UI still rewrites the entry for you
|
|
241
|
+
|
|
242
|
+
- **A singular user interface no longer carries a name.** `locales` is gone from the participant interface, the session surface and the waiting room. A game holds exactly one of each, so nothing ever labelled them: the only code that read the field was a tab strip in the dev server that renders when a game has more than one session UI, which it cannot. The field was `required`, so every author wrote a name that was never shown, `sdk build` failed a game whose `supportedLocales` were not all covered by it, and three `new` commands opened by asking for it. **To migrate:** delete the `locales` block from `ui/user-interface.sdk.json`, `session/session-user-interface.sdk.json` and `waiting_room/waiting-room-user-interface.sdk.json`. A bundle that still has one fails to load with a message saying so. Nothing else moves: a screen's own strings were always in its `src/i18n.ts`, resolved against the locale the host sends, and that is unchanged. The game keeps its `locales`, and so do scenarios and the facilitator guide, all of which are genuinely labelled. A knock-on: `update user-interface`, `update session-user-interface` and `update waiting-room-user-interface` now take a definition or a `--patch` only. With no name to edit and no identifier to rename, interactive mode had nothing to ask, so it prints the two invocations instead of prompting
|
|
243
|
+
|
|
244
|
+
- **`TeamState.revision` is required, and `createMockTeamState` exists so your fixtures need not care.** It was briefly optional so a hand-written session fixture need not carry a field no screen reads. That bought one branch in the wrapper which adopted a delta unconditionally when it had no baseline to compare against: silent last-write-wins, and the only place in the delta design that resolved a conflict by guessing. **To migrate:** use `createMockTeamState({ ... })` from `@businesssimulations/seriousgaming-sdk/ui/testing`, which fills in `revision` along with the other fields a test rarely cares about, or add `revision: 1` to the literal. The scaffolding templates `session-user-interface/src/App.test.tsx.liquid` and `session-user-interface/src/starter/App.test.tsx.liquid` use the helper now; take them from the shipped templates if you want your scaffold in step
|
|
245
|
+
|
|
246
|
+
- **Four history callbacks are now one: `getPastState(query)`.** `getPastTickSnapshot`, `getPastTickSnapshots`, `getPastCrossTeamData` and `getPastCapabilityHistory` are replaced by a single read whose query says which ticks and which teams you want. They differed only in those two axes, and each had its own message pair, its own result shape and its own miss rule (two returned `null`, two returned `[]`), so a screen that reviewed one team and then all of them wrote the same render twice. **To migrate:** `getPastTickSnapshot(t)` becomes `getPastState({ fromTick: t })`, which answers with an array — an unreadable tick is an empty result rather than a `null`, so take `entries[0] ?? null` where you checked for one. `getPastTickSnapshots(range)` becomes `getPastState(range)` unchanged. `getPastCrossTeamData(capabilityId, t)` becomes `getPastState({ fromTick: t, teams: 'all', capabilityIds: [capabilityId] })`. `getPastCapabilityHistory(capabilityId, range)` becomes `getPastState({ ...range, teams: 'all', capabilityIds: [capabilityId] })`, which returns one flat entry per (tick, team) rather than rows of teams: group by `tick` if your table is tick-major. A session surface no longer passes a trailing `teamId` — name the team in the query (`teams: 'team-2'`) or leave it out, and the host reads `own` on a session surface as the whole session. `TickSnapshotView`, `TickRange` and `TickCapabilityValues` are gone; `PastStateQuery` and `PastStateEntry` replace them
|
|
247
|
+
|
|
248
|
+
- **`useCrossTeamData()` takes no capability identifier and returns one entry per team.** It was `useCrossTeamData<T>('decisions')` returning `{ teamId, name, value }[]` for that one capability; it is now `useCrossTeamData<Context>()` returning `{ teamId, teamName, context }[]`, where `context` carries exactly the capabilities the surface declared with `crossTeam: true`. The identifier was a string that had to match a declaration the bundle already made, so misspelling it returned an empty array with nothing to say why, and a surface reading two cross-team capabilities called the hook twice and joined the results by `teamId` itself. **To migrate:** call it once with no argument, pass the cross-team context as the type argument, and read `team.context.<capability>` where you read `team.value`; `team.name` becomes `team.teamName`. This is the shape `getPastState` returns too — a `PastStateEntry` is one of these plus the tick it was captured at — so a live view and a historical view now render through the same code, which they could not before
|
|
249
|
+
|
|
250
|
+
- **A test that stubs the UI callbacks object by hand needs `getPastState`.** Only affects a test that builds a full `UserInterfaceCallbacks` or `SessionUserInterfaceCallbacks` itself rather than using `renderUserInterface` / `renderSessionUserInterface` from `@businesssimulations/seriousgaming-sdk/ui`, which supply it (defaulting to an empty result). Replace any `getPastTickSnapshot` / `getPastTickSnapshots` / `getPastCrossTeamData` / `getPastCapabilityHistory` stub with one `getPastState`. The scaffolding templates `user-interface/src/starter/App.test.tsx.liquid`, `session-user-interface/src/App.test.tsx.liquid` and `session-user-interface/src/starter/App.test.tsx.liquid` are updated; take them from the shipped templates if you want your scaffold in step
|
|
251
|
+
|
|
252
|
+
- **A negative `fromTick` reaches nothing rather than being clamped to 0.** The old range read clamped it and the old single read refused it; unified, clamping would quietly answer a different question than the one asked, handing back tick 0 to a caller who asked for tick -1 without saying so
|
|
253
|
+
|
|
254
|
+
- **A test that builds the player data context by hand needs `isRoundProcessing`.** Only affects a test that renders `UserInterfaceDataContext.Provider` itself rather than using `renderUserInterface` from `@businesssimulations/seriousgaming-sdk/ui`, which supplies it for you (and takes `isRoundProcessing` as an option if you want to render the processing state). Add `isRoundProcessing: false` to the provider value. The scaffolding templates `user-interface/src/App.test.tsx.liquid`, `user-interface/src/starter/App.test.tsx.liquid` and `waiting-room-user-interface/src/App.test.tsx.liquid` are updated; take them from the shipped templates if you want your scaffold in step
|
|
255
|
+
|
|
256
|
+
- **A hand-written `meta` in an engine test needs three more fields.** `ModuleMeta` gains `seed`, and the per-team functions (`Init`, `Run`, tick-controller logic) take a `TeamModuleMeta` that adds `teamId` and `index`. Nothing your logic does changes and nothing needs rewriting; the runtime has always known these and now passes them. But a step test that builds `meta` itself will not compile until it states them. **To migrate:** add `seed` to any `meta` you pass an aggregate, and `seed`, `teamId` and `index` to any you pass `Init`, `Run` or a tick controller: `{ currentTick: 0, locale: 'en', teamId: 'team-1', index: 0, seed: 'test-seed' }`. A fixed `seed` in a test is what makes an assertion on derived randomness stable. They are required rather than optional because author code reads them: an optional `index` would be `number | undefined` at every use site, which is worse than a one-line fixture change. The scaffolding templates `module/logic.test.ts.liquid`, `module/starter/logic.test.ts.liquid` and `module/logic-aggregate.test.ts.liquid` are updated with the new fixture and now import `TeamModuleMeta`; take them from the shipped templates if you want your scaffold in step
|
|
257
|
+
|
|
258
|
+
- **A user interface now receives only the capabilities it declares.** A game, session or waiting room interface's `data` carries exactly the capabilities in its `reads` and its `writes`, and nothing else. An undeclared capability is absent from the payload rather than present and unused, and the same projection applies to every team's context inside `SessionData` and to every past-tick snapshot. Logic is untouched: your module, hooks, validators, tick controller and endpoints still receive the whole context, because their declarations document dependencies rather than bounding what a frame may see. **This is mostly already true for you.** The SDK has always generated each interface's `Context` from its own declarations, so a screen reading an undeclared capability was already outside its own types and TypeScript was already failing the build. What changes is that the runtime now agrees. **To migrate:** if a screen renders a value, make sure the capability is in that bundle's `reads` (or `writes`, which counts, because a screen that sets a value almost always shows it). Where the dev server withholds something, it names the capabilities it withheld in the browser console rather than leaving you with an `undefined`. The reason this became a rule rather than staying an option: while enforcement was optional, every game was one undeclared read away from working on a runtime that delivers everything and breaking on a runtime that does not, and finding out in a live session. It is also the only lever that shrinks a payload without the game changing, which is what a 100-team session needs
|
|
259
|
+
|
|
260
|
+
- **The built layout mirrors the authored one.** A resource a game holds one of now ships at its slot — `dist/engine/logic.js`, `dist/ui/`, `dist/session/`, `dist/waiting_room/`, `dist/tick_controller/`, `dist/guide/` — rather than under a plural directory that repeated what the slot already said (`dist/modules/engine/`). What a package may hold several of keeps `<plural>/<identifier>/`, where the type directory is what stops a hook and a scenario sharing a name from claiming the same folder. The distribution zip mirrors `dist/`, so it follows. A runtime resolves a resource the manifest carries singular at `<identifier>/` and one it carries as an array at `<plural>/<identifier>/`, and needs no table of which is which: version 2 makes the distinction structural. Version 1 manifests keep the version 1 layout, so archived packages stay runnable. Nothing an author writes changes
|
|
261
|
+
- **A game holds one facilitator guide, in the `guide/` slot.** `facilitator-guides/<name>/` becomes `guide/`, the guide takes the slot as its identifier, and the built manifest carries `facilitatorGuide` rather than a `facilitatorGuides` array. Guides were plural only so the host could draw a second tab, which is the affordance participant UIs lost for the same reason; a quick-reference summary is a chapter, not a second guide. Move your guide directory up a level. A package with two guides concatenates them into one, renumbering chapters so the order is explicit — chapters are what express that. The per-locale `name` is the guide's title rather than a tab label, and the facilitator page renders every chapter in filename order under it
|
|
262
|
+
- **Ten `Game*Reference` types are gone from `@businesssimulations/seriousgaming-sdk`.** `GameModuleReference`, `GameUserInterfaceReference`, `GameSessionUserInterfaceReference`, `GameWaitingRoomUserInterfaceReference`, `GameTickControllerReference`, `GameScenarioReference`, `GameFacilitatorGuideReference`, `GameHookReference`, `GameValidatorReference` and `GameEndpointReference` described the reference lists a game no longer carries. Nothing emitted them and no other type referenced them; they were exported shapes for a manifest that no longer exists. Nothing replaces them — read the collections on the manifest instead
|
|
263
|
+
- **A game's `waitingRoom` no longer accepts an `identifier`.** It carries the waiting room's settings (`configuration`, `openOnStart`); which interface fills the room is the package's waiting room user interface. The loader has rejected the field since the slot move, so this only brings the schema into line with the error it already raised
|
|
264
|
+
- **The `game` command namespace is gone, and its commands take no identifier.** A package is a game, so the argument had exactly one possible value — the package identifier with `.game` appended, which nothing authors. Rename as follows: `sdk game init <id>` → `sdk init`, `sdk game tick <id>` → `sdk tick`, `sdk game view <id>` → `sdk view`, `sdk game configure <id> [config]` → `sdk configure [config]`, `sdk game update <id> <patch>` → `sdk patch <patch>`, and `sdk game blob upload <id> <file>` → `sdk blob upload <file>`. The context-mutating verb is `patch` rather than `update` because `sdk update` already names the resource namespace (`sdk update module`), and because the patch was always its only argument
|
|
265
|
+
- **`sdk tick` initialises a game that has never been run.** It used to fail with "State file not found. Run init first"; with one game and one state file there is nothing for it to be uncertain about. `--teams`, `--scenario` and `--locale` apply to that first run. `sdk init` stays, for re-initialising an existing session with a different team count, scenario or locale
|
|
266
|
+
- **Dev state moved up one directory.** `.seriousgaming-sdk/games/<safe-game-id>/` held `state.json`, `configuration.json` and `blobs/`; they now sit directly in `.seriousgaming-sdk/`. The extra level existed to separate several games' state. That directory is gitignored dev state, so migrating costs a re-init rather than a file move — run `sdk tick` and the session rebuilds
|
|
267
|
+
- **`GameRunner` and `MultiTeamGameRunner` no longer accept `gameId`.** The option was already ignored (it selected among games a package cannot have); passing it is now a type error. Delete the property from your `create()` call — nothing replaces it. The scaffolded engine test template drops it too
|
|
268
|
+
- **The scaffolded `playwright.config.ts` no longer passes `--game`.** `sdk dev` lost that flag when a package became one game, so a scaffold generated since then started its dev server with an unknown option and failed. If your package still has the old line, drop `--game ${GAME}` and the `GAME` constant above it
|
|
269
|
+
- **`sdk build` emits `manifestVersion: 2`.** Version 2 carries one of each thing a game holds one of rather than a single-element array: `game`, `module`, `userInterface`, `sessionUserInterface`, `waitingRoomUserInterface` and `tickController` replace `games`, `modules`, `userInterfaces` and the rest. A slot the package does not fill is absent rather than an empty array. Nothing an author writes changes; this is the shape a runtime reads. Version 1 stays permanently readable: `manifestVersion` is absent-means-1, and published artefacts are archived
|
|
270
|
+
- **A game no longer lists what it wires.** `modules`, `userInterfaces`, `hooks`, `validators`, `endpoints`, `scenarios`, `facilitatorGuides`, `sessionUserInterface`, `tickController`, `hookOrder` and `validatorOrder` are gone from the emitted `GameDefinition`. Each restated the collection beside it — the build synthesised the list from that collection, then emitted both. A game wires whatever its package declares, so a runtime reads the collections directly; hooks, validators and guides run in the manifest's order. `examples/factory`'s manifest lost nine keys from its game definition and 9% of its size
|
|
271
|
+
- **The emitted configuration-surface map is `configurationSurfaces`.** It was emitted as `configurationUserInterfaces`, which collided with the collection of the same name. The authored name and the emitted name now match
|
|
272
|
+
- **A UI may call any endpoint its package declares.** The error for one it cannot now reads "which this package does not declare" rather than "not listed in game.endpoints[]", and the unused-endpoint warning names the package rather than the game. There is no `game.endpoints[]` to be listed in
|
|
273
|
+
- **A game holds one of five resource kinds, so each lives in a fixed slot rather than a named directory.** `engine/` (the module), `ui/` (the participant interface), `session/`, `waiting_room/` and `tick_controller/`. The slot is the resource's identifier, so a slot resource is never renamed and its bundle names nothing. Its logic or UI files sit directly inside the slot, not in an inner `logic/` or `ui/`: `engine/logic.ts` and `ui/src/App.tsx`, not `engine/logic/logic.ts` or `ui/ui/src/App.tsx`. The name a singular resource used to carry restated its type — every one in the SDK's own 24 packages was `engine`, `market`, `counter`, `dashboard` or `decisions`. **To migrate:** move `modules/<name>/` to `engine/`, `user-interfaces/<name>/` to `ui/`, `session-user-interfaces/<name>/` to `session/`, `waiting-room-user-interfaces/<name>/` to `waiting_room/` and `tick-controllers/<name>/` to `tick_controller/`; lift each one's `logic/` or `ui/` contents up into the slot and set that bundle's `logic.directory` / `ui.directory` to `"."`; then fix the relative imports that got one level shallower — `../types` becomes `./types` in logic, `../../types` becomes `../types` in a UI's `src/`. A package that still lays any of the five out under its plural directory fails to load, naming the move it expects, rather than silently loading with no module
|
|
274
|
+
- **Configuration is keyed by slot: the bucket level is gone.** `"configuration": { "engine": { ... } }` replaces `"configuration": { "modules": { "<name>": { ... } } }`, in the manifest, in every scenario bundle, and in the centralized `configuration.json` the dev server writes (where the tick controller's bucket becomes the `tick_controller` slot). The maps could only ever hold one key each. A configuration UI's generated `Config` follows: `config.engine`, not `config.modules.<name>`. **To migrate:** replace each bucket-and-name pair with the slot — `modules` becomes `engine`, `userInterfaces` becomes `ui`, `sessionUserInterfaces` becomes `session` — and delete the centralized `configuration.json`, which regenerates. Configuration naming a slot the package does not fill is now a build error, where a value in the wrong bucket used to be accepted and discarded
|
|
275
|
+
- **A configuration UI declares the slots it configures.** `"configures": ["engine", "ui"]` replaces the `modules` and `userInterfaces` identifier lists, which could only ever name the one module and the one UI. **To migrate:** replace both arrays with `configures`, naming `engine` where `modules` was non-empty and `ui` where `userInterfaces` was
|
|
276
|
+
- **Engine tests live under `testing/engine/`.** `engine/` is the module's slot now, so a package cannot also use it for its engine suite. This is where the scaffold has always put them, so only a package that diverged needs to move; point `vitest.engine.config.ts` at `testing/engine/**/*.engine.test.ts` and exclude `testing/**` from the unit config
|
|
277
|
+
- **A resource is named by its directory, so its identifier is a short name.** `capabilities/price/`, not `capabilities/com.example.pkg.price/`, and `context.price` rather than `context['com.example.pkg.price']`. The bundle no longer carries an `identifier` at all: the directory is the name, so authoring it was a second copy that could disagree with the path. Only the package keeps its dotted reverse-DNS identifier, and its game is still that identifier with `.game` appended. To migrate, rename each resource directory to its last segment, delete `identifier` from every bundle, and replace the long form wherever it is referenced — `reads`, `writes`, `endpoints`, the manifest's `configuration` keys, and every context read. A bundle still authoring an `identifier`, or a reference still written in the long form, fails with a pointer at the short name. Short names are valid JavaScript identifiers, so generated `Context` types are now read with a dot. `update <type>` renames a resource when the definition you pass names a different one; renaming the package no longer touches any resource, because none carries its prefix
|
|
278
|
+
- **A package declares whatever is on disk: the resource lists are gone.** `modules`, `capabilities`, `userInterfaces`, `configurationUserInterfaces`, `sessionUserInterfaces`, `waitingRoomUserInterfaces`, `scenarios`, `facilitatorGuides`, `tickControllers`, `hooks`, `validators` and `endpoints` are all removed from `seriousgaming.sdk.json`. A resource exists because `<plural>/<identifier>/<singular>.sdk.json` exists. To migrate, delete the lists; the directories they named are the declaration. A manifest still carrying any of them fails to load, naming the ones it found. Across the SDK's own 24 packages every one of the 116 entries was exactly the path the layout implied, so the lists were a second copy of the filesystem kept in step by hand. `new` and `delete` no longer touch the manifest, and the inline-resource form the schema allowed (unused anywhere) goes with the lists
|
|
279
|
+
- **A resource directory with no bundle in it is now an error.** Previously an entry pointing at nothing was skipped or reported obliquely; now `modules/ghost/` with no `module.sdk.json` fails the load naming the path it expected. Half a resource is a mistake worth surfacing
|
|
280
|
+
- **Hooks, validators and facilitator guides run and render in directory-name order.** They previously followed manifest-list order. Neither `hookOrder` nor `validatorOrder` had been authorable for some time, so the old order was list position rather than anything stated deliberately; the lever is now the directory name. Where order matters, number the directories, as facilitator-guide chapters already do
|
|
281
|
+
- **The manifest is flat: `package` and `game` are gone as containers.** `seriousgaming.sdk.json` now carries the package `identifier` and the game's settings at its top level, beside the resource lists. To migrate, lift `package.definition.identifier` and everything in the `game` block up one level, and delete both wrappers. A manifest still carrying either fails to load with a pointer at the flat shape
|
|
282
|
+
- **`package.definition.name` is gone, and `version` moved to `package.json`.** The name is the game's name in its first supported locale, which is the one that was already displayed; the old field was never read, and the SDK's own example carried a stale value for three releases without anyone noticing. The version lives where npm already keeps it, so there is one copy rather than two kept in sync. `sdk package version` writes `package.json`, and `sdk package publish` requires one. A package with no `package.json` builds and runs, at version `0.0.0`. The built manifest is unchanged: it still carries a full `{identifier, name, version}`, both derived at build time
|
|
283
|
+
- **`update game` is gone; `update package` covers both.** A package is a game, so one command edits one object. `sdk update package '<JSON>'` replaces the settings and `--patch` edits them, with paths starting at the manifest (`/ticks`, `/identifier`). The resource lists are not part of an update and are never replaced, so a full replacement need not restate every path the package declares. `new package` takes the same flat shape
|
|
284
|
+
- **The configuration-surface map is now `configurationSurfaces`.** Flattened, the game's surface map and the package's list of configuration user interfaces would both have been called `configurationUserInterfaces`. The list keeps the name; the map, which says which UI serves which surface, takes the clearer one
|
|
285
|
+
- **The game moved into the package manifest, and `games/` is gone.** A game's settings (its locales, `ticks`, `teams`, `waitingRoom`, `demo`, `customScenarios`, `shell`, the configuration-UI surface map and the `configuration` overrides) are now the `game` block of `seriousgaming.sdk.json`, sitting directly after `package`. The `games` key, the `games/<identifier>/` directory and `game.sdk.json` are all removed, and so is the game's `identifier`: it is the package identifier with `.game` appended, which is what all 17 first-party packages already used, so no published session binding changes. To migrate, move the contents of `games/<identifier>/game.sdk.json`'s `definition` into a `game` block in the manifest, drop its `identifier`, delete the `games` key and the directory. A manifest still carrying `games`, or a `game` block still carrying an `identifier`, fails to load with a pointer at the replacement. The built manifest is unchanged: a runtime still reads a full game definition from `games[0]`
|
|
286
|
+
- **`new game` and `delete game` are gone.** A package is a game, so there is nothing to create and nothing to delete
|
|
287
|
+
- **A game no longer lists the resources it wires.** `modules`, `userInterfaces`, `hooks`, `hookOrder`, `validators`, `validatorOrder`, `endpoints`, `scenarios`, `facilitatorGuides`, `tickController` and `sessionUserInterface` are gone from `game.sdk.json`, and `waitingRoom` keeps only its settings. A game wires whatever its package declares, so those lists repeated what `seriousgaming.sdk.json` already said: about 100 identifier occurrences per package for 23 distinct names, across four files that had to agree. Per-resource configuration overrides move into a `configuration` block on the game, keyed by identifier under `modules` / `userInterfaces` / `sessionUserInterfaces`, which is the shape scenarios already use. Which scenario is the default is now `default: true` on the scenario itself. A game still carrying any removed field fails to load with a pointer at what replaced it. The built manifest is unchanged, so nothing a runtime sees moves
|
|
288
|
+
- **A game now wires exactly one user interface.** The build fails naming the extras. A simulation with several views carries them inside its one surface, which is what `examples/factory` now does: its decisions and results screens are tabs in a single `dashboard` UI. The dev server's UI tab bar goes with the rule, and `shell.uiTabs.hidden` with it, since there is no tab bar left to hide. `shell` now carries only `configuration.playButton.hidden`, and the `Hide Host Shell Elements` recipe is replaced by [Use requestPlay in a configuration UI](recipes/use-request-play.md), which already covered the surviving option
|
|
289
|
+
- **A game now wires exactly one module, and `moduleOrder` is gone.** The build fails naming the extras. A simulation with several steps calls them as functions from its module rather than listing them in the game manifest, so ordering is a line of code the author can read instead of an array that has to agree with two other files. Multi-team is not a reason to split: one module can export both a per-team `Run` and a `RunAggregate`, which is what `examples/coffeeshop` now demonstrates. The `moduleOrder` field on the dev server's context broadcast is also gone; nothing consumed it
|
|
290
|
+
- **`requiresDownstreamWrite` has been removed from capability reads.** It declared that something had to write a capability before the reader ran, and was satisfied by a module later in `moduleOrder`. With one module per game there is no later module, and nothing in the examples, the SDK fixtures or any first-party package ever set it. A read still carrying the field fails the build with a pointer rather than an unhelpful schema error; delete it
|
|
291
|
+
- **A package now holds exactly one game.** A `seriousgaming.sdk.json` declaring a second game fails to load, naming both, and `sdk new game` refuses in a package that already has one rather than leaving it half-created. `sdk dev` loses its `-g, --game` flag, since there is nothing to choose between, and `GameRunner`/`MultiTeamGameRunner` no longer require `gameId` (still accepted, so existing engine tests compile unchanged). Split extra games into their own packages: they were already independent at runtime, since a session binds to one game
|
|
292
|
+
- **The example packages have been reorganised.** `examples/typescript` is now `examples/factory`, named for what it simulates rather than the language it is written in, and its identifiers move from `com.businesssimulations.typescript.*` to `com.businesssimulations.factory.*`. The three games that differed only by tick controller are now `examples/timed-rounds`, `examples/target-driven`, and `examples/scheduled-sessions`, each a small package demonstrating one style of automatic timing rather than another copy of the whole simulation
|
|
293
|
+
- **npm-based resource reuse between SDK packages has been removed.** A package can no longer pull another SDK package's capabilities, modules, user interfaces or tick controllers in through a dependency; `sdkResources` in `package.json` is no longer read, and the scaffold stops emitting it. Nothing changes for the packages that merely carry the field, and nothing changes for a runtime: `sdk build` already inlined installed resources, so the built `dist/` was self-contained either way. Share code between packages with an ordinary npm library instead, exporting the capability definition so each package generates its own bundle from one source of truth. Publishing a package to the platform with `sdk package publish` is unaffected
|
|
294
|
+
- **Shell user interfaces have been removed.** The `shell-user-interface` resource type is gone, along with `game.shell.header`, `game.shell.footer`, the `shellUserInterfaces` manifest key, the `new` / `update` / `delete shell-user-interface` commands, and the `ShellUserInterfaceWrapper` and `useShellUserInterface*` exports. The `switchUI` and `setUIList` messages and the `renderShellUserInterface` test helper go with them. If a game wires one in, `sdk build` now fails naming the field: delete `game.shell.header` / `game.shell.footer` and the `shell-user-interfaces/` directory, and render that header or footer inside your user interface instead, where it can also carry its own navigation. The rest of the `shell` block is untouched: `shell.uiTabs.hidden` and `shell.configuration.playButton.hidden` behave exactly as before
|
|
295
|
+
|
|
296
|
+
### Added
|
|
297
|
+
|
|
298
|
+
- **The state projection is a published type.** `StateProjection` and the shapes it is made of now come from `@businesssimulations/seriousgaming-sdk/projection`, and the two `./e2e` helpers are typed against it: `getState()` answers a `StateProjection` rather than `unknown`, and `runCommand()` a `MutationResult`. Renaming a call to `getState()` used to compile and then fail at runtime, because the return type was loose enough to cast anything onto; it is now a type error. A suite that declared its own copy of the projection's shape can delete it
|
|
299
|
+
|
|
300
|
+
- **`new package` now scaffolds a complete, playable game.** It used to produce a counter: one capability with `Delta` and `Total`, an engine that added one to the other, and two screens. What every author then had to invent for themselves was the part that is expensive to retrofit: state shaped so two people on one team cannot overwrite each other, a validator on the thing they share, awaited writes so a refusal reaches the person who made it, and a facilitator surface that answers a real question. The scaffold is now a lemonade stand: ten days, and each morning the team decides how many lemonades, iced teas and cookies to make. Whatever they make they have paid for, whatever is left is poured away, and anyone who wanted something they had run out of simply leaves. It ships with an engine, a participant surface, a session surface, a waiting room, a validator, a `participantJoin` hook, a facilitator guide, and tests at every level. `--empty` still gives you the package files and nothing else
|
|
301
|
+
|
|
302
|
+
- **The starter game and `examples/lemonade` are the same directory.** `new package` copies the example out rather than rendering templates of its own, so there is one copy of the game and nothing for it to drift from. What a consumer is handed is what the SDK's own typecheck, lint, unit, whole-game and browser suites run against. The published package carries it under `dist/templates/starter-game/`
|
|
303
|
+
|
|
304
|
+
- **`renderUserInterface` takes the validator rejections the host returned**, through `validationRejections` and `clearRejections`. A surface that renders a refusal previously had to wire every context provider by hand to test it, because the helper supplied no validation context and the hook throws without one
|
|
305
|
+
|
|
306
|
+
- **Validators, hooks and endpoints are bounded.** Only the tick had an execution bound. A hook that never returned blocked every later signal for its team for the rest of the session.
|
|
307
|
+
- **A throwing validator is now one more rejection**, not an aborted chain, as the specification always said it should be.
|
|
308
|
+
|
|
309
|
+
- **The dev server owns the session, and every CLI command is a client of it.** `sdk tick` now advances the session your browser is watching, and a tick taken in the browser is what the next `sdk view` reports. Before this the CLI ran its own executor over `.seriousgaming-sdk/state.json`, so the two were separate sessions that a file was trying to keep in step; a decision made in one appeared in the other a moment later, or not at all. With no server running, a session command starts one in the background and carries on, so `init` then `tick` then `view` are three views of one session. `--no-server` runs a command in its own process instead — one command, one session, nothing left behind — which is what CI wants
|
|
310
|
+
|
|
311
|
+
- **New commands for everything the console can do.** `sdk undo`, `sdk reset`, `sdk team add|remove|rename`, `sdk participant add|rename|move`, `sdk waiting-room open|close`, `sdk scenario <id>`, `sdk locale <code>` and `sdk fault <kind>`. Each sends the same command the console sends over its socket, so there is one code path and nothing for the two to drift apart on, and anything a facilitator can do from a browser an agent can do from a terminal
|
|
312
|
+
|
|
313
|
+
- **`sdk fault <kind>` arms a failure the dev server cannot otherwise produce.** It serialises every write against every tick, so it structurally never emits `conflict` — the outcome a production runtime emits most, and the retry path your UI was told to write. `sdk fault conflict` makes that path reachable in development, deliberately and reproducibly. Also `denied`, `llm-failure`, `hook-throw`, `endpoint-timeout` and `tick-failure`
|
|
314
|
+
|
|
315
|
+
- **`sdk view --full` prints the whole session as one document**: the session, every team's context and roster, the shared store, the resolved configuration, the timer and any armed faults. The dev server serves the same shape at `GET /api/state` and the console renders from it, so what you read is what the browser is showing
|
|
316
|
+
|
|
317
|
+
- **`sdk dev status` and `sdk dev stop`**, for the background session a command may have started
|
|
318
|
+
|
|
319
|
+
- **A new mutation outcome, `superseded`, for an operation the state has already moved past.** A facilitator pressing the tick button twice used to get `conflict`, which invites a retry that can only be refused again, or `denied`, which reads as a permissions error. Neither is true: nothing was forbidden, and the caller got the state they wanted. `superseded` is terminal, carries the tick that actually committed, and is the one failure kind a surface can reasonably say nothing about
|
|
320
|
+
|
|
321
|
+
- **Switching scenario restarts the session, and asks first.** A scenario is a different set of starting conditions, and its overrides reach the game through `Init` — a session several ticks in has passed the only moment they mean anything, so switching one used to change the label and little else. It now returns every team to tick 0 and a game with a configuration step back to it, and the dev drawer confirms before sending it
|
|
322
|
+
|
|
323
|
+
- **Scenarios can be managed from the dev drawer**: activate, add, edit and delete. **Add scenario** names the new one and asks where its values start from — scratch, or a copy of one you already have — then writes `scenarios/<identifier>/` and opens the scenario builder on it. **Edit** opens the builder on an existing one. `DELETE /api/scenarios/:id` removes the directory and its assets, and refuses the active one, so a scenario added by accident is recoverable without going to the filesystem
|
|
324
|
+
|
|
325
|
+
- **The scenario builder is opened on a scenario, and has no entry of its own.** It was a surface you could navigate to like any other, which meant a configuration screen with nowhere to save: you edited values, and where they ended up was a separate question you answered afterwards. It now opens from Add or Edit at `/scenario-builder?scenario=<id>`, in a new tab, with the session put on the scenario it is editing — and a **Save** control that writes the configuration in force back into that scenario's bundle
|
|
326
|
+
|
|
327
|
+
- **The configuration surface closes once the session starts, and reopens deliberately.** Configuration is resolved when a session initialises, so a configuration screen reachable mid-session is one that appears to work and changes nothing. `/configure` now explains that and offers Reconfigure, which returns the session to its configuration step _and_ every team to tick 0 — the only way the new values reach `Init`. The same control is in the dev drawer, and `sdk reconfigure` does it from a terminal. While a session is at its configuration step, the drawer offers no player surfaces, because there are none to open yet
|
|
328
|
+
|
|
329
|
+
- **A join route, `/team/<n>/join`.** The one URL in the console that resolves rather than names: it is what a facilitator hands out, so it cannot know who will open it. A team with one participant opens straight into their surface
|
|
330
|
+
|
|
331
|
+
- **The dev drawer disables what cannot work**: Tick when every team is complete, Undo when no team has a round to step back to. It offered both and let the session refuse them
|
|
332
|
+
|
|
333
|
+
- **A pass over the dev panel.** One button style throughout — filled and outlined buttons in five colours made it look like a product with opinions about what matters, and it has none; every control there is equally a thing you might press. State is carried by chips and a dot rather than by a button changing shape as you press it, the panel has a close button for screens with no backtick key, and it works on a phone: the tab strip scrolls, the panel takes 70% of the height rather than a fixed 380px, controls are a 36px touch target, and the scenario form goes full screen rather than into a 280px box
|
|
334
|
+
|
|
335
|
+
- **The dev drawer's handle can sit in any corner**, and is a 56px floating button with a status dot rather than a small icon carrying a tick counter — the tick moved into the panel, where it is read rather than glanced at, and the handle is opaque so it does not vanish over a game that paints its own background. The corner is remembered per browser. Every link on the Surfaces tab opens in a new tab, so opening a team's screen no longer costs you the page you opened it from
|
|
336
|
+
|
|
337
|
+
- **A write says which round it landed in.** `setData` now carries the tick the participant was looking at, and resolves with the tick the write committed into. `useMutationReporter` exposes the comparison as `late`. Before this, a decision submitted as the round was being processed silently became the _next_ round's decision: correct behaviour under both serialisation strategies, and invisible to everyone. Nothing is refused and no submission window opens — the only change is that both ends can now name the round, so a surface can tell the participant their price counts for round five rather than round four
|
|
338
|
+
|
|
339
|
+
- **A context update can delete a key.** `setData` takes a second argument naming paths to remove: `setData({}, { deletes: [`pinboard.Notes.${id}`] })`. Deletions apply after the patch merges, so one write can replace an entry, and a path that resolves to nothing is a no-op, which is what two participants clearing the same entry need. Pass a path as segments (`[['pinboard', 'Notes', id]]`) where a key of your own could contain a dot. **This replaces the `null` tombstone convention**, which was the only way to clear an entry and cost more than it looked: the key stayed in the context for the life of the session, rode every broadcast, was multiplied by team count on the facilitator surface, sat in every per-tick history snapshot, and had to be understood as 'absent' by the UI, the engine and any archive projection alike
|
|
340
|
+
|
|
341
|
+
- **A keyed collection's item type has a name.** A capability that models a collaborative collection describes its item under `additionalProperties`, and giving that item schema a `title` now produces a declared, exported type beside `Context`, so surfaces and engine logic can say `(note: PinboardNote)` instead of indexing back into the map that holds it. A title previously named the type in the generated file without declaring it anywhere, which produced a `types.ts` that did not compile and said nothing about why. Capabilities without a title are unchanged
|
|
342
|
+
|
|
343
|
+
- **`useMutationReporter` for getting a refused write in front of the participant.** `setData` and the other mutations already returned a promise, and ignoring it was documented as safe. It is not: the local view merges the change the moment it is sent, so a write the host refuses leaves the participant looking at a decision the game never received. The hook awaits the write, tracks whether one is in flight, and hands you the failure to render; the copy stays yours, because it is game language and needs localising. It deliberately does not retry, because a runtime using optimistic concurrency re-applies a losing write itself before it ever reports one, so a `conflict` arriving at a surface is one the host has already given up on
|
|
344
|
+
|
|
345
|
+
- **`mergingSetData` for test harnesses that hold state.** The SDK defines how a `setData` payload merges into context (objects recursively, arrays and scalars whole, `null` as a tombstone) but shipped nothing that merged that way in a test. A hand-rolled `{ ...previous, ...patch }` mock replaces a whole capability with whatever the patch names, so it disagrees with the host over exactly the minimal writes a concurrency-safe surface sends: tests fail on writes that are correct in production, and can pass on writes that are not. Import it from `@businesssimulations/seriousgaming-sdk/ui/testing` and wrap your state setter with it
|
|
346
|
+
|
|
347
|
+
- **`new capability --scope private` is accepted.** The flag offered `team`, `team-readable` and `shared` while the schema and a JSON definition both took `private` as well, so the one scope you would reach for by flag rather than by hand was the one it refused. The list is now checked against the schema's enum in both directions, so a scope added to one and not the other is a compile error
|
|
348
|
+
|
|
349
|
+
- **A generated `CrossTeamContext` types what a surface may read of other teams.** It sits beside `Context` in a player surface's `types.ts` and carries only the capabilities the bundle declared with `crossTeam: true`, where `Context` carries everything it reads or writes. Pass it to `useCrossTeamData<CrossTeamContext>()` and to `getPastState<CrossTeamContext>({ teams: 'all' })`. **Why this is worth having:** a surface that read another team's capability without declaring `crossTeam` compiled and ran, and the host answered with that surface's own team and nobody else — indistinguishable from an early session where nobody has written yet, so it survived every two-team rehearsal and showed up as 'the leaderboard only ever lists us' in a real session. That read is now a compile error, in the same place the SDK already catches an undeclared `data.x`. A surface that declared no `crossTeam` reads gets `Record<string, never>`, so asking for every team reads as useless at compile time rather than returning entries with empty contexts
|
|
350
|
+
|
|
351
|
+
- **Read committed history in one call, however wide.** `getPastState({ fromTick, toTick, teams, capabilityIds })` answers with one entry per (tick, team) it reaches: a single board, one team's run of rounds, every team at one tick, or a whole results table, depending on the query. `capabilityIds` narrows each returned context to the fields you actually plot, and `teams: 'all'` fills a facilitator table that would otherwise be a call per team per round. **Why this is worth having:** a sixty-round chart through the single-tick read is sixty round trips returning sixty whole boards, so authors kept a growing log in context instead, and then paid for that log on every update to every screen for the rest of the session. Keep the current round in context and read history from the host. Both ends of the range are inclusive and `toTick` is clamped to the last completed round, so `{ fromTick: 0, toTick: currentTick }` means 'everything reviewable' with no arithmetic. Rounds that are not reviewable (undone, reset, at or above the head) are left out rather than returned as holes, so an empty array means nothing in the range is reviewable. A host may return fewer rounds than you asked for, so treat a short result as normal; the dev server caps one read at 500. Not gated by `uses`, and narrowed to what your surface declares. `examples/factory` shows both ends of it: the dashboard's review card draws a profit trend from one call, and the facilitator surface has a results table across every team
|
|
352
|
+
|
|
353
|
+
- **A play surface can say a round is being processed.** `useUserInterfaceData()` gains `isRoundProcessing`, true while the host is running a tick and false again once it commits. In a shared-state game the tick is one transaction across every team, so at a large cohort it takes long enough to notice, and the participant did not press the button: without this, the pause reads as their screen having broken. `examples/factory`'s dashboard shows it, replacing the countdown with 'Processing round…' while the round runs. **Writes are unaffected.** This is a signal, not a submission window: `setData` during a round behaves exactly as before, queued and applied after the tick, resolving when it commits. Disable your own submit control on the flag if you would rather participants waited, or leave it alone; nothing is lost either way. Session surfaces do not get the flag and do not need it, because `requestTick` is already awaitable, so a facilitator screen can render 'processing' between the call and its promise resolving. We considered actually closing writes for the duration of a round, which is arguably the more honest model for a turn-based game, and did not: measurement showed a hundred teams work without it, so a window would have added a new contract about refused writes to solve a problem that turned out to be the unexplained pause
|
|
354
|
+
|
|
355
|
+
- **`Init`, `Run` and your tick controller now know which team they are running for, and get a seed.** `meta` gains `teamId` and `index` (the team whose context is in `context`, with the same stable index `RunAggregate` already saw) and `seed`, a session-wide random seed. Two new helpers come with it: `deriveSeed(...parts)` turns the seed plus anything identifying what you are seeding into a stable seed, and `seededRandom(seed)` turns that into a generator you call like `Math.random()`. They ship in the SDK because logic runs against the ECMAScript intrinsics alone: there is no `crypto` to hash with, so a seed on its own would have left everyone writing a hash by hand. **Why derive rather than draw:** a module function may run more than once for one logical tick and only the last run is durable, so a live `Math.random()` gives a value you cannot be sure survived. And in a shared-state game it is worse, because `Init` runs once and its result is cloned to every team, so a value drawn there is the same for all of them. Per-team variation belongs in `InitAggregate`, which is the only initialisation step that sees every team; `examples/coffeeshop` now opens each shop at its own price that way, and the same session always opens the same shops. The dev server draws a seed per session and keeps it in the state file, so a restart comes back to the same world
|
|
356
|
+
|
|
357
|
+
- **A host can send a facilitator screen one team's state instead of every team's.** `SessionData` carries every team's context, so on a large cohort a session screen received all of it each time any one participant typed. Hosts may now send `updateTeamState` for the team that actually changed, and the SDK wrapper merges it into the same `SessionData` your surface already reads: nothing in your session UI changes, and you pick this up by taking the SDK release. A full `setSessionData` is still sent on first connection, whenever a team is added or removed, and whenever the wrapper notices it has missed an update and asks for a fresh one, so a surface repairs itself rather than sitting on stale state. `TeamState` gains an optional `revision` (the wrapper's sequence number: nothing on screen needs it, and hand-written test fixtures need not supply it). The dev server sends deltas too, so this path is exercised locally rather than only in production. The handshake's `protocolVersion` is now `2`, which is what tells a host whether the peer understands the new message; there is deliberately no capability-negotiation list, because no package predates the feature and a negotiation nobody can be on the wrong side of is cost without benefit
|
|
358
|
+
|
|
359
|
+
- **Capabilities can be `scope: 'private'`, for state no screen should see.** A private capability reaches your module, hooks, validators, tick controller and endpoints exactly as a team-scoped one does, and reaches no user interface at all: it is left out of `data`, out of every team's context in `SessionData`, out of cross-team data, and out of past-tick snapshots. An interface that declares one in `reads` or `writes` fails the build, naming the interface and the capability. Use it for the state a simulation accumulates purely to get from one tick to the next: a serialised engine, a smoothing accumulator, a seed, a ledger the next round consumes. Team scope could not express 'nothing outside the engine reads this', so the only enforcement was to hope; and meanwhile all of it was delivered to every screen on every update. `examples/factory` marks its new `trend` capability private, which carries last round's profit so the report can describe the direction of travel, and the dashboard cannot read it. Widening `private` to `team` later is a one-word edit; narrowing `team` to `private` means first finding out which screens had come to rely on it
|
|
360
|
+
|
|
361
|
+
- **A game and a scenario can now configure the tick controller.** `configuration.tick_controller` is accepted alongside `engine`, `ui` and `session` in a manifest and in a scenario, so tick timing can differ per scenario without touching the centralized layer. The resolver already read the key and the centralized layer already carried it; only the schema forbade authoring it, so the branch could never fire. A configuration UI may likewise declare `configures: ['tick_controller']`
|
|
362
|
+
|
|
363
|
+
### Changed
|
|
364
|
+
|
|
365
|
+
- **A configuration property that declares a `default` is generated as present.** The generated `Config` kept a property optional unless the schema also listed it in `required`, so author code carried `config.x ?? fallback` guards for a value that cannot be absent, and removing a guard failed to typecheck until `required` was updated too. Every property now declares a default, so every one is always there and the type says so. The configuration UI generator already did this; the two disagreed about the same schema
|
|
366
|
+
- **`sdk dev` prints session errors to the terminal.** A refused tick, a throwing hook or a failing tick controller reached the WebSocket and nothing else, so the only sign was a badge on the console's dev drawer: the clock did not move, the browser console was clean, and the message naming the cause sat behind a keystroke nobody had a reason to press. An undeclared write cost a reported hour of bisection for a one-line fix, and the message that would have ended it in a minute already existed
|
|
367
|
+
- **An incomplete `default` now explains itself.** `default: {}` on an object with required fields is refused, which is the natural first attempt for a property the active scenario always supplies. The message now says why — a default is used whole when nothing overrides it, so it has to be a complete value — and points at widening the type and defaulting to `null`
|
|
368
|
+
|
|
369
|
+
- **The UI test helpers accept a generated `Context`.** `renderUserInterface`'s `data` and `createMockTeamState`'s `context` were typed `Record<string, unknown>`, which a generated `Context` does not satisfy: it is an interface, and an interface has no implicit index signature. Every consumer hit it on their first surface test and wired the providers by hand instead.
|
|
370
|
+
|
|
371
|
+
- **The dev console is the game's own session surface, with a drawer behind it.** The old dev UI put a home page, a session page, a play page, a configure page and a dev-tools overlay in front of your game, all of which existed only in development. The console mounts what the platform mounts, and everything developer-only is behind one drawer on the backtick key: the session, the scenario, the surfaces, the state projection, armed faults, and errors. Every surface is addressable — `/team/2/player/1` is the first participant of the second team, with no lookup and no redirect — and `?bare` removes the chrome entirely, so a browser test about your game cannot fail because the console changed
|
|
372
|
+
|
|
373
|
+
- **`@businesssimulations/seriousgaming-sdk/e2e` matches the new console.** It now exports URL builders (`playerUrl`, `observeUrl`, `waitingRoomUrl`, `consoleUrl`, `bare`), a much smaller selector set, and three helpers: `resetSession()`, `runCommand(command)` and `getState()`. **To migrate:** `reconfigureDevServer()` becomes `resetSession()`, `getGameContext()` becomes `getState()` (which returns the whole projection, so read `state.teams[0].context`), `playUrl(n)` becomes `playerUrl(n)`, `participantUrl(n, id)` becomes `playerUrl(n, position)`, and `teamSelectors` / `participantSelectors` are gone — drive the session with `runCommand` instead of clicking the console. The scaffolded `testing/e2e/game.spec.ts` shows the new shape
|
|
374
|
+
|
|
375
|
+
- **`isRoundProcessing` is documented against both runtimes, not one.** Its doc said a mid-tick write "is queued and applied after it… nothing is lost if you do not" disable your submit control. That is one of the two serialisation strategies the specification permits, and the one the dev server implements. It now says what is true of both, and names the thing that is actually at stake: a write sent mid-tick is not dropped, but it belongs to the round after the one the participant was looking at
|
|
376
|
+
|
|
377
|
+
- **The composition simplification upgrade guide covers three things it left you to work out.** Migrating nineteen games found them: the package identifier is often also a `localStorage` namespace or an asset path, where a blanket rename is silent data loss rather than a refactor (4b, with the four searches that find every occurrence); a shared-state game whose step order interleaves per-team and aggregate work cannot express that as `Run` then `RunAggregate`, and needs the trailing step called from inside `RunAggregate` (4d, with a worked example); and a configuration UI's `Config` is no longer partial, which changes how those screens both read and write (4g)
|
|
378
|
+
|
|
379
|
+
- The dev server serves the facilitator guide from `/api/guide` rather than `/api/facilitator-guides/<id>`. A package holds one guide, in the `guide/` slot, so the identifier segment could only ever hold one value. Nothing outside the dev server's own UI calls this path and it is not part of the runtime contract, so this is visible only if you were scripting against the dev server directly
|
|
380
|
+
|
|
381
|
+
- **Every example the CLI prints comes from one source.** The `--help` text was already wired to `satisfies`-checked literals; the `Expected format` block a `new` or `update` prints when it cannot parse its input, and the expected shape a schema failure prints, were not. Twenty stale copies had accumulated between them — thirteen showing the dotted identifiers the CLI has rejected for two releases, one listing resources in the manifest, several naming a `locales` block on a definition that has none. All three families now render from the same literal, which the run-the-docs check already executes, so an example that stops working fails a test rather than reaching an author at the moment they are already stuck
|
|
382
|
+
|
|
383
|
+
- **The scaffolded `.gitignore` lists `.env`.** The documentation said it did. A package created before this release should add the line, or take the file from the shipped template: `sdk configure` and the platform commands both read `.env`, so a key put there was one `git add .` from being committed
|
|
384
|
+
|
|
385
|
+
- **`isDeliverableToSurfaces` no longer throws on the removed `shared` boolean.** It answers whether a capability's scope lets it reach a UI surface, and it is called on every push while a payload is projected. The migration message for the removed flag belongs to build validation, which reports it alongside the rest of a manifest's problems; raised from a per-push path there was nothing positioned to turn it back into a report. The answer is unchanged for every manifest that builds: the boolean only ever distinguished shared from per-team, both of which reach a surface, and an explicit `scope: 'private'` still wins
|
|
386
|
+
|
|
387
|
+
- **Your tick controller now re-runs when a participant writes something it reads.** It used to run only after a tick, so 'tick as soon as every team has submitted' could not be expressed: the controller never saw a submission until the tick that followed it. Now a context write that changes any capability in the controller's `reads` re-runs it, and the result is applied as timing exactly as it would be after a tick (no modules run, no snapshot is taken, no counter moves). A controller that declares **no** reads re-runs on **every** write, which is the honest reading of no declaration and keeps a wall-clock controller working unchanged. The gate is the point: ungated this would be one execution of your logic per keystroke per participant, and a controller that reads only a round's outputs cannot be affected by a write to that round's decisions. One thing to know if you schedule from the wall clock **and** declare reads: you will be called only when those reads change, so a deadline you would have recomputed on an unrelated write is not recomputed. That is almost always fine, because the host schedules from your last answer and fires an immediately-due tick if the target has passed; if you really want every write, declare no reads. `examples/target-driven` shows it working: its dashboard can claim points, and a claim that reaches the target ends the game there and then
|
|
388
|
+
|
|
389
|
+
- **The specification now says what cohort size it is written for, and which messages get bigger as a session grows.** A new section marks the four messages whose payload grows with team count and the one that grows with participant count, explains why the cross-team and facilitator paths multiply rather than add, and names what a runtime and an author can each do about it. The session-data rule is restated as bounded staleness rather than one push per change, so a runtime may coalesce a burst of participant writes into a single delivery, which the eventually-consistent model already allowed but nobody had said out loud. The capabilities section gains guidance on choosing between `team-readable` and `shared`: a `crossTeam` read costs one entry per team delivered to every participant on every write, where a shared aggregate is one value whatever the team count, so a screen showing a ranking, a market price or an average should compute it in `RunAggregate` instead. Nothing an author writes changes and no behaviour changes; if your cross-team screen renders a summary rather than each team's own value, the new [Choosing between team-readable and shared](guides/multi-team-games.md#choosing-between-team-readable-and-shared) section is worth reading before your next large session
|
|
390
|
+
|
|
391
|
+
- **The session configuration layer now holds only what you changed.** `.seriousgaming-sdk/configuration.json` used to be a resolved snapshot of every layer, written at `init` whether or not anyone had configured anything. It is now empty until something writes to it, and `sdk configure` stores just the keys you supplied. The resolved values are still what logic and UIs receive, and `sdk view` still shows them. **To migrate:** nothing, unless you read that file from a script; a file written by an older version still loads, and the next `init` replaces it
|
|
392
|
+
|
|
393
|
+
- **A scaffolded package's Playwright config runs its browser tests one at a time.** The tests in `testing/e2e/` share one dev server and therefore one session, so a tick in one test was visible to another running beside it and each test's starting state depended on what its neighbours happened to be doing. New packages get `workers: 1` and a web-server timeout that allows for surfaces being compiled on the first run. **To migrate:** add `workers: 1` to an existing `playwright.config.ts` if your browser tests are flaky in ways that look like they are fighting each other
|
|
394
|
+
|
|
395
|
+
- **`sdk view` reports each team's roster.** It printed contexts and ticks but no participants, so the one read a facilitator or an agent reaches for could say where a session was and not who was in it — the roster was reachable only through `sdk view --full`. Every team now carries `participants`, with `connected` alongside the name, and a single-team session reports one too
|
|
396
|
+
|
|
397
|
+
### Fixed
|
|
398
|
+
|
|
399
|
+
- **An unknown or malformed command is refused rather than acknowledged.** `POST /api/command` answered `{"type":"totallyMadeUpCommand"}` with `{ ok: true }` and did nothing, and a real command carrying a misspelled field was accepted the same way — `{"type":"addParticipant","team":1}`, where the field is `teamId`, settled as a success and then failed something unrelated several steps later. Commands are now checked against the shape each one declares, and a wrong name or field is a 400 naming it. `runCommand` is the documented way to drive a session from a test, so this is the one place a mistake should be loud
|
|
400
|
+
- **An unmatched `/api/` route answers in JSON.** It reached Express's own handler, which replies in HTML, so a suite still fetching a route the dev server no longer has failed with `SyntaxError: Unexpected token '<'` from `response.json()` rather than reporting the 404 it was
|
|
401
|
+
- **A surface request this host cannot map is answered, not dropped.** Every mutating callback returns a promise that settles on the host's reply, and a message the host did not recognise ended the exchange in silence, so the guest's promise stayed pending for the life of the page and whatever control it was wired to simply hung. A message carrying a request id now always gets an answer, even when the answer is a refusal
|
|
402
|
+
|
|
403
|
+
- **An independent game that opened with one team stopped being independent.** The interaction mode was resolved once, from the team count the session happened to start with, so a game declaring `teams.mode: 'independent'` with `min: 1` was recorded as shared-state and stayed that way when a team was added. A player's `requestTick` then advanced every team instead of their own, and the session reported the wrong mode to every surface. The mode is now read from the declaration alone, and the team count is applied where it always was, when the session is asked what mode it is in
|
|
404
|
+
|
|
405
|
+
- **Surfaces work on a plain-HTTP origin, so a participant screen opens on a phone over the LAN.** Every host↔iframe request is correlated by an id built with `crypto.randomUUID`, which browsers expose only in a secure context — HTTPS or `localhost`. Opening `http://192.168.1.157:3000/team/1/player/1` on a phone therefore threw `crypto.randomUUID is not a function` before the surface painted anything. The id now falls back to `crypto.getRandomValues`, which is not gated, and still produces a version 4 UUID. Testing a participant screen on a real device is the ordinary case this blocked
|
|
406
|
+
|
|
407
|
+
- **A page whose session has gone says so, instead of showing state that is no longer live.** Stopping the dev server left every open console and participant surface exactly as it was: controls that still pressed, a tick counter that had stopped counting, and nothing on screen to say why. Every page now carries a bar naming the lost connection and its retry attempts, `?bare` participant views included
|
|
408
|
+
|
|
409
|
+
- **A command refused from the debug panel reports the refusal.** The panel fired and forgot, so a tick the session declined, an add past the team maximum, or a fault the runtime would not arm all looked identical to success. Each control now waits for the outcome and the panel says what was refused and why
|
|
410
|
+
|
|
411
|
+
- **Starting a second dev server for a package now says so instead of moving quietly.** It found the port taken, stepped to the next one, and reported it in a line that scrolls past — leaving the browser pointed at the old server. `sdk dev` reads the lock file first and names the one already running, with how to open or stop it. When a port genuinely is in use for another reason, the address it settled on is repeated next to the startup line rather than only in the warning
|
|
412
|
+
|
|
413
|
+
- **The console says why it cannot connect, rather than waiting indefinitely.** 'Connecting to the session…' was all it ever said, and on a healthy server the socket arrives with the page — so a page that loads and then cannot connect is talking to a _different_ process, which it now explains, along with the port its server may have moved to and the commands to check
|
|
414
|
+
|
|
415
|
+
- **A session-wide undo undoes every independent team.** Per-team ticks write to per-team histories, and the session-wide undo read a session history those never touch — so it reported 'nothing to undo' over teams that plainly had rounds to undo. It is now each team's undo, exactly as a cohort tick is each team's tick
|
|
416
|
+
|
|
417
|
+
- **Undo is about rounds, not snapshots.** A participant joining pushes another snapshot at the tick already showing (the runtime replays the hook), so a team at tick 0 that somebody had joined looked undoable, and the undo reported success and visibly did nothing. `canUndo` now answers whether there is an earlier _tick_, and undo walks back past every snapshot sharing the current one
|
|
418
|
+
|
|
419
|
+
- **The facilitator guide renders, rather than showing its own markdown source.** The renderer the specification describes — default-deny HTML, filtered link schemes, admonitions, video embeds, a table of contents generated from the parsed AST with per-chapter anchors — already existed; the rebuilt console was serving raw text instead of calling it
|
|
420
|
+
|
|
421
|
+
- **An independent cohort tick advances each team from its own tick.** It computed one next tick from the furthest-ahead team and set every team to it, so a team that was behind jumped forward — two rounds of credit for one round of work, on a context that had only had one. Independent teams have their own clocks; a cohort tick is now several teams' ticks, and it carries the teams that can move past one that is complete or at the limit rather than refusing outright
|
|
422
|
+
|
|
423
|
+
- **A surface changing the colour scheme changes the console with it.** `setThemePreference` from a game UI reached the host and was dropped, so a participant toggling dark mode inside the game got a white page around it. The preference is the console's to hold, and it now holds it — and remembers it
|
|
424
|
+
|
|
425
|
+
- **The iframe sandbox and `allow` attributes match the specification token for token.** Two sandbox tokens (`allow-popups`, `allow-presentation`) and five delegated features were missing. The popups one is the visible cost: a surface's own links — a facilitator's join link, an `observeUrl`, anything with `target="_blank"` — silently did nothing when clicked. The shell also sends the `Permissions-Policy` header holding every feature it delegates, since a document cannot delegate a feature it does not hold
|
|
426
|
+
|
|
427
|
+
- **The session surface is offered the host overlays this host has.** `setAvailableOverlays` was sent empty, so a session UI rendering guide and print-sheet controls rendered 'This host offers none' instead. It now offers `print-sheets` always and `facilitator-guide` when the package ships one, and `openHostOverlay` opens them in a new tab
|
|
428
|
+
|
|
429
|
+
- **A mutation a surface awaits is answered.** Every mutating SDK callback returns a promise that settles on the host's reply, and the rebuilt console forwarded the command and said nothing. Three symptoms, one cause: a 'Next tick' button sat on 'Advancing' for ever, a validator's refusal never reached the participant who submitted the write (so a rejected decision looked accepted), and a team renamed from the waiting room appeared not to have been. The session was correct throughout, which is why nothing caught it — what was missing was the half of the conversation going back to the surface
|
|
430
|
+
|
|
431
|
+
- **A validator's reasons reach the surface that submitted the write.** Validator rejections were raised by the session and then dropped: the console held them and forwarded them to nobody. Your `useUserInterfaceValidationRejections` now receives them in the dev server as it does in production
|
|
432
|
+
|
|
433
|
+
- **The facilitator surface gets a join and observe link per team.** `setTeamLinks` was sent empty, so a session UI rendering join links rendered none. A join link now points at `/team/<n>/join`, which asks which participant you are — or opens straight into their surface when the team holds one. Player surfaces get their own team's link on `setOwnTeamJoinLink`, which was previously `null`
|
|
434
|
+
|
|
435
|
+
- **`Init` now runs once per team, with that team's identity.** In a multi-team game it ran once and had its result cloned to every team, so every team's context carried `meta.teamId: 'team-1'` and `meta.index: 0` — including a team added mid-session, which inherited the first team's identity along with its state. A game seeding per-team starting state from the index had no way to express it at all. `Init` is now a per-team call exactly as `Run` is, which is what the specification always said. Visible if your `Init` reads `meta`: it now gets the truth. An `Init` that ignores `meta` produces identical results, one per team instead of one cloned
|
|
436
|
+
|
|
437
|
+
- **Every surface mounted its React root twice.** A surface's HTML is served from under a URL prefix and Vite's module graph knows the same file by its root-relative path, so the browser fetched the entry module under both names and evaluated it twice. React said so in a console nobody was reading, and the surfaces rendered, so it went unnoticed; what it cost was two handshakes, two sets of subscriptions and two renders per surface. The document now names its modules the way Vite names them
|
|
438
|
+
|
|
439
|
+
- **A failure inside game logic reports its cause again.** Logic runs in its own realm with its own `Error` constructor, so `error instanceof Error` was false for anything it threw and a genuine `ReferenceError: structuredClone is not defined` was reported as 'Unknown error' — a failing tick with no cause, which is the one thing the message exists to give you
|
|
440
|
+
|
|
441
|
+
- **`sdk configure` reaches `Init`, not just `Run`, after a reset.** The ordering defect fixed earlier in `initialize` survived in `reset`, which reloaded configuration from disk after `Init` had already run
|
|
442
|
+
|
|
443
|
+
- **Switching scenario now takes effect.** Selecting a different scenario reinitialised the session but left the previous scenario's values in force, so only the label changed. The generated configuration snapshot carried the old scenario's values into a layer that outranks scenarios, which no later scenario could then override
|
|
444
|
+
|
|
445
|
+
- **A configured value now reaches `Init`, not just `Run`.** Configuration was established _after_ the engine's `Init` had already run, so a value written by `sdk configure` was invisible to `Init` and applied only from the first tick onwards. A game that seeds its context from configuration (`Value: config.Start`) started from the wrong number every time
|
|
446
|
+
|
|
447
|
+
- **The dev server works when you open it from another device.** The content security policy it mirrors from production named only `localhost` and `127.0.0.1` as hot-reload origins, so opening a game from a phone on the same network, through a tunnel, or at a container's published address had its Vite socket refused by the policy: a `connect-src` violation in the console and a client retrying for ever against a blocked origin. The requested host is now allowed alongside loopback, which is what you need to try a participant surface on a real device
|
|
448
|
+
|
|
449
|
+
- **A scaffolded package no longer teaches the model this release replaced.** Its README and `CLAUDE.md` both told you to run `sdk new game`, which returns `error: unknown command 'game'`; two directory trees named `facilitator-guides/` and `user-interfaces/`; and `DESIGN.md` was entirely pre-simplification, with dotted resource identifiers, a `Modules` section, `Required`/`Provided Capabilities` instead of reads and writes, and an `Events` resource type this SDK has never had. All three are rewritten. The drift test that exists to catch exactly this checked one template file out of eleven, and now checks every prose file the scaffold emits
|
|
450
|
+
|
|
451
|
+
- **`@mui/icons-material` is in a new package's dependencies.** Eighteen of the nineteen games we migrated use it, so every new package started by adding it by hand
|
|
452
|
+
|
|
453
|
+
- **An aggregate engine runs where it is declared, not where it is exported.** The dev server called `RunAggregate` whenever the engine exported it, while initialisation, the hosted runtime and the specification all dispatch on the bundle's `aggregate` declaration. An engine that lost the declaration therefore kept aggregating every tick in development, over a shared store that `InitAggregate` had never seeded, and did not aggregate at all in production. The build now also rejects an engine exporting `InitAggregate` or `RunAggregate` without declaring `aggregate`, which merging several modules into one engine makes easy to do: the package builds, typechecks and passes its unit tests while the aggregate step silently never happens
|
|
454
|
+
|
|
455
|
+
- **Generation refuses to overwrite a `types.ts` it did not write.** Each resource has its types generated into a `types.ts` beside its bundle, over whatever was there. A game keeping its own hand-written types at that path lost them on the next build, and the loss surfaced several steps later as a missing export from `./types`, naming nothing about the collision. The build now stops and names the file, so you can move it and fix the imports that reach it
|
|
456
|
+
|
|
457
|
+
- **The host-globals lint rule stops firing on your engine tests.** The scaffolded `.oxlintrc.json` scoped the rule with `**/engine/**/*.ts`, which also matches `testing/engine/`, where engine tests now live. Test files were already exempt, so what it caught was a test harness reaching for `URL` or `structuredClone` in Node, where both are perfectly ordinary. The rule describes the production runtime, and the production runtime only runs what is in the `engine/` slot, so the glob is anchored there
|
|
458
|
+
|
|
459
|
+
- **`sdk docs ask --json` prints nothing but JSON.** `Analyzing your question...`, and on a first run the embedding-model download notice, went to stdout ahead of the results, so `JSON.parse` threw on the output even under `pnpm run --silent`. They are progress, so they go to stderr, which leaves stdout parseable for the agent use the flag exists for. An interactive run still sees them
|
|
460
|
+
|
|
461
|
+
- **An empty resource directory says it is empty.** Moving a resource out file by file leaves the old directory behind, and the build correctly refuses to load a directory with no bundle in it. It named the bundle, though, which sent you looking for a file you had just deleted on purpose. It now names the directory and offers the two ways out: delete it, or put a bundle in it. A bundle that exists and will not parse still names the file, which is the useful thing there
|
|
462
|
+
|
|
463
|
+
- **A waiting room's `crossTeam` reads are checked against capability scope.** A game user interface declaring `crossTeam` on a capability that is not `team-readable` failed the build; the same declaration on a waiting room user interface passed, because the check only ran over game surfaces. Both are player surfaces carrying identical cross-team semantics, and both now generate a `CrossTeamContext` from those declarations, so an unchecked one would have promised data the host never delivers
|
|
464
|
+
|
|
465
|
+
- **A round only says it is processing on the teams whose round it is.** In an independent-mode game every team ticks on its own clock, but the dev server told every screen a round was processing whenever any team ticked. That replaced an unexplained pause with an untrue explanation, which is worse. Shared-state is unchanged, because there the tick genuinely is cohort-wide
|
|
466
|
+
|
|
467
|
+
- **Your tick controller re-runs when a declared read actually changes, not when it is merely named.** The rule was always 'changes at least one capability the controller declared', but the check looked at the keys a write named. A full-context sender names every capability on every write, so the gate fired every time for exactly the callers most likely to be chatty, and the saving it exists for evaporated. Values are compared now
|
|
468
|
+
|
|
469
|
+
- **A history read now honours the bound it documented.** A session screen could read any capability across every team over a tick range, including one it never declared and including a `scope: 'private'` one, because the rule was written in the specification and not in the code. It is enforced host-side now, on every surface: a capability the query names that the bundle did not declare in `reads` or `writes`, or whose scope is private, is dropped, and a query whose every named capability is dropped comes back empty rather than erroring — the rows alone would still disclose how many ticks and teams exist. `capabilityIds` is the only part of a history read named by the screen at call time rather than declared in its bundle, which is why it is the only part that needs a runtime check
|
|
470
|
+
|
|
471
|
+
- **`meta.teamId` is the same id your screens see.** In a single-team game `Init` and `Run` reported `'default'` while the tick controller reported `'team-1'`, and `SessionData`, `setOwnTeam` and everything else on the wire call that team `team-1`. So a value keyed off `meta.teamId` in your engine could not be looked up by a surface, which is most of what the field is for. There is one id space now: `teamId` is always `team-${index + 1}`, and `'default'` is gone. If you stored anything keyed on `'default'` during this release cycle, re-key it; nothing published is affected, since the field arrived in this cycle too
|
|
472
|
+
|
|
473
|
+
- **The dev server sends a session screen a complete timer.** Its fallback timer state omitted `extendedMs`, so a facilitator surface reading it before the first tick was scheduled saw `undefined` where the type promised a number, and 'a facilitator gave this round two more minutes' could not be rendered from it. The dev server's timer is now typed against the SDK's own `SurfaceTimer`, so a field added there fails the build here rather than arriving as `undefined`; the two test suites that each kept their own copy of the default now import the real one
|
|
474
|
+
|
|
475
|
+
- **The host-globals lint rule covers the engine and the tick controller again.** Its globs still targeted `modules/*/logic/**` and `tick-controllers/*/logic/**`, and two things had moved underneath them: the directory became the slot, and a slot resource keeps its logic at the slot root rather than in a `logic/` subdirectory. Both matched nothing, and an override matching nothing is not an error — so `pnpm run lint` passed while the rule that catches `structuredClone`, `TextEncoder`, `fetch` and the rest before deploy was off for the two likeliest places to reach for one. Every package scaffolded since the slot move has this; take the corrected `oxlintrc.json` from the shipped template. The UI slots stay out of scope on purpose: `ui/`, `session/` and `waiting_room/` run in a browser, where those globals exist
|
|
476
|
+
- **`sdk configure` survives the next run.** A configuration written with `configure` was discarded by the following `init` or `tick` whenever a scenario was active — which the default scenario always is — so the value never reached the game, and the success message named the very command that overwrote it. The saved configuration is a full resolved snapshot rather than a sparse override layer, so one built under a different scenario genuinely cannot be reused; the SDK now records which scenario a snapshot was built for and regenerates only when that has actually changed. Asking for a different scenario still re-resolves, as before
|
|
477
|
+
- **The CLI's own `new`, `update` and `delete` examples work.** Seven of the twelve `new` examples printed by `--help` failed when run: the tick-controller one omitted the required `logic` and `rootDirectory`, the configuration UI one used the `modules` and `userInterfaces` fields that `configures` replaced, the scenario one used the bucket shape that slots replaced, and the endpoint one carried literal `{...}` placeholders. Every example is now derived from the same type-checked literal the help prints above it, so the two cannot disagree again. Separately, `update` and `delete` for the five slot resources documented an identifier those commands do not accept, so each printed invocation failed with "too many arguments"; and every type offered "delete by short identifier" and "delete by full identifier" as two identical commands, left from when those differed
|
|
478
|
+
- **Interactive `new` and `update` find resources again.** They looked the resource up in the manifest's resource lists, which are gone, so the capability pickers came up empty and `update <type> <name>` with no JSON reported the resource as not found. They scan the resource directories now, like everything else. `sdk resolve` also printed a hand-copied subset of the resolved package that silently omitted session UIs, scenarios, facilitator guides, tick controllers, hooks, validators and endpoints; it prints the whole thing
|
|
479
|
+
- **`new package` rejects an identifier that is not a package identifier.** A JSON definition naming, say, `mypackage` was accepted, then produced a package whose manifest failed its own validation half-way through the scaffold, leaving the game settings unwritten behind a warning. The identifier and version are checked before anything is created
|
|
480
|
+
- **The `new`, `update` and `delete` help is generated rather than hand-copied, so it cannot drift again.** The prose was written out once per resource type and varied only by the type's name, which is why it kept going stale: `update configuration-user-interface` still showed the `modules` and `userInterfaces` fields that `configures` replaced two releases ago, every `From stdin` example carried a literal `...` in place of the fields it was abbreviating, and the `Supported types:` list printed when you name a type that does not exist named one removed in 0.0.113 while omitting five of the twelve you can delete and six of the thirteen you can update. All of it now comes from the type's registry entry and the same type-checked literal the help prints above it. An e2e test runs every `new` example exactly as `--help` prints it, because a green suite and a working command turned out not to be the same thing
|
|
481
|
+
- **A scaffolded package's Playwright spec now selects an iframe that exists.** `testing/e2e/game.spec.ts` looked for `iframe[title="dashboard"]`, but the dev server titles the frame with the interface's identifier, which for the participant UI is its slot: `ui`. Every package scaffolded since the slot move therefore shipped an e2e spec that timed out on a selector that could not match. Nothing caught it because no example package runs the scaffolded spec. Re-take `testing/e2e/game.spec.ts` from the shipped template, or change the one argument yourself
|
|
482
|
+
- **A scaffolded engine test is named after the module again.** `engine/logic.test.ts` opened with `describe('main')` from the scaffold, and with `describe('')` when the module came from the documented `new module` JSON, which carries no identifier. It is `describe('engine')` in both cases now
|
|
483
|
+
- **`new` no longer asks a slot resource for a name it cannot have.** The module, participant UI, session UI, waiting room, tick controller and guide each live in a fixed slot and take the slot as their identifier, but interactive mode still opened by asking you to name one, then discarded the answer. The prompt is gone; where a display name is genuinely used, that is still asked for. The 'Next steps' printed afterwards also pointed at the plural directories the slot move replaced (`user-interfaces/<name>/src/App.tsx` rather than `ui/src/App.tsx`), and the waiting room's told you to wire it in with `sdk update game`, a command that no longer exists
|
|
484
|
+
- **The configuration guide describes where configuration is actually written.** It documented `.seriousgaming-sdk/games/<safe-identifier>/configuration.json` and a naming rule for the identifier; a package is a game, so there is one `configuration.json` per package and nothing to key it by. The demo-mode error it quotes matches the one the dev server prints
|
|
485
|
+
|
|
486
|
+
- **A scaffolded configuration UI sits where every other user interface sits.** `new configuration-user-interface` put `index.html` and `src/` inside an extra `ui/` directory under the resource, one level deeper than the participant, session and waiting room interfaces, and deeper than the configuration UI shipped in `examples/factory`. So the scaffold and the example disagreed about the shape of the same resource type. Files now sit directly in the resource's own directory and the bundle records `"ui": { "directory": "." }`, matching its siblings. The scaffolded `configuration-user-interface/src/App.tsx.liquid` imports `../types` rather than `../../types` to suit. **Nothing breaks in an existing package:** the bundle says where its web files are, so one recording `"directory": "ui"` keeps loading from `ui/`. To adopt the flat shape, move `ui/index.html` and `ui/src/` up a level, set `"directory"` to `"."`, and drop one `../` from the types import
|
|
487
|
+
|
|
488
|
+
### Removed
|
|
489
|
+
|
|
490
|
+
- **`.seriousgaming-sdk/state.json` is gone, and nothing replaces it.** The session lives in the dev server's memory: the session is the process, so a restarted server is a new session at tick 0. The file existed so the CLI and the dev server could share a session, which they now do by the CLI being a client of the server. What is left in `.seriousgaming-sdk/` is `configuration.json` (authored content), `blobs/`, and `session.json` — a lock file carrying a pid and a port, written once at startup and removed at shutdown. **To migrate:** delete any `state.json` you have; nothing reads it. If you scripted against it, `sdk view --full` prints the same information in the shape the dev server and console both use
|
|
491
|
+
|
|
492
|
+
- **`sdk dev --in-memory` and `--config-save-delay` are gone.** Both existed because the old server wrote session state to disk as it ran: `--in-memory` turned that off for deterministic tests, and `--config-save-delay` debounced the writes so they did not slow a session down. Nothing writes session state now, so both are answers to a question that is no longer asked. **To migrate:** remove the flags. A Playwright `webServer` that passed `--in-memory` for determinism needs nothing in its place — `reuseExistingServer: false` already gives each run a fresh session
|
|
493
|
+
|
|
494
|
+
- **`sdk init --reconfigure` is gone.** It regenerated `configuration.json` from the schema defaults when the file no longer validated. The file is authored content — you write it with `sdk configure` or by hand — so a value in it that no schema declares any more is an error worth seeing rather than something to overwrite. **To migrate:** correct the file, or delete it to start again from the defaults
|
|
495
|
+
|
|
496
|
+
- **`GET /api/module-order` is gone from the dev server.** A game wires one module, so it returned a one-element array whose name promised an ordering there is nothing left to order. Nothing in the dev UI called it. Read the module from `GET /api/config` if you need it
|
|
497
|
+
|
|
498
|
+
## [0.0.120] — 2026-09-10
|
|
499
|
+
|
|
500
|
+
### Fixed
|
|
501
|
+
|
|
502
|
+
- **A package built on Windows no longer records a Windows path separator in its manifest.** Every `rootDirectory` in `dist/seriousgaming.json` was composed with the host's separator, so a build on Windows wrote `user-interfaces\com.example.dashboard` where the same build on macOS or Linux wrote `user-interfaces/com.example.dashboard`. Nothing you author was involved: a resource bundle declares `rootDirectory: "."` and the rest of the value is composed for you. A browser normalises a backslash in a URL path, so a user interface served from such a package still loaded, which is most of why this went unnoticed; nothing else that handles the path does, because an object store treats a backslash as an ordinary character rather than a separator, so the same path names a key that does not exist. The value is now composed in POSIX form on every platform. If you have published a package built on Windows, rebuild and republish it
|
|
503
|
+
- **A package built on Windows can now be installed as a dependency of a package built anywhere else.** The manifest separator above is read back as a filesystem path when one SDK package depends on another, so a Windows-built package installed into a macOS or Linux project resolved to a single directory whose name contained a backslash rather than to the resource, and `sdk build` failed copying it. Separators are now normalised as the dependency's manifest is read, so a package already published this way works without being rebuilt
|
|
504
|
+
|
|
505
|
+
### Changed
|
|
506
|
+
|
|
507
|
+
- **A `rootDirectory` containing a backslash is now refused.** `sdk new` and `sdk update` reject it, as does `sdk build`, because the value is carried into the built manifest where it addresses the resource over HTTP. Write a bundle path with forward slashes on every platform. Nothing generated by the CLI is affected: `sdk new` writes `"."`
|
|
508
|
+
|
|
509
|
+
## [0.0.119] — 2026-09-09
|
|
510
|
+
|
|
511
|
+
### Added
|
|
512
|
+
|
|
513
|
+
- **Your UI is now granted a fixed set of frame capabilities in production, where the browser supports them.** These worked in the dev server and silently did nothing in a live session, because the dev server serves your UI same-origin (where a frame inherits such capabilities from its parent) while production serves it cross-origin (where each must be granted explicitly). Every UI surface is now given a fixed, uniform set of frame capabilities that the dev server and production both apply: clipboard writing, printing, downloads, fullscreen, autoplay (which generated speech playback depends on), screen wake lock, second-screen presentation, opening an external link in a new tab, and the option to ask the browser for the microphone, camera, or location. Nothing needs declaring in your bundle. Support is not uniform: printing, downloads, fullscreen, and autoplay work broadly, while multi-screen window placement and speaker selection are Chromium-only, and clipboard writing is delegated as a Permissions Policy feature in Chromium but gated on a user gesture in Safari and Firefox, so put clipboard writes directly inside a click handler and feature-detect before relying on any of them. Top-level navigation, clipboard reading, screen capture, and hardware, credential, and payment APIs are withheld. A surface cannot navigate the page it sits in, so link out with `target="_blank" rel="noopener noreferrer"`, and give a `mailto:` or `tel:` link the same treatment. See [Production security constraints](guides/production-constraints.md)
|
|
514
|
+
|
|
515
|
+
### Changed
|
|
516
|
+
|
|
517
|
+
- **`form-action 'self'` added to the UI content security policy.** `connect-src` already confined `fetch` to your own origin, but form submission is governed by a separate directive with no fallback, so a form could still post to a third-party origin. A UI that submits a native form to an external URL will now be blocked in the dev server as well as in production
|
|
518
|
+
|
|
519
|
+
## [0.0.118] — 2026-09-09
|
|
520
|
+
|
|
521
|
+
### Fixed
|
|
522
|
+
|
|
523
|
+
- **A team created after another team has finished can now play.** In an independent game each team has its own clock and its own ending, but the dev server recorded completion once for the whole session. A session that ran single-team until someone finished therefore latched that ending, and every team added afterwards was refused its first tick for good: pressing the control did nothing, and a reload showed the team still un-started. Nothing but an undo or a reset cleared it, so a walk-up format where each group is a new team could only be run by creating every team before anybody finished. Completion is now recorded per team, so one team finishing leaves the others free to play, and a team added after the fact starts from its own beginning. A session counts as complete only once every team is
|
|
524
|
+
- **A player surface no longer renders another team's board when its tick is refused.** A refused tick broadcast the session's state without the per-team boards, and a surface with no board of its own to read fell back to the one the runtime happened to hold, which is always the first team's. The player saw another team's day, another team's scores and another team's end-of-run screen, under their own team's name and crest, until they reloaded. Every refusal now carries each team's own board
|
|
525
|
+
- **An independent team reaching its own ending is now noticed.** Once a session had more than one team, the tick controller was never consulted after a per-team tick, so `isComplete` never became true no matter what a team's own state said: the team kept ticking past its ending, and no surface was ever told the run had finished. Each team's tick controller is now evaluated against that team's own context, and a per-team undo or reset that rewinds past the ending clears it again
|
|
526
|
+
- **`GET /api/teams` now reports a single team's live context.** With one team in the session the endpoint returned the state as it stood at initialisation, however far the team had actually played, which made a correct session look broken when asserted against in a test. With two or more teams it was already correct
|
|
527
|
+
|
|
528
|
+
## [0.0.117] — 2026-09-08
|
|
529
|
+
|
|
530
|
+
### Added
|
|
531
|
+
|
|
532
|
+
- **A team can now name itself from a player surface.** `renameOwnTeam(name)` on `useUserInterfaceCallbacks()` renames the team the surface belongs to, on game user interfaces and waiting room user interfaces alike, so the name a group types is the team's real name everywhere: the host's own chrome and print sheets show it too, and your game no longer needs a display name of its own that every screen has to prefer over the platform's. It takes no team id, because the host resolves the caller's team itself, so a surface can only ever rename its own. Available to participants and observers with nothing to declare; a game that does not want its teams renamed simply does not offer the control, and `teams.defaultNames` becomes a starting point rather than a fixed label wherever it does. Render the name back from `useOwnTeam()` rather than what you sent: a facilitator can rename the same team at the same moment, there is one name per team, and the last write wins. The host trims the name and refuses a blank one or one longer than 60 characters, each as a terminal `denied` you should explain rather than retry
|
|
533
|
+
- **Player surfaces are now told how to join their own team.** `useOwnTeamJoinLink()` returns `{ joinUrl, fallbackJoinUrl? }` for the team the surface is rendering, on game user interfaces and waiting room user interfaces alike, so a lobby screen or a tablet on the table can show a join code instead of the facilitator reading a URL out. Observers get it too, which is the case it exists for: a screen parked on a table has no participant of its own, so it is opened as an observer. You always get your own team's link and only that, encode `joinUrl` in a QR code rather than any longer form you build yourself (the host picks the form, and picks the short one deliberately), and treat `fallbackJoinUrl` as a 'link not working?' affordance for networks that block shortened links. The session PIN still never reaches a player surface, so in a PIN-protected session a visible code does not admit anyone on its own. The hook reads `null` until the host delivers the link, so render a placeholder for that first frame
|
|
534
|
+
- **Built packages now record the manifest format they were built as.** `dist/seriousgaming.json` carries a `manifestVersion` field, and a runtime reading it treats an absent value as `1`, so every package built before this release is version 1 and stays runnable. Nothing about the manifest's shape changes: this release only names the format that was already there, so that a later change to it can be told apart from the current one. `MANIFEST_VERSION` and `resolveManifestVersion` are exported for anything that reads a built manifest
|
|
535
|
+
|
|
536
|
+
### Changed
|
|
537
|
+
|
|
538
|
+
- **`rfc6902` is now a runtime dependency of the tools package rather than compiled into it.** The CLI is bundled, and a library declared as a development dependency was inlined into `dist/`, where you could not reach it: a `pnpm.overrides` entry or `pnpm patch` against it resolved nothing, and working round a defect in it meant editing our shipped bundle in place. Declaring it properly means it installs alongside the CLI and you can pin or override it yourself. Nothing changes about how patches are applied
|
|
539
|
+
- The engine-testing guide now says which SDK runtime `GameRunner` executes. It is the copy compiled into the tools package when that package was built, not the `@businesssimulations/seriousgaming-sdk` you have installed, so keep the two on the same version and treat the tools version as the one that decides behaviour
|
|
540
|
+
|
|
541
|
+
### Fixed
|
|
542
|
+
|
|
543
|
+
- **Naming a team as you create it no longer names a different team.** `createTeam(name)` applied the name by deriving the new team's id from the team count, which assumed the team had been appended. It is not: a new team refills the lowest unused index, so in a session where a team had been removed the name landed on whichever team sat at the count instead. The facilitator's new team kept its default name and an unrelated team took the one they typed. The name is now applied to the index the runtime actually assigned
|
|
544
|
+
- **Renaming a team that does not exist is now refused.** Any `team-N` id was accepted, including an index no team held, and the name was stored against that index. It then applied itself to whichever team was created there next, so a typo could rename a team that did not exist yet. The rename is now refused as a terminal `denied`
|
|
545
|
+
- **A refused team rename now rejects as terminal rather than retryable.** A blank name, and a team id the host does not recognise, both came back as `{ kind: 'error' }` from `renameTeam`, which tells a caller the operation might work if it waits and tries again. Neither ever will, so both are now `{ kind: 'denied' }`. This affects the facilitator's `renameTeam` as well as the new player-side rename, and only the outcome kind changes: the refusals themselves are unchanged
|
|
546
|
+
- **An array your module built is no longer rebuilt as an index-keyed object by a JSON patch.** Module logic is evaluated in its own JavaScript environment, so the arrays and objects it returns carry that environment's constructors. The runtime stored them as they came, and `rfc6902`, which the tools package uses to apply an RFC 6902 patch, decides whether a value is an array by comparing its constructor rather than with `Array.isArray`. An array that module logic had touched therefore survived a `GameRunner.update()` or a `game tick --patch` as `{"0":"a","1":"b"}`, and the next tick threw `TypeError: ....reduce is not a function` from inside your own code. Schema defaults were unaffected, because the host builds those, which is why the fault appeared only once logic had written to the field. Everything a logic bundle returns is now brought back onto the host environment before it is stored, so every later read of it, a tick result and `getContext()` included, is a plain array or object
|
|
547
|
+
- Assertions in your own engine tests can meet the same environment boundary: `expect(value).toBeInstanceOf(Array)` fails on an array your module built, even though the value is correct. Use `Array.isArray(value)` instead. See ['A note on `instanceof` and module-produced values'](guides/engine-testing.md) in the engine-testing guide
|
|
548
|
+
- **On Windows, a built package no longer ships empty directories.** The sweep added in 0.0.116 only ever worked on macOS and Linux. It compared each directory the bundler walked against a list of the empty ones, but the two were written differently on Windows, where a path is separated by backslashes and a zip entry name by forward slashes, so nothing matched and every empty directory went into the zip as before. The half of the 0.0.116 fix that stops `sdk build` creating a directory for a resource kind the package does not use was unaffected and worked everywhere, so this only mattered for an empty directory arriving from somewhere else. The comparison is now made in one form on every platform
|
|
549
|
+
|
|
550
|
+
- **Assigning to a context you read back no longer changes game state.** `getContext()` and the context from `tick()` were shallow copies, so the capability values inside them were the ones the runtime held. Assigning to a field of one therefore wrote game state, quietly, without going through `update()`, which meant without a validator, without the write-authorisation check, and without a persist. Reads are now independent copies, matching `getSharedContext()`, which always was one. If you were relying on this to set up a test, the assignment now does nothing and you should use `update()` instead
|
|
551
|
+
|
|
552
|
+
## [0.0.116] — 2026-09-05
|
|
553
|
+
|
|
554
|
+
### Fixed
|
|
555
|
+
|
|
556
|
+
- **A partial `setData` no longer blanks sibling fields of an object your module built.** The context deep-merge decided whether a value was a plain object by comparing its prototype against the host runtime's `Object.prototype`. Module logic runs in its own JavaScript realm, so an object literal it returns carries that realm's prototype instead, failed the comparison, and was treated as a value that replaces rather than merges: a UI sending the documented minimal patch into such an object lost every key it did not name. The wrappers' optimistic local view applied the same rule, so the UI and the host agreed on the truncated result and nothing errored. Plain objects are now detected independently of the realm they were built in. Arrays were never affected: they replaced wholesale before and still do
|
|
557
|
+
- **A built package no longer ships empty directories for resource kinds it does not use.** `sdk build` created a subdirectory for every resource kind before it knew whether the package had any, and `sdk package bundle` wrote each one into the zip as a bare directory entry. The platform's upload validator checks every entry against its allow-list of resource kinds, so a package built before a resource type was retired was refused on upload for a directory that never held anything: a build on 0.0.106 still emitted `manage-team-user-interfaces/` and `manage-session-user-interfaces/`, both removed in 0.0.113. Only directories with something in them are created now, and the bundler leaves out any empty directory it still finds, whatever its source. If you are blocked on an older version and cannot upgrade, delete the offending directories from `dist/`, re-run `sdk package bundle`, then pass the zip path explicitly to `sdk package publish` so it does not rebuild
|
|
558
|
+
- **A game UI rendering in the opposite mode to the dev host gets that mode's colours.** The dev server built the theme payload from whichever palette its own chrome had live and wrote it into both the `light` and the `dark` half, so a UI painting dark while the chrome sat in light mode received the light navy as its dark-mode `primary`. Under MUI that is the colour of an outlined button's label, which left controls near-black on a near-black surface with nothing logged. Each half now carries its own palette; `mode` still reports the host's current mode
|
|
559
|
+
- **A session surface is no longer told a one-team game is shared-state.** `useSessionUserInterfaceTeamConfiguration().mode` was derived from the dev server's internal independent-mode flag, which carries a `teamCount > 1` term so the dev tools can hide per-team tick counters. A game declaring `teams.mode: 'independent'` therefore reported `shared-state` until a second team existed, flipping to `independent` on a team being added and back on its removal: a mode the specification fixes at authoring time changed twice over one session. The cost lands on any surface that branches on it, and is self-locking where the branch guards team creation, since gating 'add team' on `mode !== 'shared-state'` disables the control in precisely the one-team state a facilitator needs it from. The mode is now read from the game definition alongside `min`, `max` and `perTeam`, defaulting to `independent` when the manifest omits it. Dev-server only: a production host resolves its own team configuration
|
|
560
|
+
|
|
561
|
+
## [0.0.115] — 2026-09-02
|
|
562
|
+
|
|
563
|
+
### Added
|
|
564
|
+
|
|
565
|
+
- A scenario can configure the session user interface, through a `sessionUserInterfaces` bucket in its `configuration` alongside `modules` and `userInterfaces`. The surface's configuration now resolves through all four layers like any other resource: schema defaults, the game's `sessionUserInterface` reference, the active scenario, then the centralized layer. Session UIs get their own bucket rather than sharing `userInterfaces`, so a player UI and a session UI may carry the same identifier without crossing. See the [session UI guide](guides/session.md#configuring-the-surface)
|
|
566
|
+
- A session user interface can open the host's own overlays with `openHostOverlay()`, so a facilitator does not have to leave the surface to reach the facilitator guide or a team's print sheet. The host still renders them, in its chrome above the surface, and reports which ones it offers through `useSessionUserInterfaceHostOverlays()`. Render a control only for an overlay in that list: a production host and the dev server do not offer the same set. See the [session UI guide](guides/session.md#opening-host-overlays)
|
|
567
|
+
- `extendTick(seconds)` moves the deadline of the currently scheduled tick from a session user interface, so 'give them two more minutes' no longer means pausing the timer and remembering to resume it. It moves that one window: the interval your tick controller returns for later ticks is untouched, a negative value pulls the deadline in, and the adjustment reads back as `extendedMs` on the team's timer. See the [session UI guide](guides/session.md#extending-a-tick)
|
|
568
|
+
- The dev server serves team print sheets at `/print-sheets`, one card per team with its join and observe URLs behind a print stylesheet. Print every team from the session menu, or one team from its card's own menu; a session UI reaches both through `openHostOverlay('print-sheets', teamId?)`. The sheets carry no QR code and no PIN, because the dev server has neither; a production host prints both
|
|
569
|
+
- A game can start its sessions with the waiting room already open, by setting `"openOnStart": true` on its `waitingRoom` reference. Without it the room is closed at the start, so the first player to follow a join link lands in the game rather than the holding screen unless the facilitator opened the room first. It seeds the session and nothing more: the facilitator owns the flag from then on, a reset does not re-park the session, and a dev server restart restores the flag the session was left with. Available to a game that authors its own waiting room UI. See the [waiting room guide](guides/waiting-room.md#starting-held)
|
|
570
|
+
- `new`, `update` and `delete` accept `-j, --json`, matching every other command in the CLI. The envelope names the command, the resource type, the identifier, and every file, reference and directory the operation touched; a failure reports the same shape with `"success": false` and an `error`, and exits non-zero. Because prompts cannot produce JSON, `new` and `update` need a definition or a `--patch` under the flag, and `delete` needs `--force`. See the [CLI reference](cli-reference.md#json-output)
|
|
571
|
+
|
|
572
|
+
### Changed
|
|
573
|
+
|
|
574
|
+
- `sdk dev` prints one URL on startup instead of a block of them. The list it used to print included `/manage-team/1`, a route that stopped existing when the `manage-team-user-interface` resource type was removed in 0.0.113, and it printed the root URL twice. Every surface it listed (each team's play page, the session surface) is reachable from the home page, which stays current on its own
|
|
575
|
+
- Session and configuration user interfaces scaffolded by `sdk new` no longer paint their own page background. They are embedded in the host's page, and a surface that paints its own ground reads as a panel bolted on rather than part of the page, so the templates now leave the body transparent and let the host's background show through. Existing packages are not touched: add a `<GlobalStyles>` setting the body background to `transparent` beside your `CssBaseline` to get the same result. The new [embedded rendering](guides/session.md#rendering-inside-the-host) section covers the rest of it, including why a centred dialog inside a frame is worse than a full-screen one
|
|
576
|
+
- The Playwright config scaffolded by `sdk package new` now starts the dev server with `--in-memory` and sets `reuseExistingServer: false`. Without them a suite inherits whatever `.seriousgaming-sdk/games/<game-id>/state.json` holds from an earlier run, and because that file is keyed by game identifier rather than by port, a dev server left running on another port counts as an earlier run. Existing packages are not touched: add `--in-memory` to your own `webServer.command` to get the same determinism. See the [e2e testing recipe](recipes/e2e-testing.md)
|
|
577
|
+
|
|
578
|
+
### Fixed
|
|
579
|
+
|
|
580
|
+
- A configuration override addressing something that does not exist is now an error rather than silently discarded. An identifier naming no resource of that bucket's type, and an unrecognised bucket name, were both accepted and then dropped, so a misspelled identifier or a value placed in the wrong bucket read as applied and changed nothing. Both now fail `sdk build`, `sdk build --validate-only`, `game configure` and session start; where the identifier exists in a different bucket the error names it, since misplacement is the likelier cause than a typo. Checked for the centralized configuration, game-level overrides and scenario overrides alike. A package carrying one of these mistakes will start failing its build, which is the point: that configuration was never being applied
|
|
581
|
+
- `sdk build` accepts a partial game-level `configuration` block again. A game reference that set some of a resource's configured keys and left the rest to their schema defaults failed the build with `must have required property '<key>'`, because the check enforced the schema's top-level `required` against a partial document. Since the convention is to mark every property required, this meant a game reference had to restate every key of every resource it configured. Omitted keys now fall through to their defaults as the configuration model says they should; undeclared and wrongly-typed keys are still rejected. Affected modules, player UIs, tick controllers, hooks, validators, the session UI, and shell header and footer alike
|
|
582
|
+
- A session user interface now receives its configuration. `useSessionUserInterfaceData().config` was `undefined` on every render, so neither the surface's own `configurationSchema` defaults nor the `configuration` block on the game's `sessionUserInterface` reference reached it, and a game reading `config.SomeKey` in good faith got nothing at runtime despite the type saying otherwise. Games that routed settings the long way round (scenario → module configuration → a capability written at `Init` → read back from context) can now read them from `config` directly. A scenario still cannot configure a session UI: `CentralizedConfiguration` and a scenario's `configuration` have buckets for modules and player UIs only, so that route needs a schema change and is tracked separately
|
|
583
|
+
- `sdk dev --teams N` is no longer ignored when the package already holds a saved session. The saved team count used to win in silence, so a game whose session was last run with eight teams kept starting eight however many were asked for. An explicit `--teams` that disagrees with the saved session now discards it and re-initialises, saying so; omitting the flag still resumes the saved session exactly as before
|
|
584
|
+
- `sdk dev --teams N` is held to the game's `teams.min` and `teams.max`, the same bounds the session UI enforces when a facilitator adds or removes a team. It previously accepted any number, so `--teams 50` against a game declaring a maximum of eight opened 50 teams. A value outside the bounds is now clamped, with a warning naming what was asked for and what is running
|
|
585
|
+
- `sdk dev` derives its starting team count from the game's `teams.min` again. The `--teams` option carried a default of `1`, which the server could not tell apart from a value typed by hand, so the derivation never ran and a game requiring three teams started with one
|
|
586
|
+
- `sdk build` and `sdk build --validate-only` now check a scenario's `configuration` overrides against the `configurationSchema` of each module and user interface they target. Game-level overrides were already checked, scenario overrides were not, so a scenario that set a key the schema does not declare, or set a declared key to the wrong value, passed the whole local verification chain and then failed at session start. Overrides stay partial: omitting a key is still fine, and only undeclared or invalid keys fail
|
|
587
|
+
- The error raised when configuration does not match a `configurationSchema` no longer asserts that the offending entries are undeclared keys. It reports type and enum mismatches too, and those are as often a schema that has moved on as configuration that is stale, so the message now names both the configuration and the schema as candidates and reserves the `--reconfigure` suggestion for the case it actually fixes
|
|
588
|
+
|
|
589
|
+
## [0.0.114] — 2026-08-24
|
|
590
|
+
|
|
591
|
+
### Breaking Changes
|
|
592
|
+
|
|
593
|
+
- Four selectors are gone from `DEV_SERVER.selectors` in `@businesssimulations/seriousgaming-sdk/e2e`, because the elements they addressed are gone: `sessionButton` (the manage-session dialog), `configureButton`, `localeChip`, and `teamSelectors(n).manageButton` (the per-team facilitator dialog). Their replacements are `platformViewButton` / `gameViewButton` for the view toggle, `quickConfigButton` and `languageMenuItem` inside the session menu, and nothing at all for the manage-team dialog. A suite using any of the four fails on an element that never appears, so migrate against the table in the [e2e testing recipe](recipes/e2e-testing.md). The exported set is now checked against the dev UI on every run, so a selector cannot go stale again without a test failing
|
|
594
|
+
|
|
595
|
+
### Added
|
|
596
|
+
|
|
597
|
+
- `renderSessionUserInterface()` provides the hook-failures context, so a surface calling `useSessionUserInterfaceHookFailures()` can be rendered in a test at all — the hook throws without its provider, and the helper omitted it. New `hookFailures` and `clearHookFailures` options seed failures and observe the clear, matching the other pushed-state options
|
|
598
|
+
- `DEV_SERVER` now covers the session shell and the participant roster, which the session surface work left unreachable through the supported export. New selectors for the chrome band and canvas (`sessionBar`, `viewToggle`, `gameViewButton`, `platformViewButton`, `sessionCanvas`, `sessionCanvasFrame`), every session menu entry (`quickConfigButton`, `scenarioChip`, `guideButton`, `waitingRoomButton`, `languageMenuItem`, `themeToggleButton`, `sessionReloadButton`), the waiting room strip, the rest of the dev tools panel, and the scenarios dialog. `teamSelectors(n)` gains `addParticipantButton` and `observeButton`; a new `participantSelectors(n, id)` addresses one participant's row; `localeOption(code)` picks a locale inside the language submenu. Suites that had resorted to hardcoding `[data-testid="platform-view-button"]` can drop the literal
|
|
599
|
+
- `DEV_SERVER.participantUrl(n, id)` and `DEV_SERVER.observeUrl(n)` build the canonical participant path and the observer path, so a multi-role suite stops hand-assembling `/play/1/participant/3`. The [e2e testing recipe](recipes/e2e-testing.md) now documents the participant flow end to end: create over `POST /api/teams/:teamId/participants`, then address the id it returns. Worth reading if your suite creates a second participant, since `GET /api/teams/:teamId/participant` resolves the team's _default_ whatever you pass it, and a suite that assumed otherwise has been asserting on the first participant while believing it drove the second
|
|
600
|
+
|
|
601
|
+
### Changed
|
|
602
|
+
|
|
603
|
+
- The `moveParticipant` docs no longer say a move loses 'anything they had on screen unsubmitted', which read as though moving one participant discarded the team's pending decisions for the next tick. It never did: a move does not touch team context. What is lost is local component state in that participant's browser, because their surface remounts. The consequence worth designing for is the other one — their earlier `setData` writes stay with the source team and do not travel — and the answer is the `participantMove` hook re-seeding on `arrival`, which the callback's docs did not mention at all. `requestTick` no longer describes itself as a 'submit' button either, since 'submitted' means the `setData` mutation everywhere else in the SDK
|
|
604
|
+
- The [session user interfaces guide](guides/session.md) is migrated to the 0.0.113 API. It still taught `useSessionUserInterfaceTeams()` returning `{ teams, interactionMode }`, documented the removed `setInteractionMode` message, and called the game-flow callbacks fire-and-forget when they return promises; none of its snippets compiled. It now covers the roster, meta, team-links and hook-failures APIs it had omitted entirely
|
|
605
|
+
- The scaffolding template's `CLAUDE.md` no longer tells you to build facilitator UIs as `manage-team-user-interface` at `/manage-team`, a resource type 0.0.113 removed, so a package scaffolded today starts on guidance that matches the SDK it is scaffolded against
|
|
606
|
+
- The docs are now guarded against naming an API the SDK no longer exports. Nothing type-checks the snippets in these pages, so a removed API could keep working in prose long after the build rejected it in code — which is how the session surface guide reached 0.0.113 with none of its examples compiling. A deny-list check now runs over every consumer-facing document and the scaffolding template, with migration guides and this changelog exempt so a removal can still be explained. It catches an API that is gone, not a snippet that is merely wrong; the fuller extract-and-compile pass is still open
|
|
607
|
+
|
|
608
|
+
### Fixed
|
|
609
|
+
|
|
610
|
+
- **Session mutations now settle.** `createTeam`, `renameTeam`, `renameParticipant`, `moveParticipant` and `setWaitingRoom` applied their change and never sent the correlated response, so the promise each returns was pending for ever and `await callbacks.createTeam()` hung after the first call. The symptom reads as slowness rather than a hang, which is how it costs an afternoon: the team is created, the acknowledgement never arrives, and a surface reporting progress honestly sits on 'adding team 1 of 5'. Their failures were silent in the same way — a refusal broadcast an error to every client and never rejected the caller — so the documented `MutationError` was unreachable for all five. A refusal that cannot be retried (the team count is at the maximum, a shared-state session has already ticked, the participant named is not on the team addressed) now rejects as terminal `denied`; anything else as `error`. The existing suites drove these commands without a `requestId`, so nothing asserted the reply; there is now a test covering the whole specified mutation list, and adding a fallible mutation without acknowledging it fails it
|
|
611
|
+
- **A capability referenced only by a hook, endpoint, or facilitator surface is seeded with its schema defaults.** Defaults were collected from modules, player user interfaces, and validators only, so a capability declared in the `reads` of a hook, an endpoint, a session UI, a shell UI, a waiting-room UI, or a tick controller was absent from context until something wrote it: reading it gave `undefined` rather than its declared default, and a shared-scope one was missing from the shared store too. Reported from a game whose session surface read a new shared capability on a clean session. If your game worked around this with a fallback on the read, the fallback is now dead code
|
|
612
|
+
- **A session surface no longer paints before the host has sent its team bounds.** `setTeamConfiguration` now gates first paint alongside the other required messages. Until it arrived a surface held `max: 0`, so the natural `teams.length < max` check read as 'this session is full' and disabled the control it guarded — a game declaring a maximum of 100 could show 'this session cannot take any more teams'. The pre-handshake default is unchanged and now unreachable in a running session. A host that never sends `setTeamConfiguration` leaves the surface on its fallback, which is the correct response to a missing required message
|
|
613
|
+
- **A moved participant's surface is remounted against their new team.** The dev server reassigned the record and dispatched `participantMove` but left the participant's own page rendering the team they had left, until they reloaded by hand — so a `participantMove` hook could not be seen working in development, though production runtimes implement the remount. A write already in flight against the previous team is now denied as terminal rather than being applied to a team the participant is no longer on
|
|
614
|
+
- Reconfiguring the dev server no longer drops the state a `participantJoin` hook seeded. `POST /api/reconfigure` rebuilt the context without replaying the join hooks, so every participant already in the roster lost whatever the hook had written for them: role assignments, seat allocations, per-participant state of any kind. It failed silently, and nothing restored it, because the roster survives a reconfigure and so the lazy default-participant seeding that would have re-fired a join was a no-op. Reset, Play, and a code reload all replayed correctly; reconfigure was the one rebuild path that did not. It bit e2e suites hardest, since `reconfigureDevServer()` is the documented `beforeEach` reset, so a game seeding roles on join started every test with them missing. A reconfigure of a game that wires no `quick-config` surface also used to announce a configuration stage that game has no way to render, sending any connected view to a blank configure screen while a view connecting a moment later got the play stage for the same session; it now rebuilds the session in place, which is what a game with no pre-play step actually wants
|
|
615
|
+
|
|
616
|
+
## [0.0.113] — 2026-08-09
|
|
617
|
+
|
|
618
|
+
### Breaking Changes
|
|
619
|
+
|
|
620
|
+
Work through the upgrade guide: it gives the ordered steps, the rename table, and what the build will tell you when one is missed.
|
|
621
|
+
|
|
622
|
+
- The `manage-team-user-interface` resource type is **removed**, and `manage-session-user-interface` is renamed to `session-user-interface`. A game now ships one whole-session facilitator surface that the host renders full-bleed beneath a thin band of its own chrome, owning everything below it: teams, participants, and game controls. The per-team facilitator dialog is gone, and everything it did is reachable from the session surface, which already receives every team's context through `SessionData` and can target any team by `teamId`. Rebuild against the new SDK: rename the `manage-session-user-interfaces/` directory and its `.sdk.json` files, delete any manage-team UI, and move its behaviour into the session surface. In code, every `ManageSessionUserInterface*` export is now `SessionUserInterface*` (wrapper, contexts, hooks, callbacks), every `ManageTeamUserInterface*` export is gone, and `ManageTeamParticipant` is now `SessionParticipant`: the shape is unchanged, and it was never manage-team-specific
|
|
623
|
+
- A game's session UI is wired in through a **singular** `definition.sessionUserInterface` object rather than the old `manageSessionUserInterfaces` array, so a game cannot express two. The host renders the surface with no tab strip, no card, and no toolbar, and so has no affordance by which a facilitator could pick between them
|
|
624
|
+
- `useSessionUserInterfaceTeams()` now returns `TeamInfo[]` directly instead of `{ teams, interactionMode }`. The interaction mode moved to the new `useSessionUserInterfaceTeamConfiguration()` alongside the team bounds, so there is one home for how teams work and one for which teams exist
|
|
625
|
+
|
|
626
|
+
### Added
|
|
627
|
+
|
|
628
|
+
- The session surface can now read the session itself. `useSessionUserInterfaceMeta()` returns `{ sessionId, name, readOnly, waitingRoomOpen, joinPin }`, pushed on connect and again on every change. `readOnly` marks a frozen session so a surface can disable its own controls and explain why rather than offering affordances that will be denied: it is a courtesy signal, and the host enforces the freeze itself on every write. `waitingRoomOpen` closes a real gap: a session UI could already _write_ the waiting-room flag through `setWaitingRoom`, but nothing ever told it the current value, and since the host usually renders its own waiting-room control too, both writers exist by design and the surface must read the result back rather than assume its write won
|
|
629
|
+
- `useSessionUserInterfaceTeamLinks()` returns each team's facilitator-only URLs, so a game draws its own QR codes, join sheets and watch-this-team controls instead of calling back for them. They travel on their own message to that surface alone, deliberately not as a field on `TeamInfo`, which player surfaces also receive: a join URL together with the PIN is joinable capability, and the boundary belongs in which surface gets which message rather than in a rule nobody can see. There is no session-level entry; every one names a team. Each carries:
|
|
630
|
+
- `joinUrl`, the form the host chose. A host may hold several forms of the same link (a shortened one, a longer one on another domain, a canonical one carrying opaque ids), and picking between them, including deciding a short link has expired, is host bookkeeping. A game never has to know which form it received, and MUST encode this one in a QR code: the host chose the short form because a denser code is harder to scan across a room
|
|
631
|
+
- `fallbackJoinUrl`, present only where the host has a distinct alternative, for the one failure a game can help with: some networks block URL shorteners, and the facilitator who finds that out is standing in front of the game's surface. Show it on request rather than beside `joinUrl` with equal weight
|
|
632
|
+
- `observeUrl`, which opens the team's play surface as an observer (no participant identity, no presence). It is required, not optional: The runtime specification now states that observing is required of every compliant runtime, because a facilitator has to be able to see what a team sees and a session UI that owns the canvas leaves the host nowhere else to put that. Observing admits nobody to the session, so unlike joining it is tooling rather than capability and a game may link to it directly
|
|
633
|
+
- `useSessionUserInterfaceTeamConfiguration()` returns `{ mode, min?, max, perTeam? }`. `max` is the **effective** maximum the host will actually enforce, already resolved as the lower of the game's declared maximum and the platform's own ceiling, so a surface disables 'add team' at the right point rather than at a manifest number the platform may never reach. Every bound here is for rendering honest affordances, not for enforcement: the host checks all of them itself
|
|
634
|
+
- The session surface owns the roster. Four new callbacks: `createTeam(name?)`, `renameTeam(teamId, name)`, `renameParticipant(participantId, name)` and `moveParticipant(participantId, teamId)`: each resolve on commit and reject with a `MutationError`. Three refusals arrive as `{ kind: 'denied' }`: the team count is at the effective maximum, a team is being added to a shared-state session (whose team count is fixed at creation, though the dev server relaxes that pre-tick as an authoring convenience), or the session is read-only. **A denial is terminal**: unlike `conflict`, waiting and re-sending cannot change the outcome, so surface it rather than looping. Deleting a team, and creating or erasing a participant, are deliberately not exposed
|
|
635
|
+
- `useSessionUserInterfaceHookFailures()` reports hook chains that threw, so a facilitator hears about them on the surface they are actually looking at. The runtime specification previously said only to surface a hook error 'to the facilitator or operator', with the transport implementation-defined, which was written when that surface was host-rendered: under a session takeover the facilitator is looking at the game's iframe and would never see it. It is deliberately not a failed mutation: hooks fire after the change they react to is applied, so the mutation that triggered them has already settled `ok: true`, and reporting it as failed would invite retrying an action that worked
|
|
636
|
+
- A new `participantMove` hook signal fires when a facilitator reassigns a participant. It is dispatched **once per affected team**: against the source team's context with `phase: 'departure'`, then against the destination's with `phase: 'arrival'`: because a hook returns one team's context and a move touches two. `phase` is explicit rather than inferred: `participant.teamId` is post-change on both invocations and cannot distinguish them. The signal exists because `participantJoin` is how a game seeds per-participant state in a team's context, and a move otherwise strands that state in the source team with nothing to re-seed it in the destination; no amount of pushed roster data fixes that, because the fix is a write. Note there is no transfer channel: the departure hook's return value is not visible to the arrival hook, so a shared-state game carries state across through the shared store and an independent-mode game re-derives it. The roster of record remains `setParticipantList`, which is pushed live and accurate the instant a move lands: a context-resident roster is a derived cache
|
|
637
|
+
|
|
638
|
+
- `sdk build` now warns when a resource declares a read of a capability that nothing in the package writes, so the read can only ever see the capability's schema defaults. This is usually a leftover from a writer that was deleted, or a set of constants that belongs in configuration rather than in a capability. It is a warning and never fails the build; the check is package-wide, so a capability written anywhere, by any module, user interface, or hook, is never flagged, however the games are composed. It does not detect the narrower case of a resource that declares a read it never actually uses in its logic, which needs analysis of the logic bundle
|
|
639
|
+
- `sdk build` now checks every `default` declared in a resource's schemas against the subschema that declares it, so a default that cannot satisfy its own schema is caught at build time instead of seeding invalid state at runtime. It covers `variablesSchema`, `configurationSchema`, and an endpoint's `requestSchema` / `responseSchema`, at any depth, and catches the whole class: an object default carrying a property the subschema does not declare under `additionalProperties: false`, a default of the wrong type, one outside a declared `enum`, `minimum` / `maximum`, or `pattern`, and one that omits a field its own object marks `required`. The error names the resource, the path to the offending default, and the constraint it breaks. Only packages that already carry such a default are affected; if yours does, either correct the default or widen the subschema to admit it. The SDK's own `examples/typescript` had one (a `HealthStatus` defaulting to `'Unknown'`, a value absent from its own `enum`), which is fixed in this release, and its generated union now includes `'Unknown'` so the type matches the value the capability actually holds before the first tick
|
|
640
|
+
- Lint now catches author logic reaching for a host global that the production runtime does not provide, at author time instead of at tick time. The scaffolded `oxlintrc.json.template` restricts around 40 absent globals (`structuredClone`, `fetch`, `TextEncoder` / `TextDecoder`, `setTimeout` and the other timers, `process`, `Buffer`, `URL`, `crypto`, `window`, `document`, and the rest) inside every resource's `logic/` directory, each with the replacement to use: so `pnpm run lint` and your editor flag the reference before you run anything. Since 0.0.108 the dev server and CLI already threw on these, matching the bare-V8 production runtime; this moves the signal earlier. Two deliberate gaps: the rule matches bare identifiers, so `globalThis.fetch(...)` is not caught, and it is off for colocated `*.test.ts` files, because those run under Vitest in Node where the globals genuinely exist: meaning a green unit test still does not prove your logic is portable. Existing packages should re-merge `oxlintrc.json.template` and `CLAUDE.md.template` from the shipped templates. The restricted environment is now documented for consumers too, in the [modules guide](guides/engine.md#the-language-environment-your-logic-runs-in) and the [testing guide](guides/testing.md#the-runtime-your-tests-do-not-reproduce), where it previously appeared only in this changelog
|
|
641
|
+
- `useOwnTeam()`, a new player UI hook (from `@businesssimulations/seriousgaming-sdk/ui`), returns the `TeamInfo` of the team whose surface the UI is rendering: `{ teamId, index, name }`, or `null` if the host supplies neither an own team nor a participant to derive one from. It closes a gap left by the observer feature in 0.0.111: own-team identity was only reachable through `useUserInterfaceParticipant()?.teamId`, which is `null` under observation, so a UI that needed its own `teamId` (to split a `useCrossTeamData()` result into 'mine' and 'theirs', to label a board, or to key a write) worked when played and broke when observed. The host always knows which team's surface it mounted, so it now says so directly and the value is present for participants and observers alike. Note the difference from the plural `useUserInterfaceTeams()`, which lists every team in the session with no marker of which is the caller's. The `renderUserInterface` test helper takes a matching `ownTeam` option; for a player surface it defaults to the participant's team resolved against `teams`, so most existing tests need no change. See the [adapt a UI for observers recipe](recipes/adapt-ui-for-observers.md)
|
|
642
|
+
- Two new recipes for games where teams transact with each other. [Negotiate across teams](recipes/negotiate-across-teams.md) documents the shape every negotiation game needs, since a player UI cannot write another team's state: each side publishes intent into a `team-readable` ledger keyed by proposal id, counterparties read across with `useCrossTeamData`, and an aggregate module settles mutually-agreed pairs atomically at tick time, with the rules that matter (only settle an agreed pair, reject what cannot be paid, make ordering deterministic) called out. [Share logic between a module and a UI](recipes/share-logic-between-engine-and-ui.md) documents something that already worked but was written down nowhere: a pure helper in a plain file can be imported by both module logic and a UI, because esbuild and Vite each inline what they import, so a live preview and the settled result can run the identical formula instead of two copies kept in step by hand. Both are exercised in `examples/coffeeshop`, whose market rules now live in `shared/market.ts` and back both the aggregate module's settlement and the decisions UI's 'at this price you would win N%' preview
|
|
643
|
+
|
|
644
|
+
- A new `waiting-room-user-interface` resource type lets a game supply its own holding screen for the session waiting room, instead of accepting the host's stock panel. The waiting room is a single per-session flag the facilitator toggles: while it is open every player sees your bundle instead of the game UIs, and closing it swaps them back with no reload. Game state is untouched in both directions: per-team context, the current tick, and timers all survive, which is the point, since a facilitator uses it to park a live session for a discussion and resume exactly where teams were. Scaffold one with `pnpm run sdk new waiting-room-user-interface` and reference it from a game under `definition.waitingRoom`; a game references at most one, and a game that references none keeps the host's built-in panel. The room is a player user interface in a different display context rather than a new kind of surface: `reads`, `writes`, `endpoints`, `crossTeam` reads, and `crossTeamParticipants` all behave exactly as they do on a `user-interface`, and it renders under the same `UserInterfaceWrapper` with the same `useUserInterface*` hooks, so there is no second API to learn. It fills the play surface alone: no tab bar, and the shell header and footer are suppressed while it is up, so carry your own branding inside the bundle. Read `currentTick` to tell 'not started yet' (0) from 'parked mid-play', which is the distinction the built-in panel's fixed 'waiting to begin' wording gets wrong. See [create a waiting room UI](recipes/create-waiting-room-ui.md)
|
|
645
|
+
- Tick controllers now receive `waitingRoomOpen` in their arguments, so a game decides for itself whether an open waiting room stops the clock. Opening the room pauses nothing on its own: automatic ticks keep firing while players wait, matching the production runtime: so a game that wants play genuinely halted returns `timerEnabled: false` while the flag is true, and a game whose simulation should keep advancing in the background ignores it. Without this, a facilitator who parks a session for a ten-minute discussion returns to teams whose game has moved on without them
|
|
646
|
+
- Manage-session user interfaces gain a `setWaitingRoom(open)` callback, so a game can build its own 'start session' control rather than relying on the host's toggle. Alone among the session controls it takes no `teamId`: the waiting room is one flag for the whole session. It resolves once the host confirms the change and rejects with a `MutationError` otherwise, and it is not gated by `uses`: it sits with the other session flow controls outside the `ApiUse` enum
|
|
647
|
+
- The dev server surfaces the waiting room on its home page, next to the other session-level controls, for any game that wires a waiting room UI in. The toggle is hidden for games that do not, since the dev server does not reimplement the host's built-in panel. The flag persists to `state.json`, so a parked session stays parked across a server restart
|
|
648
|
+
|
|
649
|
+
### Changed
|
|
650
|
+
|
|
651
|
+
- The dev server's home page is now the session shell it will be in production: a thin band of host chrome over a canvas the game fills. The game's session UI renders full-bleed under the bar rather than in a dialog, and a Game view / Platform view toggle swaps it for the roster. That toggle sits outside the iframe, where a game cannot suppress or restyle it, and the choice is sticky per package. A package that ships no session UI gets no toggle at all: the roster is simply the session page rather than a mode
|
|
652
|
+
- The chrome bar carries three things and no more: the game name, the view toggle, and one session menu. Waiting room, facilitator guide, quick config, scenario, language and theme all live in that menu, each showing its current value beside it, so the state is readable without opening anything and costs the bar no width. The bar no longer moves items between containers at a breakpoint: only the toggle's labels drop below `sm`, and the title truncates: which is what made it fall apart on a phone
|
|
653
|
+
- An open waiting room is now a full-width strip under the bar rather than a chip inside it. It is a session-wide condition that hides the game from every participant, not a setting: closed it costs nothing, open it cannot be missed and carries its own Close
|
|
654
|
+
- The runtime specification no longer calls the whole `teams` block advisory, which had drifted from what runtimes actually do. Enforcement is now stated per field: `teams.max` is hard at every point and resolved against the platform's ceiling (40 on the reference platform, previously invisible to authors), `teams.min` is hard at session creation only, and the `perTeam` participant bounds are parsed but unenforced today. Making the `perTeam` bounds hard is tracked separately
|
|
655
|
+
- The game UI now gates first paint on `setAssetUrls`, matching what the SDK wrapper has always done. One part of the specification said the wrapper gated on it while another listed it only for configuration UIs; the spec now says the same thing in both places
|
|
656
|
+
|
|
657
|
+
- The scaffolded package template (`package/package.json.liquid`) now pins refreshed dependency versions (React 19.2.8, Vite 8.2, jsdom 30, `@testing-library/jest-dom` 7, Playwright 1.62, i18next 26.3.6, oxlint 1.76, and oxfmt 0.61, among others), clearing all but one of the outstanding `pnpm audit` advisories. Every version is one that was published at least seven days before the release, so none of them is a just-published build that nobody has had time to look at. The exception is `nanoid`: the release that patches its one remaining advisory (a high, reached only through `vite` at build time) was five days old at the time of writing, and holding to the seven-day rule mattered more than clearing a finding that will clear itself. Two bumps need a follow-up step once you have reinstalled: Playwright 1.62 ships a new browser build, so run `pnpm exec playwright install chromium` or your `test:e2e` run fails on a missing executable, and oxfmt 0.61 collapses an object literal whose only content is a block comment onto one line, so expect a small formatting diff on the first `pnpm run format`
|
|
658
|
+
- `new package` now scaffolds a `pnpm-workspace.yaml`, and the pnpm `overrides` block has moved there out of `package.json`. This is a fix rather than a tidy-up: pnpm 11 no longer reads the `pnpm` key in `package.json` at all, and ignores it without failing the install, so on pnpm 11 the overrides a scaffolded package shipped with have been resolving nothing. The visible symptom is `pnpm audit` reporting advisories against `sharp` and `protobufjs` that the overrides were supposed to have cleared, and a one-line `[WARN] The "pnpm" field in package.json is no longer read by pnpm` buried in the install output. **Existing packages should copy `pnpm-workspace.yaml` from the shipped template, delete the `pnpm` key from their `package.json`, and reinstall**: then confirm the file took effect by checking that `pnpm-lock.yaml` has an `overrides:` block near the top. pnpm 10 reads the new location too, so the move is safe whichever you are on
|
|
659
|
+
- The scaffolded overrides are down from six to two, now that the rest are no longer earning their place. Only `sharp` (`^0.35.0`) and `protobufjs` (`>=8.4.1`) remain, both pulled in by the SDK tooling's embedding dependencies through parents whose declared ranges reach nothing patched. The `undici` override in particular is gone: it existed to hold jsdom's copy to a patched 7.x, and jsdom 30 now depends on the 8.x line directly. `fast-uri`, `js-yaml`, `nanoid`, `postcss`, and `tar` were each verified to resolve to a patched version unaided and dropped. If you carried these overrides in your own package, drop them too rather than copying them forward: an override that pins a transitive dependency you do not otherwise control is a thing to remove as soon as the parent's range catches up, not to accumulate
|
|
660
|
+
- `sdk dev` now starts the Vite server that serves your user interfaces on the first request for a UI asset, rather than during startup. The server is ready to accept connections sooner, a session that only drives the CLI or the REST API never starts one at all, and hot reload behaves as before once a UI is open. Two consequences worth knowing: a misconfigured `vite.config.ts` now surfaces when you first open a UI instead of when the server boots, and the HMR port (the one reported as in use if something else has claimed it) is not bound until then. The motivation is stability rather than speed: Vite 8's native bundler releases references to JavaScript objects from one of its own threads, which corrupts the interpreter's bookkeeping and can take the whole process down with a segmentation fault that leaves no error message behind. Every Vite server created is another chance to hit it, so the ones nothing asked for are no longer created
|
|
661
|
+
|
|
662
|
+
### Fixed
|
|
663
|
+
|
|
664
|
+
- The dev server now hot-reloads the logic of every logic-bearing resource type, not just modules. Editing a hook, validator, endpoint, or tick-controller `logic.ts` while `sdk dev` was running previously did nothing at all: only `modules/**/*.ts` was watched, and each compiled bundle is cached until the game reinitialises, so the edit was silently ignored until you stopped and restarted the server. All five kinds now reload the same way a module edit does
|
|
665
|
+
- Saving a colocated test file (`*.test.ts`, `*.spec.ts`) next to a resource's logic no longer reinitialises the running session. Tests are never part of a logic bundle, so the reset was pure interruption while writing them
|
|
666
|
+
- The documented aggregate-module pattern no longer models a per-team value as a shared capability, in the [multi-team games guide](guides/multi-team-games.md), the [modules concept page](guides/engine.md#aggregate-engines), and `examples/coffeeshop`. All three put `MarketShare` in a `scope: 'shared'` capability and then wrote a different value per team into the `teams` slice, which cannot work: a shared capability holds one value for the whole session and every team's view re-composes from it, so each team read the shared placeholder (`0`) instead of its own share, and any revenue derived from it came out at zero. Copying the pattern into your own game reproduced the bug, and the failure is silent when the shared slice carries a placeholder, since nothing diverges for the runtime to reject. Market share now lives in its own default-scope capability written per team, and the guide spells out the split, including the quiet variant of the mistake. If your game has a per-team figure in a shared capability, split the capability along its actual scope: session-wide values stay shared and travel in the `shared` slice, per-team values move to a default-scope capability and travel in the `teams` slice
|
|
667
|
+
- Corrected the `game update` invocations in the multi-team guide and the [CLI simulation testing guide](cli-simulation-testing.md). They passed the patch as `--patch '<json>'`, a flag `game update` does not accept (it belongs to `game tick`), so every copied command failed with `unknown option '--patch'`. The patch is a positional argument: `pnpm run sdk game update <game-id> '<json-patch>' -- --team 1`, as `cli-reference.md` already documented
|
|
668
|
+
|
|
669
|
+
- Dev-server participant ids are now unique across the session rather than within a team. Two teams could each hold a participant `1`, which made 'the host resolves which team owns this participant' ambiguous and meant a facilitator move could not preserve the participant's id the way the runtime contract requires. Restoring a persisted roster keeps the counter ahead of every restored id, not just the team's
|
|
670
|
+
|
|
671
|
+
### Removed
|
|
672
|
+
|
|
673
|
+
- `renderManageTeamUserInterface()` and `createMockManageTeamTheme()` from the testing helpers, along with every `ManageTeamUserInterface*` context, hook and wrapper. Use `renderSessionUserInterface()` and `createMockSessionTheme()`
|
|
674
|
+
|
|
675
|
+
## [0.0.112] — 2026-07-24
|
|
676
|
+
|
|
677
|
+
### Fixed
|
|
678
|
+
|
|
679
|
+
- Game validation now treats a game with no declared `teams.max` as potentially multi-team, matching how the production runtime resolves team count. A game in the default `independent` mode whose module writes a shared-scope capability now fails the build unless it either declares `teams.mode: 'shared-state'`, sets `teams.max: 1` for a genuine single-team game, or makes the capability team-scoped. Previously an omitted `max` was read as single-team, so the build passed locally but the same game was rejected on deploy to production. Only games that already relied on this gap are affected; if yours does, pick one of the three resolutions the error message now lists
|
|
680
|
+
- The dev server's automatic port fallback no longer spins the CPU when the preferred port is in use: the port search now steps linearly to the next free port and cleans up after each attempt, instead of accumulating listeners and fanning out under contention
|
|
681
|
+
|
|
682
|
+
## [0.0.111] — 2026-07-24
|
|
683
|
+
|
|
684
|
+
### Breaking Changes
|
|
685
|
+
|
|
686
|
+
- `useUserInterfaceParticipant()` now returns `UserInterfaceParticipant | null` (previously `UserInterfaceParticipant`). It returns `null` when the surface is being observed rather than played (see the observer feature below), because an observer has no caller identity. A player UI that reads the participant unconditionally must add a `null` guard, for example `const participant = useUserInterfaceParticipant(); if (!participant) return <Observing />;` before reading `participant.id` / `.name` / `.index` / `.teamId`. UIs that never expose an observe entry point still only ever see a non-null participant at runtime, but the type change is surfaced so the observer case cannot be missed
|
|
687
|
+
|
|
688
|
+
### Added
|
|
689
|
+
|
|
690
|
+
- `useViewerRole()`, a new player and shell UI hook (from `@businesssimulations/seriousgaming-sdk/ui`), returns whether the current viewer is a `'participant'` or an `'observer'`. An observer is a facilitator watching a team's live play surface without joining — presence-invisible, with no participant identity (`useUserInterfaceParticipant()` is `null`). Most UIs need no change: an observer sees and can drive the surface exactly as a participant does. Use the hook to adapt — hide a participant-only control, reveal otherwise-hidden state, or present a read-only debrief view. On the dev server, observe a team from its overflow menu ('Observe team') or at `/play/<team>/observe`. See the [adapt a UI for observers recipe](recipes/adapt-ui-for-observers.md)
|
|
691
|
+
- `getPastCrossTeamData(capabilityId, tick)`, a new player and manage-team UI callback (from `useUserInterfaceCallbacks` / `useManageTeamUserInterfaceCallbacks`), reads every team's value of a `team-readable` capability as it stood at a committed past tick — the historical counterpart of the live `useCrossTeamData` hook. It returns the same `{ teamId, name, value }` entries, so a review screen renders live and past cross-team values with the same code. Head-exclusive and `null`-for-miss like `getPastTickSnapshot`, shared-state games only, and gated by the UI's declared `crossTeam` reads (not `uses`). See the [review a past day recipe](recipes/review-a-past-day.md). `CrossTeamCapabilityValue` is now also exported from the root `@businesssimulations/seriousgaming-sdk` entry
|
|
692
|
+
- `getAllTeamsParticipants()`, a new player and manage-team UI callback (from `useUserInterfaceCallbacks` / `useManageTeamUserInterfaceCallbacks`), reads the current participant roster of every team so a player UI can render a cross-team directory of who is on each team. It returns one flat `UserInterfaceTeamParticipant[]` (`{ id, name, index, connected, teamId }`), each entry tagged with its `teamId`; join against `useUserInterfaceTeams()` for team names. It is a point-in-time snapshot, not a live subscription (call it again to refresh; the caller's own team stays live through `useUserInterfaceParticipants`). Available in shared-state games to a UI that declares `crossTeamParticipants: true` in its bundle; an undeclared UI receives an empty array. A player or manage-team UI that declares `crossTeamParticipants` in a game that is not shared-state now fails the build. See the [show all teams' participants recipe](recipes/show-all-teams-participants.md)
|
|
693
|
+
|
|
694
|
+
### Changed
|
|
695
|
+
|
|
696
|
+
- The scaffolded player and manage-team UI test templates (`user-interface/src/starter/App.test.tsx`, `manage-team-user-interface/src/App.test.tsx` and its `starter` variant) now include `getPastCrossTeamData` and `getAllTeamsParticipants` in their mock callbacks, since both are required members of `UserInterfaceCallbacks` / `ManageTeamUserInterfaceCallbacks`. A package that hand-builds a full callbacks mock in its own UI tests must add these (for example `vi.fn()`) when it upgrades; re-merge from the shipped templates
|
|
697
|
+
|
|
698
|
+
### Fixed
|
|
699
|
+
|
|
700
|
+
- A manage-session UI now receives the participant roster of every team in the session, not just `team-1`. The dev server previously fed manage-session UIs a single team's roster, so `useManageSessionUserInterfaceParticipants()` under-reported in multi-team sessions
|
|
701
|
+
|
|
702
|
+
## [0.0.110] — 2026-07-23
|
|
703
|
+
|
|
704
|
+
### Added
|
|
705
|
+
|
|
706
|
+
- `ContextUpdate<T>`, exported from `@businesssimulations/seriousgaming-sdk` and `.../ui`, is the deep-partial type `setData` now accepts on every surface (player, manage-team, and manage-session): object values may be given partially and merge recursively, while arrays and scalars are given whole and replace. Writes that previously had to spread the full capability to satisfy `Partial<T>` can now pass just the changed fields
|
|
707
|
+
- `deepMergeContext`, exported from `@businesssimulations/seriousgaming-sdk`, implements the runtime contract's context merge (plain objects recursed; arrays, scalars, and `null` replace; prototype-polluting keys ignored) shared by the dev server and the UI wrappers' optimistic local merge
|
|
708
|
+
|
|
709
|
+
### Changed
|
|
710
|
+
|
|
711
|
+
- The dev server now deep-merges `updateContext` payloads into the team's current context, matching the runtime contract (plain objects merged recursively; arrays, scalars, and `null` replace the existing value). Previously a single-team write replaced the whole context and a team-addressed write only shallow-merged top-level keys. A write can no longer delete a key by omitting it from a full-context payload: set the value to `null` instead and treat `null` as absent
|
|
712
|
+
- Facilitator shared writes (`updateSharedData`) deep-merge into the canonical session store with the same merge semantics, so a partial shared write no longer blanks untouched fields of the shared capability
|
|
713
|
+
- The scaffolded starter user interface (`user-interface/src/starter/App.tsx` and its `App.test.tsx`) now sends only the changed field in its `setData` call instead of spreading the whole capability; existing packages can re-merge from the shipped templates
|
|
714
|
+
|
|
715
|
+
### Fixed
|
|
716
|
+
|
|
717
|
+
- `setData` no longer transmits the participant's entire merged context: each call sends only the update you pass, and the host deep-merges it. Two participants on the same team can now submit different decisions at (nearly) the same time without overwriting each other's untouched fields through stale full-context payloads. The same applies to the manage-team `setData` and the manage-session `setData(teamId, update)`
|
|
718
|
+
- The optimistic local merge behind `setData` now deep-merges nested objects instead of replacing top-level keys wholesale, so a minimal nested patch no longer makes sibling fields vanish from the local view until the next snapshot. Spreading the whole capability into every write (`setData({ cap: { ...data.cap, field } })`) is no longer necessary; prefer sending only the changed fields. The new [design state for concurrent writes recipe](recipes/design-state-for-concurrent-writes.md) covers the patterns that build on this: id-keyed maps instead of shared arrays, `null` tombstones, keying by participant id, and validator arbitration for contested slots
|
|
719
|
+
|
|
720
|
+
## [0.0.108] — 2026-07-06
|
|
721
|
+
|
|
722
|
+
### Breaking Changes
|
|
723
|
+
|
|
724
|
+
- The UI testing helpers (`createMockTheme` and its per-surface variants, `createMockParticipant`/`createMockParticipants`, the `renderUserInterface` family, and their `Render*Options`/`Render*Result` types) are no longer exported from `@businesssimulations/seriousgaming-sdk/ui`. Import them from the dedicated `@businesssimulations/seriousgaming-sdk/ui/testing` subpath instead. This keeps `@testing-library/react` — now declared as an **optional** peer dependency — out of the runtime `/ui` entry, so a game bundle no longer has to resolve it in production. To migrate, move any `createMock*` / `render*` import from `.../ui` to `.../ui/testing`; a package that uses these helpers should have `@testing-library/react` in its devDependencies (it is only needed for tests). The scaffolded UI test templates (`user-interface`, `configuration-user-interface`, `manage-team-user-interface`, `manage-session-user-interface`, and `shell-user-interface` `App.test.tsx`, including their `starter` variants) and the guidance in `CLAUDE.md.template`, `workflow-create-user-interface.md`, and `workflow-test-module-or-ui.md` now import from the new subpath; existing packages should re-merge these from the shipped templates
|
|
725
|
+
|
|
726
|
+
### Added
|
|
727
|
+
|
|
728
|
+
- `encodeUtf8` and `decodeUtf8` helpers, imported from `@businesssimulations/seriousgaming-sdk`, convert between a string and its UTF-8 `Uint8Array`. Use them to produce bytes for `writeBlob` or read bytes from `readAsset` / `extractText`: the production runtime has no `TextEncoder` / `TextDecoder`, and these compile into your bundle so they run identically in the dev server and in production. They handle the full Unicode range, including emoji and other astral-plane characters
|
|
729
|
+
|
|
730
|
+
### Changed
|
|
731
|
+
|
|
732
|
+
- The dev server and CLI now run module, hook, tick-controller, validator, and endpoint logic in a restricted JavaScript environment that matches the bare-V8 production runtime: only the ECMAScript intrinsics are available as globals. Host globals that Node happens to provide (`structuredClone`, `fetch`, `setTimeout`, `TextEncoder`, `process`, `Buffer`, `URL`, and the like) are gone. Previously logic ran with Node's full global scope, so code reaching for one of these worked locally and then threw `ReferenceError` only once deployed. Such code now fails the same way during `sdk dev` and `game tick`, surfacing the problem before deploy. Migrate by moving the work behind a callback or an equivalent: replace `structuredClone(value)` with `JSON.parse(JSON.stringify(value))` (game state is JSON-serialisable), and `new TextEncoder().encode(text)` with `encodeUtf8(text)`. `console` is unaffected: the SDK now compiles a `console` shim into every bundle, so `console.log` keeps working in development and is safely discarded in production instead of throwing (previously it worked in dev and crashed in the production runtime).
|
|
733
|
+
- The `@businesssimulations/seriousgaming-sdk/e2e` subpath now ships TypeScript types: its `exports` entry gained a `types` condition, so importing it resolves types under `nodenext`/`node16` module resolution instead of silently falling back to `any`
|
|
734
|
+
- The dev server now coalesces a burst of file-change events (for example an editor 'save all') into a single reinitialise-and-reset within a short window, instead of running one full cycle per changed file
|
|
735
|
+
- The resource `new`, `update`, and `delete` commands (for every resource type) now route their step-by-step output through the shared logger, so it respects `--verbose`. By default a successful command prints just its confirmation line (for example `✓ Module created successfully!`); the detailed file and reference changes are shown with `--verbose`. Errors and warnings always print
|
|
736
|
+
- The scaffolded package template (`package/package.json.liquid`) now pins refreshed dependency versions (React 19.2.7, MUI 9.2, Vite 8.1.3, Vitest 4.1.10, i18next 26.3, oxlint 1.72, and oxfmt 0.57, among others) and holds its pnpm overrides for `protobufjs` (`>=8.4.1`) and the jsdom test-only `undici` transitive dependency (`^7.28.0`) to their patched releases, clearing the outstanding `pnpm audit` advisories. A newly scaffolded game starts on the current toolchain; existing packages can re-merge these from the shipped template
|
|
737
|
+
|
|
738
|
+
### Removed
|
|
739
|
+
|
|
740
|
+
- The Electron desktop builder (the `builder/` workspace package) is removed. It was a thin wrapper that opened a package and embedded the dev server in an iframe; run `pnpm run sdk dev` directly instead, which provides the same game execution and testing surface in your browser
|
|
741
|
+
|
|
742
|
+
### Fixed
|
|
743
|
+
|
|
744
|
+
- The dev server's scenario and shared-asset routes now reject a request whose resolved path escapes into a sibling directory that merely shares the assets-root name prefix (for example an `assets-evil` folder next to `assets`), applying the same strict path-boundary check the facilitator-guide route already used. Previously a bare prefix check could serve such a file
|
|
745
|
+
- In a multi-team game with a `participantJoin` hook, adding a participant to one team mid-session no longer throws the teams out of step on a later session-wide undo. Previously the join grew only that team's history, so a session undo rewound the other teams a tick too far — leaving `getPastTickSnapshot` returning `null` for a committed past tick on the teams that were not joined. Undo now rewinds every team to the same tick
|
|
746
|
+
- An external write to a running session's `state.json` (for example a CLI `game tick` against the package the dev server is serving) is now applied under the same lock as ticks, undo, and shared writes, so it can no longer interleave a tick in progress or silently drop a persist that lands during the reload
|
|
747
|
+
- When a saved game configuration no longer matches the current schemas, the error now leads with the remediation ('Configuration is invalid or incompatible with current schemas...' plus the `--reconfigure` command to run) instead of burying it under a misleading 'Failed to load configuration file' prefix. That prefix is now reserved for genuine read or JSON-parse failures
|
|
748
|
+
- Resource commands (`new`, `update`, and `delete` for every resource type) now exit with a non-zero status code when they fail. Previously a failed command (invalid definition, missing package file, unknown resource) printed the error to stderr but exited 0, so CI pipelines, scripts, and AI agents saw it as a success. Error messages are unchanged apart from formatting, so anything matching on stderr text keeps working
|
|
749
|
+
- Tick controllers now receive their configuration at runtime. A tick controller that read a value from `config` (for example a completion threshold) previously got `undefined` for every field, even those with a schema `default`, because tick-controller configuration was never resolved. Defaults declared in a tick controller's `configurationSchema` are now applied, so `config`-driven timing and completion logic works as documented
|
|
750
|
+
- The manage-session and manage-team pages in the dev server now refresh when you add, remove, or rewire a facilitator user interface while the server is running. Previously the open page kept showing the old set of UIs (a newly added one appeared to be missing) until you restarted the dev server; it now reloads on a resource or manifest change, matching the participant play surface
|
|
751
|
+
- Renaming a module with `update module` now rewrites the game's `moduleOrder` alongside its `modules` list. Previously the ordering entry kept the old identifier, leaving the game referencing a module that no longer existed
|
|
752
|
+
- Deleting or renaming a capability now updates every resource that reads or writes it — hooks, validators, endpoints, tick controllers, and the manage-team, manage-session, and shell user interfaces — not just modules and standard user interfaces. Previously those references were left dangling
|
|
753
|
+
- Renaming a module or user interface now updates the `modules` / `userInterfaces` lists of any configuration user interface that references it, instead of leaving a stale identifier
|
|
754
|
+
- Renaming a scenario now updates the games that reference it, and renaming or deleting a facilitator guide now updates its game references too. Previously these left the game pointing at a missing resource
|
|
755
|
+
- Renaming a package now migrates every resource type it contains — including tick controllers, hooks, validators, endpoints, scenarios, facilitator guides, and the manage-team, manage-session, shell, and configuration user interfaces — and preserves per-game user-interface configuration. Previously it only rewrote capabilities, modules, user interfaces, and games, and dropped any `configuration` on a game's user-interface references
|
|
756
|
+
|
|
757
|
+
## [0.0.107] — 2026-07-02
|
|
758
|
+
|
|
759
|
+
### Breaking Changes
|
|
760
|
+
|
|
761
|
+
- The `generateText`, `extractText`, and `generateSpeech` callbacks are removed from every user interface surface (participant, manage-team, manage-session, configuration, and shell). They reach credential-holding external services (LLM, document parsing, text-to-speech) and are now exposed only to server-side resources: modules, hooks, tick controllers, and endpoints. A user interface reaches them by declaring an endpoint that lists the callback in its own `uses` and invoking that endpoint by identifier (`callbacks.endpoints['<id>'](request)`), so the credential-holding work stays server-side; the dev server no longer proxies these callbacks to iframes (the previous non-blocking warning is now a hard error). To migrate, move each direct `callbacks.generateText` / `callbacks.extractText` / `callbacks.generateSpeech` call in a UI into an endpoint and invoke the endpoint instead, then remove the callback from the UI bundle's `uses` array — a UI that still lists one fails `sdk build`. See the [call an endpoint from a UI recipe](recipes/call-endpoint-from-ui.md)
|
|
762
|
+
- A game's singular `configurationUserInterface` reference is replaced by a `configurationUserInterfaces` map keyed by configuration surface (`scenario-builder`, `quick-config`), so a game can wire in a distinct configuration UI per context. Each surface is optional, and the same identifier may be wired into both. The host shows one launch affordance per configured surface and tells each UI which surface it is rendering (read it with `useConfigurationUserInterfaceSurface()`), so a shared bundle can branch. To migrate, move your existing reference into the map under the surface that matches its role — most existing config UIs are scenario builders, so `scenario-builder` is the default mapping. See the upgrade guide
|
|
763
|
+
- The UI `uploadBlob` callback no longer accepts a `scope` option. A blob's deletion lifetime is now assigned by the host from the UI type: a configuration UI's uploads survive game resets (and are cleared only on reconfigure), and every other UI surface's uploads are cleared on game reset. This removes a footgun (a configuration UI that forgot the option silently uploaded a reset-wiped blob) and keeps the bundle blind to retention, a host concern. To migrate, drop the `{ scope: ... }` argument from any `callbacks.uploadBlob(file, { scope })` call in a UI; a call that still passes it fails to type-check. Module and endpoint `writeBlob` are unchanged and still take an explicit `scope`. See [File handling](guides/file-handling.md#blob-scopes)
|
|
764
|
+
|
|
765
|
+
### Added
|
|
766
|
+
|
|
767
|
+
- Multi-participant testing in the dev server. Each team's home-page card now has an expandable participant roster where you can add, rename, remove, and open individual participants, so a game with several roles can be driven from one dev-server session instead of juggling separate browsers. Every team starts with one default participant, and opening a participant now uses a canonical URL (`/play/:n/participant/:id`); the bare `/play/:n` still works and resolves the team's default. A stale or removed participant link shows a recoverable 'participant not found' screen rather than silently creating a mismatched participant. The roster (names included) survives a dev-server restart. See the [test a multi-role game recipe](recipes/test-a-multi-role-game.md)
|
|
768
|
+
- A `getPastTickSnapshot` UI callback for building read-only 'review a past day' screens: a game, manage-team, or manage-session UI can read a committed past tick's team view (`callbacks.getPastTickSnapshot<Context>(day)`, or `getPastTickSnapshot(day, teamId)` on a manage-session UI). It returns the board as it stood at that tick (the point-in-time shared store included) and `null` for any tick that is not a committed past tick (at or above the head, undone, reset, or out of range); a genuine read fault rejects with a `TickSnapshotError`. Participant UIs also gain `currentTick` on `useUserInterfaceData()` for bounding a scrubber. Not gated by `uses`. Note that a reset now bulk-deletes reviewable history, so it is a player-visible, destructive action wherever a UI exposes review; warn facilitators before reset. Debounce scrubbing and cache fetched snapshots (they are immutable until an undo lowers the head). See the [review a past day recipe](recipes/review-a-past-day.md). If your UI tests build a callbacks or data-context object by hand (rather than via the SDK `renderUserInterface` helpers), add `getPastTickSnapshot` to the callbacks and `currentTick` to the participant data context; the scaffolded `src/App.test.tsx` templates for `user-interface`, `manage-team-user-interface`, and `manage-session-user-interface` now include them
|
|
769
|
+
- A 'Reload everything' action in the dev server's session menu (the three-dots menu on the home page) that clears all participants, reloads scenarios and resources from disk, and resets the session to a clean slate — more thorough than a reset, useful after editing bundles on disk
|
|
770
|
+
- A `shared:<path>` asset reference scheme that resolves against a package-level `shared-assets/` directory at the package root, so several scenarios can reference the same media from one source instead of each shipping a copy. Use it anywhere an `asset:` reference is valid (for example `shared:storyline/avatars/ceo.png`); it resolves in the dev server and a hosted runtime, is resolvable even when no scenario is active, and `getAssetUrl` / `readAsset` / `extractText` handle it like any other reference. See the [share assets across scenarios recipe](recipes/share-assets-across-scenarios.md)
|
|
771
|
+
|
|
772
|
+
### Fixed
|
|
773
|
+
|
|
774
|
+
- `sdk delete scenario` now also removes the scenario from any game's `scenarios` array, not just the package manifest. Previously a deleted scenario left a dangling reference in referencing games, which surfaced as a broken entry in the dev server's scenario picker
|
|
775
|
+
|
|
776
|
+
## [0.0.106] — 2026-06-28
|
|
777
|
+
|
|
778
|
+
### Breaking Changes
|
|
779
|
+
|
|
780
|
+
- Game-level config (`modules[].configuration` in `game.sdk.json`) and scenario overrides are now validated against each module's and user interface's `configurationSchema`, the same way `configuration.json` already was. Previously an undeclared or misspelled key in these two sources was silently dropped, so the declared field kept its schema default (a hard-to-trace bug where a control 'only worked after opening the admin config and pressing play'). A key that is not declared in the target resource's schema now fails validation. To migrate, remove or rename any config key in a `game.sdk.json` module override or a scenario override that is not declared in that resource's `configurationSchema`. At `game init` or CLI load the offending keys are named and the command exits non-zero; in a running dev server the change is rejected, the last-good config keeps running, and the failure surfaces as a dismissible error
|
|
781
|
+
|
|
782
|
+
### Added
|
|
783
|
+
|
|
784
|
+
- `createMockParticipant` and `createMockParticipants` test factories in `@businesssimulations/seriousgaming-sdk/ui`, alongside the existing `createMockTheme` helpers, for testing participant-aware UIs. The test-a-user-interface recipe now documents the `participant` and `participants` options on `renderUserInterface()`
|
|
785
|
+
|
|
786
|
+
### Changed
|
|
787
|
+
|
|
788
|
+
- The runtime specification now documents that a hosted runtime may use optimistic concurrency and re-run a tick when a write races it, so a module's `Init`, `Run`, `RunAggregate` and tick-controller logic may execute more than once for a single logical tick. Author logic must be idempotent: derive output only from the input context, configuration and callback results, and never rely on a side effect (a counter, or a single `generateText` / `writeBlob` call) firing exactly once per tick. The dev server serialises fully and runs each function once, so non-idempotent logic passes locally yet can misbehave under load on a hosted runtime
|
|
789
|
+
- The `new package` scaffold's `.claude/settings.json` now allows `Bash(pnpm start:*)` and no longer denies `pnpm run sdk dev` or `pnpm start`, so an agent working in a scaffolded package can launch the builder when asked. Affects `claude/settings.json.template`; to adopt in an existing package, merge from the shipped template at `node_modules/@businesssimulations/seriousgaming-sdk-tools/dist/templates/package/`
|
|
790
|
+
- The scaffolded UIs now bind their tick controls as `onClick={() => requestTick()}` (and likewise for `undoTick` / `resetGame`). The wrapping arrow keeps the React click event out of the callback (these callbacks now return a promise, and the multi-team forms take a leading `teamId`, so a bare `onClick={requestTick}` would pass the event in) and discards the returned promise. The previously-used optional chain (`requestTick?.()`) is dropped: `requestTick`, `undoTick`, and `resetGame` are non-optional on every callbacks type, so the `?.` was misleading. Affects `manage-team-user-interface/src/App.tsx.liquid` (and its `starter/`), `manage-session-user-interface/src/App.tsx.liquid` (and its `starter/`), and `user-interface/src/starter/App.tsx.liquid`; merge from the shipped templates at `node_modules/@businesssimulations/seriousgaming-sdk-tools/dist/templates/package/`
|
|
791
|
+
- The `new package` scaffold's guidance now uses `pnpm run --silent` for every `--json` command, so output an AI agent or CI script parses is clean JSON rather than JSON preceded by pnpm's banner line (which makes `JSON.parse` throw). `CLAUDE.md.template` gained a note explaining the rule, a pointer to `renderUserInterface()` plus the `createMockParticipant` / `createMockParticipants` factories for participant-aware UI tests, and the `workflow-create-user-interface.md` rule gained a note that a UI's `config` is only its own bucket (never a module's). Affected templates: `CLAUDE.md.template` and the `workflow-create-capability.md`, `workflow-create-module.md`, `workflow-create-endpoint.md`, `workflow-create-facilitator-guide.md`, `workflow-create-user-interface.md`, `workflow-test-module-or-ui.md`, `workflow-update-sdk-version.md`, and `workflow-publish-package.md` rules. To adopt in an existing package, merge these from the shipped templates at `node_modules/@businesssimulations/seriousgaming-sdk-tools/dist/templates/package/`
|
|
792
|
+
- The fallible UI state callbacks — `setData`, `updateConfig`, `requestTick`, `undoTick` and `resetGame` (including the manage-session shared and per-team `setData` forms) — now return a promise. It resolves once the operation commits (a `setData` once the write is persisted, a `requestTick` once that tick has finished) and rejects with a `MutationError` whose `outcome` discriminates the failure: `conflict` (retryable — the write raced a tick), `validation-rejected` (carries the validator rejections), `denied`, `tick-failed` or `error`. Existing call sites that ignore the return value keep working unchanged, though a no-floating-promises lint may flag them. Await the call to detect and retry a `conflict`:
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
import { MutationError } from '@businesssimulations/seriousgaming-sdk/ui';
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
await setData({ revenue });
|
|
799
|
+
} catch (e) {
|
|
800
|
+
if (e instanceof MutationError && e.outcome.kind === 'conflict') {
|
|
801
|
+
await setData({ revenue }); // a tick raced the write; retry
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
`pauseTimer`, `resumeTimer`, `setThemePreference` and `switchUI` are unchanged, as they cannot meaningfully fail. Against a hosted runtime that does not yet send the correlated response an awaited mutation stays pending until the connection resets; the dev server always responds. See [Awaiting state changes](guides/ui.md)
|
|
807
|
+
|
|
808
|
+
### Fixed
|
|
809
|
+
|
|
810
|
+
- The dev server home page no longer briefly flashes the configured game view before redirecting an unconfigured (or just-reset) game to the configuration screen. Previously the home page rendered for a frame on connect and then bounced to `/configure`, a visible flicker for facilitators
|
|
811
|
+
- Dev server controls (Play, Tick, Undo, Reset, Add team, and the rest) no longer silently drop when clicked before the WebSocket has finished connecting or during a reconnect. The command is now queued and delivered once the connection is ready, so a Play click on a cold-starting server reliably starts the session instead of leaving the page stuck on the configure screen
|
|
812
|
+
- The dev server now sends `X-Content-Type-Options: nosniff` on game UI documents, mirroring the production asset host. A bundle that relied on browser MIME sniffing (a script served as `text/plain`, a stylesheet with the wrong type) now fails on localhost as it would in production, instead of passing locally and breaking live. See [Production security constraints](guides/production-constraints.md)
|
|
813
|
+
- Creating a capability with a `scope` (`team-readable` or `shared`) now writes that scope to `capability.sdk.json`. Previously `new capability` dropped the field, so the capability defaulted to per-team and the scope had to be re-added with `update capability`. You can also set it without writing JSON via `seriousgaming-sdk new capability --scope shared`
|
|
814
|
+
- Calling `setData(...)` immediately followed by `requestTick()` from a UI now reaches the host in that order, so the write is applied before the tick runs. Previously the `setData` message was deferred and arrived after the tick, overwriting the tick result with stale pre-tick data, which made any 'submit then advance' flow appear to do nothing. Affects the participant, manage-team and manage-session UIs
|
|
815
|
+
|
|
816
|
+
## [0.0.105] — 2026-06-18
|
|
817
|
+
|
|
818
|
+
### Breaking Changes
|
|
819
|
+
|
|
820
|
+
- `callbacks.generateText` now runs through the Serious Gaming platform's LLM API instead of calling OpenAI directly, authenticated with the same platform API key `package publish` uses. Set `SERIOUSGAMING_API_KEY` (or `platform.apiKey` in global settings, or `--api-key` on `sdk dev`); the `OPENAI_API_KEY`, `OPENAI_API_BASE`, `OPENAI_MODEL_QUICK/STANDARD/EXPERT`, and `LLM_PROMPT_CACHE` environment variables, the `openai.*` global settings keys, and the `--openai-*` and per-tier model flags are removed. Tier-to-model mapping is configured on the platform: calls select a model by `tier` only, raw model names are not accepted, and pointing the SDK at a custom OpenAI-compatible endpoint is no longer supported. `GameRunner`'s `llm` option is renamed `platform` (`{ apiKey, apiHost }`)
|
|
821
|
+
|
|
822
|
+
### Added
|
|
823
|
+
|
|
824
|
+
- `generateText` responses now carry an optional `usage` field (`inputTokens` / `outputTokens`) with the provider-reported token counts for the call
|
|
825
|
+
- The participant view's Dev Tools overlay gains Reset, Undo and a light/dark theme toggle alongside the existing timer and locale controls. Reset prompts for confirmation, Undo is disabled when there is nothing to undo, and in independent-mode multi-team games both target the viewed team
|
|
826
|
+
|
|
827
|
+
### Changed
|
|
828
|
+
|
|
829
|
+
- The dev server now serves game UIs under the same content security policy they run under in production (assets must be first-party, no external CDNs or fonts, no blob-URL workers, outbound network only through a module, endpoint, or host capability). UIs that relied on external resources will now fail in the dev server as they would in production, surfacing the problem before deployment. See the new [Production security constraints](guides/production-constraints.md) guide
|
|
830
|
+
- The user interface scaffold templates now size their root with `100dvh` and a `100vh` fallback rather than `100vh` alone, so a UI opened standalone on iOS is not cut off behind the URL bar. To adopt this in an existing package, merge the change from `templates/user-interface/src/starter/App.tsx`, `templates/manage-team-user-interface/src/App.tsx` (and its `starter/`), and `templates/manage-session-user-interface/src/App.tsx` (and its `starter/`). See [Sizing to the viewport on mobile](guides/ui.md)
|
|
831
|
+
|
|
832
|
+
### Fixed
|
|
833
|
+
|
|
834
|
+
- The dev server host shell now sizes to `100dvh` with a `100vh` fallback, so on iOS Safari and Chrome game content is no longer hidden behind the browser's URL bar. Applies to the play, configure, manage-team, manage-session, guide, and disconnected screens
|
|
835
|
+
- `sdk update capability` now preserves a capability's `scope` (and any other fields you do not re-supply) instead of silently resetting it to the default `team` scope when the replacement JSON or interactive edit omits it. A shared capability no longer becomes per-team after an unrelated edit
|
|
836
|
+
- `sdk build` now starts from a clean output directory, so files you renamed or removed since the last build (for example UI assets) no longer linger in `dist/` and ship in the published package
|
|
837
|
+
- CLI `game` commands now write `state.json` atomically (temp file plus rename). When `game tick` runs against a live `sdk dev` server, the server's file watcher can no longer observe a half-written, unparseable state file
|
|
838
|
+
- A no-op resource bundle save (for example a formatter rewriting a `.sdk.json` with no semantic change) no longer rewinds a running single-team session. The dev server now checks whether the configuration actually changed before reloading, instead of re-initialising the game first and only then comparing
|
|
839
|
+
|
|
840
|
+
## [0.0.104] — 2026-06-07
|
|
841
|
+
|
|
842
|
+
### Breaking Changes
|
|
843
|
+
|
|
844
|
+
- Shared-scope capabilities now live in one canonical session-wide store; team contexts carry views of it. Module and UI code is unchanged (`context['com.example.settings']` still works), but three things tighten up: a capability returned in the `shared` slice of an aggregate result must be `scope: 'shared'` (anything else fails the tick, naming the module and capability); an aggregate result whose `teams` contexts carry divergent values for a shared capability fails the tick instead of silently tearing state; and a module that writes a shared capability in an independent-mode multi-team game now fails game validation, because independent clocks give no coherent writer order for session-wide state. Shared capabilities in independent games are facilitator-only and truly session-wide
|
|
845
|
+
|
|
846
|
+
### Added
|
|
847
|
+
|
|
848
|
+
- Add a facilitator noticeboard to the typescript example: a shared-scope capability whose note is posted session-wide from the manage-session UI via the one-argument `setData` typed with the generated `SharedContext`, shown on the player decisions screen, and carried forward each tick by an aggregate module through the `shared` slice of its results. Manage-session team tabs are labelled by the stable team `index`
|
|
849
|
+
|
|
850
|
+
### Changed
|
|
851
|
+
|
|
852
|
+
- The specification now defines `teamId` as an opaque, session-stable string: `team-${index + 1}` is the reference derivation the CLI, dev server, and testing client use, and production runtimes may substitute globally unique identifiers. Games must not parse `teamId`; the stable team `index` is the only ordinal contract. The SDK and dev tooling behaviour is unchanged
|
|
853
|
+
|
|
854
|
+
### Fixed
|
|
855
|
+
|
|
856
|
+
- The `shared` slice of `InitAggregate` and `RunAggregate` results now writes the shared store, as the multi-team guide always documented. Previously every runner (CLI `game tick`, dev server, and `MultiTeamGameRunner`) discarded it, so state carried only through the shared slice stayed frozen at its init values with no error. Modules that write shared values into each `teams` context instead are unaffected: a unanimous value through that channel still lands in the store
|
|
857
|
+
- A facilitator shared write now survives a server restart: the store persists immediately on every write, even when no tick has happened since. Previously a mid-round write existed only in memory until the next tick boundary
|
|
858
|
+
- A per-team undo or reset in an independent-mode game no longer rolls back session-wide shared state: the restored team re-composes with the current store, so a facilitator note survives individual team rewinds. Lockstep undo in shared-state games still rewinds the round wholesale, shared store included
|
|
859
|
+
- A manage-session per-team override or context editor save no longer fails with 'touches shared capability' when the payload merely echoes shared values it did not change; a changed value is still rejected
|
|
860
|
+
- `TeamInfo` now carries the session-stable team `index`, completing the 0.0.103 entry that promised it. The dev server was already sending the index to every teams context at runtime, but the published type still read `{ teamId, name }`, forcing a cast to reach it. The manage-session user interface scaffold templates (`manage-session-user-interface/src/App.test.tsx.liquid` and its `starter/` variant) mock teams with the required `index` accordingly
|
|
861
|
+
- Generated `SharedContext` is now a type alias instead of an interface, so the documented pattern of typing the one-argument `setData` with it compiles. Previously it failed with `TS2344: Type 'SharedContext' does not satisfy the constraint 'Record<string, unknown>'`
|
|
862
|
+
|
|
863
|
+
## [0.0.103] — 2026-06-06
|
|
864
|
+
|
|
865
|
+
### Breaking Changes
|
|
866
|
+
|
|
867
|
+
- Removing a team no longer renumbers the teams above it. A removed team's ids and participants keep their values, so the `team-N` sequence can have gaps; the next team you add refills the lowest unused slot, recovering its name and identity. Code that assumed teams were a contiguous `1..N` range, or that addressed teams by array position, should key off the stable team `index` instead
|
|
868
|
+
- Shared-state games no longer accept new teams after the first tick. A team added before the first tick re-initialises the session so `InitAggregate` sees every team; after the first tick the request is rejected, because the aggregate state has already been computed without it. Independent-mode games are unaffected
|
|
869
|
+
- A player or manage-team user interface that declares `writes` on a `scope: 'shared'` capability now fails the build, and a team-addressed context update (player `setData`, `game update --team N`) that touches a shared capability is rejected by the dev server in multi-team games. Previously such writes were silently half-applied to one team's context, leaving teams seeing different 'shared' values. Session-wide writes belong to manage-session UIs (see Added)
|
|
870
|
+
|
|
871
|
+
### Added
|
|
872
|
+
|
|
873
|
+
- Teams now carry a session-stable `index`, surfaced on `AggregateFunctionTeam`, on `TeamInfo` in every teams context, and used to apply `teams.defaultNames`, giving shared-state games an authoritative mapping from teams to scenario roster slots
|
|
874
|
+
- Manage-session user interfaces can now write shared capabilities session-wide. Declare the `scope: 'shared'` capability in the UI's `writes` and call the new one-argument form `setData(update)`: one call, every team observes the same value, atomically. A write that lands while a tick is executing applies after the tick instead of being overwritten, and the next tick consumes it as input. Generated types gain a `SharedContext` interface covering the declared shared capabilities, and `MultiTeamGameRunner.updateShared(patches)` mirrors the write for headless tests. This replaces the per-team replicate-and-resync pattern for facilitator dials (live prices, thresholds, toggles). See the [multi-team games guide](guides/multi-team-games.md)
|
|
875
|
+
|
|
876
|
+
### Fixed
|
|
877
|
+
|
|
878
|
+
- Restarting the dev server after a configuration fix, or after a start-up recovery from an invalid configuration, no longer loses per-team state in multi-team games. Both paths wrote a snapshot without team contexts, so a restore from `state.json` fell back to freshly initialised teams; they now persist every team's context and stable index, and a configuration fix rebuilds per-team state the same way a reset does
|
|
879
|
+
- The dev server now serialises state-mutating operations (ticks, context updates, undo, reset, team changes), so a `setData` or `game update` that lands while a tick is executing is applied after the tick instead of being silently overwritten by it. Read-only operations are unaffected and never wait behind a tick
|
|
880
|
+
- The dev server now refuses a tick once the game's `ticks` limit is reached, broadcasting a `TickLimitError` instead of executing. Previously a `requestTick` from a game-owned UI could advance a finished game, so every game had to re-implement `latestTick < maxTicks` gating in its own controls; that check is now a UX nicety rather than a correctness requirement
|
|
881
|
+
- `sdk new module` no longer silently drops `"aggregate": true` from the definition. The created `module.sdk.json` now carries the flag, and the scaffolded logic exports `Init`, `InitAggregate`, and `RunAggregate` instead of the per-team `Init` and `Run`. Previously the module was created as a plain per-team module, so the build failed with confusing 'must export Init and Run' errors; patching the flag back with `sdk update module` is no longer needed
|
|
882
|
+
- Freshly scaffolded module and endpoint tests no longer fail `tsc` out of the box. The module test template (`module/logic.test.ts.liquid`) was missing the required `currentTick` and `locale` fields on its `ModuleMeta` literal, and the endpoint test template (`endpoint/logic.test.ts.liquid`) was missing the required `locale` argument to `Handle`
|
|
883
|
+
- Scaffolded tests for resources that declare `reads`, `writes`, or a `configurationSchema` with required properties now compile out of the box. The TODO placeholder objects in the module, hook, and endpoint test templates (`module/logic.test.ts.liquid`, `hook/logic.test.ts.liquid`, `endpoint/logic.test.ts.liquid`) are now cast (`as Context`, `as Config`), matching the validator template, so a generated `Context` or `Config` with required keys no longer rejects the placeholder
|
|
884
|
+
|
|
885
|
+
## [0.0.102] — 2026-06-05
|
|
886
|
+
|
|
887
|
+
### Breaking Changes
|
|
888
|
+
|
|
889
|
+
- The boolean `shared` flag on capability definitions has been replaced by a `scope` field. Change `"shared": true` to `"scope": "shared"` in each affected `capability.sdk.json`; a capability that still declares `shared` fails the build with a migration message. Capabilities without a `scope` remain per-team, exactly as before
|
|
890
|
+
- The dev server's built-in 'Next Tick' buttons are gone from the play, manage team, and manage session views, along with the `shell.submitButton`, `shell.manageTeam`, and `shell.manageSession` game options that hid them. Games provide their own tick controls through the `requestTick` callback (the scaffolded starter package already wires one on every surface); a game that still declares a removed option fails the build with a migration message naming the field. `shell.uiTabs` and `shell.configuration.playButton` are unchanged, and the dev tools panel keeps a developer-only 'Next tick' button. For Playwright tests, the `nextTickButton` selector is replaced by `devToolsNextTickButton` (open the panel with `devToolsButton` first), and the scaffolded `testing/e2e/game.spec.ts` template now ticks through the starter dashboard's own button — see the [E2E testing recipe](recipes/e2e-testing.md)
|
|
891
|
+
|
|
892
|
+
### Added
|
|
893
|
+
|
|
894
|
+
- Capabilities can now be marked cross-team readable in shared-state games. Set `scope: 'team-readable'` on a capability and declare the read with `crossTeam: true` on a player or manage-team user interface, and the new `useCrossTeamData` hook returns every team's live value of that capability (own team included), updating within about a second of any team's `setData`, without a tick. Each team still writes only its own copy, undeclared capabilities never reach other teams' screens, and undo, redo, and module execution are unaffected. See the [Observe other teams' state live](recipes/observe-other-teams-state.md) recipe
|
|
895
|
+
- Facilitator guides: a new `facilitator-guide` resource type bundles markdown facilitation content (per-locale chapters plus shared images) into the package, rendered natively on the dev server's facilitator page. A game declares guides via `definition.facilitatorGuides`; each appears as its own tab with a table of contents, a guide-local locale picker (defaulting to the session locale, with per-chapter fallback and a visible 'not yet translated' notice), and a print stylesheet. Content supports six admonition types (`note`, `warning`, `tip`, `say`, `do`, `timing`) and sandboxed YouTube embeds via fenced `video` blocks; raw HTML is escaped, never executed. Scaffold with `pnpm run sdk new facilitator-guide`; `pnpm run sdk build -- --validate-only` checks locale completeness, video blocks, and image references. The `new package` scaffold's CLAUDE.md and `.claude/rules/` now cover authoring guides
|
|
896
|
+
- Engine tests can now pin the scenario they run against. `GameRunner.create()` and `MultiTeamGameRunner.create()` accept a `scenarioId` option: pass a scenario identifier to select that scenario, `null` to opt out of the game's default scenario and test the base configuration, or omit it to keep the current behaviour (the game's default scenario, when one is declared). The chosen scenario applies on every `init()`. See the [engine testing guide](guides/engine-testing.md)
|
|
897
|
+
- The dev server home page has a session language switcher: a Language chip next to the scenario chip (shown when the game declares more than one locale in `supportedLocales`) that sets the locale for the whole session. Changes reach every open view live, including the dev tools locale select, manage dialogs, and participant tabs, and the choice now persists with the session so it survives a dev server restart. A `localeChip` selector is available in the `@businesssimulations/seriousgaming-sdk/e2e` constants
|
|
898
|
+
|
|
899
|
+
### Changed
|
|
900
|
+
|
|
901
|
+
- The scaffolded `workflow-update-sdk-version.md` rule now explains how to adopt updated scaffold guidance after a version bump: scan the changelog entries between the old and new version for scaffold and guidance mentions, then merge new or changed rules and `CLAUDE.md` advice from the templates shipped at `node_modules/@businesssimulations/seriousgaming-sdk-tools/dist/templates/package/`, keeping local edits. `CLAUDE.md.template` gained a pointer to the rule for bumps that happen outside it. Existing packages can adopt this convention the same way: copy the updated rule from the shipped templates path into `.claude/rules/`
|
|
902
|
+
|
|
903
|
+
### Fixed
|
|
904
|
+
|
|
905
|
+
- `new package` now copies the `workflow-create-endpoint.md` rule into the scaffolded `.claude/rules/` directory; the rule existed in the SDK but was missing from the scaffold's copy list
|
|
906
|
+
- The dev tools locale select on the manage team and manage session views only changed the language locally; it now updates the session locale on the server, exactly like the play view's select
|
|
907
|
+
- A locale set through the CLI (`game init --locale`, `game tick --locale`) was dropped from `state.json` the next time the dev server saved session state; the dev server now preserves and restores it
|
|
908
|
+
|
|
909
|
+
## [0.0.101] — 2026-06-04
|
|
910
|
+
|
|
911
|
+
### Breaking Changes
|
|
912
|
+
|
|
913
|
+
- The Playwright selector constants in `@businesssimulations/seriousgaming-sdk/e2e` have changed with the dev server redesign. `tickCounter`, `undoButton`, `resetButton`, `gameDrawer`, `menuButton`, `themeSelect`, and the old `manageTeamButton` are gone; new selectors cover the home page (`gameTitle`, `addTeamButton`, `manageSessionButton`, `sessionMenuButton`, `sessionUndoButton`, `sessionResetButton`, `confirmButton`, `iframeDialog`), per-team cards via `DEV_SERVER.teamSelectors(n)`, and a `DEV_SERVER.playUrl(n)` helper. Tests that started at `/` and clicked the Play FAB now land on the home page; navigate to `DEV_SERVER.playUrl(n)` for the participant view. See the [E2E testing recipe](recipes/e2e-testing.md)
|
|
914
|
+
|
|
915
|
+
### Added
|
|
916
|
+
|
|
917
|
+
- The dev server has a new home page at `/`. It shows the game, its active scenario, and a card per team with participant count, current tick, and per-team actions (manage team, open team in a new tab, and a menu with undo, reset, and remove). Session-level controls live in the header: theme toggle, configure, add team, manage session, and (in shared mode) undo tick and reset session. The configuration screen moved to `/configure`; an unconfigured game redirects there, and Play returns to the home page when the session starts
|
|
918
|
+
- Teams can now be added and removed at runtime from the dev server home page. A new team starts from the game's initial state (joining at the session's current tick in shared-state mode, at tick 0 in independent mode); removing a team renumbers the teams above it, participants included. Both actions respect the game's `teams.min` and `teams.max` bounds, and runtime team changes survive a dev server restart
|
|
919
|
+
- The home page shows the active scenario as a chip; clicking it opens a scenario picker. Activating a scenario from the home page restarts the session straight into play on the new scenario's defaults, without visiting the configuration screen (the configure screen's picker still returns to configuration)
|
|
920
|
+
|
|
921
|
+
- `new package` now scaffolds a manage team UI and a manage session UI alongside the team dashboard, all wired into the starter game. The manage team UI shows a team's values with facilitator overrides and tick controls; the manage session UI lists every team's values in one table with a session-wide tick button
|
|
922
|
+
- Games can now seed default team names. Declare `teams.defaultNames` in a game definition (an ordered, localised list where entry N names the Nth team) and the runtime uses those names as the defaults facilitators see, instead of 'Team 1', 'Team 2', and so on. A scenario can override them with its own `teams.defaultNames`, so a single game can ship different brands per scenario (for example a 'soft drinks' scenario naming its teams Coca-Cola, Pepsi, and Fanta). Names resolve in the session's locale, falling back to another declared locale and then to the positional default; operators can still rename teams at session setup
|
|
923
|
+
|
|
924
|
+
### Changed
|
|
925
|
+
|
|
926
|
+
- The dev server now detects on startup when the package configuration has changed since `state.json` was saved (for example after a branch switch while the server was down) and starts a fresh session instead of restoring state in the old shape. Game state now records the configuration it was created against (`game init` included); state files written by older SDK versions are treated as changed and reset once
|
|
927
|
+
- Manage session and manage team now open in dismissible dialogs (with a fullscreen toggle) on the dev server home page, instead of new browser tabs
|
|
928
|
+
- The dev tools panel has been redesigned. Play views (`/play/:n` and the manage pages) expose button position, locale, timer pause and resume, next tick, context, and config; the configure screen swaps those for theme, config, and scenarios. Session-level undo and reset, the team switcher, and the manage session and manage team buttons moved to the home page
|
|
929
|
+
- Saving the current configuration as a scenario ('Save', 'Save as new…') and resetting it to base now live in the configure screen's scenarios dialog, under 'Current configuration'
|
|
930
|
+
- The starter scaffold created by `new package` is now a styled, interactive landing page rather than a bare value readout. The capability carries `Delta` and `Total`, the dashboard sets `Delta` with increment and decrement buttons and requests ticks, and the module applies `Total += Delta` each tick, so a new project demonstrates the full decision → tick → compute → display loop out of the box. Each scaffolded UI ships a logo component, a distinct accent per surface, and localised strings, and the scaffolded engine and Playwright tests assert the real data flow instead of leaving commented-out placeholders
|
|
931
|
+
- The standalone `new manage-team-user-interface` and `new manage-session-user-interface` templates now scaffold working facilitator controls (and, for manage session UIs, a live table of teams and ticks) instead of blocks of commented-out example code
|
|
932
|
+
|
|
933
|
+
### Removed
|
|
934
|
+
|
|
935
|
+
- The module execution trace view has been removed from the dev tools panel
|
|
936
|
+
|
|
937
|
+
### Fixed
|
|
938
|
+
|
|
939
|
+
- `new package` now scaffolds the dependency versions the SDK is tested against (`@mui/material` 9, `i18next` 26, `react-i18next` 17, `vite` 8, and refreshed minor versions). Previously a fresh package installed several majors behind the workspace
|
|
940
|
+
- Changing a capability's `variablesSchema` (or module logic) while a multi-team game was running only rebuilt the shared context: each team kept serving its old-shape context, so team UIs reading newly added fields crashed, and the stale state survived a dev server restart via `state.json`. Watcher-triggered reloads now rebuild every team's context, history, and tick counter, and persist the reinitialised state, exactly like a manual reset
|
|
941
|
+
- A fresh `new package` scaffold now passes `pnpm run typecheck`, `pnpm run lint`, and `pnpm run format:check` out of the box. Previously a brand-new project failed all three before any code was written: the scaffolded `tsconfig.json` used the deprecated `baseUrl` option (a hard error under the pinned TypeScript 6), the font imports in `main.tsx` and the node globals in `playwright.config.ts` and the engine test had no type declarations, and several scaffolded files were not oxfmt-formatted. The scaffold now ships `@types/node`, a `globals.d.ts` covering CSS-only font packages, formatted templates throughout, and an `.oxfmtrc.json` that treats the CLI-managed `*.sdk.json` manifests and generated `types.ts` files as build artifacts
|
|
942
|
+
- Scaffolded manage session UI tests now mock the data context with the correct `sessionData` shape; previously they provided a `data` key that does not exist on the manage session data context, so the scaffolded test failed `tsc` as soon as the data hook was used
|
|
943
|
+
|
|
944
|
+
## [0.0.100] — 2026-06-02
|
|
945
|
+
|
|
946
|
+
### Breaking Changes
|
|
947
|
+
|
|
948
|
+
- `ModuleMeta` now carries a required `locale` field (alongside `currentTick`). At runtime the SDK always supplies it, so module, aggregate, and tick-controller logic need no changes. Unit tests that construct a `meta` object by hand must add it, for example `const meta = { currentTick: 0, locale: 'en' }`. Hook and endpoint handler args likewise gain a required `locale` field, so tests that build those args by hand must add it too
|
|
949
|
+
- A game's interaction mode is now a single, fixed property. The `teams.modes` array (which could hold `independent` and/or `shared-state`) has been replaced by a single `teams.mode` enum, so the mode is decided once by the game author rather than chosen per session. To migrate, replace `"modes": ["shared-state"]` with `"mode": "shared-state"` (and likewise for `independent`); a game that still declares `teams.modes` now fails the build with a message pointing at `teams.mode`. A game that declared both modes must pick one. If `teams.mode` is omitted it defaults to `independent`. Note the behaviour shift this implies: a multi-team game that previously omitted `teams.modes` ran as shared-state (synchronised ticks), but now defaults to `independent` (per-team ticks), so set `"mode": "shared-state"` explicitly if you rely on synchronised ticking
|
|
950
|
+
|
|
951
|
+
### Added
|
|
952
|
+
|
|
953
|
+
- Module, aggregate, and tick-controller logic can now read the active session locale through `meta.locale`, and hook functions and endpoint handlers receive it as a top-level `locale` argument, so logic that generates prose at runtime (for example via `callbacks.generateText`) can produce it in the session's language. The locale is session-wide: every participant in a session shares one language. The [internationalization guide](guides/internationalization.md) now shows how to localise scenario-authored content (inbox documents, narrative emails, briefings, transcripts) by resolving locale-keyed assets in the UI, rather than baking a single language into context at `Init`. `pnpm run sdk game init` and `pnpm run sdk game tick` accept a new `--locale` flag for testing a locale headlessly
|
|
954
|
+
|
|
955
|
+
### Fixed
|
|
956
|
+
|
|
957
|
+
- Scenario assets (avatars, hero images, video, audio, PDFs) served via `getAssetUrl` no longer 404 when the package is checked out under a path containing a dot-prefixed directory segment, such as a `.claude/worktrees/...` git worktree or a project under `~/.config/...`. Document text loaded as JSON, so the failure previously showed as 'images broken but documents present'
|
|
958
|
+
|
|
959
|
+
## [0.0.98] — 2026-05-31
|
|
960
|
+
|
|
961
|
+
### Added
|
|
962
|
+
|
|
963
|
+
- `pnpm run sdk package publish` now uploads under a named publisher. The platform's upload endpoint moved to `/api/v0/admin/publishers/<slug>/packages`, so the command resolves a publisher slug from (highest priority first) the new `--publisher` flag, the `SERIOUSGAMING_PUBLISHER` environment variable, or `platform.defaultPublisher` in your global settings, and fails with guidance if none is set. The first upload of a package binds it to that publisher and later versions must match (the platform returns 400 on mismatch). A 400 response now renders each structured validation error as `field: message` on its own line, so a rejected upload reads clearly. The default upload host is now `https://app.businesssimulations.com`. See the [Publish a package recipe](recipes/publish-a-package.md)
|
|
964
|
+
- New global settings file at `~/.seriousgaming-sdk/config.json` (override the location with the `SERIOUSGAMING_SDK_CONFIG` environment variable, which must be an absolute path). Store your OpenAI, ElevenLabs, and Serious Gaming platform keys and defaults once and every project picks them up, instead of re-exporting environment variables in each shell. Manage it with the new `pnpm run sdk config get <key>`, `pnpm run sdk config set <key> <value>`, and `pnpm run sdk config path` commands. The file is the lowest-priority configuration source: a `.env` file, environment variables, and CLI flags all override it, so CI and one-off shells keep winning. The file is plaintext and is created with owner-only permissions on macOS and Linux; never commit it to git. See the [Global settings guide](guides/global-settings.md)
|
|
965
|
+
- New `intervalTick(seconds)` builder for tick controllers, alongside the existing `scheduledTick()`, as the sanctioned way to return an interval-based result. It validates the cadence against the supported bounds (a 30-second floor and a 7-day ceiling on the effective interval) and rejects sub-floor, over-ceiling, fractional, zero, negative, `NaN`, and `Infinity` values with a clear message that names the limit and points at the helper to use. `scheduledTick()` now applies the same bounds to the effective cadence (`nextTickAt` minus now), so a schedule that ticks too often or too rarely is caught at authoring time rather than silently honoured at runtime. The SDK also publishes a runtime `TickControllerResult` JSON schema (`tickControllerResultSchema`), distinct from the resource-bundle manifest schema, so tooling and runtimes validate results against one shared definition. The tick-controller scaffold now returns `intervalTick(...)` by default
|
|
966
|
+
- The dev server now enforces the tick timing-safety contract at runtime: it rejects a sub-floor, over-ceiling, or malformed tick controller result with a clear error rather than scheduling it, bounds each automatic tick's execution time at the effective interval and surfaces a tick that overruns, and never crashes on a malformed result
|
|
967
|
+
- `pnpm run sdk build` now statically flags a tick controller whose declared or schema-default `interval` is below the 30-second floor or above the 7-day ceiling, with a message that names the limit and points at `intervalTick()`. The check is static-only (it catches literal and default values, while runtime-computed intervals are caught by the dev server and the production runtime) and the build output says so
|
|
968
|
+
- New `MultiTeamGameRunner` client for driving shared-state, multi-team games in-process, alongside the existing single-team `GameRunner`. Create it with an optional `teamCount` (falling back to the game's `teams.min`, then 1), then `init()`, `tick()`, `update(patches, { teamId })` and `getContext(teamId)` work per team. `tick()` returns each team's context keyed by positional team id (`team-1` … `team-N`), and `getContexts()` returns them all. Per-team `update()` rejects patches that target shared capabilities, since shared state may only change through `RunAggregate`. This lets games configured with `teams.modes: ['shared-state']` and more than one team be tested through the same SDK wrap as every other game, rather than hand-rolling an engine harness
|
|
969
|
+
|
|
970
|
+
### Fixed
|
|
971
|
+
|
|
972
|
+
- The dev server now restores per-team contexts when it loads `state.json`, so multi-team games keep each team's saved values across a restart. Previously only the shared context was restored and every team fell back to its post-`Init` defaults, which also meant `game update --team N` against a running dev server never surfaced in the UI and a multi-team game could not be seeded from a `state.json` fixture
|
|
973
|
+
|
|
974
|
+
## [0.0.97] — 2026-05-29
|
|
975
|
+
|
|
976
|
+
### Added
|
|
977
|
+
|
|
978
|
+
- User interfaces can now read, react to, and change the host's theme preference. Read it reactively with the new `useUserInterfaceThemePreference()` hook (and the matching per-surface hooks for manage-team, manage-session, configuration and shell UIs), which returns `'light'`, `'dark'`, or `'system'` and re-renders when it changes, just like `useUserInterfaceTheme()`. Change it with the `setThemePreference(preference)` callback, available on every UI surface: the host applies the choice, persists it, and broadcasts it back so every UI updates. This is the operator's underlying choice, distinct from the resolved light or dark colours delivered through the theme context, so a UI reading `'system'` knows the host is following the operating system rather than an explicit override. The dev server's light, dark and system switcher stays in sync with the preference, and no `uses` declaration is required because theme preference is not gated
|
|
979
|
+
|
|
980
|
+
### Fixed
|
|
981
|
+
|
|
982
|
+
- Multi-team games declaring shared-state mode now advance every team in lockstep when ticking from the dev server. Previously the dev-tools popover ticked (and undid) one team at a time, so per-team tick counters drifted apart and the shared cross-team aggregate was computed against stale peer state. Per-team tick counters are now shown only for independent-mode games
|
|
983
|
+
- Auto-reset triggered by editing a `.sdk.json` file now replays `participantJoin` hooks, so hook-driven state (such as role assignments) survives the reset. Previously, the auto-reset path silently dropped hook-driven state while the manual reset button preserved it
|
|
984
|
+
|
|
985
|
+
## [0.0.96] — 2026-05-20
|
|
986
|
+
|
|
987
|
+
### Fixed
|
|
988
|
+
|
|
989
|
+
- `pnpm run sdk build` now emits hooks and validators. Previously, packages declaring either built successfully but the resulting `dist/seriousgaming.json` omitted the `hooks` and `validators` arrays and the matching `dist/hooks/` and `dist/validators/` directories were never created, so any game subscribing to a hook or installing a validator silently dropped every side effect at runtime
|
|
990
|
+
- `pnpm run sdk build` now regenerates per-resource `types.ts` for hooks and validators alongside the existing module, UI, and tick-controller type generation. Previously, running `sdk build` after a schema change without first running `sdk generate` shipped stale generated types for hook and validator logic files
|
|
991
|
+
- `pnpm run sdk generate` now generates per-resource `types.ts` for manage session user interfaces. Previously, manage session UI types were only refreshed as a side effect of `sdk build`
|
|
992
|
+
- Packaged builder now scaffolds new packages against the latest published `@businesssimulations/seriousgaming-sdk-tools` instead of pinning to the builder's own version, so a builder cut against an older SDK can still produce packages on the current toolchain
|
|
993
|
+
|
|
994
|
+
## [0.0.95] — 2026-05-17
|
|
995
|
+
|
|
996
|
+
### Breaking Changes
|
|
997
|
+
|
|
998
|
+
- Material UI bumped from v7 to v9 (v8 was skipped by MUI). Consumer UI source files (user interfaces, manage-team UIs, manage-session UIs, configuration UIs, and shell UIs) that import from `@mui/material` need to follow the v9 prop removals. The patterns we hit while migrating the bundled examples and dev-server UI:
|
|
999
|
+
- `TextField inputProps={...}` is now `slotProps={{ htmlInput: {...} }}`. `InputProps`, `SelectProps`, `InputLabelProps`, and `FormHelperTextProps` move to the matching `slotProps.*` key
|
|
1000
|
+
- `Stack alignItems`, `justifyContent`, and `flexWrap` are no longer direct props; move them into `sx`
|
|
1001
|
+
- `Typography fontWeight`, `display`, and other system props are no longer direct props; move them into `sx`. `color="text.secondary"` and `color="textSecondary"` still work
|
|
1002
|
+
- `Box` no longer accepts system props (`display`, `paddingLeft`, etc.) as direct props; move them into `sx`. MUI's `npx @mui/codemod@latest v9.0.0/system-props <path>` automates the Box / Grid / Link / Typography sweep
|
|
1003
|
+
- Templates emitted by `pnpm run sdk new user-interface` (and siblings) were already on the v9 patterns; no template changes needed
|
|
1004
|
+
- Runtime plumbing callbacks are no longer optional on the SDK callback interfaces. `getAssetUrl`, `readAsset`, `requestTick`, `uploadBlob`, and `endpoints` are now required on `UserInterfaceCallbacks`, `ManageTeamUserInterfaceCallbacks`, `ManageSessionUserInterfaceCallbacks`, and `ConfigurationUserInterfaceCallbacks` where the host already provides them. `pauseTimer`, `resumeTimer`, `resetGame`, and `undoTick` are required on the manage-team and manage-session interfaces, and `readAsset` is required on `SignatureCallbacks`. ApiUse-gated callbacks (`generateText`, `extractText`, `generateSpeech`, `writeBlob`) and `requestPlay` remain optional. Existing code calling these with `?.` keeps working. Hosts (and consumer tests that construct a callbacks context value directly) MUST now provide the previously-optional plumbing callbacks
|
|
1005
|
+
- TypeScript bumped to 6.0. New defaults that surfaced fixes:
|
|
1006
|
+
- `document.getElementById('root')` returns `HTMLElement | null` against React's stricter `createRoot` types, so SDK templates and example UIs now use the non-null assertion (`createRoot(document.getElementById('root')!)`)
|
|
1007
|
+
- `noUncheckedSideEffectImports` defaults to true; example and builder tsconfigs explicitly set it to false to allow CSS-only side-effect imports like `@fontsource-variable/*`
|
|
1008
|
+
- The `node` (`node10`) `moduleResolution` value is deprecated; the builder migrates to `bundler`
|
|
1009
|
+
|
|
1010
|
+
### Changed
|
|
1011
|
+
|
|
1012
|
+
- Workspace dependency refresh: React 19.2.6, Vite v8, vite-node v6, @vitejs/plugin-react v6, vite-plugin-svgr v5, vite-plugin-checker 0.13, OpenAI SDK v6, Electron 42 + @electron/fuses v2, i18next v26 (aligned across SDK and example), react-i18next v17, react-markdown v10, @inquirer/prompts v8, @lancedb/lancedb v0.27, dotenv v17, esbuild v0.28, oxlint 1.65, oxfmt 0.50, plus assorted minor and patch bumps. OpenAI v6's tool-call wire shape gained a custom-tool variant; the LLM client now narrows to function tool calls. dotenv v17 prints a banner to stdout by default — every CLI `dotenv.config` call now passes `quiet: true` so JSON output stays parseable
|
|
1013
|
+
- `pnpm run sdk build` (and `--validate-only`) now reject manifests that would fail under a spec-compliant runtime. New build errors:
|
|
1014
|
+
- A user interface, manage-team UI, manage-session UI, or configuration UI requests an endpoint that the game does not list in `game.endpoints[]`
|
|
1015
|
+
- `game.endpoints[]`, `game.hooks[]`, `game.validators[]`, `game.tickController.identifier`, `game.manageTeamUserInterfaces[]`, or `game.manageSessionUserInterfaces[]` references a resource the package does not declare
|
|
1016
|
+
- `game.hookOrder` or `game.validatorOrder`, when present, contains an unknown identifier, has duplicates, or omits a declared hook or validator (parity with the existing `moduleOrder` check; both fields remain optional and the runtime continues to fall back to declaration order when omitted)
|
|
1017
|
+
- An endpoint, hook, validator, or tick controller reads or writes a capability that the package does not declare; the same check now also applies to configuration UIs
|
|
1018
|
+
- A manage-team UI, manage-session UI, or shell header/footer is missing a translation for a locale listed in `game.supportedLocales`
|
|
1019
|
+
- The `configuration` block on `game.tickController`, any `game.hooks[i]`, any `game.validators[i]`, any manage-team or manage-session UI reference, or `game.shell.header`/`footer` fails AJV validation against the target resource's `configurationSchema`
|
|
1020
|
+
- `pnpm run sdk build` also emits a non-blocking warning when `game.endpoints[]` lists an endpoint that no user interface in the game requests (dead allowlist)
|
|
1021
|
+
|
|
1022
|
+
## [0.0.94] — 2026-05-17
|
|
1023
|
+
|
|
1024
|
+
### Breaking Changes
|
|
1025
|
+
|
|
1026
|
+
- Cross-origin asset hosting. The host ↔ iframe protocol now pins origins and the iframe fetches assets directly from the asset host, with cookie-based auth refreshed over a new message. Consumer-visible changes:
|
|
1027
|
+
- The `readAsset` callback in user-interfaces is now a local `fetch(getAssetUrl(reference), { credentials: 'include' })` from inside the iframe. The host no longer mediates the body.
|
|
1028
|
+
- `pnpm run sdk build` now refuses to produce a bundle when `package.definition.version` in `seriousgaming.sdk.json` is not a valid Semantic Versioning 2.0.0 string. Builds that previously succeeded with a value like `'1.0'` or `'v1.0.0'` will fail with the offending value and the path to the source file. Run `pnpm run sdk package version <valid-semver>` (or hand-edit the file) and rebuild. Production runtimes can now order packages by semver precedence without re-parsing the field
|
|
1029
|
+
|
|
1030
|
+
### Added
|
|
1031
|
+
|
|
1032
|
+
- `pnpm run sdk new endpoint` (plus `update endpoint` and `delete endpoint`) for authoring server-side handlers that user interfaces invoke by identifier. An endpoint declares `requestSchema`, `responseSchema`, `reads`, and `uses`; the runtime validates the request, snapshots context for declared `reads`, builds guarded callbacks for declared `uses`, runs the handler, validates the response, and surfaces the result to the calling UI. Endpoints cannot mutate game context; binary results are returned via the existing blob protocol. UIs gate access by listing endpoints in their bundle's `endpoints` array, and `callbacks.endpoints['com.example.summarise'](request)` is typed end-to-end through the generated `types.ts`. New `writeBlob` `ApiUse` exposes server-side blob writes from endpoint handlers
|
|
1033
|
+
- `pnpm run sdk package publish` for uploading a built and bundled package to the seriousgaming.com platform. Auto-runs `package bundle` when the zip is missing, POSTs as `multipart/form-data` with `Authorization: Bearer $SERIOUSGAMING_API_KEY`, and prints the server's `PackageVersionResponse` on success. Configure with the new `SERIOUSGAMING_API_KEY` and optional `SERIOUSGAMING_API_HOST` (origin only - the CLI appends `/api/v0/admin/packages`) environment variables (or `.env`), or override per-invocation with `--api-key` and `--host`. Requires the API key holder to have the global SystemAdmin role
|
|
1034
|
+
- `pnpm run sdk build` now emits a non-blocking warning for every user interface, configuration UI, manage-team UI, manage-session UI, or shell UI whose bundle declares `'generateText'`, `'extractText'`, or `'generateSpeech'` in `uses`. These callbacks are proxied by the dev server but production runtimes are not required to expose them to UI iframes. The warning names the UI and the offending callbacks and points at the endpoints pattern. For deployed games, wrap the call in an endpoint (`pnpm run sdk new endpoint`) and invoke it from the UI via `callbacks.endpoints['…']`. Quick prototyping under `pnpm run sdk dev` is unaffected — the warning does not fail the build
|
|
1035
|
+
|
|
1036
|
+
### Fixed
|
|
1037
|
+
|
|
1038
|
+
- Show a clear disconnected state in the dev server UI, with a manual reconnect button and ongoing jittered automatic retry when the WebSocket drops.
|
|
1039
|
+
- CLI failures now print just the error message instead of a Node stack trace. Affects every command - errors from `package publish`, `build`, `package bundle`, etc. now read as `Error: <message>` followed by exit code 1.
|
|
1040
|
+
|
|
1041
|
+
## [0.0.93] — 2026-05-15
|
|
1042
|
+
|
|
1043
|
+
### Added
|
|
1044
|
+
|
|
1045
|
+
- 'Reset configuration' button in the dev server's Dev Tools FAB on the configuration and play stages. Clicking it confirms, then returns the centralised configuration to the active scenario's defaults (or to schema-level defaults if no scenario is active). Open configuration UIs and the 'Configuration' dialog re-sync automatically; game state (tick, context) is left untouched. Backed by a new `POST /api/configuration/reset` dev-server endpoint
|
|
1046
|
+
- 'Configuration' button in the dev server's Dev Tools FAB on the configuration and play stages. Mirrors the existing 'Context' button: opens a read-only dialog rendering the centralised configuration flattened one level (`modules.com.example.sales`, `userInterfaces.com.example.dashboard`, and so on) so authors can verify what their configuration UI wrote without leaving the dev server
|
|
1047
|
+
- Save the live configuration as a package scenario from the dev server. The 'Scenarios' panel in the debug menu now has 'Save' (overwrite the active scenario in place) and 'Save as new…' (open a modal for name, identifier, description, and 'make default' flag) actions. Both write a real `scenarios/<identifier>/scenario.sdk.json` bundle, materialise any `blob:*` references as files under `scenarios/<identifier>/assets/shared/`, rewrite them to `asset:*`, and patch the active game's `definition.scenarios` so the saved scenario is picked up on the next reload. The author-time counterpart to runtime custom scenarios: same primitive, opposite translation direction
|
|
1048
|
+
- Declare `customScenarios: { supported: true }` on `GameDefinition` to opt a game into facilitator-built scenarios. The field is a production-policy signal: hosted runtimes that support the feature persist scenarios authored at session time and surface them in the init-time picker alongside built-in scenarios. The runtime contract specifies how. The SDK dev server is not a runtime simulator, so it doesn't persist custom scenarios; the dev server's debug menu gains a 'Scenarios' switcher for selecting between the built-in scenarios declared by the active game
|
|
1049
|
+
|
|
1050
|
+
### Breaking Changes
|
|
1051
|
+
|
|
1052
|
+
- Unify asset and blob references under a single URI scheme. Every file — package-bundled or runtime-uploaded — is now identified by a self-describing string: `asset:<path>` resolves against the active scenario's `assets/` directory; `blob:<id>` resolves against the runtime blob store. The old two-API split is removed:
|
|
1053
|
+
- `callbacks.getBlobUrl(id)` and `callbacks.getScenarioAssetUrl(path)` → `callbacks.getAssetUrl(reference)`. The wrapper inspects the prefix and selects the matching URL prefix automatically.
|
|
1054
|
+
- `callbacks.readBlob(id)` → `callbacks.readAsset(reference)`. Available wherever the old callbacks lived plus on every UI surface that previously had `getBlobUrl`/`getScenarioAssetUrl` (game, manage-team, manage-session, configuration). The shape of the response is unchanged (`{ data, mimeType, name }`).
|
|
1055
|
+
- `callbacks.uploadBlob(file)` now returns a `blob:`-prefixed reference (`'blob:abc123'`) rather than a bare id. Add the prefix to anything you already had in flight if you were short-circuiting around the callback.
|
|
1056
|
+
- `generateSpeech` now returns `{ reference: 'blob:<id>', mimeType, ... }`. Read `audio.reference` instead of `audio.blobId`.
|
|
1057
|
+
- **Migration**: edit every `scenario.sdk.json` so any configuration value that points to a bundled file in `scenarios/<id>/assets/` is prefixed with `asset:`. For example `"Logo": "shared/logo.svg"` becomes `"Logo": "asset:shared/logo.svg"`. Anything stored or returned by `uploadBlob` already starts with `blob:` from this release onwards; persisted state from older versions needs its bare blob ids prefixed manually. The new `concepts/assets.md` guide walks through the full upgrade path
|
|
1058
|
+
|
|
1059
|
+
## [0.0.92] — 2026-05-13
|
|
1060
|
+
|
|
1061
|
+
### Breaking Changes
|
|
1062
|
+
|
|
1063
|
+
- Split the SDK package into two entry points so module, hook, tick-controller, and validator bundles no longer pull React into their compiled output. Import React-free symbols (`InitializationFunction`, `RunFunction`, `GetTickControllerFunction`, `HookFunction`, `ValidatorFunction`, `getText`, `LLMMessage`, `SDK*`, callback interfaces, etc.) from `@businesssimulations/seriousgaming-sdk` as before. Import React wrappers, hooks, contexts, theme/locale/timer types, and `renderUserInterface` test helpers from `@businesssimulations/seriousgaming-sdk/ui`. Before, a module that imported a runtime SDK value (such as the new `getText`) silently inlined `react/jsx-runtime` and crashed in the V8 module sandbox with `ReferenceError: process is not defined`; after the split, UI symbols stop reaching module bundles. Update UI imports such as `import { UserInterfaceWrapper, useUserInterfaceData } from '@businesssimulations/seriousgaming-sdk'` to use the `/ui` subpath
|
|
1064
|
+
|
|
1065
|
+
## [0.0.91] — 2026-05-13
|
|
1066
|
+
|
|
1067
|
+
### Breaking Changes
|
|
1068
|
+
|
|
1069
|
+
- `generateText` now returns a content-block array plus a `stopReason` instead of `{ content: string }`. A single call can carry text and one or more `toolUse` intents, and the new `stopReason` distinguishes normal completion (`'endTurn'`) from tool use (`'toolUse'`), truncation (`'maxTokens'`), and refusal (`'refusal'`). Callers that just want the text can use the new `getText(response)` helper. Callers that walked the old `response.content` string directly must either switch to `getText` or iterate the content array. The `LLMMessage` `content` field also widens to accept a content-block array so multi-turn callers can feed assistant `toolUse` blocks and user `toolResult` blocks back into the conversation. See the [LLM integration guide](./guides/llm-integration.md#tool-use)
|
|
1070
|
+
|
|
1071
|
+
### Added
|
|
1072
|
+
|
|
1073
|
+
- Add native tool use to `generateText`. Pass a `tools` array (each entry has `name`, `description`, and an `inputSchema`) and the model may emit one or more `toolUse` content blocks with `stopReason: 'toolUse'`. The SDK never executes tools itself — the caller runs them in its own code and feeds results back via `toolResult` blocks on the next call. The dev server validates tool names against `^[a-zA-Z0-9_-]{1,64}$` at the boundary so an invalid name fails fast instead of with a cryptic provider error. Tool calls remain gated by `uses: ['generateText']`; no separate `ApiUse` value is introduced. See the [LLM integration guide](./guides/llm-integration.md#tool-use)
|
|
1074
|
+
|
|
1075
|
+
## [0.0.90] — 2026-05-12
|
|
1076
|
+
|
|
1077
|
+
### Added
|
|
1078
|
+
|
|
1079
|
+
- Cache stable system prompts automatically on supported LLM providers, reducing input cost and latency for repeated `generateText` calls within a five-minute window. The dev server marks `systemPrompt` with the provider's native cache annotation; providers that don't support caching (e.g. OpenAI) silently ignore it. No consumer-facing API change — keep stable content (instructions, reference data, schemas) in `systemPrompt` and per-call variation in `prompt` / `messages` to benefit. Set `LLM_PROMPT_CACHE=off` to disable. See the [LLM integration guide](./guides/llm-integration.md#6-put-stable-content-in-systemprompt)
|
|
1080
|
+
- Add `requestPlay` callback to configuration user interfaces and `shell.configuration.playButton.hidden` to the game's `shell` block, so a configuration UI can own the 'start the game' control instead of relying on the host-rendered Play button. See the [Use requestPlay in a configuration UI recipe](./recipes/use-request-play.md)
|
|
1081
|
+
- Add `generateSpeech` callback for provider-agnostic text-to-speech. Modules, hooks, tick controllers, and every user interface surface (including shell UIs) can synthesise audio by passing text and a role-based voice preset (`narrator`, `announcer`, `executive`, `coach`, `neutral`); the runtime returns a blob ID for the synthesised audio. The dev server ships with an ElevenLabs adapter configured via `ELEVENLABS_API_KEY` and `TTS_VOICE_*` / `TTS_MODEL_*` env vars. Declare `'generateSpeech'` in the resource bundle's `uses` array to allow the call. See the [speech generation guide](./guides/speech-generation.md)
|
|
1082
|
+
|
|
1083
|
+
## [0.0.89] — 2026-05-11
|
|
1084
|
+
|
|
1085
|
+
### Breaking Changes
|
|
1086
|
+
|
|
1087
|
+
- Remove the SDK-defined roles system. This includes `roles` on capability and game definitions, `visibleToRoles` on game UI references, the `role` field on participant types, the `setParticipantRole` callback, and all role-related API endpoints and dev server UI. Games that need role-based behaviour should implement it in their own context and UI code
|
|
1088
|
+
- Remove events as a first-class resource type. The `events` array on game definitions, the `events` array on configuration UI definitions, the scenario-level event configuration overrides, and the `sdk new/update/delete event` CLI commands are all gone. Existing events should be migrated to modules: tick-restricted events become modules that early-return on the wrong tick using the new `meta.currentTick`, and condition-based events become modules whose `Run` checks the condition. Logic-less marker events that relied on the runtime's fired-on-tick record should be deleted; if you need a notification or document log, model it as a capability that a module writes to and a UI reads from
|
|
1089
|
+
- Enforce `uses` declarations on runtime callbacks. Any module, hook, tick controller, or user interface that calls `callbacks.generateText` or `callbacks.extractText` must list the API in its bundle's `uses` array. Resources whose source was previously calling these callbacks without declaring them will now throw `UndeclaredApiUseError` at runtime. UI iframes whose bundle did not declare `generateText` / `extractText` will receive `{ ok: false, error }` responses instead. Migrate by adding `"uses": ["generateText"]` (or `"extractText"`, or both) to every affected bundle, then run `pnpm run sdk generate` to refresh the per-resource `Callbacks` type alias. See the [declare API uses recipe](./recipes/declare-api-uses.md)
|
|
1090
|
+
|
|
1091
|
+
### Added
|
|
1092
|
+
|
|
1093
|
+
- Add `uses: ApiUse[]` field to module, hook, tick controller, validator, and all user interface bundle definitions. `uses` lists the runtime callbacks the resource is permitted to invoke (`'generateText'`, `'extractText'`); calls to undeclared callbacks throw at runtime. The per-resource `types.ts` generator narrows the `Callbacks` type alias to the declared APIs so undeclared calls fail at type-check too. See the [API uses concept](./guides/llm-integration.md#declare-the-apis-a-resource-uses) and the [declare API uses recipe](./recipes/declare-api-uses.md)
|
|
1094
|
+
|
|
1095
|
+
- Add `pnpm run sdk package version <value>` for setting the consumer package version. `<value>` is either a literal semver string or a bump keyword (`patch`, `minor`, `major`, `prerelease`). Writes both `seriousgaming.sdk.json` (the canonical source) and the sibling `package.json` when present, with rollback on partial write failure. Useful in CI to bump the version before `package bundle` so production runtimes accept the new upload. `--json` emits `{ previous, next, files }` for capture
|
|
1096
|
+
- Add `pnpm run sdk package bundle` for packaging a built `dist/` directory into a distributable zip archive. The archive root mirrors the `dist/` layout with `seriousgaming.json` at the top, suitable for upload to a production runtime. See the [distribute a built package recipe](./recipes/distribute-package.md)
|
|
1097
|
+
- Add **hooks** as a first-class resource type — single-signal handlers that react to out-of-tick lifecycle events. The first signal is `participantJoin`, fired when the dev server creates a new participant record. Hooks declare `reads` and `writes` like modules, fire after the runtime applies the change, and chain through `hookOrder` when multiple hooks subscribe to the same signal. Use `pnpm run sdk new hook` to scaffold one. See the [hooks guide](./guides/hooks.md)
|
|
1098
|
+
- Add `sdk new hook`, `sdk update hook`, and `sdk delete hook` CLI commands for managing hook resources
|
|
1099
|
+
- Add **validators** as a first-class resource type — read-only checks that gate UI-submitted state mutations before they're applied. Validators receive `previous`, `next`, the submitting `actor` (`Participant`), and per-game `config`, and return `{ ok: true }` or `{ ok: false, message }`. All referenced validators run on every mutation and every rejection is collected and surfaced to the UI through the host ↔ iframe channel as a `validationRejected` port message. Use `pnpm run sdk new validator` to scaffold one. See the [validators guide](./guides/validators.md)
|
|
1100
|
+
- Add `sdk new validator`, `sdk update validator`, and `sdk delete validator` CLI commands for managing validator resources
|
|
1101
|
+
- Add `validators[]` and `validatorOrder` fields to game definitions for referencing validators and pinning execution order
|
|
1102
|
+
- Add npm-based SDK package reuse. Any project scaffolded by `sdk new package` is now publishable as an npm package — its `package.json` carries `sdkResources: "./dist/seriousgaming.json"`, `files: ["dist"]`, and a peer dependency on the SDK runtime. Consumers run `pnpm add <pkg>` and the dev server, type generation, and resource resolution all pick up its capabilities, modules, user interfaces, and tick controllers automatically. Local resources with the same identifier shadow installed ones. `sdk build` copies installed-package assets into the consumer's `dist/` so production bundles remain self-contained without `node_modules`. See the SDK Packages guide, removed with the feature
|
|
1103
|
+
- `meta.currentTick` is now populated for module `Init`, `Run`, `InitAggregate`, `RunAggregate`, and tick-controller `GetTickController` calls. Use it to gate logic on specific ticks (e.g. `if (meta.currentTick !== 10) return context;`)
|
|
1104
|
+
- Add `extractText` callback for extracting text content from uploaded document blobs (PDF). Available in modules and all UI types. Use `uploadBlob` to upload a file, then `extractText(blobId)` to get its text content for LLM processing
|
|
1105
|
+
- Add **scenarios** as a first-class resource type — bundle different flavours of a game (storylines, difficulty presets, asset sets) that facilitators can select at init time. Scenarios provide configuration overrides for modules and user interfaces, plus a directory of static assets served at `/api/scenario/assets/*`
|
|
1106
|
+
- Add `sdk new scenario`, `sdk update scenario`, and `sdk delete scenario` CLI commands for managing scenario resources
|
|
1107
|
+
- Add `--scenario <id>` option to `sdk game init` and `sdk dev` for selecting a scenario at game initialization
|
|
1108
|
+
- Add `getScenarioAssetUrl()` callback to all UI wrapper contexts (player, manage-team, manage-session, configuration) for loading scenario-specific assets
|
|
1109
|
+
- Build output now includes scenario definitions and copies scenario assets to the `dist/scenarios/` directory
|
|
1110
|
+
- Scenario configuration overrides slot between game-level defaults and centralized config in the 4-tier config resolution order: schema defaults → game overrides → **scenario overrides** → centralized config
|
|
1111
|
+
- `sdk new package` now scaffolds a `.claude/` directory alongside `CLAUDE.md`. It contains five auto-loading workflow rules (create capability, create module, create user interface, update SDK version, test module or UI) that attach when Claude Code touches matching paths, plus a `settings.json` allow-listing the routine SDK CLI commands (`pnpm run sdk *`, `pnpm run generate`, `pnpm run build`, `pnpm run lint:*`, `pnpm test:*`) and explicitly denying `pnpm run sdk dev` and `pnpm start` so they remain a user action. Pass `--empty` to opt out
|
|
1112
|
+
|
|
1113
|
+
### Changed
|
|
1114
|
+
|
|
1115
|
+
- Refresh `CLAUDE.md` scaffolded by `sdk new package` to describe hooks instead of the removed events resource type. Mental-model bullets, file-organisation tree, recipe search queries, and CLI examples now reference `hooks/` and `pnpm run sdk new hook`
|
|
1116
|
+
|
|
1117
|
+
---
|
|
1118
|
+
|
|
1119
|
+
## [0.0.86] — 2026-04-20
|
|
1120
|
+
|
|
1121
|
+
### Added
|
|
1122
|
+
|
|
1123
|
+
- Add `nextTickAt` field to `TickControllerResult` for scheduling ticks at absolute wall-clock times instead of relative intervals — useful for games that tick at fixed times of day (e.g. "every day at midnight")
|
|
1124
|
+
- Add scheduling helpers `nextDailyAt`, `nextWeeklyAt`, `nextDailyFromSchedule`, and `scheduledTick` for computing the next occurrence of daily or weekly times, with optional IANA timezone support
|
|
1125
|
+
- Dev server auto-derives team count from `teams.min` in `game.sdk.json` when `--teams` is not passed — games that declare a minimum team count now start with multiple teams automatically
|
|
1126
|
+
- Dev server manage-session UI now receives all team contexts in multi-team mode, enabling testing of leaderboards and cross-team dashboards
|
|
1127
|
+
- Add `/manage-team/:n` URL routing in the dev server — manage-team UIs can now be viewed per-team with a team switcher in the dev tools, matching the existing `/play/:n` pattern
|
|
1128
|
+
- Dev server supports per-team ticking in independent mode — when a game declares only `"modes": ["independent"]`, each team advances on its own schedule via the "Next Tick" button on its play or manage-team page
|
|
1129
|
+
- Dev server debounces configuration writes to disk — config changes from the UI are applied in-memory immediately but only flushed to disk after a delay, preventing slow saves and overwrites during rapid editing. Control the delay with `--config-save-delay <seconds>` (default: 10)
|
|
1130
|
+
|
|
1131
|
+
### Fixed
|
|
1132
|
+
|
|
1133
|
+
- Fix `new manage-session-user-interface` scaffold generating broken files — `App.tsx`, `App.test.tsx`, and `theme.ts` were copied from the player user-interface template and used wrong hooks/contexts (`useUserInterfaceData`, `useUserInterfaceTheme`, `UserInterfaceDataContext`, etc.) instead of the `ManageSession` variants, causing a blank screen at runtime
|
|
1134
|
+
- Fix `new manage-team-user-interface` scaffold generating a `theme.ts` that imports `useUserInterfaceTheme` instead of `useManageTeamUserInterfaceTheme`
|
|
1135
|
+
- Fix `build --validate-only` skipping manage-team and manage-session user interfaces — they are now included in validation alongside other resource types
|
|
1136
|
+
|
|
1137
|
+
### Changed
|
|
1138
|
+
|
|
1139
|
+
- Add `manageTeamUserInterfaces` and `manageSessionUserInterfaces` fields to `update game --help` example JSON
|
|
1140
|
+
|
|
1141
|
+
---
|
|
1142
|
+
|
|
1143
|
+
## [0.0.84] — 2026-04-16
|
|
1144
|
+
|
|
1145
|
+
### Added
|
|
1146
|
+
|
|
1147
|
+
- Add `renderUserInterface()`, `renderManageTeamUserInterface()`, `renderConfigurationUserInterface()`, `renderManageSessionUserInterface()`, and `renderShellUserInterface()` test helpers that wrap all required context providers with sensible defaults — eliminates the 3–6 nested `<Context.Provider>` boilerplate in UI tests
|
|
1148
|
+
- Add `createMockManageTeamTheme()` and `createMockManageSessionTheme()` theme helpers for manage-team and manage-session UI tests
|
|
1149
|
+
- Document the `setData` + `requestTick` intermediate state timing behaviour and mitigation strategies
|
|
1150
|
+
|
|
1151
|
+
---
|
|
1152
|
+
|
|
1153
|
+
## [0.0.83] — 2026-04-15
|
|
1154
|
+
|
|
1155
|
+
Initial tracked release. Earlier history is available via `git log` in the SDK
|
|
1156
|
+
repository.
|