Skip to content

Commit bdc26e2

Browse files
author
DavidQ
committed
Force Workspace V2 export to manifest schema and block non-manifest session wrappers - PR_11_277
1 parent 2e392a0 commit bdc26e2

7 files changed

Lines changed: 347 additions & 52 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# PR_11_277 Workspace V2 Manifest-Only Export Enforcement Report
2+
3+
## Scope
4+
Workspace V2 export/import only, plus minimal workspace manifest schema support required for Workspace V2 portable session persistence.
5+
6+
## Files Changed
7+
- tools/workspace-v2/index.html
8+
- tools/workspace-v2/index.js
9+
- tools/schemas/workspace.manifest.schema.json
10+
- tests/runtime/V2CurrentSessionExport.test.mjs
11+
- docs/pr/PLAN_PR_11_277_WORKSPACE_V2_MANIFEST_ONLY_EXPORT_ENFORCEMENT.md
12+
- docs/pr/BUILD_PR_11_277_WORKSPACE_V2_MANIFEST_ONLY_EXPORT_ENFORCEMENT.md
13+
- docs/dev/reports/PR_11_277_workspace_v2_manifest_only_export_enforcement_report.md
14+
15+
## Implementation Summary
16+
- Replaced Workspace V2 custom wrapper export (`version/toolId/workspaceSession`) with workspace manifest root export shape.
17+
- Added `workspaceV2Session` as a manifest-schema-approved session block for Workspace V2 portable session data.
18+
- Export path now validates manifest contract before download and blocks with explicit actionable messages if invalid.
19+
- Import path now accepts manifest-root payload and restores session data from `workspaceV2Session`.
20+
- Added explicit wrapper guard: `workspaceSession` root is rejected.
21+
- Kept Save/Load/Diff/Merge logic paths unchanged and preserved the same active-session source (`readActiveSessionPayloadForLibraryActions`).
22+
23+
## Validation Commands Run
24+
1. `node --check tools/workspace-v2/index.js`
25+
- PASS
26+
2. `node --check tests/runtime/V2CurrentSessionExport.test.mjs`
27+
- PASS
28+
3. `node tests/runtime/V2CurrentSessionExport.test.mjs`
29+
- PASS
30+
- Results: `tmp/v2-current-session-export-results.json`
31+
32+
## Manifest Contract Evidence
33+
- Export root shape is now workspace manifest fields (`documentKind/schema/version/id/name/tools`) with `workspaceV2Session`.
34+
- Export no longer emits `workspaceSession` wrapper.
35+
- Runtime validator blocks wrapper payloads and blocks runtime-only fields.
36+
- Export/import success path preserves active tool session payload under `workspaceV2Session.toolSessions`.
37+
38+
## Save/Load/Diff/Merge Compatibility Note
39+
- Session library, diff, and merge action methods remain in Workspace V2 unchanged and still bind to the same active session source.
40+
- This PR only changed Workspace V2 export/import contract wiring and schema gating.
41+
42+
## Additional Targeted Regression Notes
43+
- Existing broad standalone tests `V2SessionLibrary.test.mjs` and `V2SessionDiff.test.mjs` currently fail on pre-existing baseline token/contract expectations unrelated to PR_11_277 export/import contract changes.
44+
- `V2SessionMerge.test.mjs` passes.
45+
46+
## Full Samples Smoke Decision
47+
- Skipped full samples smoke test.
48+
- Reason: scope is limited to Workspace V2 export/import and workspace manifest schema contract gating; targeted runtime validation covers the changed behavior.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# BUILD_PR_11_277_WORKSPACE_V2_MANIFEST_ONLY_EXPORT_ENFORCEMENT
2+
3+
## Purpose
4+
Implement manifest-only export/import enforcement for Workspace V2 with schema-gated validation and no custom wrapper.
5+
6+
## Files
7+
- tools/workspace-v2/index.html
8+
- tools/workspace-v2/index.js
9+
- tools/schemas/workspace.manifest.schema.json
10+
- tests/runtime/V2CurrentSessionExport.test.mjs
11+
- docs/dev/reports/PR_11_277_workspace_v2_manifest_only_export_enforcement_report.md
12+
13+
## Implementation
14+
1. Replace Workspace V2 export builder from custom `{ version, toolId, workspaceSession }` wrapper to manifest-root document:
15+
- `documentKind`, `schema`, `version`, `id`, `name`, `tools`
16+
- `workspaceV2Session` payload block for Workspace V2 session persistence.
17+
2. Enforce export/import validation through one manifest-contract validator method in Workspace V2 before export download/import apply.
18+
3. Block export/import when wrapper payload is supplied (`workspaceSession` root), with explicit actionable message.
19+
4. Keep runtime-only fields blocked from portable export/import payloads.
20+
5. Import path reads manifest-root `workspaceV2Session` and restores sessionStorage/library + active payload.
21+
6. Update runtime test to assert:
22+
- no custom wrapper
23+
- manifest-root shape
24+
- schema field presence for `workspaceV2Session`
25+
- active payload preservation under `workspaceV2Session.toolSessions`.
26+
7. Add only minimal workspace manifest schema additions needed for Workspace V2 session persistence.
27+
28+
## Acceptance
29+
- Workspace export root is manifest shape, not wrapper.
30+
- Export/import are blocked when manifest contract is invalid.
31+
- No `workspaceSession` wrapper is emitted.
32+
- Runtime-only fields are excluded from portable payload.
33+
- Save/Load/Diff/Merge remain operational under same session source.
34+
35+
## Validation
36+
- `node --check tools/workspace-v2/index.js`
37+
- `node --check tests/runtime/V2CurrentSessionExport.test.mjs`
38+
- `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_277_WORKSPACE_V2_MANIFEST_ONLY_EXPORT_ENFORCEMENT
2+
3+
## Purpose
4+
Enforce manifest-only Workspace V2 export/import so exported JSON is workspace manifest root shape, not a custom wrapper.
5+
6+
## Scope
7+
- tools/workspace-v2/index.html
8+
- tools/workspace-v2/index.js
9+
- tools/schemas/workspace.manifest.schema.json (minimal Workspace V2 session field support only)
10+
- tests/runtime/V2CurrentSessionExport.test.mjs
11+
- docs/dev/reports/PR_11_277_workspace_v2_manifest_only_export_enforcement_report.md
12+
13+
## Goals
14+
- Export root shape is workspace manifest (`documentKind/schema/version/id/name/tools`).
15+
- No `workspaceSession` wrapper export.
16+
- Workspace V2 session persistence lives in manifest-allowed field(s) only.
17+
- Export path validates manifest contract before download and blocks with actionable error when invalid.
18+
- Import accepts manifest-root shape and restores Workspace V2 tool sessions/saved sessions.
19+
- Save/Load/Diff/Merge flows remain intact (no behavioral rewrites).
20+
21+
## Out Of Scope
22+
- No tool schema changes.
23+
- No non-Workspace-V2 behavior changes.
24+
- No platformShell/tools/shared rewiring.
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: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,15 @@ function simulateWorkspaceModeExport(activePayload, currentHostContextId, librar
6767
toolSessions[payloadToolId][sessionId] = payload;
6868
});
6969
const container = {
70-
version: "v2",
71-
toolId: "workspace-v2",
72-
workspaceSession: {
73-
workspaceToolId: "workspace-v2",
74-
workspaceSessionId: activeHostContextId,
70+
$schema: "../../tools/schemas/workspace.manifest.schema.json",
71+
documentKind: "workspace-manifest",
72+
schema: "html-js-gaming.project",
73+
version: 1,
74+
id: `workspace-v2-${activeHostContextId || "session"}`,
75+
name: "Workspace V2 Session Export",
76+
tools: {},
77+
workspaceV2Session: {
78+
schema: "html-js-gaming.workspace-v2-session/1",
7579
defaultToolId: "palette-manager-v2",
7680
activeToolId,
7781
activeHostContextId,
@@ -94,6 +98,9 @@ export function run() {
9498
const workspaceJsExists = fs.existsSync(workspaceJsPath);
9599
const workspaceHtml = workspaceHtmlExists ? fs.readFileSync(workspaceHtmlPath, "utf8") : "";
96100
const workspaceJs = workspaceJsExists ? fs.readFileSync(workspaceJsPath, "utf8") : "";
101+
const workspaceManifestSchema = JSON.parse(
102+
fs.readFileSync(path.join(repoRoot, "tools", "schemas", "workspace.manifest.schema.json"), "utf8")
103+
);
97104
const workspaceJsSyntax = checkSyntax(workspaceJsPath);
98105
const testSyntax = checkSyntax(testPath);
99106

@@ -132,17 +139,33 @@ export function run() {
132139
const requiredWorkspaceJsTokens = [
133140
"importWorkspaceSessionJson()",
134141
"exportWorkspaceSessionJson()",
135-
"buildPortableWorkspaceSessionContainer()",
142+
"buildWorkspaceManifestDocument()",
143+
"validateWorkspaceManifestDocument(",
144+
"workspaceV2Session",
136145
"runtime-only fields are not allowed in portable workspace payload",
137146
"toolSessions",
138-
"savedSessions"
147+
"savedSessions",
148+
"workspaceSession wrapper is not allowed"
139149
];
140150
requiredWorkspaceJsTokens.forEach((token) => {
141151
if (!workspaceJs.includes(token)) {
142152
failures.push(`Missing required workspace contract JS token: ${token}`);
143153
}
144154
});
145155

156+
const requiredSessionFlowTokens = [
157+
"saveNamedSession(",
158+
"loadNamedSession(",
159+
"computeSelectedSessionDiff(",
160+
"computeSelectedSessionMerge(",
161+
"readActiveSessionPayloadForLibraryActions()"
162+
];
163+
requiredSessionFlowTokens.forEach((token) => {
164+
if (!workspaceJs.includes(token)) {
165+
failures.push(`Missing expected session flow token: ${token}`);
166+
}
167+
});
168+
146169
TOOL_IDS.forEach((toolId) => {
147170
const toolHtmlPath = path.join(repoRoot, "tools", toolId, "index.html");
148171
const toolHtmlExists = fs.existsSync(toolHtmlPath);
@@ -181,24 +204,49 @@ export function run() {
181204
if (!workspaceExport.ok) failures.push("Workspace export should succeed when active payload exists.");
182205
const workspaceExportParsed = workspaceExport.serialized ? JSON.parse(workspaceExport.serialized) : null;
183206
if (!workspaceExportParsed || typeof workspaceExportParsed !== "object" || Array.isArray(workspaceExportParsed)) {
184-
failures.push("Workspace export should be an object container.");
185-
} else if (!workspaceExportParsed.workspaceSession || typeof workspaceExportParsed.workspaceSession !== "object") {
186-
failures.push("Workspace export should include workspaceSession container.");
207+
failures.push("Workspace export should be an object.");
187208
} else {
188-
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceSession, "sessionHistory")) {
209+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed, "workspaceSession")) {
210+
failures.push("Workspace export must not include workspaceSession wrapper.");
211+
}
212+
if (workspaceExportParsed.documentKind !== "workspace-manifest") {
213+
failures.push("Workspace export documentKind must be workspace-manifest.");
214+
}
215+
if (!workspaceExportParsed.workspaceV2Session || typeof workspaceExportParsed.workspaceV2Session !== "object") {
216+
failures.push("Workspace export should include workspaceV2Session payload.");
217+
}
218+
if (!workspaceExportParsed.tools || typeof workspaceExportParsed.tools !== "object" || Array.isArray(workspaceExportParsed.tools)) {
219+
failures.push("Workspace export should include tools object.");
220+
}
221+
const requiredSchemaKeys = Array.isArray(workspaceManifestSchema.required) ? workspaceManifestSchema.required : [];
222+
requiredSchemaKeys.forEach((schemaKey) => {
223+
if (!Object.prototype.hasOwnProperty.call(workspaceExportParsed, schemaKey)) {
224+
failures.push(`Workspace export is missing required schema key: ${schemaKey}`);
225+
}
226+
});
227+
if (!Object.prototype.hasOwnProperty.call(workspaceManifestSchema.properties || {}, "workspaceV2Session")) {
228+
failures.push("workspace.manifest schema should define workspaceV2Session property.");
229+
}
230+
if (!workspaceManifestSchema.properties?.workspaceV2Session?.properties?.toolSessions) {
231+
failures.push("workspace.manifest schema should define workspaceV2Session.toolSessions.");
232+
}
233+
if (!workspaceManifestSchema.properties?.workspaceV2Session?.properties?.savedSessions) {
234+
failures.push("workspace.manifest schema should define workspaceV2Session.savedSessions.");
235+
}
236+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceV2Session, "sessionHistory")) {
189237
failures.push("Workspace export must not include sessionHistory runtime field.");
190238
}
191-
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceSession, "sessionSelection")) {
239+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceV2Session, "sessionSelection")) {
192240
failures.push("Workspace export must not include sessionSelection runtime field.");
193241
}
194-
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceSession, "mergeAuditLog")) {
242+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceV2Session, "mergeAuditLog")) {
195243
failures.push("Workspace export must not include mergeAuditLog runtime field.");
196244
}
197-
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceSession, "activeSessionPayload")) {
245+
if (Object.prototype.hasOwnProperty.call(workspaceExportParsed.workspaceV2Session, "activeSessionPayload")) {
198246
failures.push("Workspace export must not include lone activeSessionPayload runtime field.");
199247
}
200248
const activeEntry =
201-
workspaceExportParsed.workspaceSession.toolSessions?.["palette-manager-v2"]?.["palette-manager-v2-1234567890123-abcd1234"];
249+
workspaceExportParsed.workspaceV2Session.toolSessions?.["palette-manager-v2"]?.["palette-manager-v2-1234567890123-abcd1234"];
202250
if (JSON.stringify(activeEntry) !== JSON.stringify(activePayload)) {
203251
failures.push("Workspace export should preserve active payload under toolSessions by tool/session id.");
204252
}

tools/schemas/workspace.manifest.schema.json

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
},
3333
"tools": {
3434
"type": "object",
35-
"required": ["palette-browser"],
35+
"required": [],
3636
"additionalProperties": false,
3737
"description": "Workspace-owned tool entry map keyed by canonical registry ids; payload schemas are referenced, never inlined here.",
3838
"properties": {
@@ -88,6 +88,111 @@
8888
"$ref": "./tools/3d-camera-path-editor.schema.json"
8989
}
9090
}
91+
},
92+
"workspaceV2Session": {
93+
"type": "object",
94+
"required": ["schema", "defaultToolId", "activeToolId", "activeHostContextId", "toolSessions", "savedSessions", "exportedAt"],
95+
"additionalProperties": false,
96+
"properties": {
97+
"schema": {
98+
"type": "string",
99+
"const": "html-js-gaming.workspace-v2-session/1"
100+
},
101+
"defaultToolId": {
102+
"type": "string",
103+
"minLength": 1
104+
},
105+
"activeToolId": {
106+
"type": "string",
107+
"minLength": 1
108+
},
109+
"activeHostContextId": {
110+
"type": "string",
111+
"minLength": 1
112+
},
113+
"toolSessions": {
114+
"type": "object",
115+
"additionalProperties": false,
116+
"patternProperties": {
117+
"^.*$": {
118+
"type": "object",
119+
"additionalProperties": false,
120+
"patternProperties": {
121+
"^.*$": {
122+
"$ref": "#/$defs/jsonValue"
123+
}
124+
},
125+
"propertyNames": {
126+
"type": "string"
127+
}
128+
}
129+
},
130+
"propertyNames": {
131+
"type": "string"
132+
}
133+
},
134+
"savedSessions": {
135+
"type": "object",
136+
"additionalProperties": false,
137+
"patternProperties": {
138+
"^.*$": {
139+
"$ref": "#/$defs/jsonValue"
140+
}
141+
},
142+
"propertyNames": {
143+
"type": "string"
144+
}
145+
},
146+
"exportedAt": {
147+
"type": "string",
148+
"minLength": 1
149+
}
150+
}
151+
}
152+
},
153+
"$defs": {
154+
"jsonValue": {
155+
"oneOf": [
156+
{
157+
"type": "string"
158+
},
159+
{
160+
"type": "number"
161+
},
162+
{
163+
"type": "integer"
164+
},
165+
{
166+
"type": "boolean"
167+
},
168+
{
169+
"type": "null"
170+
},
171+
{
172+
"$ref": "#/$defs/jsonArray"
173+
},
174+
{
175+
"$ref": "#/$defs/jsonObject"
176+
}
177+
]
178+
},
179+
"jsonArray": {
180+
"type": "array",
181+
"items": {
182+
"$ref": "#/$defs/jsonValue"
183+
}
184+
},
185+
"jsonObject": {
186+
"type": "object",
187+
"additionalProperties": false,
188+
"patternProperties": {
189+
"^.*$": {
190+
"$ref": "#/$defs/jsonValue"
191+
}
192+
},
193+
"propertyNames": {
194+
"type": "string"
195+
}
91196
}
92197
}
93198
}

tools/workspace-v2/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ <h2>Producer</h2>
3737
<h2>Import / Export Session JSON</h2>
3838
<p>Workspace mode actions (navWorkspace): import/export a portable Workspace V2 session manifest.</p>
3939
<label for="workspaceV2ImportJson">Workspace Session JSON</label>
40-
<textarea id="workspaceV2ImportJson" rows="12" spellcheck="false" placeholder='{"version":"v2","toolId":"workspace-v2","workspaceSession":{}}'></textarea>
40+
<textarea id="workspaceV2ImportJson" rows="12" spellcheck="false" placeholder='{"documentKind":"workspace-manifest","schema":"html-js-gaming.project","version":1,"id":"workspace-v2-session","name":"Workspace V2 Session","tools":{},"workspaceV2Session":{}}'></textarea>
4141
<label for="workspaceV2ImportFile">Import File</label>
4242
<input id="workspaceV2ImportFile" type="file" accept="application/json,.json" />
4343
<div>

0 commit comments

Comments
 (0)