Skip to content

Commit 1cbaf36

Browse files
committed
Remove Audio SFX cross tool import without introducing new state abstraction - PR_26145_021-audio-sfx-remove-cross-tool-import-without-new-state-layer
1 parent 8283659 commit 1cbaf36

4 files changed

Lines changed: 172 additions & 10 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# PR_26145_021 Audio / SFX No State Abstraction
2+
3+
Date: 2026-05-25
4+
5+
## Targeted validation
6+
7+
- `node --check` passed for:
8+
- `src/tools/common/WorkspaceDirtyNotifier.js`
9+
- `tools/audio-sfx-playground-v2/js/bootstrap.js`
10+
- `tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js`
11+
- `tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js`
12+
- JSON parse validation passed for:
13+
- `tools/schemas/game.manifest.schema.json`
14+
- `tools/schemas/tools/audio-sfx-playground-v2.schema.json`
15+
- HTML external asset guard passed for `tools/audio-sfx-playground-v2/index.html`.
16+
- Static validation confirmed:
17+
- no `src/tools/common/WorkspaceToolStateContract.js`
18+
- no `tools/audio-sfx-playground-v2/js/services/WorkspaceDirtyBridge.js`
19+
- no Audio / SFX JavaScript import from `tools/workspace-manager-v2`
20+
- Node validation confirmed `notifyWorkspaceToolDirty` marks the existing normalized Workspace session dirty and updates the persisted host context.
21+
22+
## npm run test:workspace-v2
23+
24+
Blocked by local Playwright browser installation:
25+
26+
```text
27+
browserType.launch: Executable doesn't exist at C:\Users\DavidQ\AppData\Local\ms-playwright\chromium-1217\chrome-win64\chrome.exe
28+
```
29+
30+
The one-failure run confirmed the missing bundled Chromium cache before Workspace V2 tests could execute.
31+
32+
## Focused Playwright validation
33+
34+
Ran a focused Playwright script with installed Microsoft Edge.
35+
36+
Validated:
37+
38+
- Audio / SFX does not hold a `WorkspaceManagerV2ContextService` instance.
39+
- Audio / SFX uses existing `GameManifestLoader` for workspace context loading.
40+
- Audio / SFX uses only the small dirty notifier function for dirty plumbing.
41+
- Add/edit marks the existing Workspace tool session dirty.
42+
- Workspace Save/Cancel dirty gate observes the Audio / SFX dirty session.
43+
- Copy JSON and Export JSON do not mark dirty.
44+
- Copy JSON succeeds through Clipboard API and failure is visible/actionable with no fallback copy.
45+
- Valid Import JSON marks dirty.
46+
- Workspace refresh includes the Audio / SFX payload.
47+
- Manifest/schema references validate.
48+
- No console errors.
49+
50+
Result: focused impacted validation passed.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { deepClone } from "../../shared/json/clone.js";
2+
import { isPlainObject } from "../../shared/object/objects.js";
3+
4+
const WORKSPACE_TOOL_SESSION_KEY_PREFIX = "workspace.tools.";
5+
6+
function workspaceToolSessionKey(toolId) {
7+
return `${WORKSPACE_TOOL_SESSION_KEY_PREFIX}${toolId}`;
8+
}
9+
10+
function readJson(sessionStorageRef, key) {
11+
const rawValue = sessionStorageRef?.getItem(key);
12+
if (!rawValue) {
13+
return { ok: false, message: `${key} was not found in sessionStorage.` };
14+
}
15+
try {
16+
const value = JSON.parse(rawValue);
17+
return isPlainObject(value)
18+
? { ok: true, value }
19+
: { ok: false, message: `${key} must contain a JSON object.` };
20+
} catch (error) {
21+
return { ok: false, message: `${key} contains invalid JSON: ${error.message}` };
22+
}
23+
}
24+
25+
function changedKeysForDirty(nextChangedKeys, previousDirty = {}) {
26+
return Array.from(new Set([
27+
...(Array.isArray(previousDirty.changedKeys) ? previousDirty.changedKeys : []),
28+
...(Array.isArray(nextChangedKeys) ? nextChangedKeys : ["data"])
29+
]
30+
.map((key) => String(key || "").trim())
31+
.filter(Boolean)));
32+
}
33+
34+
export function notifyWorkspaceToolDirty({
35+
changedKeys = ["data"],
36+
payload,
37+
reason = "tool-payload-updated",
38+
toolId,
39+
windowRef = window
40+
} = {}) {
41+
const params = new URLSearchParams(windowRef.location?.search || "");
42+
if (params.get("launch") !== "workspace" || params.get("fromTool") !== "workspace-manager-v2") {
43+
return { ok: true, skipped: true, message: "Not launched from Workspace Manager V2." };
44+
}
45+
46+
const normalizedToolId = String(toolId || "").trim();
47+
const hostContextId = String(params.get("hostContextId") || "").trim();
48+
if (!normalizedToolId) {
49+
return { ok: false, message: "Workspace dirty notification requires a tool id." };
50+
}
51+
if (!hostContextId) {
52+
return { ok: false, message: "Workspace dirty notification requires hostContextId." };
53+
}
54+
55+
const sessionStorageRef = windowRef.sessionStorage;
56+
const sessionKey = workspaceToolSessionKey(normalizedToolId);
57+
const sessionResult = readJson(sessionStorageRef, sessionKey);
58+
if (!sessionResult.ok) {
59+
return sessionResult;
60+
}
61+
const session = sessionResult.value;
62+
if (!isPlainObject(session.schema)
63+
|| !isPlainObject(session.workspace)
64+
|| !Object.prototype.hasOwnProperty.call(session, "data")
65+
|| !isPlainObject(session.dirty)) {
66+
return { ok: false, message: `${sessionKey} must use the normalized schema/workspace/data/dirty object shape.` };
67+
}
68+
if (session.workspace.source !== "workspace-manager-v2" || session.workspace.toolId !== normalizedToolId) {
69+
return { ok: false, message: `${sessionKey}.workspace must be for ${normalizedToolId}.` };
70+
}
71+
72+
const nextData = deepClone(payload);
73+
if (JSON.stringify(session.data) === JSON.stringify(nextData)) {
74+
return { ok: true, changed: false, key: sessionKey, session: deepClone(session) };
75+
}
76+
77+
const nextSession = {
78+
...session,
79+
data: nextData,
80+
dirty: {
81+
isDirty: true,
82+
reason,
83+
changedAt: new Date().toISOString(),
84+
changedKeys: changedKeysForDirty(changedKeys, session.dirty)
85+
}
86+
};
87+
sessionStorageRef.setItem(sessionKey, JSON.stringify(nextSession));
88+
89+
const contextResult = readJson(sessionStorageRef, hostContextId);
90+
if (contextResult.ok && isPlainObject(contextResult.value.tools)) {
91+
const nextContext = deepClone(contextResult.value);
92+
nextContext.tools[normalizedToolId] = deepClone(nextData);
93+
sessionStorageRef.setItem(hostContextId, JSON.stringify(nextContext));
94+
}
95+
96+
return { ok: true, changed: true, key: sessionKey, session: deepClone(nextSession) };
97+
}

tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,22 @@ export class AudioSfxPlaygroundV2App {
6666
audioEngine,
6767
controls,
6868
inspector,
69+
manifestLoader = null,
6970
preview,
7071
serializer,
7172
shell,
7273
statusLog,
7374
tileList,
7475
windowRef = window,
75-
workspaceContextService = null
76+
workspaceDirtyNotifier = null
7677
}) {
7778
this.accordions = accordions;
7879
this.actionNav = actionNav;
7980
this.activeSoundId = "";
8081
this.audioEngine = audioEngine;
8182
this.controls = controls;
8283
this.inspector = inspector;
84+
this.manifestLoader = manifestLoader;
8385
this.nextSoundNumber = 1;
8486
this.preview = preview;
8587
this.serializer = serializer;
@@ -88,7 +90,7 @@ export class AudioSfxPlaygroundV2App {
8890
this.statusLog = statusLog;
8991
this.tileList = tileList;
9092
this.window = windowRef;
91-
this.workspaceContextService = workspaceContextService;
93+
this.workspaceDirtyNotifier = workspaceDirtyNotifier;
9294
}
9395

9496
start() {
@@ -132,18 +134,22 @@ export class AudioSfxPlaygroundV2App {
132134
}
133135

134136
async loadWorkspacePayload() {
135-
if (!this.workspaceContextService) {
137+
if (!this.manifestLoader) {
136138
return;
137139
}
138-
const result = await this.workspaceContextService.readWorkspaceToolPayload(TOOL_ID);
139-
if (result.skipped || result.payload === null) {
140+
const result = await this.manifestLoader.loadInitialManifest();
141+
if (result.skipped) {
140142
return;
141143
}
142144
if (!result.ok) {
143145
this.statusLog.error(`Workspace payload load failed: ${result.message}`);
144146
return;
145147
}
146-
const validation = this.serializer.readToolState(toolStateFromPayload(result.payload));
148+
const payload = result.manifest?.tools?.[TOOL_ID] || null;
149+
if (!payload) {
150+
return;
151+
}
152+
const validation = this.serializer.readToolState(toolStateFromPayload(payload));
147153
if (!validation.valid) {
148154
this.statusLog.error(`Workspace payload load failed: ${validation.message}`);
149155
return;
@@ -196,15 +202,15 @@ export class AudioSfxPlaygroundV2App {
196202
}
197203

198204
async syncWorkspaceDirty(reason, changedKeys) {
199-
if (!this.workspaceContextService) {
205+
if (typeof this.workspaceDirtyNotifier !== "function") {
200206
return;
201207
}
202208
const { toolState, validation } = this.currentToolState();
203209
if (!validation.valid || !toolState) {
204210
this.statusLog.error(`Workspace dirty sync skipped: ${validation.message}`);
205211
return;
206212
}
207-
const result = await this.workspaceContextService.writeWorkspaceToolPayload(TOOL_ID, toolState.payload, { reason, changedKeys });
213+
const result = await this.workspaceDirtyNotifier(toolState.payload, { reason, changedKeys });
208214
if (result.skipped) {
209215
return;
210216
}

tools/audio-sfx-playground-v2/js/bootstrap.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { StatusLogControl } from "./controls/StatusLogControl.js";
99
import { ToolShellControl } from "./controls/ToolShellControl.js";
1010
import { AudioSfxEngine } from "./services/AudioSfxEngine.js";
1111
import { ToolStateSerializer } from "./services/ToolStateSerializer.js";
12-
import { WorkspaceManagerV2ContextService } from "../../workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js";
12+
import { GameManifestLoader } from "../../../src/tools/common/GameManifestLoader.js";
13+
import { notifyWorkspaceToolDirty } from "../../../src/tools/common/WorkspaceDirtyNotifier.js";
14+
15+
const TOOL_ID = "audio-sfx-playground-v2";
1316

1417
function requireElement(selector) {
1518
const element = document.querySelector(selector);
@@ -74,14 +77,20 @@ window.addEventListener("DOMContentLoaded", () => {
7477
waveformSelect: requireElement("#waveformSelect")
7578
}),
7679
inspector: new InspectorControl(requireElement("#inspectorOutput")),
80+
manifestLoader: new GameManifestLoader({ windowRef: window }),
7781
preview: new SfxPreviewControl(requireElement("#previewOutput")),
7882
serializer,
7983
shell: new ToolShellControl(),
8084
statusLog,
8185
tileList: new SfxTileListControl({
8286
list: requireElement("#sfxTileList")
8387
}),
84-
workspaceContextService: new WorkspaceManagerV2ContextService(),
88+
workspaceDirtyNotifier: (payload, options) => notifyWorkspaceToolDirty({
89+
...options,
90+
payload,
91+
toolId: TOOL_ID,
92+
windowRef: window
93+
}),
8594
windowRef: window
8695
});
8796

0 commit comments

Comments
 (0)