Skip to content

Commit 552bcec

Browse files
author
DavidQ
committed
Restore workspace manifest schema to clean pre-session state (tools-only contract)
1 parent fcd536e commit 552bcec

8 files changed

Lines changed: 304 additions & 334 deletions
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# PR_11_279 Workspace Schema Restore + Minimal Workspace V2 Session Block Report
2+
3+
## Scope
4+
Workspace schema restore and Workspace V2 export/import contract correction only.
5+
6+
## Files Changed
7+
- tools/schemas/workspace.schema.json
8+
- tools/workspace-v2/index.html
9+
- tools/workspace-v2/index.js
10+
- tests/runtime/V2CurrentSessionExport.test.mjs
11+
- docs/pr/PLAN_PR_11_279_WORKSPACE_SCHEMA_RESTORE_PLUS_MINIMAL_WORKSPACE_SESSION_BLOCK.md
12+
- docs/pr/BUILD_PR_11_279_WORKSPACE_SCHEMA_RESTORE_PLUS_MINIMAL_WORKSPACE_SESSION_BLOCK.md
13+
- docs/dev/reports/PR_11_279_workspace_schema_restore_and_minimal_workspace_session_block_report.md
14+
15+
## What Was Restored
16+
- Restored `games[]` to project/workspace manifest entry purpose only.
17+
- Removed `games[].session` schema support.
18+
- Removed Workspace V2 runtime logic that wrote session snapshots into `games[]`.
19+
20+
## Minimal Session Fields Added
21+
Added one optional strict top-level block in `tools/schemas/workspace.schema.json`:
22+
- `workspaceSession`
23+
- `schema`
24+
- `defaultToolId`
25+
- `activeToolId`
26+
- `activeHostContextId`
27+
- `activeSession`
28+
- `savedSessions`
29+
30+
No extra runtime/transient fields were added (no merge audit, diff output, undo stack, UI status text, or preview data).
31+
32+
## Why games[].session Was Removed
33+
`games[]` is for workspace/project manifest entries, not session snapshot storage. Putting session payload snapshots in `games[]` mixed project metadata with transient resume state and violated manifest boundary clarity. Resume state now lives only in the optional root `workspaceSession` block.
34+
35+
## Export/Import Contract Result
36+
- Workspace V2 export now emits:
37+
- manifest root fields (`documentKind`, `schema`, `version`, `games`)
38+
- optional `workspaceSession` resume block
39+
- Export does not emit:
40+
- `workspaceV2Session`
41+
- `toolSessions`
42+
- root `savedSessions`
43+
- `exportedAt`
44+
- Workspace V2 import validates against workspace schema shape and accepts valid payloads with/without optional `workspaceSession`.
45+
46+
## Workspace Manifest Schema File Decision
47+
- `tools/schemas/workspace.manifest.schema.json` was not modified.
48+
- Workspace V2 export/import validation in this PR targets `tools/schemas/workspace.schema.json` per requirement.
49+
50+
## Same-Tool Diff Guard
51+
- Cross-tool diff remains blocked with required message:
52+
- `Diff requires sessions from the same tool.`
53+
- Same-tool diff behavior remains unchanged.
54+
55+
## Validation Commands Run
56+
1. `node --check tools/workspace-v2/index.js`
57+
- PASS
58+
2. `node --check tests/runtime/V2CurrentSessionExport.test.mjs`
59+
- PASS
60+
3. `node tests/runtime/V2CurrentSessionExport.test.mjs`
61+
- PASS
62+
- Results: `tmp/v2-current-session-export-results.json`
63+
4. `node tests/runtime/V2SessionMerge.test.mjs`
64+
- PASS
65+
- Results: `tmp/v2-session-merge-results.json`
66+
67+
## Full Samples Smoke Decision
68+
- Skipped full samples smoke test.
69+
- Reason: scope is limited to workspace schema and Workspace V2 export/import + diff gating, covered by targeted runtime validation.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# BUILD_PR_11_279_WORKSPACE_SCHEMA_RESTORE_PLUS_MINIMAL_WORKSPACE_SESSION_BLOCK
2+
3+
## Purpose
4+
Correct workspace schema/session contract boundaries after PR_11_278 by restoring `games[]` manifest shape and moving Workspace V2 resume state to a minimal root `workspaceSession` block.
5+
6+
## Files
7+
- tools/schemas/workspace.schema.json
8+
- tools/workspace-v2/index.html
9+
- tools/workspace-v2/index.js
10+
- tests/runtime/V2CurrentSessionExport.test.mjs
11+
- docs/dev/reports/PR_11_279_workspace_schema_restore_and_minimal_workspace_session_block_report.md
12+
13+
## Implementation
14+
1. Remove `games[].session` from `tools/schemas/workspace.schema.json`.
15+
2. Add optional strict root `workspaceSession` block with only:
16+
- `schema`
17+
- `defaultToolId`
18+
- `activeToolId`
19+
- `activeHostContextId`
20+
- `activeSession`
21+
- `savedSessions`
22+
3. Keep `games[]` validation and usage for manifest/project entries only.
23+
4. Update Workspace V2 export builder to:
24+
- emit workspace manifest fields + unchanged `games[]`
25+
- emit optional `workspaceSession` resume block
26+
- avoid `workspaceV2Session/toolSessions/savedSessions/exportedAt` wrappers.
27+
5. Update Workspace V2 import validator to accept only workspace-schema-conformant payloads and process optional `workspaceSession` resume block.
28+
6. Keep diff same-tool guard behavior unchanged.
29+
7. Update targeted runtime test to validate restored boundary and minimal session block.
30+
31+
## Acceptance
32+
- `games[]` no longer stores session snapshots.
33+
- `workspaceSession` is the only minimal resume state block.
34+
- Workspace export/import validates against `tools/schemas/workspace.schema.json`.
35+
- Cross-tool diff stays blocked with required message.
36+
37+
## Validation
38+
- `node --check tools/workspace-v2/index.js`
39+
- `node --check tests/runtime/V2CurrentSessionExport.test.mjs`
40+
- `node tests/runtime/V2CurrentSessionExport.test.mjs`
41+
- `node tests/runtime/V2SessionMerge.test.mjs`
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# PLAN_PR_11_279_WORKSPACE_SCHEMA_RESTORE_PLUS_MINIMAL_WORKSPACE_SESSION_BLOCK
2+
3+
## Purpose
4+
Restore `tools/schemas/workspace.schema.json` games entry shape to project-manifest usage and add one minimal optional `workspaceSession` resume block for Workspace V2 export/import.
5+
6+
## Scope
7+
- tools/schemas/workspace.schema.json
8+
- tools/workspace-v2/index.html
9+
- tools/workspace-v2/index.js
10+
- tests/runtime/V2CurrentSessionExport.test.mjs
11+
- docs/dev/reports/PR_11_279_workspace_schema_restore_and_minimal_workspace_session_block_report.md
12+
13+
## Goals
14+
- Remove `games[].session` from workspace schema and keep games entries manifest/project-only.
15+
- Add strict optional root `workspaceSession` with only:
16+
- `schema`
17+
- `defaultToolId`
18+
- `activeToolId`
19+
- `activeHostContextId`
20+
- `activeSession`
21+
- `savedSessions`
22+
- Keep payloads in `workspaceSession.activeSession/savedSessions`, never in `games[]`.
23+
- Enforce Workspace V2 export/import validation against `tools/schemas/workspace.schema.json`.
24+
- Preserve same-tool diff guard behavior from PR_11_278.
25+
26+
## Out Of Scope
27+
- No tool schema changes.
28+
- No `tools/schemas/workspace.manifest.schema.json` changes.
29+
- No unrelated Workspace V2 features.
30+
31+
## Validation
32+
- `node --check tools/workspace-v2/index.js`
33+
- `node --check tests/runtime/V2CurrentSessionExport.test.mjs`
34+
- `node tests/runtime/V2CurrentSessionExport.test.mjs`
35+
- `node tests/runtime/V2SessionMerge.test.mjs`

tests/runtime/V2CurrentSessionExport.test.mjs

Lines changed: 81 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function checkSyntax(filePath) {
3333
}
3434
}
3535

36-
function simulateWorkspaceSchemaExport(activePayload, currentHostContextId, library) {
36+
function simulateWorkspaceSchemaExport(gamesSnapshot, activePayload, currentHostContextId, library) {
3737
if (!activePayload || typeof activePayload !== "object" || Array.isArray(activePayload)) {
3838
return {
3939
ok: false,
@@ -56,45 +56,19 @@ function simulateWorkspaceSchemaExport(activePayload, currentHostContextId, libr
5656
serialized: ""
5757
};
5858
}
59-
const games = [
60-
{
61-
id: activeHostContextId,
62-
level: "workspace-v2-active-session",
63-
tool: activeToolId,
64-
tools: [activeToolId],
65-
session: {
66-
hostContextId: activeHostContextId,
67-
payload: activePayload
68-
}
69-
}
70-
];
71-
Object.keys(library).forEach((sessionId) => {
72-
const payload = library[sessionId];
73-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
74-
return;
75-
}
76-
const payloadToolId = typeof payload.toolId === "string" && payload.toolId.trim()
77-
? payload.toolId.trim()
78-
: "";
79-
if (!payloadToolId) {
80-
return;
81-
}
82-
games.push({
83-
id: sessionId,
84-
level: "workspace-v2-saved-session",
85-
tool: payloadToolId,
86-
tools: [payloadToolId],
87-
session: {
88-
hostContextId: sessionId,
89-
payload
90-
}
91-
});
92-
});
9359
const container = {
9460
documentKind: "workspace-manifest",
9561
schema: "html-js-gaming.workspace-v2-session-export/1",
9662
version: 1,
97-
games
63+
games: Array.isArray(gamesSnapshot) ? JSON.parse(JSON.stringify(gamesSnapshot)) : [],
64+
workspaceSession: {
65+
schema: "html-js-gaming.workspace-v2-session/1",
66+
defaultToolId: "palette-manager-v2",
67+
activeToolId,
68+
activeHostContextId,
69+
activeSession: activePayload,
70+
savedSessions: library
71+
}
9872
};
9973
return {
10074
ok: true,
@@ -147,6 +121,31 @@ export function run() {
147121
}
148122
});
149123

124+
const requiredWorkspaceJsTokens = [
125+
"importWorkspaceSessionJson()",
126+
"exportWorkspaceSessionJson()",
127+
"buildWorkspaceSchemaDocument()",
128+
"validateWorkspaceSchemaDocument(",
129+
"workspaceManifestGames",
130+
"workspaceSession",
131+
"Diff requires sessions from the same tool."
132+
];
133+
requiredWorkspaceJsTokens.forEach((token) => {
134+
if (!workspaceJs.includes(token)) {
135+
failures.push(`Missing required workspace contract JS token: ${token}`);
136+
}
137+
});
138+
139+
const forbiddenWorkspaceJsTokens = [
140+
"workspace-v2-active-session",
141+
"workspace-v2-saved-session"
142+
];
143+
forbiddenWorkspaceJsTokens.forEach((token) => {
144+
if (workspaceJs.includes(token)) {
145+
failures.push(`Workspace V2 export/import should not model session snapshots in games[] (${token}).`);
146+
}
147+
});
148+
150149
const forbiddenWorkspaceHtmlTokens = [
151150
'id="workspaceV2NavModeSelect"',
152151
'id="workspaceV2NavToolsActions"',
@@ -161,20 +160,6 @@ export function run() {
161160
}
162161
});
163162

164-
const requiredWorkspaceJsTokens = [
165-
"importWorkspaceSessionJson()",
166-
"exportWorkspaceSessionJson()",
167-
"buildWorkspaceSchemaDocument()",
168-
"validateWorkspaceSchemaDocument(",
169-
"tools/schemas/workspace.schema.json",
170-
"Diff requires sessions from the same tool."
171-
];
172-
requiredWorkspaceJsTokens.forEach((token) => {
173-
if (!workspaceJs.includes(token)) {
174-
failures.push(`Missing required workspace contract JS token: ${token}`);
175-
}
176-
});
177-
178163
TOOL_IDS.forEach((toolId) => {
179164
const toolHtmlPath = path.join(repoRoot, "tools", toolId, "index.html");
180165
const toolHtmlExists = fs.existsSync(toolHtmlPath);
@@ -204,8 +189,17 @@ export function run() {
204189
failures.push(`workspace.schema.json must require root.${key}.`);
205190
}
206191
});
207-
if (!workspaceSchema.properties.games?.items?.properties?.session) {
208-
failures.push("workspace.schema.json must allow games[].session for Workspace V2 session export/import.");
192+
if (workspaceSchema.properties.games?.items?.properties?.session) {
193+
failures.push("workspace.schema.json must not allow games[].session.");
194+
}
195+
if (!workspaceSchema.properties.workspaceSession) {
196+
failures.push("workspace.schema.json must define optional root.workspaceSession.");
197+
} else {
198+
const requiredWorkspaceSessionKeys = workspaceSchema.properties.workspaceSession.required || [];
199+
const expectedWorkspaceSessionKeys = ["schema", "defaultToolId", "activeToolId", "activeHostContextId", "activeSession", "savedSessions"];
200+
if (JSON.stringify(requiredWorkspaceSessionKeys) !== JSON.stringify(expectedWorkspaceSessionKeys)) {
201+
failures.push("workspace.schema.json workspaceSession required keys do not match minimal contract.");
202+
}
209203
}
210204
}
211205

@@ -221,47 +215,64 @@ export function run() {
221215
payloadJson: { assetCatalog: { entries: [{ id: "asset-1" }] } }
222216
}
223217
};
224-
const workspaceExport = simulateWorkspaceSchemaExport(activePayload, "palette-manager-v2-1234567890123-abcd1234", library);
218+
const projectGames = [
219+
{
220+
id: "0207",
221+
level: "phase-02",
222+
tool: "sprite-editor",
223+
tools: ["sprite-editor"],
224+
palette: "samples/phase-02/0207/sample.0207.palette.json"
225+
}
226+
];
227+
const workspaceExport = simulateWorkspaceSchemaExport(projectGames, activePayload, "palette-manager-v2-1234567890123-abcd1234", library);
225228
if (!workspaceExport.ok) failures.push("Workspace export should succeed when active payload exists.");
226229
const workspaceExportParsed = workspaceExport.serialized ? JSON.parse(workspaceExport.serialized) : null;
227230
if (!workspaceExportParsed || typeof workspaceExportParsed !== "object" || Array.isArray(workspaceExportParsed)) {
228231
failures.push("Workspace export should be an object matching workspace.schema.json.");
229232
} else {
230233
const rootKeys = Object.keys(workspaceExportParsed).sort((left, right) => left.localeCompare(right));
231-
const expectedRootKeys = ["documentKind", "games", "schema", "version"];
234+
const expectedRootKeys = ["documentKind", "games", "schema", "version", "workspaceSession"];
232235
if (JSON.stringify(rootKeys) !== JSON.stringify(expectedRootKeys)) {
233236
failures.push(`Workspace export root keys mismatch. Expected ${expectedRootKeys.join(", ")} and received ${rootKeys.join(", ")}.`);
234237
}
235-
if (!Array.isArray(workspaceExportParsed.games) || workspaceExportParsed.games.length < 1) {
236-
failures.push("Workspace export must include games array entries.");
237-
}
238-
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed, "workspaceSession")) {
239-
failures.push("Workspace export must not include workspaceSession wrapper.");
238+
if (!Array.isArray(workspaceExportParsed.games) || workspaceExportParsed.games.length !== 1) {
239+
failures.push("Workspace export must preserve project games[] entries.");
240+
} else {
241+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.games[0], "session")) {
242+
failures.push("Workspace export games[] entries must not include session snapshots.");
243+
}
244+
if (workspaceExportParsed.games[0].id !== "0207" || workspaceExportParsed.games[0].tool !== "sprite-editor") {
245+
failures.push("Workspace export must keep original games[] entry values unchanged.");
246+
}
240247
}
241248
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed, "workspaceV2Session")) {
242249
failures.push("Workspace export must not include workspaceV2Session wrapper.");
243250
}
244251
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed, "toolSessions")) {
245252
failures.push("Workspace export must not include toolSessions wrapper.");
246253
}
247-
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed, "savedSessions")) {
248-
failures.push("Workspace export must not include savedSessions wrapper.");
254+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed, "savedSessions") && !Object.prototype.hasOwnProperty.call(workspaceExportParsed, "workspaceSession")) {
255+
failures.push("Workspace export must not include root savedSessions wrapper.");
249256
}
250257
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed, "exportedAt")) {
251258
failures.push("Workspace export must not include exportedAt root field.");
252259
}
253-
const activeEntry = workspaceExportParsed.games.find((entry) => entry.level === "workspace-v2-active-session");
254-
if (!activeEntry) {
255-
failures.push("Workspace export must include one active session entry in games.");
260+
if (!workspaceExportParsed.workspaceSession || typeof workspaceExportParsed.workspaceSession !== "object") {
261+
failures.push("Workspace export must include workspaceSession resume block.");
256262
} else {
257-
if (activeEntry.tool !== "palette-manager-v2") {
258-
failures.push("Active session games entry must preserve tool id.");
263+
const workspaceSessionKeys = Object.keys(workspaceExportParsed.workspaceSession).sort((left, right) => left.localeCompare(right));
264+
const expectedWorkspaceSessionKeys = ["activeHostContextId", "activeSession", "activeToolId", "defaultToolId", "savedSessions", "schema"];
265+
if (JSON.stringify(workspaceSessionKeys) !== JSON.stringify(expectedWorkspaceSessionKeys)) {
266+
failures.push("workspaceSession keys do not match minimal allowed set.");
267+
}
268+
if (workspaceExportParsed.workspaceSession.activeHostContextId !== "palette-manager-v2-1234567890123-abcd1234") {
269+
failures.push("workspaceSession.activeHostContextId must preserve active hostContextId.");
259270
}
260-
if (activeEntry.session?.hostContextId !== "palette-manager-v2-1234567890123-abcd1234") {
261-
failures.push("Active session games entry must preserve hostContextId.");
271+
if (workspaceExportParsed.workspaceSession.activeToolId !== "palette-manager-v2") {
272+
failures.push("workspaceSession.activeToolId must preserve active toolId.");
262273
}
263-
if (JSON.stringify(activeEntry.session?.payload) !== JSON.stringify(activePayload)) {
264-
failures.push("Active session games entry must preserve payload.");
274+
if (JSON.stringify(workspaceExportParsed.workspaceSession.activeSession) !== JSON.stringify(activePayload)) {
275+
failures.push("workspaceSession.activeSession must preserve active payload.");
265276
}
266277
}
267278
}

0 commit comments

Comments
 (0)