Skip to content

Commit f961918

Browse files
author
DavidQ
committed
build(assets): enforce shared tool asset and palette handoff contract
1 parent 6e1ad6d commit f961918

12 files changed

Lines changed: 426 additions & 60 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,11 @@
11
MODEL: GPT-5.4
22
REASONING: high
3+
COMMAND: Create BUILD_PR_LEVEL_09_05_SHARED_ASSET_HANDOFF_ENFORCEMENT as a docs-first, repo-structured delta. Use docs/pr/BUILD_PR_LEVEL_09_05_SHARED_ASSET_HANDOFF_ENFORCEMENT.md as the source of truth. Implement the smallest safe enforcement slice that makes first-class tools launch and consume shared asset/palette handoffs through the normalized contract established by 09_04. Keep scope limited to first-class tools, shared launch/handoff helpers, and focused tests only. Do not modify engine core APIs, game runtime behavior, legacy tool surfacing, or perform broad UI cleanup.
34

4-
GOAL:
5-
Simplify asset structure by removing redundant container layers.
5+
FINAL STEP:
6+
- Package all created and modified files into a repo-structured ZIP
7+
- Write the result ZIP to: <project folder>/tmp/BUILD_PR_LEVEL_09_05_SHARED_ASSET_HANDOFF_ENFORCEMENT.zip
8+
- Preserve exact repo-relative structure inside the ZIP
9+
- Include only files relevant to this PR
10+
- Do not include unrelated files, full-repo copies, dependencies, or build artifacts
611

7-
RULES:
8-
- eliminate data/ if it is just a wrapper
9-
- eliminate platform/ as default container
10-
- keep only type-first folders under assets/
11-
- allow nested grouping only when needed
12-
13-
TARGET STRUCTURE:
14-
assets/
15-
palettes/
16-
sprites/
17-
tilemaps/
18-
19-
OPTIONAL:
20-
assets/sprites/platform/
21-
assets/tilemaps/platform/
22-
23-
CONSTRAINTS:
24-
- preserve files
25-
- update references
26-
- do not touch runtime code
27-
- one PR scope only
28-
29-
OUTPUT:
30-
ZIP to <project folder>/tmp/

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Level 09.04 — simplify asset structure by removing redundant data/platform containers
1+
build(assets): enforce shared tool asset and palette handoff contract

docs/dev/NEXT_COMMAND.txt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
# Validate BUILD_PR_LEVEL_09_03_ASSET_BUCKET_TAXONOMY_STANDARD
2-
rg -n ""assets/platform/"" games samples tools src tests --glob '!start_of_day/**'
3-
node tests/tools/VectorAssetSystem.test.mjs
1+
APPLY_PR_LEVEL_09_05_SHARED_ASSET_HANDOFF_ENFORCEMENT
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
Removes redundant asset container layers (data/platform) and standardizes to type-first structure.
1+
BUILD bundle created for the post-09_04 enforcement slice.
2+
3+
Summary:
4+
- continues directly from 09_04 asset structure simplification
5+
- preserves the 09_04 asset usage contract as the source baseline
6+
- narrows the next PR to first-class tool shared handoff enforcement only
7+
- avoids engine changes, gameplay changes, and broad tool cleanup
8+
- requires focused validation for canonical query parameters, storage keys, payload shapes, and safe fallback behavior
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
Validation:
2-
- no data/platform nesting remains
3-
- top-level assets folders are type-only
4-
- plural naming enforced
5-
- references updated
1+
[ ] PR scope remains limited to shared handoff enforcement.
2+
[ ] Asset Browser launch flows use normalized `view` and `sourceToolId` parameters.
3+
[ ] Shared action labels remain exact and normalized.
4+
[ ] `toolboxaid.shared.assetHandoff` is the only asset handoff key used.
5+
[ ] `toolboxaid.shared.paletteHandoff` is the only palette handoff key used.
6+
[ ] Asset payload preserves canonical top-level fields.
7+
[ ] Palette payload preserves canonical top-level fields.
8+
[ ] Consuming tools prefer valid shared handoffs over hidden private defaults.
9+
[ ] Invalid/missing handoff payloads degrade safely.
10+
[ ] Starter template remains compatible.
11+
[ ] Legacy tools remain excluded.
12+
[ ] No engine core API files are changed.
Binary file not shown.
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# BUILD_PR_LEVEL_09_05_SHARED_ASSET_HANDOFF_ENFORCEMENT
2+
3+
## Purpose
4+
Implement the smallest enforcement slice that makes the shared asset and shared palette handoff contract real across first-class tools after 09_04 asset structure simplification.
5+
6+
This PR must tighten launch/consume behavior for shared asset flows without widening into engine refactors, sample work, or broad UI redesign.
7+
8+
## Why This PR Exists
9+
09_04 established the asset usage contract, moved active template surfaces under `tools/templates/`, and proved normalized shared asset locations across Asteroids and the starter project baseline. However, the contract is still only partially effective until active tools consistently:
10+
- launch shared browse/import/manage flows through the common shell
11+
- publish normalized handoff payloads under the approved storage keys
12+
- consume shared handoff summaries without silently forking tool-private defaults
13+
14+
This PR closes that enforcement gap.
15+
16+
## Source Baseline
17+
Treat these as the architectural baseline for this PR:
18+
- `docs/specs/asset_usage_contract.md`
19+
- `tools/shared/vectorAssetSystem.js`
20+
- `tools/Asset Browser/main.js`
21+
- `tools/templates/starter-project-template/config/starter.project.json`
22+
- `tests/tools/VectorAssetSystem.test.mjs`
23+
24+
## In Scope
25+
- first-class tool launch and consume enforcement for shared asset/palette handoff
26+
- shared shell action normalization where required for the in-scope tools
27+
- storage-key and payload-shape enforcement against the 09_04 contract
28+
- narrow validation/tests that prove the contract is being used consistently
29+
- starter template compatibility only where required to preserve the contract
30+
31+
## Out of Scope
32+
- engine core API changes
33+
- game runtime behavior changes
34+
- sample curriculum work
35+
- broad tool UI cleanup or redesign
36+
- legacy tool reintegration
37+
- filesystem mutation/import-copy automation expansion
38+
- broad asset registry redesign
39+
40+
## Required Contract Enforcement
41+
42+
### 1. Shared action labels remain exact
43+
For active first-class tools in scope, shared actions must continue using these exact labels:
44+
- `Browse Assets`
45+
- `Import Assets`
46+
- `Browse Palettes`
47+
- `Manage Palettes`
48+
49+
Do not introduce tool-specific label drift.
50+
51+
### 2. Shared launch query contract must be real
52+
When an in-scope tool launches the shared Asset Browser or Palette Browser flow, it must use the normalized query-parameter contract defined in `docs/specs/asset_usage_contract.md`:
53+
- `view`
54+
- `sourceToolId`
55+
56+
Examples:
57+
- `../Asset Browser/index.html?view=import&sourceToolId=tile-map-editor`
58+
- `../Palette Browser/index.html?view=browse&sourceToolId=sprite-editor`
59+
60+
No alternate ad hoc query names may be introduced.
61+
62+
### 3. Shared handoff storage keys must be canonical
63+
Asset handoff must use:
64+
- `toolboxaid.shared.assetHandoff`
65+
66+
Palette handoff must use:
67+
- `toolboxaid.shared.paletteHandoff`
68+
69+
Do not add duplicate parallel keys for the same responsibility.
70+
71+
### 4. Handoff payload shape must be normalized
72+
For the tools touched in this PR, handoff payloads must preserve the stable top-level fields documented in the contract.
73+
74+
Asset handoff must preserve at minimum:
75+
- `assetId`
76+
- `assetType`
77+
- `sourcePath`
78+
- `displayName`
79+
- `metadata`
80+
- `sourceToolId`
81+
- `selectedAt`
82+
83+
Palette handoff must preserve at minimum:
84+
- `paletteId`
85+
- `displayName`
86+
- `colors`
87+
- `metadata`
88+
- `sourceToolId`
89+
- `selectedAt`
90+
91+
Metadata may extend the shape but must not replace the stable top-level fields.
92+
93+
### 5. Active tools must consume the shared handoff, not fork private defaults
94+
For each in-scope tool, consuming a handoff must prefer the shared contract payload over hidden tool-private default copies whenever the handoff payload is valid.
95+
96+
Graceful degradation is required:
97+
- invalid or missing handoff payloads must not crash the tool
98+
- invalid or missing payloads may fall back safely
99+
- fallback must not overwrite the canonical handoff contract with malformed data
100+
101+
## Required Implementation Shape
102+
Implement the smallest safe slice that proves the contract across the most important active tools.
103+
104+
### Minimum in-scope tools
105+
At minimum, cover:
106+
- `Asset Browser / Import Hub`
107+
- `Sprite Editor`
108+
- `Vector Map Editor`
109+
- `Vector Asset Studio`
110+
- `Tile Map Editor`
111+
- `Parallax Editor`
112+
113+
If some tools already comply, keep edits surgical and only normalize the drift that still exists.
114+
115+
### Likely file targets
116+
Use only if needed; do not expand beyond proven necessity.
117+
- `tools/shared/...` shared launch/handoff helper(s)
118+
- active tool entry scripts for the in-scope tools
119+
- `tools/Asset Browser/main.js`
120+
- palette browser/manager entry script if present and required
121+
- focused tests under `tests/tools/`
122+
- starter template config only if compatibility needs a narrow update
123+
124+
## Acceptance Criteria
125+
1. In-scope first-class tools launch shared browse/import/manage flows using the normalized query contract.
126+
2. Asset and palette handoffs use the canonical storage keys only.
127+
3. Published handoff payloads preserve the stable top-level contract fields.
128+
4. Consuming tools prefer valid shared handoff payloads over hidden tool-private defaults.
129+
5. Invalid or missing handoff payloads degrade safely without crashes.
130+
6. Legacy tools remain excluded.
131+
7. No engine core API files change.
132+
8. No unrelated tool-shell cleanup is bundled into this PR.
133+
134+
## Validation Requirements
135+
Provide focused proof, not broad regression theater.
136+
137+
### Required validation lanes
138+
- targeted unit/integration coverage for launch query generation and handoff payload normalization
139+
- targeted coverage for consumer-side safe fallback on invalid/missing payloads
140+
- targeted coverage that shared asset selection preserves canonical fields and source paths
141+
- targeted coverage that shared palette selection preserves canonical fields and colors
142+
143+
### Manual validation checklist
144+
1. Launch each in-scope tool and confirm shared actions use normalized labels.
145+
2. Trigger `Browse Assets` or `Import Assets` from at least two tools and confirm the browser opens with `view` and `sourceToolId`.
146+
3. Select a shared asset and confirm `toolboxaid.shared.assetHandoff` stores the normalized payload.
147+
4. Select a shared palette and confirm `toolboxaid.shared.paletteHandoff` stores the normalized payload.
148+
5. Re-open a consuming tool and confirm it reads the shared handoff without creating a hidden private duplicate.
149+
6. Corrupt the stored handoff payload and confirm the tool degrades safely.
150+
7. Confirm starter project template still opens without contract breakage.
151+
8. Confirm no engine-core files changed.
152+
153+
## Constraints
154+
- Docs-first, surgical PR only.
155+
- One purpose only: enforce shared handoff behavior.
156+
- Preserve 09_04 asset ownership and template relocation outcomes.
157+
- Do not move assets again in this PR.
158+
- Do not add new showcase/sample surfacing.
159+
- Do not change engine/game runtime contracts.
160+
161+
## Approved Commit Comment
162+
build(assets): enforce shared tool asset and palette handoff contract
163+
164+
## Next Command
165+
APPLY_PR_LEVEL_09_05_SHARED_ASSET_HANDOFF_ENFORCEMENT

tests/run-tests.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ import { run as runProductionReadiness } from './production/ProductionReadiness.
8686
import { run as runEnginePublicBarrelImports } from './production/EnginePublicBarrelImports.test.mjs';
8787
import { run as runStorageService } from './persistence/StorageService.test.mjs';
8888
import { run as runAssetValidationEngine } from './tools/AssetValidationEngine.test.mjs';
89+
import { run as runAssetUsageIntegration } from './tools/AssetUsageIntegration.test.mjs';
8990
import { run as runAssetRemediationSystem } from './tools/AssetRemediationSystem.test.mjs';
9091
import { run as runProjectPackagingSystem } from './tools/ProjectPackagingSystem.test.mjs';
9192
import { run as runRuntimeAssetLoader } from './tools/RuntimeAssetLoader.test.mjs';
@@ -197,6 +198,7 @@ const tests = [
197198
['MouseState', runMouseState],
198199
['StorageService', runStorageService],
199200
['AssetValidationEngine', runAssetValidationEngine],
201+
['AssetUsageIntegration', runAssetUsageIntegration],
200202
['AssetRemediationSystem', runAssetRemediationSystem],
201203
['ProjectPackagingSystem', runProjectPackagingSystem],
202204
['RuntimeAssetLoader', runRuntimeAssetLoader],
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 {
3+
SHARED_ACTION_LABELS,
4+
SHARED_ASSET_HANDOFF_KEY,
5+
SHARED_PALETTE_HANDOFF_KEY,
6+
createAssetHandoff,
7+
createPaletteHandoff,
8+
getSharedShellActions,
9+
readSharedAssetHandoff,
10+
readSharedPaletteHandoff,
11+
writeSharedAssetHandoff,
12+
writeSharedPaletteHandoff
13+
} from "../../tools/shared/assetUsageIntegration.js";
14+
15+
function createLocalStorageHarness() {
16+
const store = new Map();
17+
return {
18+
getItem(key) {
19+
return store.has(key) ? store.get(key) : null;
20+
},
21+
setItem(key, value) {
22+
store.set(String(key), String(value));
23+
},
24+
removeItem(key) {
25+
store.delete(String(key));
26+
}
27+
};
28+
}
29+
30+
export async function run() {
31+
const previousWindow = globalThis.window;
32+
const localStorage = createLocalStorageHarness();
33+
globalThis.window = {
34+
localStorage,
35+
location: {
36+
search: ""
37+
}
38+
};
39+
40+
try {
41+
const actions = getSharedShellActions("sprite-editor", "tool");
42+
assert.equal(actions.length, 4);
43+
assert.deepEqual(
44+
actions.map((entry) => entry.label),
45+
[
46+
SHARED_ACTION_LABELS.browseAssets,
47+
SHARED_ACTION_LABELS.importAssets,
48+
SHARED_ACTION_LABELS.browsePalettes,
49+
SHARED_ACTION_LABELS.managePalettes
50+
]
51+
);
52+
assert.equal(
53+
actions[0].href,
54+
"../Asset Browser/index.html?view=browse&sourceToolId=sprite-editor"
55+
);
56+
assert.equal(
57+
actions[3].href,
58+
"../Palette Browser/index.html?view=manage&sourceToolId=sprite-editor"
59+
);
60+
61+
const validAsset = createAssetHandoff({
62+
assetId: "asset-vector-player",
63+
assetType: "vector",
64+
sourcePath: "../../games/Asteroids/assets/vectors/asteroids-ship.vector.json",
65+
displayName: "Asteroids Ship Vector",
66+
metadata: { category: "Vector Assets" },
67+
sourceToolId: "tile-map-editor"
68+
});
69+
assert.equal(writeSharedAssetHandoff(validAsset), true);
70+
const storedAsset = readSharedAssetHandoff();
71+
assert.equal(storedAsset.assetId, "asset-vector-player");
72+
assert.equal(storedAsset.assetType, "vector");
73+
assert.equal(storedAsset.sourcePath, "../../games/Asteroids/assets/vectors/asteroids-ship.vector.json");
74+
assert.equal(storedAsset.displayName, "Asteroids Ship Vector");
75+
assert.equal(storedAsset.sourceToolId, "tile-map-editor");
76+
assert.equal(typeof storedAsset.selectedAt, "string");
77+
assert.equal(storedAsset.metadata.category, "Vector Assets");
78+
79+
assert.equal(writeSharedAssetHandoff({ assetId: "broken-only-id" }), false);
80+
const stillStoredAsset = readSharedAssetHandoff();
81+
assert.equal(stillStoredAsset.assetId, "asset-vector-player");
82+
83+
const validPalette = createPaletteHandoff({
84+
paletteId: "builtin:crayola032",
85+
displayName: "crayola032",
86+
colors: [{ symbol: "!", hex: "#232323", name: "Black" }],
87+
metadata: { source: "engine" },
88+
sourceToolId: "sprite-editor"
89+
});
90+
assert.equal(writeSharedPaletteHandoff(validPalette), true);
91+
const storedPalette = readSharedPaletteHandoff();
92+
assert.equal(storedPalette.paletteId, "builtin:crayola032");
93+
assert.equal(storedPalette.displayName, "crayola032");
94+
assert.equal(storedPalette.colors[0].hex, "#232323");
95+
assert.equal(storedPalette.sourceToolId, "sprite-editor");
96+
assert.equal(typeof storedPalette.selectedAt, "string");
97+
assert.equal(storedPalette.metadata.source, "engine");
98+
99+
assert.equal(writeSharedPaletteHandoff({ displayName: "missing-id" }), false);
100+
const stillStoredPalette = readSharedPaletteHandoff();
101+
assert.equal(stillStoredPalette.paletteId, "builtin:crayola032");
102+
103+
localStorage.setItem(SHARED_ASSET_HANDOFF_KEY, '{"assetId":"only-id"}');
104+
localStorage.setItem(SHARED_PALETTE_HANDOFF_KEY, '{"paletteId":"only-id"}');
105+
assert.equal(readSharedAssetHandoff(), null);
106+
assert.equal(readSharedPaletteHandoff().paletteId, "only-id");
107+
} finally {
108+
globalThis.window = previousWindow;
109+
}
110+
}

tools/Asset Browser/main.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,10 @@ function useSelectedAssetInActiveTool() {
348348
},
349349
sourceToolId: context.sourceToolId || "asset-browser"
350350
});
351-
writeSharedAssetHandoff(handoff);
352-
refs.launchContextText.textContent = `Shared asset handoff updated for ${getToolDisplayName(context.sourceToolId, "active tool")}: ${selectedAsset.label}`;
351+
const stored = writeSharedAssetHandoff(handoff);
352+
refs.launchContextText.textContent = stored
353+
? `Shared asset handoff updated for ${getToolDisplayName(context.sourceToolId, "active tool")}: ${selectedAsset.label}`
354+
: "Shared asset handoff was not updated because the payload was invalid.";
353355
}
354356

355357
function syncImportFormFromFile() {

0 commit comments

Comments
 (0)