Skip to content

Commit 3588447

Browse files
author
DavidQ
committed
Schema-backed tool/workspace manifest boundary cleanup
- Adds explicit schema contracts for tool/sample/workspace payloads - Locks sample palette behavior to named palettes - Keeps workspace-owned assets inside workspace.manifest - Gates valid UI actions by loaded context
1 parent d528818 commit 3588447

14 files changed

Lines changed: 840 additions & 175 deletions

docs/dev/bundle_readme.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Bundle Contents
2+
3+
This ZIP is repo-structured and contains docs-first PR instructions only.
4+
5+
Files:
6+
7+
- `docs/dev/plan_pr_tool_workspace_schema_manifest_boundaries.md`
8+
- `docs/dev/codex_commands.md`
9+
- `docs/dev/commit_comment.txt`
10+
- `docs/dev/next_command.txt`
11+
12+
No implementation code is included.

docs/dev/codex_commands.md

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,73 @@
1-
Run via your standard injected rules workflow.
1+
# BUILD_PR: Codex Execution Notes
2+
3+
## Model
4+
5+
GPT-5.4 or GPT-5.3-codex
6+
7+
## Reasoning
8+
9+
High
10+
11+
## Codex Command
12+
13+
```bash
14+
codex
15+
```
16+
17+
## Task
18+
19+
Implement the PLAN_PR in `docs/dev/plan_pr_tool_workspace_schema_manifest_boundaries.md`.
20+
21+
## Constraints
22+
23+
- Smallest valid change.
24+
- One PR purpose only.
25+
- Docs-first schema/manifest boundary cleanup.
26+
- Do not modify `start_of_day`.
27+
- Do not delete preserved legacy folders.
28+
- Do not write broad custom validation code where `*.schema` files can define the contract.
29+
- Preserve existing sample behavior.
30+
- Samples must use named palettes only and remain locked.
31+
- Workspaces must use duplicated palettes and enforce used-palette replacement restrictions.
32+
- Workspace assets must live in `workspace.manifest`; only rendered/exportable artifacts such as PNG files may be saved separately.
33+
34+
## Suggested Implementation Order
35+
36+
1. Inventory only the specific files referencing:
37+
- `buildDefaultPayload`
38+
- 3D camera path editor payload setup
39+
- 3D JSON payload normalizer
40+
- viewer asset save/export paths
41+
- workspace/sample manifest loading
42+
- palette mutation rules
43+
2. Add schema files near the owning tool or manifest contract.
44+
3. Wire sample tool payloads to validate/load through the same tool schema.
45+
4. Wire workspace manifest validation through `workspace.manifest` schema.
46+
5. Centralize button/action enablement by loaded context:
47+
- tool context
48+
- workspace context
49+
6. Add/update minimal tests or fixtures proving:
50+
- sample palette lock
51+
- workspace palette duplication
52+
- used swatch replacement protection
53+
- workspace-owned assets persisted in manifest
54+
- PNG export still works
55+
7. Update roadmap/status markers only if the repo has an active roadmap file for this phase.
56+
57+
## Test Commands
58+
59+
Use the repo’s existing test commands. If none exist, run the smallest available smoke checks:
60+
61+
```bash
62+
npm test
63+
npm run build
64+
```
65+
66+
If the repo does not use npm for this area, document the actual commands run in the PR notes.
67+
68+
## Required Output From Codex
69+
70+
- List changed files.
71+
- List tests run.
72+
- Note any compatibility shims left in place.
73+
- Note any old references that were intentionally preserved.

docs/dev/commit_comment.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
Enforce extended anti-pattern rules in codex_rules.md
1+
Schema-backed tool/workspace manifest boundary cleanup
2+
3+
- Adds explicit schema contracts for tool/sample/workspace payloads
4+
- Locks sample palette behavior to named palettes
5+
- Keeps workspace-owned assets inside workspace.manifest
6+
- Gates valid UI actions by loaded context
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# PLAN_PR: Tool / Workspace Schema + Manifest Boundary Cleanup
2+
3+
## Purpose
4+
5+
Make tool data, sample data, workspace data, palette rules, and valid UI actions explicit through schemas and manifests instead of scattered validation code or hidden payload builders.
6+
7+
## Scope
8+
9+
One PR only:
10+
11+
- Remove/replace remaining dependency paths around:
12+
- `buildDefaultPayload`
13+
- 3D Camera Path Editor legacy payload assumptions
14+
- 3D JSON payload normalizer legacy assumptions
15+
- Viewer assets being separately saved/exported outside manifests
16+
- Introduce actual `*.schema` files for tool/workspace/sample payload validation.
17+
- Clarify palette behavior:
18+
- Samples use named palettes only.
19+
- Samples are locked and cannot mutate palettes.
20+
- Workspaces use duplicated palettes.
21+
- Once a workspace palette swatch is used, that palette cannot be replaced with a different palette.
22+
- Used workspace palettes may allow swatch edits only.
23+
- Other palettes may be viewed for reference/copying swatches into the selected workspace palette.
24+
- Put all workspace-owned assets in `workspace.manifest`.
25+
- Allow only exportable/rendered artifacts such as PNG files to be saved/exported outside the manifest.
26+
- Ensure UI buttons/actions are enabled by loaded context:
27+
- Tool-loaded context enables only tool-valid actions.
28+
- Workspace-loaded context enables only workspace-valid actions.
29+
30+
## Non-Goals
31+
32+
- Do not rewrite tool implementations.
33+
- Do not change sample runtime behavior except where required to load schema-backed data.
34+
- Do not modify `start_of_day` folders.
35+
- Do not delete legacy folders unless explicitly approved.
36+
- Do not add repo-wide validation frameworks if smaller local schema files are enough.
37+
- Do not implement new editor features beyond the boundary cleanup.
38+
39+
## Desired Manifest Shape
40+
41+
Workspace:
42+
43+
```json
44+
{
45+
"palette": {},
46+
"tools": {
47+
"<tool>": {
48+
"tool data": {}
49+
}
50+
}
51+
}
52+
```
53+
54+
Sample tool payload files:
55+
56+
```json
57+
{
58+
"<tool>": {
59+
"tool data": {}
60+
}
61+
}
62+
```
63+
64+
The sample payload shape should match the same tool schema used by the tool manifest loader.
65+
66+
## Acceptance Criteria
67+
68+
- `buildDefaultPayload` is no longer required for current tool/sample/workspace loading paths, or it is reduced to a compatibility shim with clear deprecation comments.
69+
- Each active tool has a dedicated schema file.
70+
- Workspace manifest has a schema file.
71+
- Sample tool payloads validate against the same schema used by the corresponding tool.
72+
- Viewer/vector assets that belong to a workspace are stored inside `workspace.manifest`.
73+
- PNG or rendered output remains exportable outside the manifest.
74+
- Samples are locked to named palettes and cannot mutate palette data.
75+
- Workspaces duplicate palettes and enforce swatch-use locking rules.
76+
- UI action availability is derived from loaded context and schema capabilities, not hard-coded workspace/tool assumptions.
77+
- Existing samples continue to load.
78+
- `samples/index.html` is updated only if sample entries are changed.

tests/run-tests.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import { run as runAssetUsageIntegration } from './tools/AssetUsageIntegration.t
9696
import { run as runAssetRemediationSystem } from './tools/AssetRemediationSystem.test.mjs';
9797
import { run as runToolBoundaryEnforcement } from './tools/ToolBoundaryEnforcement.test.mjs';
9898
import { run as runProjectToolDataContracts } from './tools/ProjectToolDataContracts.test.mjs';
99+
import { run as runToolWorkspaceSchemaManifestBoundaries } from './tools/ToolWorkspaceSchemaManifestBoundaries.test.mjs';
99100
import { run as runAssetPipelineTooling } from './tools/AssetPipelineTooling.test.mjs';
100101
import { run as runAssetOwnershipStrategyCloseout } from './tools/AssetOwnershipStrategyCloseout.test.mjs';
101102
import { run as runAssetErrorHandlingStandard } from './tools/AssetErrorHandlingStandard.test.mjs';
@@ -234,6 +235,7 @@ const tests = [
234235
['AssetRemediationSystem', runAssetRemediationSystem],
235236
['ToolBoundaryEnforcement', runToolBoundaryEnforcement],
236237
['ProjectToolDataContracts', runProjectToolDataContracts],
238+
['ToolWorkspaceSchemaManifestBoundaries', runToolWorkspaceSchemaManifestBoundaries],
237239
['AssetPipelineTooling', runAssetPipelineTooling],
238240
['AssetOwnershipStrategyCloseout', runAssetOwnershipStrategyCloseout],
239241
['AssetErrorHandlingStandard', runAssetErrorHandlingStandard],
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import assert from "node:assert/strict";
2+
import { readFileSync } from "node:fs";
3+
import {
4+
createDefaultCameraPathPayload,
5+
validateCameraPathPayload
6+
} from "../../tools/3D Camera Path Editor/cameraPathPayload.schema.js";
7+
import {
8+
createDefaultMapPayload,
9+
validateMapPayload
10+
} from "../../tools/3D JSON Payload Normalizer/mapPayload.schema.js";
11+
import {
12+
createDefaultAssetPayload,
13+
validateAssetPayload
14+
} from "../../tools/3D Asset Viewer/assetPayload.schema.js";
15+
import {
16+
validateWorkspaceManifestSchema,
17+
WORKSPACE_MANIFEST_SCHEMA,
18+
WORKSPACE_MANIFEST_VERSION,
19+
WORKSPACE_DOCUMENT_KIND
20+
} from "../../tools/shared/workspaceManifest.schema.js";
21+
22+
export async function run() {
23+
const cameraValidation = validateCameraPathPayload(createDefaultCameraPathPayload(), {
24+
requireSchema: true,
25+
requireWaypoints: true
26+
});
27+
assert.equal(cameraValidation.valid, true, cameraValidation.issues.join(" "));
28+
29+
const mapValidation = validateMapPayload(createDefaultMapPayload(), {
30+
requireSchema: true,
31+
requirePoints: true
32+
});
33+
assert.equal(mapValidation.valid, true, mapValidation.issues.join(" "));
34+
35+
const assetValidation = validateAssetPayload(createDefaultAssetPayload(), {
36+
requireSchema: true,
37+
requireVertices: true
38+
});
39+
assert.equal(assetValidation.valid, true, assetValidation.issues.join(" "));
40+
41+
const workspaceManifest = {
42+
documentKind: WORKSPACE_DOCUMENT_KIND,
43+
schema: WORKSPACE_MANIFEST_SCHEMA,
44+
version: WORKSPACE_MANIFEST_VERSION,
45+
id: "workspace-test",
46+
name: "Workspace Test",
47+
tools: {
48+
"3d-asset-viewer": {
49+
schema: "tools.3d-asset-viewer.asset/1",
50+
assetId: "asset-test",
51+
vertices: [{ x: 0, y: 0, z: 0 }]
52+
}
53+
},
54+
sharedLibrary: {
55+
assets: [
56+
{
57+
id: "asset-test",
58+
type: "vector",
59+
displayName: "Asset Test",
60+
sourcePath: "/games/Test/assets/vectors/asset-test.vector.json",
61+
sourceToolId: "3d-asset-viewer"
62+
}
63+
],
64+
palettes: []
65+
},
66+
exportArtifacts: [
67+
{
68+
kind: "png",
69+
path: "/games/Test/exports/asset-preview.png",
70+
sourceToolId: "3d-asset-viewer"
71+
}
72+
]
73+
};
74+
const workspaceValidation = validateWorkspaceManifestSchema(workspaceManifest);
75+
assert.equal(workspaceValidation.valid, true, workspaceValidation.issues.join(" "));
76+
77+
const invalidWorkspaceValidation = validateWorkspaceManifestSchema({
78+
...workspaceManifest,
79+
externalAssets: ["/games/Test/assets/outside-manifest.asset.json"],
80+
exportArtifacts: [{ kind: "jpg", path: "/games/Test/exports/asset-preview.jpg" }]
81+
});
82+
assert.equal(invalidWorkspaceValidation.valid, false);
83+
assert.equal(
84+
invalidWorkspaceValidation.issues.some((issue) => issue.includes("workspace.manifest")),
85+
true
86+
);
87+
assert.equal(
88+
invalidWorkspaceValidation.issues.some((issue) => issue.includes(".png")),
89+
true
90+
);
91+
92+
const paletteBrowserSource = readFileSync(new URL("../../tools/Palette Browser/main.js", import.meta.url), "utf8");
93+
assert.match(paletteBrowserSource, /function duplicateSelectedPalette\(/);
94+
assert.match(paletteBrowserSource, /createCustomPalette\(nextName, palette\.entries\)/);
95+
assert.match(paletteBrowserSource, /Duplicate a built-in palette before editing swatches\./);
96+
assert.match(paletteBrowserSource, /Shared palette is locked to .*Edit swatches instead\./);
97+
assert.match(paletteBrowserSource, /if \(isReadOnlyPalette\(palette\)\) \{\s*return;\s*\}/s);
98+
99+
const spriteEditorSource = readFileSync(new URL("../../tools/Sprite Editor/modules/spriteEditorApp.js", import.meta.url), "utf8");
100+
assert.match(spriteEditorSource, /image\/png/);
101+
assert.match(spriteEditorSource, /sprite-frame-.*\.png/);
102+
assert.match(spriteEditorSource, /sprite-sheet-.*\.png/);
103+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
export const ASSET_VIEWER_PAYLOAD_SCHEMA = "tools.3d-asset-viewer.asset/1";
2+
export const ASSET_VIEWER_REPORT_SCHEMA = "tools.3d-asset-viewer.report/1";
3+
4+
function sanitizeNumber(value, fallback = 0) {
5+
const numeric = Number(value);
6+
if (Number.isNaN(numeric) || numeric === Infinity || numeric === -Infinity) {
7+
return fallback;
8+
}
9+
return numeric;
10+
}
11+
12+
function toObject(value) {
13+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
14+
}
15+
16+
function normalizeVertex(rawVertex) {
17+
const vertex = toObject(rawVertex);
18+
return {
19+
x: sanitizeNumber(vertex.x),
20+
y: sanitizeNumber(vertex.y),
21+
z: sanitizeNumber(vertex.z)
22+
};
23+
}
24+
25+
export function createDefaultAssetPayload() {
26+
return {
27+
schema: ASSET_VIEWER_PAYLOAD_SCHEMA,
28+
assetId: "ship-hull",
29+
vertices: [
30+
{ x: -1, y: -0.5, z: -2 },
31+
{ x: 1, y: -0.5, z: -2 },
32+
{ x: 0, y: 0.75, z: 2 }
33+
],
34+
metadata: {
35+
sourceToolId: "vector-asset-studio"
36+
}
37+
};
38+
}
39+
40+
export function normalizeAssetPayload(rawPayload, options = {}) {
41+
const source = toObject(rawPayload);
42+
const fallbackAssetId = typeof options.fallbackAssetId === "string" && options.fallbackAssetId.trim()
43+
? options.fallbackAssetId.trim()
44+
: "asset-3d";
45+
const vertices = Array.isArray(source.vertices)
46+
? source.vertices.map((vertex) => normalizeVertex(vertex))
47+
: [];
48+
return {
49+
schema: ASSET_VIEWER_PAYLOAD_SCHEMA,
50+
assetId: typeof source.assetId === "string" && source.assetId.trim() ? source.assetId.trim() : fallbackAssetId,
51+
vertices,
52+
metadata: source.metadata && typeof source.metadata === "object" ? { ...source.metadata } : {}
53+
};
54+
}
55+
56+
export function validateAssetPayload(rawPayload, options = {}) {
57+
const source = toObject(rawPayload);
58+
const issues = [];
59+
const requireSchema = options.requireSchema !== false;
60+
const requireVertices = options.requireVertices === true;
61+
const payload = normalizeAssetPayload(source, options);
62+
63+
if (requireSchema) {
64+
const schemaValue = typeof source.schema === "string" ? source.schema.trim() : "";
65+
if (!schemaValue) {
66+
issues.push(`3D asset schema is required (${ASSET_VIEWER_PAYLOAD_SCHEMA}).`);
67+
} else if (schemaValue !== ASSET_VIEWER_PAYLOAD_SCHEMA) {
68+
issues.push(`3D asset schema must be ${ASSET_VIEWER_PAYLOAD_SCHEMA}.`);
69+
}
70+
}
71+
72+
if (!Array.isArray(source.vertices)) {
73+
issues.push("3D asset vertices must be an array.");
74+
} else if (requireVertices && source.vertices.length === 0) {
75+
issues.push("3D asset payload requires at least one vertex.");
76+
}
77+
78+
if (!payload.assetId) {
79+
issues.push("3D asset assetId is required.");
80+
}
81+
82+
return {
83+
valid: issues.length === 0,
84+
issues,
85+
payload
86+
};
87+
}

0 commit comments

Comments
 (0)