Skip to content

Commit b03cbc6

Browse files
author
DavidQ
committed
PR 8.1: Schema-driven validation only
- Added schemas - Removed validation utilities - Enforced no-fallback rule
1 parent 9d442f6 commit b03cbc6

21 files changed

Lines changed: 405 additions & 615 deletions

docs/dev/codex_commands.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Codex Commands
1+
MODEL: GPT-5.3-codex
2+
REASONING: medium
23

3-
Model: GPT-5.4-mini
4-
Reasoning: medium
5-
6-
```bash
7-
codex exec "Apply BUILD_PR docs/dev/BUILD_PR.md. Keep the change docs-first and schema-location only. Do not add root-level schema files. Place all schema contracts under tools/schemas, with tool-specific schemas under tools/schemas/tools. Do not modify locked samples or start_of_day folders."
8-
```
4+
TASK:
5+
- Add schema files exactly as defined
6+
- Insert $schema references into JSON files
7+
- Remove ALL validation utilities
8+
- Do NOT add fallback logic

docs/dev/commit_comment.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
Move JSON schema contracts under tools/schemas
2-
3-
Keeps schema ownership with the tool/workspace system instead of adding root-level schema files. Establishes shared manifest schemas under tools/schemas and tool-specific payload schemas under tools/schemas/tools.
1+
PR 8.1: Schema-driven validation only
2+
- Added schemas
3+
- Removed validation utilities
4+
- Enforced no-fallback rule
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# PR 8.1 — Schema-Driven Validation Only
2+
3+
## Summary
4+
- Enforces schema-only validation
5+
- Removes all fallback logic
6+
- Establishes schema ownership boundaries
7+
8+
## Rules
9+
- workspace.manifest = source of truth
10+
- tools define their own schema
11+
- shared schemas live only in tools/schemas/
12+
- samples consume schemas only
13+
- NO fallback logic allowed

tests/tools/ToolWorkspaceSchemaManifestBoundaries.test.mjs

Lines changed: 36 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,47 @@
11
import assert from "node:assert/strict";
2-
import { readFileSync } from "node:fs";
3-
import {
4-
createDefaultCameraPathPayload,
5-
validateCameraPathPayload
6-
} from "../../tools/schemas/tools/cameraPathPayload.schema.js";
7-
import {
8-
createDefaultMapPayload,
9-
validateMapPayload
10-
} from "../../tools/schemas/tools/mapPayload.schema.js";
11-
import {
12-
createDefaultAssetPayload,
13-
validateAssetPayload
14-
} from "../../tools/schemas/tools/assetPayload.schema.js";
15-
import {
16-
validateWorkspaceManifestSchema,
17-
WORKSPACE_MANIFEST_SCHEMA,
18-
WORKSPACE_MANIFEST_VERSION,
19-
WORKSPACE_DOCUMENT_KIND
20-
} from "../../tools/schemas/workspaceManifest.schema.js";
2+
import { existsSync, readFileSync } from "node:fs";
213

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(" "));
4+
function readJson(relativePath) {
5+
return JSON.parse(readFileSync(new URL(`../../${relativePath}`, import.meta.url), "utf8"));
6+
}
287

29-
const mapValidation = validateMapPayload(createDefaultMapPayload(), {
30-
requireSchema: true,
31-
requirePoints: true
8+
export async function run() {
9+
const schemaFiles = [
10+
"tools/schemas/workspace.manifest.schema.json",
11+
"tools/schemas/tool.manifest.schema.json",
12+
"tools/schemas/palette.schema.json",
13+
"tools/schemas/sample.tool-payload.schema.json",
14+
"tools/schemas/tools/vector-map-editor.schema.json",
15+
"tools/schemas/tools/vector-asset-studio.schema.json",
16+
"tools/schemas/tools/sprite-editor.schema.json"
17+
];
18+
schemaFiles.forEach((relativePath) => {
19+
const schema = readJson(relativePath);
20+
assert.equal(schema.$schema, "https://json-schema.org/draft/2020-12/schema");
21+
assert.equal(typeof schema.$id, "string");
22+
assert.equal(schema.$id.length > 0, true);
3223
});
33-
assert.equal(mapValidation.valid, true, mapValidation.issues.join(" "));
3424

35-
const assetValidation = validateAssetPayload(createDefaultAssetPayload(), {
36-
requireSchema: true,
37-
requireVertices: true
25+
const toolSchemaFiles = [
26+
"tools/vector-map-editor/tool.schema.json",
27+
"tools/vector-asset-studio/tool.schema.json",
28+
"tools/palette-editor/tool.schema.json"
29+
];
30+
toolSchemaFiles.forEach((relativePath) => {
31+
const document = readJson(relativePath);
32+
assert.equal(document.$schema, "https://json-schema.org/draft/2020-12/schema");
3833
});
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(" "));
7634

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" }]
35+
const removedValidationUtilities = [
36+
"tools/schemas/workspaceManifest.schema.js",
37+
"tools/schemas/tools/cameraPathPayload.schema.js",
38+
"tools/schemas/tools/mapPayload.schema.js",
39+
"tools/schemas/tools/assetPayload.schema.js"
40+
];
41+
removedValidationUtilities.forEach((relativePath) => {
42+
const absolutePath = new URL(`../../${relativePath}`, import.meta.url);
43+
assert.equal(existsSync(absolutePath), false);
8144
});
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-
);
9145

9246
const paletteBrowserSource = readFileSync(new URL("../../tools/Palette Browser/main.js", import.meta.url), "utf8");
9347
assert.match(paletteBrowserSource, /function duplicateSelectedPalette\(/);

tools/3D Asset Viewer/main.js

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { safeParseJson, toPrettyJson } from "../shared/debugInspectorData.js";
22
import { registerToolBootContract } from "../shared/toolBootContract.js";
3-
import {
4-
ASSET_VIEWER_REPORT_SCHEMA,
5-
createDefaultAssetPayload,
6-
validateAssetPayload
7-
} from "../schemas/tools/assetPayload.schema.js";
83

94
const refs = {
105
inspectButton: document.getElementById("inspect3dAssetButton"),
@@ -13,6 +8,9 @@ const refs = {
138
output: document.getElementById("asset3dOutput")
149
};
1510

11+
const ASSET_VIEWER_PAYLOAD_SCHEMA = "tools.3d-asset-viewer.asset/1";
12+
const ASSET_VIEWER_REPORT_SCHEMA = "tools.3d-asset-viewer.report/1";
13+
1614
function normalizeSamplePresetPath(pathValue) {
1715
if (typeof pathValue !== "string") {
1816
return "";
@@ -50,7 +48,7 @@ function setStatus(message) {
5048

5149
function sanitizeNumber(value) {
5250
const numeric = Number(value);
53-
return Number.isFinite(numeric) ? numeric : 0;
51+
return Number.isNaN(numeric) || numeric === Infinity || numeric === -Infinity ? 0 : numeric;
5452
}
5553

5654
function computeBounds(vertices) {
@@ -84,8 +82,43 @@ function computeBounds(vertices) {
8482
}
8583

8684
function buildDefaultPayload() {
87-
// Deprecated compatibility shim for older call-sites.
88-
return createDefaultAssetPayload();
85+
return {
86+
schema: ASSET_VIEWER_PAYLOAD_SCHEMA,
87+
assetId: "ship-hull",
88+
vertices: [
89+
{ x: -1, y: -0.5, z: -2 },
90+
{ x: 1, y: -0.5, z: -2 },
91+
{ x: 0, y: 0.75, z: 2 }
92+
],
93+
metadata: {
94+
sourceToolId: "vector-asset-studio"
95+
}
96+
};
97+
}
98+
99+
function normalizeAssetPayload(rawPayload) {
100+
if (!rawPayload || typeof rawPayload !== "object") {
101+
return buildDefaultPayload();
102+
}
103+
const rawVertices = Array.isArray(rawPayload.vertices) ? rawPayload.vertices : [];
104+
const vertices = rawVertices.map((rawVertex) => {
105+
const vertex = rawVertex && typeof rawVertex === "object" ? rawVertex : {};
106+
return {
107+
x: sanitizeNumber(vertex.x),
108+
y: sanitizeNumber(vertex.y),
109+
z: sanitizeNumber(vertex.z)
110+
};
111+
});
112+
return {
113+
schema: typeof rawPayload.schema === "string" && rawPayload.schema.trim()
114+
? rawPayload.schema.trim()
115+
: ASSET_VIEWER_PAYLOAD_SCHEMA,
116+
assetId: typeof rawPayload.assetId === "string" && rawPayload.assetId.trim()
117+
? rawPayload.assetId.trim()
118+
: "asset-3d",
119+
vertices,
120+
metadata: rawPayload.metadata && typeof rawPayload.metadata === "object" ? { ...rawPayload.metadata } : {}
121+
};
89122
}
90123

91124
function extractAssetPayloadFromPreset(rawPreset) {
@@ -127,17 +160,13 @@ async function tryLoadPresetFromQuery() {
127160
if (!extractedPayload) {
128161
throw new Error("Preset payload did not include a 3D asset payload.");
129162
}
130-
const validation = validateAssetPayload(extractedPayload, {
131-
requireSchema: true,
132-
requireVertices: true
133-
});
134-
if (!validation.valid) {
135-
throw new Error(validation.issues.join(" "));
163+
if (!Array.isArray(extractedPayload.vertices) || extractedPayload.vertices.length === 0) {
164+
throw new Error("Preset payload must include at least one vertex.");
136165
}
137166
if (!(refs.input instanceof HTMLTextAreaElement)) {
138167
throw new Error("Asset input is unavailable.");
139168
}
140-
refs.input.value = toPrettyJson(validation.payload);
169+
refs.input.value = toPrettyJson(normalizeAssetPayload(extractedPayload));
141170
setStatus(buildPresetLoadedStatus(sampleId, samplePresetPath));
142171
} catch (error) {
143172
setStatus(`Preset load failed: ${error instanceof Error ? error.message : "unknown error"}`);
@@ -149,16 +178,12 @@ function inspectAssetPayload() {
149178
return;
150179
}
151180
const parsed = safeParseJson(refs.input.value);
152-
const validation = validateAssetPayload(parsed, {
153-
requireSchema: false,
154-
requireVertices: true
155-
});
156-
if (!validation.valid) {
157-
setStatus(`Input JSON is invalid. ${validation.issues.join(" ")}`);
181+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.vertices) || parsed.vertices.length === 0) {
182+
setStatus("Input JSON is invalid. 3D asset payload requires at least one vertex.");
158183
return;
159184
}
160185

161-
const assetPayload = validation.payload;
186+
const assetPayload = normalizeAssetPayload(parsed);
162187
const vertices = Array.isArray(assetPayload.vertices) ? assetPayload.vertices : [];
163188
const bounds = computeBounds(vertices);
164189
const report = {

0 commit comments

Comments
 (0)