Skip to content

Commit 762080d

Browse files
author
DavidQ
committed
Fix Workspace V2 fixture payload shape and manifest-only import export flow - PR_11_304
1 parent 3c2b100 commit 762080d

2 files changed

Lines changed: 171 additions & 88 deletions

File tree

Lines changed: 42 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,60 @@
1-
# PR_11_304 Report - Workspace V2 Clean Export/Import Implementation
1+
# PR_11_304 Report - Workspace V2 Import/Export Continuation Fix
22

33
## Purpose
4-
Replace Workspace V2 import/export with a clean, manifest-only implementation in `tools/workspace-v2/index.js`.
4+
Continue/fix PR_11_304 for fixture/load/export/import/session-id UX in `tools/workspace-v2/index.js` only.
55

66
## Scope
77
- `tools/workspace-v2/index.js` only
88
- No schema changes
99

10-
## Files Changed
11-
- `tools/workspace-v2/index.js`
12-
13-
## Implementation Summary
14-
1. Added Import/Export section-local status wiring
15-
- Added `initializeImportExportSectionStatusNode()`.
16-
- Added `setImportExportStatus(message)`.
17-
- Import/Export actions now always write visible status in the Import/Export section.
18-
19-
2. Export implementation cleanup
20-
- `exportWorkspaceSessionJson()` now uses one direct path:
21-
- `Export clicked`
22-
- build full workspace manifest JSON
23-
- validate manifest
24-
- write JSON to textarea
25-
- download via Blob + temporary anchor click
26-
- `Export success` or `Export error: ...`
27-
- Removed legacy wrapper dependence (`workspaceSession`, `games`) from export structure.
28-
29-
3. Manifest export shape updated
30-
- `buildWorkspaceSchemaDocument()` now produces:
31-
- `documentKind`
32-
- `schema`
10+
## Issues Fixed
11+
1. Load Fixture schema alignment for Palette Manager V2
12+
- Added fixture normalization/validation path in load flow:
13+
- `normalizeFixtureSessionContext(toolId, sessionContext)`
14+
- `normalizePaletteFixtureSwatches(paletteJson)`
15+
- Palette fixture session now lands in active state with `paletteJson.swatches` and without `paletteJson.colors`.
16+
- Palette fixture rejects `payloadJson` for `palette-manager-v2`.
17+
18+
2. Workspace Session JSON textarea manifest-only after Load Fixture
19+
- Load Fixture no longer writes raw tool payload into textarea.
20+
- Added `syncWorkspaceManifestTextarea()` and wired it in fixture load/init path.
21+
- Textarea now shows full workspace manifest JSON after fixture load.
22+
23+
3. Export real payload shape preservation
24+
- Export path uses active session payload source and preserves real tool shape.
25+
- For palette manager, exported `tools.workspace-v2.activeSession` keeps:
3326
- `version`
34-
- `id`
35-
- `name`
36-
- `tools.workspace-v2`
37-
- `tools.workspace-v2` includes:
38-
- `schema`
39-
- `game`
40-
- `defaultToolId`
41-
- `activeToolId`
42-
- `activeHostContextId`
43-
- `activeSession`
44-
- `savedSessions`
45-
- `activeSession` and `savedSessions` preserve real payload objects without rewriting.
46-
47-
4. Import implementation cleanup
48-
- `importWorkspaceSessionJson()` now uses one direct path:
49-
- `Import clicked`
50-
- parse textarea JSON
51-
- reject raw tool payload JSON with exact message:
52-
- `Workspace import requires a workspace manifest JSON`
53-
- validate manifest structure
54-
- load `activeSession` into `sessionStorage`
55-
- load `savedSessions` into `localStorage`
56-
- set current active session/tool
57-
- keep textarea as manifest JSON
58-
- `Import success` or `Import error: ...`
59-
60-
5. Validation function alignment
61-
- Reworked `validateWorkspaceSchemaDocument()` to validate manifest-only structure with `tools.workspace-v2`.
62-
- Added `validateWorkspaceToolSessionPayload()` for active/saved tool session payload validation.
27+
- `toolId`
28+
- `paletteJson.swatches`
29+
- No `payloadJson` wrapper is emitted for fresh palette fixture flow.
30+
31+
4. Session ID validation message
32+
- Updated invalid ID message to exact required text:
33+
- `Invalid session ID. Use letters, numbers, hyphen, or underscore only.`
34+
- Save remains disabled when ID is invalid.
35+
- Removed silent input normalization for this flow:
36+
- `selectedSessionName()` now reads raw input.
37+
- `isValidNewSessionId()` enforces `^[A-Za-z0-9_-]+$`.
6338

6439
## Validation Commands Run
6540
1. `node --check tools/workspace-v2/index.js`
66-
2. `@' ... legacy wrapper removal checks ... '@ | node`
67-
3. `rg -n "initializeImportExportSectionStatusNode\(|setImportExportStatus\(|Workspace import requires a workspace manifest JSON|Export clicked|Export success|Import clicked|Import success" tools/workspace-v2/index.js`
68-
4. `Select-String -Path tools/workspace-v2/index.js -Pattern 'documentKind: "workspace-manifest"','schema: "html-js-gaming.project"','"workspace-v2": \{','activeSession: this.cloneSessionValue\(activePayload\)','savedSessions: this.cloneSessionValue\(library\)'`
41+
2. `node tests/runtime/V2CurrentSessionExport.test.mjs`
42+
3. Inline Node executable check script writing `tmp/pr_11_304_fix_results.json`
43+
4. `rg -n "selectedSessionName\(|return /\^\[A-Za-z0-9_-\]\+\$/.test\(sessionId\);|normalizeFixtureSessionContext\(|normalizePaletteFixtureSwatches\(|syncWorkspaceManifestTextarea\(" tools/workspace-v2/index.js`
6944

7045
## Validation Results
7146
- Command 1: PASS
72-
- Command 2: PASS (`legacy wrapper removal checks: ok`)
73-
- Command 3: PASS (required status and import-rejection tokens found)
74-
- Command 4: PASS (required manifest export shape tokens found)
47+
- Command 2: FAIL (legacy test expectation mismatch: still expects old `workspace.schema.json`/wrapper contract in this branch)
48+
- Command 3: PASS (`tmp/pr_11_304_fix_results.json`, no failures)
49+
- Command 4: PASS (required continuation-fix tokens found)
7550

76-
## Requirement Mapping
77-
- Clean full workspace manifest export: PASS
78-
- `tools.workspace-v2` required fields present: PASS
79-
- Active payload preserved (no payload rewrite): PASS
80-
- Blob + anchor download path: PASS
81-
- Import parses and validates manifest: PASS
82-
- Tool payload import rejection message: PASS
83-
- Section-local status messages (`Export clicked/success/error`, `Import clicked/success/error`): PASS
84-
- No hard-override/multi-handler import-export junk paths introduced: PASS
51+
## Acceptance Mapping
52+
- Load Fixture -> manifest textarea with `tools.workspace-v2.activeSession.paletteJson.swatches`: PASS
53+
- Export downloads without validation error using active payload shape: PASS (targeted executable checks)
54+
- No `paletteJson.colors` in fresh fixture/export flow: PASS
55+
- No `payloadJson` for `palette-manager-v2` fresh fixture/export flow: PASS
56+
- Invalid New Session ID shows actionable message and keeps Save disabled: PASS
8557

8658
## Full Samples Smoke Decision
8759
- Skipped full samples smoke test.
88-
- Reason: change is limited to Workspace V2 import/export logic in one file and validated via targeted checks.
60+
- Reason: scope limited to one file (`tools/workspace-v2/index.js`) and validated via targeted syntax + focused runtime checks.

tools/workspace-v2/index.js

Lines changed: 129 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ class WorkspaceV2SessionProducer {
225225
}
226226

227227
selectedSessionName() {
228-
return typeof this.sessionNameNode.value === "string" ? this.sessionNameNode.value.trim() : "";
228+
return typeof this.sessionNameNode.value === "string" ? this.sessionNameNode.value : "";
229229
}
230230

231231
currentNavMode() {
@@ -285,6 +285,22 @@ class WorkspaceV2SessionProducer {
285285
}
286286
}
287287

288+
createProducerPayloadForTool(toolId) {
289+
if (toolId === "palette-manager-v2") {
290+
return {
291+
version: "v2",
292+
toolId: "palette-manager-v2",
293+
paletteJson: {
294+
swatches: []
295+
}
296+
};
297+
}
298+
return this.withSessionVersion({
299+
toolId,
300+
payloadJson: {}
301+
});
302+
}
303+
288304
initializeWorkspaceProducerSession() {
289305
if (this.isValidSessionPayload(this.currentSessionPayload) && this.currentHostContextId) {
290306
return;
@@ -294,10 +310,7 @@ class WorkspaceV2SessionProducer {
294310
this.statusNode.textContent = "Workspace V2 initialization blocked: default tool is missing.";
295311
return;
296312
}
297-
const initialPayload = this.withSessionVersion({
298-
toolId: selectedToolId,
299-
payloadJson: {}
300-
});
313+
const initialPayload = this.createProducerPayloadForTool(selectedToolId);
301314
const sizeValidation = this.validateSessionPayloadSize(initialPayload);
302315
if (!sizeValidation.ok) {
303316
this.statusNode.textContent = sizeValidation.message;
@@ -307,7 +320,7 @@ class WorkspaceV2SessionProducer {
307320
sessionStorage.setItem(hostContextId, sizeValidation.metrics.serializedPayload);
308321
this.currentHostContextId = hostContextId;
309322
this.setCurrentSessionPayload(initialPayload, "workspace-v2-init");
310-
this.importJsonNode.value = JSON.stringify(initialPayload, null, 2);
323+
this.syncWorkspaceManifestTextarea();
311324
this.statusNode.textContent = `Workspace V2 initialized.\nTool: ${selectedToolId}\nHostContextId: ${hostContextId}\nSession is active for Save Session.`;
312325
}
313326

@@ -323,22 +336,21 @@ class WorkspaceV2SessionProducer {
323336
if (typeof sessionId !== "string") {
324337
return false;
325338
}
326-
const trimmed = sessionId.trim();
327-
if (!trimmed) {
339+
if (!sessionId) {
328340
return false;
329341
}
330-
return /^[a-z0-9][a-z0-9-_]{1,63}$/i.test(trimmed);
342+
return /^[A-Za-z0-9_-]+$/.test(sessionId);
331343
}
332344

333345
savedSessionIdExists(sessionId) {
334-
if (typeof sessionId !== "string" || !sessionId.trim()) {
346+
if (typeof sessionId !== "string" || !sessionId) {
335347
return false;
336348
}
337349
const library = this.readSessionLibrary();
338350
if (library === null) {
339351
return false;
340352
}
341-
return Object.prototype.hasOwnProperty.call(library, sessionId.trim());
353+
return Object.prototype.hasOwnProperty.call(library, sessionId);
342354
}
343355

344356
updateSessionLibraryActionState() {
@@ -348,7 +360,7 @@ class WorkspaceV2SessionProducer {
348360
return;
349361
}
350362
if (!model.libraryIdValid) {
351-
this.libraryStatusNode.textContent = "Enter a valid new session ID before saving.";
363+
this.libraryStatusNode.textContent = "Invalid session ID. Use letters, numbers, hyphen, or underscore only.";
352364
return;
353365
}
354366
if (model.librarySavedSessionExists) {
@@ -2789,6 +2801,78 @@ class WorkspaceV2SessionProducer {
27892801
this.diagnosticsPayloadNode.textContent = snapshot.payloadPreview;
27902802
}
27912803

2804+
normalizePaletteFixtureSwatches(paletteJson) {
2805+
if (!paletteJson || typeof paletteJson !== "object" || Array.isArray(paletteJson)) {
2806+
return { ok: false, message: "Fixture is invalid. paletteJson must be an object for palette-manager-v2.", value: null };
2807+
}
2808+
if (Object.prototype.hasOwnProperty.call(paletteJson, "colors") && !Array.isArray(paletteJson.swatches)) {
2809+
if (!Array.isArray(paletteJson.colors)) {
2810+
return { ok: false, message: "Fixture is invalid. paletteJson.colors must be an array when swatches is missing.", value: null };
2811+
}
2812+
const convertedSwatches = [];
2813+
for (let index = 0; index < paletteJson.colors.length; index += 1) {
2814+
const colorEntry = paletteJson.colors[index];
2815+
if (typeof colorEntry === "string") {
2816+
if (!/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(colorEntry)) {
2817+
return { ok: false, message: `Fixture is invalid. paletteJson.colors[${index}] must be #RRGGBB or #RRGGBBAA.`, value: null };
2818+
}
2819+
convertedSwatches.push({
2820+
symbol: String.fromCharCode(65 + (index % 26)),
2821+
hex: colorEntry,
2822+
name: `Color ${index + 1}`
2823+
});
2824+
continue;
2825+
}
2826+
if (!colorEntry || typeof colorEntry !== "object" || Array.isArray(colorEntry)) {
2827+
return { ok: false, message: `Fixture is invalid. paletteJson.colors[${index}] must be a string or object.`, value: null };
2828+
}
2829+
if (typeof colorEntry.hex !== "string" || !/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(colorEntry.hex)) {
2830+
return { ok: false, message: `Fixture is invalid. paletteJson.colors[${index}].hex must be #RRGGBB or #RRGGBBAA.`, value: null };
2831+
}
2832+
const symbol = typeof colorEntry.symbol === "string" && colorEntry.symbol.length === 1
2833+
? colorEntry.symbol
2834+
: String.fromCharCode(65 + (index % 26));
2835+
const name = typeof colorEntry.name === "string" && colorEntry.name.trim()
2836+
? colorEntry.name
2837+
: `Color ${index + 1}`;
2838+
convertedSwatches.push({
2839+
symbol,
2840+
hex: colorEntry.hex,
2841+
name
2842+
});
2843+
}
2844+
const normalizedPalette = { ...paletteJson };
2845+
delete normalizedPalette.colors;
2846+
normalizedPalette.swatches = convertedSwatches;
2847+
return { ok: true, message: "", value: normalizedPalette };
2848+
}
2849+
if (!Array.isArray(paletteJson.swatches)) {
2850+
return { ok: false, message: "Fixture is invalid. paletteJson.swatches must be an array for palette-manager-v2.", value: null };
2851+
}
2852+
return { ok: true, message: "", value: paletteJson };
2853+
}
2854+
2855+
normalizeFixtureSessionContext(toolId, sessionContext) {
2856+
if (!this.isValidSessionPayload(sessionContext)) {
2857+
return { ok: false, message: "Fixture is invalid. Missing sessionContext object.", value: null };
2858+
}
2859+
const normalizedSession = this.withSessionVersion(this.cloneSessionValue(sessionContext));
2860+
if (toolId === "palette-manager-v2") {
2861+
if (Object.prototype.hasOwnProperty.call(normalizedSession, "payloadJson")) {
2862+
return { ok: false, message: "Fixture is invalid. payloadJson is not supported for palette-manager-v2.", value: null };
2863+
}
2864+
if (!Object.prototype.hasOwnProperty.call(normalizedSession, "paletteJson")) {
2865+
return { ok: false, message: "Fixture is invalid. paletteJson is required for palette-manager-v2.", value: null };
2866+
}
2867+
const normalizedPalette = this.normalizePaletteFixtureSwatches(normalizedSession.paletteJson);
2868+
if (!normalizedPalette.ok) {
2869+
return { ok: false, message: normalizedPalette.message, value: null };
2870+
}
2871+
normalizedSession.paletteJson = normalizedPalette.value;
2872+
}
2873+
return { ok: true, message: "", value: normalizedSession };
2874+
}
2875+
27922876
async loadSelectedFixture() {
27932877
const toolId = this.selectedToolId();
27942878
if (!toolId) {
@@ -2808,15 +2892,29 @@ class WorkspaceV2SessionProducer {
28082892
this.setCurrentSessionPayload(null, "");
28092893
return;
28102894
}
2811-
if (!this.isValidSessionPayload(fixture.sessionContext)) {
2812-
this.statusNode.textContent = "Fixture is invalid. Missing sessionContext object.";
2895+
const normalizedFixtureSession = this.normalizeFixtureSessionContext(toolId, fixture.sessionContext);
2896+
if (!normalizedFixtureSession.ok) {
2897+
this.statusNode.textContent = normalizedFixtureSession.message;
28132898
this.setCurrentSessionPayload(null, "");
28142899
return;
28152900
}
2816-
this.setCurrentSessionPayload(fixture.sessionContext, `fixture:${toolId}`);
2817-
this.currentHostContextId = "";
2901+
const fixtureHostContextId = typeof fixture.hostContextId === "string" && fixture.hostContextId.trim()
2902+
? fixture.hostContextId.trim()
2903+
: this.createHostContextId(toolId);
2904+
const sizeValidation = this.validateSessionPayloadSize(normalizedFixtureSession.value);
2905+
if (!sizeValidation.ok) {
2906+
this.statusNode.textContent = sizeValidation.message;
2907+
this.setCurrentSessionPayload(null, "");
2908+
return;
2909+
}
2910+
sessionStorage.setItem(fixtureHostContextId, sizeValidation.metrics.serializedPayload);
2911+
this.setCurrentSessionPayload(normalizedFixtureSession.value, `fixture:${toolId}`);
2912+
this.currentHostContextId = fixtureHostContextId;
28182913
this.renderDiagnosticsPanel();
2819-
this.importJsonNode.value = JSON.stringify(fixture.sessionContext, null, 2);
2914+
if (!this.syncWorkspaceManifestTextarea()) {
2915+
this.statusNode.textContent = "Fixture loaded but workspace manifest sync failed.";
2916+
return;
2917+
}
28202918
this.statusNode.textContent = `Fixture loaded for ${toolId}.\nSession payload is ready for launch, export, share, or library save.`;
28212919
} catch (error) {
28222920
this.setCurrentSessionPayload(null, "");
@@ -2924,6 +3022,19 @@ class WorkspaceV2SessionProducer {
29243022
}
29253023
}
29263024

3025+
syncWorkspaceManifestTextarea() {
3026+
const workspaceSchemaDocument = this.buildWorkspaceSchemaDocument();
3027+
if (!workspaceSchemaDocument) {
3028+
return false;
3029+
}
3030+
const validation = this.validateWorkspaceSchemaDocument(workspaceSchemaDocument);
3031+
if (!validation.ok) {
3032+
return false;
3033+
}
3034+
this.workspaceJsonNode.value = JSON.stringify(workspaceSchemaDocument, null, 2);
3035+
return true;
3036+
}
3037+
29273038
buildWorkspaceSchemaDocument() {
29283039
const activePayload = this.resolveActiveSessionPayloadForWorkspaceManifest();
29293040
if (!this.isValidSessionPayload(activePayload)) {
@@ -3241,7 +3352,7 @@ class WorkspaceV2SessionProducer {
32413352
return;
32423353
}
32433354
if (!overwriteExisting && !this.isValidNewSessionId(sessionName)) {
3244-
this.setLibraryStatus("Enter a valid new session ID before saving.");
3355+
this.setLibraryStatus("Invalid session ID. Use letters, numbers, hyphen, or underscore only.");
32453356
return;
32463357
}
32473358
const library = this.readSessionLibrary();

0 commit comments

Comments
 (0)