Skip to content

Commit 4c12673

Browse files
author
DavidQ
committed
314
1 parent 5315af0 commit 4c12673

8 files changed

Lines changed: 277 additions & 2 deletions

File tree

docs/dev/codex_commands.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,10 @@ PR_11_313 (single contract rename: asset-manager-v2)
138138
```bash
139139
npx @openai/codex run --model gpt-5.3-codex --reasoning medium "Implement PR_11_313: rename legacy asset browser v2 id to asset-manager-v2 across tool ID, paths, payloads, registry links, tests, and docs with zero old references."
140140
```
141+
142+
---
143+
PR_11_314
144+
145+
```bash
146+
npx @openai/codex run --model gpt-5.3-codex --reasoning medium "Implement PR_11_314: persist valid Asset Manager V2 payloadJson.assetCatalog into Workspace V2 active session/export path and block invalid writes."
147+
```

docs/dev/commit_comment.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Rename legacy asset browser v2 id to asset-manager-v2 as a single repo-wide contract with zero legacy references - PR 11.313
1+
Persist valid Asset Manager V2 assetCatalog into Workspace V2 activeSession export path and block invalid write-back - PR 11.314
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# PR_11_314 Report
2+
3+
## Purpose
4+
Persist valid Asset Manager V2 session payloads into Workspace V2 manifest export, while blocking invalid payload writes.
5+
6+
## Files Changed
7+
- `tools/asset-manager-v2/index.js`
8+
- `tools/workspace-v2/index.js`
9+
- `tests/runtime/V2AssetManagerWorkspacePersistence.test.mjs`
10+
- `docs/pr/PR_11_314_ASSET_MANAGER_WORKSPACE_PERSISTENCE/PLAN_PR.md`
11+
- `docs/pr/PR_11_314_ASSET_MANAGER_WORKSPACE_PERSISTENCE/BUILD_PR.md`
12+
- `docs/dev/reports/PR_11_314_report.md`
13+
- `docs/dev/codex_commands.md`
14+
- `docs/dev/commit_comment.txt`
15+
16+
## Implementation Summary
17+
- Added Asset Manager V2 valid-session persistence write-back to `sessionStorage` at current `hostContextId`.
18+
- Removed valid-session deferred workspace-write message:
19+
- old deferred wording removed
20+
- valid state now reports persistence status for export path
21+
- Added Workspace V2 restore-from-URL host context logic for valid `asset-manager-v2` sessions.
22+
- Ensured invalid payloads still block writes and are not exported through active-session restore.
23+
24+
## Validation Commands
25+
- `node --check tools/asset-manager-v2/index.js` -> **PASS**
26+
- `node --check tools/workspace-v2/index.js` -> **PASS**
27+
- `node --check tests/runtime/V2AssetManagerWorkspacePersistence.test.mjs` -> **PASS**
28+
- `node tests/runtime/V2AssetManagerWorkspacePersistence.test.mjs` -> **PASS**
29+
- `rg -n "asset-browser-v2" .` -> **PASS** (zero matches)
30+
- `rg --files | rg "asset-browser-v2"` -> **PASS** (zero matches)
31+
32+
## Targeted Test Notes
33+
- Test verifies:
34+
- deferred message removed for valid Asset Manager sessions
35+
- valid session persistence path exists and writes via host context
36+
- Workspace V2 host-context restore path exists and syncs export source
37+
- valid writes allowed, invalid writes blocked
38+
39+
## Full Samples Smoke
40+
- **Skipped intentionally**.
41+
- Reason: PR scope is isolated to Asset Manager/Workspace persistence path; targeted runtime checks and syntax checks cover the changed behavior.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# BUILD_PR_11_314
2+
3+
## Implementation
4+
- Added Asset Manager V2 write-back method `persistValidSessionForWorkspace(sessionContext, assetCatalog)`:
5+
- Writes only valid `asset-manager-v2` session payloads to `sessionStorage` using current `hostContextId`.
6+
- Preserves strict `payloadJson.assetCatalog` contract and blocks invalid writes.
7+
- Keeps session size guard enforcement.
8+
- Updated valid-state workspace readout in Asset Manager V2:
9+
- Removed deferred wording.
10+
- Reports persisted status for Workspace V2 export.
11+
- Added Workspace V2 URL-host restore path:
12+
- `restoreActiveSessionFromHostContextIdUrl()`
13+
- Restores `activeSession` from `?hostContextId=` when payload validates and `toolId === "asset-manager-v2"`.
14+
- Syncs manifest textarea so exports include restored `activeSession.payloadJson.assetCatalog`.
15+
- Added targeted runtime test:
16+
- `tests/runtime/V2AssetManagerWorkspacePersistence.test.mjs`
17+
18+
## Validation
19+
- `node --check tools/asset-manager-v2/index.js`
20+
- `node --check tools/workspace-v2/index.js`
21+
- `node --check tests/runtime/V2AssetManagerWorkspacePersistence.test.mjs`
22+
- `node tests/runtime/V2AssetManagerWorkspacePersistence.test.mjs`
23+
- `rg -n "asset-browser-v2" .`
24+
- `rg --files | rg "asset-browser-v2"`
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# PLAN_PR_11_314
2+
3+
## Purpose
4+
Persist valid Asset Manager V2 session payloads into the Workspace V2 manifest export path.
5+
6+
## Scope
7+
- `tools/asset-manager-v2/index.js`
8+
- `tools/workspace-v2/index.js`
9+
- `tests/runtime/V2AssetManagerWorkspacePersistence.test.mjs`
10+
- `docs/dev/reports/PR_11_314_report.md`
11+
- `docs/dev/codex_commands.md`
12+
- `docs/dev/commit_comment.txt`
13+
14+
## Steps
15+
1. Add valid-session persistence write-back in Asset Manager V2 after strict `payloadJson.assetCatalog` validation.
16+
2. Remove the deferred workspace-write message for valid Asset Manager sessions and replace with persistence status text.
17+
3. Add Workspace V2 host-context restoration so `activeSession` is restored from `?hostContextId=` when it contains a valid `asset-manager-v2` payload.
18+
4. Keep invalid payloads blocked from write-back/export.
19+
5. Add targeted runtime validation for persistence/restore behavior.
20+
6. Run targeted syntax checks and legacy-ID zero-match audit.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import { execFileSync } from "node:child_process";
5+
import { fileURLToPath, pathToFileURL } from "node:url";
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
const repoRoot = path.resolve(__dirname, "..", "..");
10+
const assetManagerPath = path.join(repoRoot, "tools", "asset-manager-v2", "index.js");
11+
const workspacePath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
12+
const resultsPath = path.join(repoRoot, "tmp", "pr_11_314_asset_manager_workspace_persistence_results.json");
13+
14+
function readText(filePath) {
15+
return fs.readFileSync(filePath, "utf8");
16+
}
17+
18+
function checkSyntax(filePath) {
19+
execFileSync(process.execPath, ["--check", filePath], {
20+
cwd: repoRoot,
21+
stdio: ["ignore", "pipe", "pipe"]
22+
});
23+
}
24+
25+
function simulatePersistence(hostContextId, sessionContext, limitBytes) {
26+
if (!hostContextId) return { ok: false, message: "No hostContextId is available for Workspace V2 persistence." };
27+
if (!sessionContext || typeof sessionContext !== "object" || Array.isArray(sessionContext)) {
28+
return { ok: false, message: "Session context is invalid for Workspace V2 persistence." };
29+
}
30+
if (!sessionContext.payloadJson || typeof sessionContext.payloadJson !== "object" || Array.isArray(sessionContext.payloadJson)) {
31+
return { ok: false, message: "payloadJson is invalid for Workspace V2 persistence." };
32+
}
33+
if (!sessionContext.payloadJson.assetCatalog || typeof sessionContext.payloadJson.assetCatalog !== "object" || Array.isArray(sessionContext.payloadJson.assetCatalog)) {
34+
return { ok: false, message: "payloadJson.assetCatalog is invalid for Workspace V2 persistence." };
35+
}
36+
const payload = {
37+
version: "v2",
38+
toolId: "asset-manager-v2",
39+
payloadJson: sessionContext.payloadJson
40+
};
41+
const serialized = JSON.stringify(payload);
42+
if (serialized.length > limitBytes) {
43+
return { ok: false, message: `Session size exceeds allowed limit for Workspace V2 persistence. Payload is ${serialized.length} bytes and limit is ${limitBytes} bytes.` };
44+
}
45+
return { ok: true, payload: JSON.parse(serialized) };
46+
}
47+
48+
export function run() {
49+
checkSyntax(assetManagerPath);
50+
checkSyntax(workspacePath);
51+
52+
const assetSource = readText(assetManagerPath);
53+
const workspaceSource = readText(workspacePath);
54+
55+
const validSession = {
56+
version: "v2",
57+
toolId: "asset-manager-v2",
58+
payloadJson: {
59+
assetCatalog: {
60+
name: "Catalog",
61+
entries: [
62+
{ id: "a1", label: "Ship", kind: "svg", path: "assets/ship.svg" }
63+
]
64+
}
65+
}
66+
};
67+
const invalidSession = {
68+
version: "v2",
69+
toolId: "asset-manager-v2",
70+
payloadJson: {}
71+
};
72+
73+
const validWrite = simulatePersistence("asset-manager-v2-123", validSession, 1024 * 1024);
74+
const invalidWrite = simulatePersistence("asset-manager-v2-123", invalidSession, 1024 * 1024);
75+
76+
const summary = {
77+
generatedAt: new Date().toISOString(),
78+
checks: {
79+
noDeferredWorkspaceWriteMessage: !assetSource.includes("Workspace writes are deferred for this isolated V2 entry."),
80+
hasValidSessionPersistenceMethod: assetSource.includes("persistValidSessionForWorkspace(sessionContext, assetCatalog)") &&
81+
assetSource.includes("window.sessionStorage.setItem(this.urlState.hostContextId, serializedPayload);"),
82+
hasWorkspacePersistenceReadout: assetSource.includes("Workspace session context was read and persisted for Workspace V2 export."),
83+
hasWorkspaceHostContextRestore: workspaceSource.includes("restoreActiveSessionFromHostContextIdUrl()") &&
84+
workspaceSource.includes("this.restoreActiveSessionFromHostContextIdUrl();") &&
85+
workspaceSource.includes("if (parsed.value.toolId !== \"asset-manager-v2\")") &&
86+
workspaceSource.includes("this.syncWorkspaceManifestTextarea();"),
87+
validWriteAllowed: validWrite.ok === true && validWrite.payload?.payloadJson?.assetCatalog?.name === "Catalog",
88+
invalidWriteBlocked: invalidWrite.ok === false
89+
}
90+
};
91+
92+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
93+
fs.writeFileSync(resultsPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
94+
95+
for (const [key, value] of Object.entries(summary.checks)) {
96+
assert.equal(value, true, `${key} failed`);
97+
}
98+
99+
return summary;
100+
}
101+
102+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
103+
try {
104+
const summary = run();
105+
console.log(JSON.stringify(summary, null, 2));
106+
} catch (error) {
107+
console.error(error);
108+
process.exitCode = 1;
109+
}
110+
}

tools/asset-manager-v2/index.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,43 @@ class AssetBrowserV2 {
185185
this.renderCatalog(versionCheck.payload.payloadJson.assetCatalog, versionCheck.payload);
186186
}
187187

188+
persistValidSessionForWorkspace(sessionContext, assetCatalog) {
189+
if (!this.urlState.hostContextId) {
190+
return { ok: false, message: "No hostContextId is available for Workspace V2 persistence." };
191+
}
192+
if (!sessionContext || typeof sessionContext !== "object" || Array.isArray(sessionContext)) {
193+
return { ok: false, message: "Session context is invalid for Workspace V2 persistence." };
194+
}
195+
if (!sessionContext.payloadJson || typeof sessionContext.payloadJson !== "object" || Array.isArray(sessionContext.payloadJson)) {
196+
return { ok: false, message: "payloadJson is invalid for Workspace V2 persistence." };
197+
}
198+
const payload = {
199+
version: "v2",
200+
toolId: "asset-manager-v2",
201+
payloadJson: {
202+
...sessionContext.payloadJson,
203+
assetCatalog: {
204+
name: assetCatalog.name.trim(),
205+
entries: assetCatalog.entries.map((entry) => ({
206+
id: entry.id.trim(),
207+
label: entry.label.trim(),
208+
kind: entry.kind.trim(),
209+
path: entry.path.trim()
210+
}))
211+
}
212+
}
213+
};
214+
const serializedPayload = JSON.stringify(payload);
215+
if (serializedPayload.length > this.sessionPayloadBytesLimit) {
216+
return {
217+
ok: false,
218+
message: `Session size exceeds allowed limit for Workspace V2 persistence. Payload is ${serializedPayload.length} bytes and limit is ${this.sessionPayloadBytesLimit} bytes.`
219+
};
220+
}
221+
window.sessionStorage.setItem(this.urlState.hostContextId, serializedPayload);
222+
return { ok: true, message: "" };
223+
}
224+
188225
renderCatalog(assetCatalog, sessionContext) {
189226
if (typeof assetCatalog.name !== "string" || !assetCatalog.name.trim()) {
190227
this.renderError("Asset Manager V2 session data is invalid. Expected assetCatalog.name.");
@@ -216,7 +253,10 @@ class AssetBrowserV2 {
216253

217254
document.getElementById("assetBrowserV2SessionReadout").textContent = `Session: loaded\nContext: ${this.urlState.hostContextId}\nTool: ${typeof sessionContext.toolId === "string" && sessionContext.toolId.trim() ? sessionContext.toolId.trim() : "not provided"}${this.optionalUrlStateSummary() ? `\nURL State: ${this.optionalUrlStateSummary()}` : ""}`;
218255
document.getElementById("assetBrowserV2ContractReadout").textContent = "payloadJson loaded\npayloadJson.assetCatalog valid\nentries[] valid";
219-
document.getElementById("assetBrowserV2WorkspaceReadout").textContent = "Workspace session context was read. Workspace writes are deferred for this isolated V2 entry.";
256+
const persistence = this.persistValidSessionForWorkspace(sessionContext, assetCatalog);
257+
document.getElementById("assetBrowserV2WorkspaceReadout").textContent = persistence.ok
258+
? "Workspace session context was read and persisted for Workspace V2 export."
259+
: `Workspace session context was read but could not be persisted: ${persistence.message}`;
220260
document.getElementById("assetBrowserV2Title").textContent = assetCatalog.name.trim();
221261
document.getElementById("assetBrowserV2Count").textContent = `${assetCatalog.entries.length} asset${assetCatalog.entries.length === 1 ? "" : "s"}`;
222262
document.getElementById("assetBrowserV2State").textContent = assetCatalog.entries.length === 0

tools/workspace-v2/index.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ class WorkspaceV2SessionProducer {
210210
this.initializeWorkspaceToolsSummaryNode();
211211
this.initializeHiddenImportFileInput();
212212
this.decodeSessionParamFromUrl();
213+
this.restoreActiveSessionFromHostContextIdUrl();
213214
this.ensureWorkspaceActivePaletteBaseline();
214215
this.initializeWorkspaceProducerSession();
215216
this.refreshPaletteOwnershipStateAndUi();
@@ -1551,6 +1552,38 @@ class WorkspaceV2SessionProducer {
15511552
}
15521553
}
15531554

1555+
restoreActiveSessionFromHostContextIdUrl() {
1556+
const params = new URL(window.location.href).searchParams;
1557+
const hostContextId = typeof params.get("hostContextId") === "string" ? params.get("hostContextId").trim() : "";
1558+
if (!hostContextId) {
1559+
return;
1560+
}
1561+
const serializedPayload = sessionStorage.getItem(hostContextId);
1562+
if (typeof serializedPayload !== "string") {
1563+
return;
1564+
}
1565+
const parsed = this.safeParseJson(serializedPayload);
1566+
if (!parsed.ok || !this.isValidSessionPayload(parsed.value)) {
1567+
return;
1568+
}
1569+
const payloadValidation = this.validateWorkspaceToolSessionPayload(parsed.value, `sessionStorage.${hostContextId}`);
1570+
if (!payloadValidation.ok) {
1571+
return;
1572+
}
1573+
if (parsed.value.toolId !== "asset-manager-v2") {
1574+
return;
1575+
}
1576+
const activation = this.activateWorkspaceSession(hostContextId, parsed.value, "workspace-host-context-url");
1577+
if (!activation.ok) {
1578+
return;
1579+
}
1580+
if (Array.from(this.toolSelect.options).some((option) => option.value === parsed.value.toolId)) {
1581+
this.toolSelect.value = parsed.value.toolId;
1582+
}
1583+
this.syncWorkspaceManifestTextarea();
1584+
this.statusNode.textContent = `Workspace V2 restored active session from hostContextId: ${hostContextId}`;
1585+
}
1586+
15541587
readSessionLibrary() {
15551588
const rawLibrary = localStorage.getItem(this.libraryStorageKey);
15561589
if (!rawLibrary) {

0 commit comments

Comments
 (0)