Skip to content

Commit a9c4a7a

Browse files
author
DavidQ
committed
Lock game manifest boundary between runtime game data and editor workspace state - PR_26128_008-game-workspace-boundary-contract
1 parent 7f990ef commit a9c4a7a

6 files changed

Lines changed: 98 additions & 2 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# PR_26128_008 Game Workspace Boundary Contract
2+
3+
## Summary
4+
- Clarified the `game.manifest.json` boundary in `tools/schemas/game.manifest.schema.json`.
5+
- `game.gameData` is the runtime contract.
6+
- `game.workspace` is the editor/tool contract.
7+
- Runtime must ignore `game.workspace`.
8+
- Tools may read `game.gameData`.
9+
- Tools may write `game.workspace`.
10+
- Tools may update `game.gameData` only through explicit validated apply/build/export actions.
11+
12+
## Validation Alignment
13+
- Workspace Manager V2 Active Game discovery still validates discovered `game.manifest.json` files with the dedicated game manifest schema.
14+
- `game.gameData.workspace` is rejected with an actionable validation reason because runtime data must not depend on editor/tool workspace state.
15+
- `game.gameData.tools` is rejected because editor/tool state belongs in `game.workspace`.
16+
- The existing Workspace Manager V2 launch/session payload remains workspace-shaped and is derived from `game.workspace`; session/toolState behavior was not changed.
17+
- Workspace Manager V2 now reports the boundary contract in the status log when a valid game manifest is selected or imported.
18+
19+
## Scope Notes
20+
- No separate `workspace.manifest.json` was introduced.
21+
- No sample JSON was modified.
22+
- No roadmap content was modified.
23+
- No cross-tool communication was added.
24+
- Full samples smoke test was skipped because this BUILD explicitly requested targeted Workspace Manager V2 validation and to skip full samples smoke.
25+
26+
## Validation
27+
- PASS: `node --check tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js`
28+
- PASS: `node --check tools/workspace-manager-v2/js/WorkspaceManagerV2App.js`
29+
- PASS: `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
30+
- PASS: JSON parse for `tools/schemas/game.manifest.schema.json`, `games/Asteroids/game.manifest.json`, `games/GravityWell/game.manifest.json`, and `games/Pong/game.manifest.json`
31+
- PASS: `npm run test:workspace-v2` - 11 passed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Playwright Game Workspace Boundary Contract
2+
3+
## Command
4+
`npm run test:workspace-v2`
5+
6+
## Result
7+
PASS: 11 passed
8+
9+
## Targeted Coverage
10+
- Verified Workspace Manager V2 still launches from the tools index.
11+
- Verified Active Game starts empty and disabled before repo selection.
12+
- Verified Active Game still discovers schema-valid `game.manifest.json` files.
13+
- Verified `game.manifest.json` remains the only required game project manifest for Active Game discovery.
14+
- Verified `game.gameData` is present as the runtime lane.
15+
- Verified `game.workspace` is present as the editor/tool lane and remains nested under the game manifest.
16+
- Verified root game manifests do not expose root `documentKind` or root `tools` as runtime-required data.
17+
- Verified `game.gameData.workspace` is rejected with an actionable boundary validation message.
18+
- Verified the nested `game.workspace` payload still validates as the existing Workspace Manager V2 launch/session context.
19+
- Verified Workspace Manager V2 status reporting includes the boundary contract when a game manifest is selected or imported.
20+
21+
## Skipped
22+
- Full samples smoke test was skipped as requested. The targeted Workspace Manager V2 Playwright suite covers the changed schema boundary, Active Game discovery, invalid-manifest logging, and affected launch/session paths.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,18 +285,21 @@ test.describe("Workspace Manager V2 bootstrap", () => {
285285
const manifest = await fetch("/games/Asteroids/game.manifest.json", { cache: "no-store" }).then((response) => response.json());
286286
const { WorkspaceManagerV2ContextService } = await import("/tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js");
287287
const service = new WorkspaceManagerV2ContextService();
288+
const invalidRuntimeWorkspaceManifest = structuredClone(manifest);
289+
invalidRuntimeWorkspaceManifest.game.gameData.workspace = {};
288290
return {
289291
gameManifestValidation: await service.validateGameManifest(manifest),
290292
hasGameData: Boolean(manifest.game?.gameData),
291293
hasRootTools: Boolean(manifest.tools),
292294
hasWorkspace: Boolean(manifest.game?.workspace),
295+
runtimeWorkspaceValidation: await service.validateGameManifest(invalidRuntimeWorkspaceManifest),
293296
rootDocumentKind: manifest.documentKind || "",
294297
schema: manifest.schema,
295298
workspaceDocumentKind: manifest.game?.workspace?.documentKind,
296299
workspaceValidation: await service.validateGeneratedManifest(manifest.game.workspace)
297300
};
298301
});
299-
expect(asteroidsGameManifestShape).toEqual({
302+
expect(asteroidsGameManifestShape).toMatchObject({
300303
gameManifestValidation: { ok: true },
301304
hasGameData: true,
302305
hasRootTools: false,
@@ -306,6 +309,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
306309
workspaceDocumentKind: "workspace-manifest",
307310
workspaceValidation: { ok: true }
308311
});
312+
expect(asteroidsGameManifestShape.runtimeWorkspaceValidation.ok).toBe(false);
313+
expect(asteroidsGameManifestShape.runtimeWorkspaceValidation.message).toContain("runtime data must not depend on editor/tool workspace state");
309314

310315
await page.evaluate(() => {
311316
window.__workspaceManagerV2MockRepoConfig = {
@@ -478,6 +483,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
478483
{ height: 142, width: 180 },
479484
{ height: 142, width: 180 }
480485
]);
486+
await expect(page.locator("#statusLog")).toHaveValue(/OK Boundary contract: game\.gameData is runtime data; game\.workspace is editor\/tool state\. Runtime ignores game\.workspace; tools may read game\.gameData, write game\.workspace, and update game\.gameData only through explicit validated apply\/build\/export actions\./);
481487
await expect(page.locator("#statusLog")).toHaveValue(/OK Loaded Asteroids from \/games\/Asteroids\/game\.manifest\.json with 11 active palette colors and 14 managed assets\./);
482488

483489
const downloadPromise = page.waitForEvent("download");
@@ -773,6 +779,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
773779
await expect(page.locator("#workspaceContextOutput")).toHaveValue(/"id": "workspace-manager-v2-Asteroids-imported"/);
774780
await expect(page.locator("#activeAssetRegistrySummary")).toHaveCount(0);
775781
await expect(page.locator('[data-workspace-tool-id="asset-manager-v2"]')).toBeEnabled();
782+
await expect(page.locator("#statusLog")).toHaveValue(/OK Boundary contract: game\.gameData is runtime data; game\.workspace is editor\/tool state\. Runtime ignores game\.workspace; tools may read game\.gameData, write game\.workspace, and update game\.gameData only through explicit validated apply\/build\/export actions\./);
776783
await expect(page.locator("#statusLog")).toHaveValue(/OK Imported schema-valid Workspace Manager V2 manifest workspace-manager-v2-Asteroids-imported\./);
777784

778785
const downloadPromise = page.waitForEvent("download");

tools/schemas/game.manifest.schema.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://json-schema.org/draft/2020-12/schema",
33
"$id": "tools/schemas/game.manifest.schema.json",
44
"title": "Game Manifest",
5-
"description": "Schema for games/**/game.manifest.json as the single source of truth. Runtime game data lives in game.gameData; editor and workspace state lives in game.workspace.",
5+
"description": "Schema for games/**/game.manifest.json as the single source of truth. game.gameData is the runtime contract. game.workspace is the editor/tool contract. Runtime code must ignore game.workspace. Tools may read game.gameData, may write game.workspace, and may update game.gameData only through explicit validated apply/build/export actions.",
66
"type": "object",
77
"required": ["schema", "version", "game"],
88
"additionalProperties": false,
@@ -38,9 +38,11 @@
3838
"pattern": "^[^/\\\\]+$"
3939
},
4040
"gameData": {
41+
"description": "Runtime contract for data the game needs to run. Editor/tool workspace state must not be required from this object.",
4142
"$ref": "#/$defs/gameData"
4243
},
4344
"workspace": {
45+
"description": "Editor/tool contract for workspace state needed to edit and build the game. Runtime code must ignore this object.",
4446
"$ref": "#/$defs/gameWorkspace"
4547
}
4648
}
@@ -49,8 +51,21 @@
4951
"$defs": {
5052
"gameData": {
5153
"type": "object",
54+
"description": "Runtime-only game data. Tools may read this object; writes are limited to explicit validated apply/build/export actions.",
5255
"required": ["launch"],
5356
"additionalProperties": true,
57+
"allOf": [
58+
{
59+
"not": {
60+
"required": ["workspace"]
61+
}
62+
},
63+
{
64+
"not": {
65+
"required": ["tools"]
66+
}
67+
}
68+
],
5469
"properties": {
5570
"launch": {
5671
"type": "object",
@@ -74,6 +89,7 @@
7489
},
7590
"gameWorkspace": {
7691
"type": "object",
92+
"description": "Editor/tool workspace state. Tools may write this object; runtime data belongs in game.gameData.",
7793
"required": ["documentKind", "schema", "version", "id", "name", "gameId", "gameRoot", "assetsPath", "tools"],
7894
"additionalProperties": false,
7995
"properties": {

tools/workspace-manager-v2/js/WorkspaceManagerV2App.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ export class WorkspaceManagerV2App {
150150
if (result.assetWarning) {
151151
this.statusLog.info(`Warning: ${result.assetWarning}`);
152152
}
153+
if (result.boundaryContract) {
154+
this.statusLog.ok(result.boundaryContract);
155+
}
153156
this.statusLog.ok(`Loaded ${result.game.name} from ${result.game.manifestPath} with ${result.paletteSwatches.length} active palette colors and ${result.assetCount} managed assets.`);
154157
this.statusLog.ok("Asset Manager V2 production launch context is session/state based only.");
155158
}
@@ -292,6 +295,9 @@ export class WorkspaceManagerV2App {
292295
if (result.assetWarning) {
293296
this.statusLog.info(`Warning: ${result.assetWarning}`);
294297
}
298+
if (result.boundaryContract) {
299+
this.statusLog.ok(result.boundaryContract);
300+
}
295301
this.statusLog.ok(`Imported schema-valid Workspace Manager V2 manifest ${result.context.id}.`);
296302
} catch (error) {
297303
this.statusLog.fail(`Import Manifest failed: ${error.message}`);

tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ function isGameManifest(value) {
104104
&& isPlainObject(value.game.workspace);
105105
}
106106

107+
function gameManifestBoundaryContractMessage() {
108+
return "Boundary contract: game.gameData is runtime data; game.workspace is editor/tool state. Runtime ignores game.workspace; tools may read game.gameData, write game.workspace, and update game.gameData only through explicit validated apply/build/export actions.";
109+
}
110+
107111
function schemaProperties(schema) {
108112
return isPlainObject(schema?.properties) ? schema.properties : {};
109113
}
@@ -407,6 +411,7 @@ export class WorkspaceManagerV2ContextService {
407411
},
408412
assetCount,
409413
assetWarning,
414+
boundaryContract: game.manifestKind === "game-manifest" ? gameManifestBoundaryContractMessage() : "",
410415
paletteSwatches
411416
};
412417
}
@@ -456,7 +461,16 @@ export class WorkspaceManagerV2ContextService {
456461
}
457462
const errors = validateSchemaValue(manifest, schemaResult.schema, "root", schemaResult.schema);
458463
const gameInfo = manifest?.game || {};
464+
const gameData = gameInfo.gameData || {};
459465
const workspace = gameInfo.workspace || {};
466+
if (isPlainObject(gameData)) {
467+
if (Object.prototype.hasOwnProperty.call(gameData, "workspace")) {
468+
errors.push("root.game.gameData.workspace is not allowed; runtime data must not depend on editor/tool workspace state");
469+
}
470+
if (Object.prototype.hasOwnProperty.call(gameData, "tools")) {
471+
errors.push("root.game.gameData.tools is not allowed; tool/editor state belongs in root.game.workspace");
472+
}
473+
}
460474
if (isPlainObject(gameInfo) && isPlainObject(workspace)) {
461475
const expectedGameRoot = `games/${gameInfo.folder}/`;
462476
const expectedAssetsPath = `games/${gameInfo.folder}/assets`;

0 commit comments

Comments
 (0)