Skip to content

Commit 287c786

Browse files
author
DavidQ
committed
Separate Workspace V2 tool and workspace nav actions and align export with workspace schema - PR_11_276
1 parent 0684dee commit 287c786

6 files changed

Lines changed: 532 additions & 97 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# PR_11_276 Workspace V2 Nav Mode Separation + Workspace Export Contract Report
2+
3+
## Scope
4+
Workspace V2 only:
5+
- import/export controls
6+
- tool-vs-workspace nav mode separation
7+
- workspace export/import contract alignment
8+
9+
## Files Changed
10+
- tools/workspace-v2/index.html
11+
- tools/workspace-v2/index.js
12+
- tests/runtime/V2CurrentSessionExport.test.mjs
13+
- docs/pr/PLAN_PR_11_276_WORKSPACE_V2_NAV_MODE_SEPARATION_AND_EXPORT_CONTRACT_CORRECTION.md
14+
- docs/pr/BUILD_PR_11_276_WORKSPACE_V2_NAV_MODE_SEPARATION_AND_EXPORT_CONTRACT_CORRECTION.md
15+
- docs/dev/reports/PR_11_276_workspace_v2_nav_mode_and_export_contract_report.md
16+
17+
## Implementation Summary
18+
- Added explicit nav mode selector:
19+
- `Tool Mode (navTools)`
20+
- `Workspace Mode (navWorkspace)`
21+
- Split UI actions by mode:
22+
- navTools: tool session import/export only
23+
- navWorkspace: workspace session import/export only
24+
- Added mode-gated handler checks so actions cannot run from the wrong mode.
25+
- Tool mode export (`exportCurrentSessionJson`) now exports only the active tool payload JSON.
26+
- Workspace mode export (`exportWorkspaceSessionJson`) now exports portable workspace container contract:
27+
- `version`, `toolId: workspace-v2`
28+
- `workspaceSession` with workspace identity, default/active tool, active host context, `toolSessions` grouped by toolId/sessionId, and `savedSessions`
29+
- Removed runtime-only fields from workspace export payload contract:
30+
- `sessionHistory`
31+
- `sessionSelection`
32+
- `mergeAuditLog`
33+
- lone `activeSessionPayload`
34+
- Added workspace import (`importWorkspaceSessionJson`) with contract validation and state hydration for active session + saved sessions.
35+
36+
## Workspace Contract Alignment Notes
37+
- Existing repo schemas under `tools/schemas/workspace*.json` are workspace/game manifest contracts for legacy tool ids and are not directly compatible with Workspace V2 tool-session lane.
38+
- This PR keeps schema files untouched and enforces a Workspace V2 portable session wrapper contract in code/tests.
39+
- No schema file correction was required for this scoped Workspace V2 runtime/export lane.
40+
41+
## Validation Commands
42+
1. `node --check tools/workspace-v2/index.js`
43+
- PASS
44+
2. `node --check tests/runtime/V2CurrentSessionExport.test.mjs`
45+
- PASS
46+
3. `node tests/runtime/V2CurrentSessionExport.test.mjs`
47+
- PASS
48+
- Results: `tmp/v2-current-session-export-results.json`
49+
50+
## Full Samples Smoke Decision
51+
- Skipped full samples smoke test.
52+
- Reason: changes are confined to Workspace V2 import/export/nav-mode logic and validated with targeted runtime checks.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# BUILD_PR_11_276_WORKSPACE_V2_NAV_MODE_SEPARATION_AND_EXPORT_CONTRACT_CORRECTION
2+
3+
## Purpose
4+
Implement Workspace V2 nav mode separation for import/export controls and align workspace export/import with a portable workspace-level session contract.
5+
6+
## Files
7+
- tools/workspace-v2/index.html
8+
- tools/workspace-v2/index.js
9+
- tests/runtime/V2CurrentSessionExport.test.mjs
10+
- docs/dev/reports/PR_11_276_workspace_v2_nav_mode_and_export_contract_report.md
11+
12+
## Implementation
13+
1. Add navigation mode selector in Workspace V2:
14+
- `tools` (navTools)
15+
- `workspace` (navWorkspace)
16+
2. Split Import/Export UI into mode-owned action groups:
17+
- navTools section keeps single-tool import/export controls
18+
- navWorkspace section adds workspace import/export controls
19+
3. Wire mode rendering in JS with explicit show/hide behavior.
20+
4. Enforce mode-gated actions:
21+
- tool import/export blocked outside tool mode
22+
- workspace import/export blocked outside workspace mode
23+
5. Tool mode export remains tool-payload scoped.
24+
6. Workspace mode export outputs portable workspace wrapper containing:
25+
- workspace identity/version
26+
- active/default tool identity
27+
- included tool payloads grouped by `toolId` and `sessionId`
28+
- saved session map
29+
- excludes runtime-only fields (`sessionHistory`, `sessionSelection`, `mergeAuditLog`, lone `activeSessionPayload`)
30+
7. Workspace import validates workspace wrapper contract and restores active session + saved sessions to operational state.
31+
8. Add targeted runtime coverage for mode separation and export contract behavior.
32+
33+
## Acceptance
34+
- Tool mode only exposes tool import/export actions.
35+
- Workspace mode only exposes workspace import/export actions.
36+
- Workspace export is portable wrapper contract and excludes runtime-only fields.
37+
- Save/Load/Overwrite/Diff/Merge remain operational after workspace import.
38+
39+
## Validation
40+
- node --check tools/workspace-v2/index.js
41+
- node --check tests/runtime/V2CurrentSessionExport.test.mjs
42+
- node tests/runtime/V2CurrentSessionExport.test.mjs
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# PLAN_PR_11_276_WORKSPACE_V2_NAV_MODE_SEPARATION_AND_EXPORT_CONTRACT_CORRECTION
2+
3+
## Purpose
4+
Separate tool-vs-workspace navigation modes in Workspace V2 import/export controls and correct workspace export to a portable workspace-level contract.
5+
6+
## Scope
7+
- tools/workspace-v2/index.html
8+
- tools/workspace-v2/index.js
9+
- tests/runtime/V2CurrentSessionExport.test.mjs
10+
- docs/report only
11+
12+
## Goals
13+
- Add explicit nav mode separation:
14+
- tool mode (navTools) exposes only tool import/export actions
15+
- workspace mode (navWorkspace) exposes only workspace import/export actions
16+
- Keep tool mode import/export tool-session scoped.
17+
- Make workspace mode export a portable workspace wrapper contract.
18+
- Remove runtime-only fields from workspace export payload.
19+
- Keep Save/Load/Overwrite/Diff/Merge operational with imported/exported workspace data.
20+
21+
## Out of Scope
22+
- No schema file rewrites.
23+
- No unrelated tool changes.
24+
- No merge/diff algorithm refactors.
25+
26+
## Validation
27+
- node --check tools/workspace-v2/index.js
28+
- node --check tests/runtime/V2CurrentSessionExport.test.mjs
29+
- node tests/runtime/V2CurrentSessionExport.test.mjs

tests/runtime/V2CurrentSessionExport.test.mjs

Lines changed: 142 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
77
const __filename = fileURLToPath(import.meta.url);
88
const __dirname = path.dirname(__filename);
99
const repoRoot = path.resolve(__dirname, "..", "..");
10+
const workspaceHtmlPath = path.join(repoRoot, "tools", "workspace-v2", "index.html");
1011
const workspaceJsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
1112
const testPath = path.join(repoRoot, "tests", "runtime", "V2CurrentSessionExport.test.mjs");
1213
const resultsPath = path.join(repoRoot, "tmp", "v2-current-session-export-results.json");
@@ -23,7 +24,7 @@ function checkSyntax(filePath) {
2324
}
2425
}
2526

26-
function simulateExport(activePayload, selectedToolId, currentHostContextId, library, history, selection, mergeAuditLog) {
27+
function simulateToolModeExport(activePayload, currentHostContextId) {
2728
if (!activePayload || typeof activePayload !== "object" || Array.isArray(activePayload)) {
2829
return {
2930
ok: false,
@@ -33,112 +34,195 @@ function simulateExport(activePayload, selectedToolId, currentHostContextId, lib
3334
};
3435
}
3536
const payloadToolId = typeof activePayload.toolId === "string" ? activePayload.toolId.trim() : "";
36-
const filenameToolId = payloadToolId || selectedToolId || "workspace-v2";
37-
const filenameSessionId = typeof currentHostContextId === "string" && currentHostContextId.trim()
37+
const toolIdForFile = payloadToolId || "workspace-v2";
38+
const sessionIdForFile = typeof currentHostContextId === "string" && currentHostContextId.trim()
3839
? currentHostContextId.trim()
3940
: "session";
40-
const exportedContainer = {
41+
return {
42+
ok: true,
43+
status: "Exported current workspace session JSON.",
44+
filename: `${toolIdForFile}-${sessionIdForFile}.json`,
45+
serialized: JSON.stringify(activePayload, null, 2)
46+
};
47+
}
48+
49+
function simulateWorkspaceModeExport(activePayload, currentHostContextId, library) {
50+
if (!activePayload || typeof activePayload !== "object" || Array.isArray(activePayload)) {
51+
return {
52+
ok: false,
53+
status: "No active Workspace V2 session is available to export.",
54+
filename: "",
55+
serialized: ""
56+
};
57+
}
58+
const activeToolId = typeof activePayload.toolId === "string" && activePayload.toolId.trim()
59+
? activePayload.toolId.trim()
60+
: "workspace-v2";
61+
const activeHostContextId = typeof currentHostContextId === "string" && currentHostContextId.trim()
62+
? currentHostContextId.trim()
63+
: "";
64+
const toolSessions = {};
65+
if (activeHostContextId) {
66+
toolSessions[activeToolId] = {
67+
[activeHostContextId]: activePayload
68+
};
69+
}
70+
Object.keys(library).forEach((sessionId) => {
71+
const payload = library[sessionId];
72+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
73+
return;
74+
}
75+
const payloadToolId = typeof payload.toolId === "string" && payload.toolId.trim()
76+
? payload.toolId.trim()
77+
: activeToolId;
78+
if (!Object.prototype.hasOwnProperty.call(toolSessions, payloadToolId)) {
79+
toolSessions[payloadToolId] = {};
80+
}
81+
toolSessions[payloadToolId][sessionId] = payload;
82+
});
83+
const container = {
4184
version: "v2",
4285
toolId: "workspace-v2",
4386
workspaceSession: {
4487
workspaceToolId: "workspace-v2",
45-
workspaceSessionId: filenameSessionId === "session" ? "" : filenameSessionId,
46-
activeToolId: filenameToolId,
47-
activeHostContextId: filenameSessionId === "session" ? "" : filenameSessionId,
48-
activeSessionPayload: activePayload,
49-
sessionLibrary: library,
50-
sessionHistory: history,
51-
sessionSelection: selection,
52-
mergeAuditLog
88+
workspaceSessionId: activeHostContextId,
89+
defaultToolId: "palette-manager-v2",
90+
activeToolId,
91+
activeHostContextId,
92+
toolSessions,
93+
savedSessions: library,
94+
exportedAt: "2026-05-02T00:00:00.000Z"
5395
}
5496
};
5597
return {
5698
ok: true,
5799
status: "Exported current workspace session JSON.",
58-
filename: `workspace-v2-${filenameToolId}-${filenameSessionId}.json`,
59-
serialized: JSON.stringify(exportedContainer, null, 2)
100+
filename: `workspace-v2-${activeToolId}-${activeHostContextId || "session"}.json`,
101+
serialized: JSON.stringify(container, null, 2)
60102
};
61103
}
62104

63105
export function run() {
64106
const failures = [];
107+
const workspaceHtmlExists = fs.existsSync(workspaceHtmlPath);
65108
const workspaceJsExists = fs.existsSync(workspaceJsPath);
109+
const workspaceHtml = workspaceHtmlExists ? fs.readFileSync(workspaceHtmlPath, "utf8") : "";
66110
const workspaceJs = workspaceJsExists ? fs.readFileSync(workspaceJsPath, "utf8") : "";
67111
const workspaceJsSyntax = checkSyntax(workspaceJsPath);
68112
const testSyntax = checkSyntax(testPath);
69113

114+
if (!workspaceHtmlExists) failures.push("Missing tools/workspace-v2/index.html.");
70115
if (!workspaceJsExists) failures.push("Missing tools/workspace-v2/index.js.");
71116
if (!workspaceJsSyntax.ok) failures.push("tools/workspace-v2/index.js failed syntax check.");
72117
if (!testSyntax.ok) failures.push("tests/runtime/V2CurrentSessionExport.test.mjs failed syntax check.");
73118

74-
const requiredTokens = [
75-
"readActiveSessionPayloadForLibraryActions()",
76-
"No active Workspace V2 session is available to export.",
77-
"Exported current workspace session JSON.",
78-
"URL.createObjectURL",
79-
"downloadLink.download",
80-
"workspaceSession",
81-
"sessionLibrary",
82-
"sessionHistory",
83-
"activeSessionPayload"
119+
const requiredHtmlTokens = [
120+
'id="workspaceV2NavModeSelect"',
121+
'id="workspaceV2NavToolsActions"',
122+
'id="workspaceV2NavWorkspaceActions"',
123+
'id="workspaceV2ImportWorkspaceButton"',
124+
'id="workspaceV2ExportWorkspaceButton"'
125+
];
126+
requiredHtmlTokens.forEach((token) => {
127+
if (!workspaceHtml.includes(token)) {
128+
failures.push(`Missing nav mode separation HTML token: ${token}`);
129+
}
130+
});
131+
132+
const requiredJsTokens = [
133+
"currentNavMode()",
134+
"renderNavModeActions()",
135+
"importWorkspaceSessionJson()",
136+
"exportWorkspaceSessionJson()",
137+
"buildPortableWorkspaceSessionContainer()",
138+
"if (!this.inToolsNavMode())",
139+
"if (!this.inWorkspaceNavMode())",
140+
"toolSessions",
141+
"savedSessions",
142+
"runtime-only fields are not allowed in portable workspace payload"
84143
];
85-
requiredTokens.forEach((token) => {
144+
requiredJsTokens.forEach((token) => {
86145
if (!workspaceJs.includes(token)) {
87-
failures.push(`Missing export token: ${token}`);
146+
failures.push(`Missing nav/export contract JS token: ${token}`);
88147
}
89148
});
90-
if (workspaceJs.includes("No current session payload to export. Load fixture or import JSON first.")) {
91-
failures.push("Legacy export-empty message is still present.");
92-
}
93149

94150
const activePayload = {
95151
version: "v2",
96152
toolId: "palette-manager-v2",
97-
payloadJson: { swatches: ["#000000", "#ffffff"] }
153+
payloadJson: { swatches: ["#112233", "#445566"] }
98154
};
99-
const activeResult = simulateExport(
100-
activePayload,
101-
"asset-browser-v2",
102-
"palette-manager-v2-1234567890123-abcd1234",
103-
{ "saved-1": { version: "v2", toolId: "asset-browser-v2", payloadJson: { id: 1 } } },
104-
[{ hostContextId: "palette-manager-v2-1234567890123-abcd1234", tool: "palette-manager-v2", timestamp: "2026-05-02T12:00:00.000Z", payload: activePayload }],
105-
{ sessionA: "a", sessionB: "b" },
106-
[{ sourceSessionContextId: "a", targetSessionContextId: "b" }]
107-
);
108-
if (!activeResult.ok) failures.push("Active session export should be allowed.");
109-
if (activeResult.status !== "Exported current workspace session JSON.") {
110-
failures.push("Active session export status mismatch.");
111-
}
112-
if (activeResult.filename !== "workspace-v2-palette-manager-v2-palette-manager-v2-1234567890123-abcd1234.json") {
113-
failures.push("Export filename should include tool/session identity.");
155+
const library = {
156+
"asset-browser-v2-saved": {
157+
version: "v2",
158+
toolId: "asset-browser-v2",
159+
payloadJson: { assetCatalog: { entries: [{ id: "asset-1" }] } }
160+
}
161+
};
162+
163+
const toolExport = simulateToolModeExport(activePayload, "palette-manager-v2-1234567890123-abcd1234");
164+
if (!toolExport.ok) failures.push("Tool mode export should succeed when active payload exists.");
165+
if (toolExport.filename !== "palette-manager-v2-palette-manager-v2-1234567890123-abcd1234.json") {
166+
failures.push("Tool mode export filename mismatch.");
114167
}
115-
const parsedActive = activeResult.serialized ? JSON.parse(activeResult.serialized) : null;
116-
if (!parsedActive || typeof parsedActive !== "object" || Array.isArray(parsedActive)) {
117-
failures.push("Exported JSON should be a workspace container object.");
118-
} else if (!parsedActive.workspaceSession || typeof parsedActive.workspaceSession !== "object") {
119-
failures.push("Exported JSON should include workspaceSession container.");
120-
} else if (JSON.stringify(parsedActive.workspaceSession.activeSessionPayload) !== JSON.stringify(activePayload)) {
121-
failures.push("Exported workspace container does not preserve active session payload exactly.");
168+
const toolExportParsed = toolExport.serialized ? JSON.parse(toolExport.serialized) : null;
169+
if (JSON.stringify(toolExportParsed) !== JSON.stringify(activePayload)) {
170+
failures.push("Tool mode export should preserve active tool payload exactly.");
122171
}
123172

124-
const noActiveResult = simulateExport(null, "asset-browser-v2", "", {}, [], { sessionA: "", sessionB: "" }, []);
125-
if (noActiveResult.ok) failures.push("Export should be blocked when no active session exists.");
126-
if (noActiveResult.status !== "No active Workspace V2 session is available to export.") {
127-
failures.push("No-active export status mismatch.");
173+
const workspaceExport = simulateWorkspaceModeExport(activePayload, "palette-manager-v2-1234567890123-abcd1234", library);
174+
if (!workspaceExport.ok) failures.push("Workspace mode export should succeed when active payload exists.");
175+
if (workspaceExport.filename !== "workspace-v2-palette-manager-v2-palette-manager-v2-1234567890123-abcd1234.json") {
176+
failures.push("Workspace mode export filename mismatch.");
128177
}
178+
const workspaceExportParsed = workspaceExport.serialized ? JSON.parse(workspaceExport.serialized) : null;
179+
if (!workspaceExportParsed || typeof workspaceExportParsed !== "object" || Array.isArray(workspaceExportParsed)) {
180+
failures.push("Workspace mode export should be an object container.");
181+
} else if (!workspaceExportParsed.workspaceSession || typeof workspaceExportParsed.workspaceSession !== "object") {
182+
failures.push("Workspace mode export should include workspaceSession container.");
183+
} else {
184+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceSession, "sessionHistory")) {
185+
failures.push("Workspace export must not include sessionHistory runtime field.");
186+
}
187+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceSession, "sessionSelection")) {
188+
failures.push("Workspace export must not include sessionSelection runtime field.");
189+
}
190+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceSession, "mergeAuditLog")) {
191+
failures.push("Workspace export must not include mergeAuditLog runtime field.");
192+
}
193+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceSession, "activeSessionPayload")) {
194+
failures.push("Workspace export must not include lone activeSessionPayload runtime field.");
195+
}
196+
if (JSON.stringify(workspaceExportParsed.workspaceSession.savedSessions) !== JSON.stringify(library)) {
197+
failures.push("Workspace export should preserve savedSessions payload map.");
198+
}
199+
const activeEntry =
200+
workspaceExportParsed.workspaceSession.toolSessions?.["palette-manager-v2"]?.["palette-manager-v2-1234567890123-abcd1234"];
201+
if (JSON.stringify(activeEntry) !== JSON.stringify(activePayload)) {
202+
failures.push("Workspace export should preserve active payload under toolSessions by tool/session id.");
203+
}
204+
}
205+
206+
const noActiveToolExport = simulateToolModeExport(null, "");
207+
if (noActiveToolExport.ok) failures.push("Tool export should fail when active payload is missing.");
208+
const noActiveWorkspaceExport = simulateWorkspaceModeExport(null, "", {});
209+
if (noActiveWorkspaceExport.ok) failures.push("Workspace export should fail when active payload is missing.");
129210

130211
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
131212
fs.writeFileSync(resultsPath, `${JSON.stringify({
132213
generatedAt: new Date().toISOString(),
133214
failures,
134215
checks: {
216+
workspaceHtmlExists,
135217
workspaceJsExists,
136218
workspaceJsSyntax,
137219
testSyntax
138220
},
139221
scenarios: {
140-
activeResult,
141-
noActiveResult
222+
toolExport,
223+
workspaceExport,
224+
noActiveToolExport,
225+
noActiveWorkspaceExport
142226
}
143227
}, null, 2)}\n`, "utf8");
144228

0 commit comments

Comments
 (0)