Skip to content

Commit e2b1885

Browse files
author
DavidQ
committed
Validate all tools against strict schema contract and remove hidden defaults/fallbacks - PR_11_312
1 parent 97c9362 commit e2b1885

11 files changed

Lines changed: 195 additions & 63 deletions

File tree

docs/dev/codex_commands.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,11 @@ PR_11_311
108108
```bash
109109
npx @openai/codex run --model gpt-5.3-codex --reasoning medium "Implement PR_11_311 ..."
110110
```
111+
112+
113+
---
114+
PR_11_312
115+
116+
```bash
117+
npx @openai/codex run --model gpt-5.3-codex --reasoning medium "Implement PR_11_312 ..."
118+
```

docs/dev/commit_comment.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Enforce strict schema validation-only acceptance for Workspace V2 import/load/activation paths - PR 11.311
1+
Enforce strict payloadJson-only contract and visible invalid-state rejection across V2 tools - PR 11.312
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"pr": "PR_11_312",
3+
"generatedAt": "2026-05-03T11:28:53.990280",
4+
"scope": [
5+
"tools/asset-browser-v2/index.js",
6+
"tools/palette-manager-v2/index.js",
7+
"tools/svg-asset-studio-v2/index.js",
8+
"tools/tilemap-studio-v2/index.js",
9+
"tools/vector-map-editor-v2/index.js",
10+
"tests/fixtures/v2-tools/palette-manager-v2.json"
11+
],
12+
"contractEnforcement": {
13+
"payloadJsonOnly": true,
14+
"toolIdMatchRequired": true,
15+
"invalidPayloadRejected": true,
16+
"noSilentFallback": true,
17+
"noAutoGeneratedData": true,
18+
"noSchemaChanges": true
19+
},
20+
"toolChecks": [
21+
{
22+
"toolId": "asset-browser-v2",
23+
"validPath": "payloadJson.assetCatalog",
24+
"invalidBehavior": "render blocked with visible INVALID state and message",
25+
"strictToolIdCheck": "enabled"
26+
},
27+
{
28+
"toolId": "palette-manager-v2",
29+
"validPath": "payloadJson.paletteDocument",
30+
"invalidBehavior": "render blocked with visible INVALID state and message",
31+
"legacyBlocked": [
32+
"paletteJson root",
33+
"non-swatch fields"
34+
],
35+
"strictToolIdCheck": "enabled"
36+
},
37+
{
38+
"toolId": "svg-asset-studio-v2",
39+
"validPath": "payloadJson.vectorAssetDocument",
40+
"invalidBehavior": "render blocked with visible INVALID state and message",
41+
"strictToolIdCheck": "enabled"
42+
},
43+
{
44+
"toolId": "tilemap-studio-v2",
45+
"validPath": "payloadJson.tileMapDocument",
46+
"invalidBehavior": "render blocked with visible INVALID state and message",
47+
"strictToolIdCheck": "enabled"
48+
},
49+
{
50+
"toolId": "vector-map-editor-v2",
51+
"validPath": "payloadJson.vectorMapDocument",
52+
"invalidBehavior": "render blocked with visible INVALID state and message",
53+
"strictToolIdCheck": "enabled"
54+
}
55+
],
56+
"commands": [
57+
"node --check tools/asset-browser-v2/index.js",
58+
"node --check tools/palette-manager-v2/index.js",
59+
"node --check tools/svg-asset-studio-v2/index.js",
60+
"node --check tools/tilemap-studio-v2/index.js",
61+
"node --check tools/vector-map-editor-v2/index.js"
62+
],
63+
"results": {
64+
"syntaxChecks": "PASS",
65+
"fullSamplesSmoke": "SKIPPED",
66+
"fullSamplesReason": "Targeted tool entry-point contract hardening only; no shared sample framework changes."
67+
}
68+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# BUILD_PR_11_312
2+
3+
## Implementation
4+
- Added strict `toolId` checks for all V2 tool entry points.
5+
- Enforced `payloadJson` contract across tools.
6+
- Updated `palette-manager-v2` to consume only `payloadJson.paletteDocument` and reject legacy `paletteJson` payloads.
7+
- Removed palette auto-correction/fallback parsing logic and required strict swatch object shape.
8+
- Updated palette fixture to strict payloadJson contract for targeted validation.
9+
10+
## Validation
11+
- `node --check tools/asset-browser-v2/index.js`
12+
- `node --check tools/palette-manager-v2/index.js`
13+
- `node --check tools/svg-asset-studio-v2/index.js`
14+
- `node --check tools/tilemap-studio-v2/index.js`
15+
- `node --check tools/vector-map-editor-v2/index.js`
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# PLAN_PR_11_312
2+
3+
## Purpose
4+
Enforce strict Workspace V2 tool contract validation so tools only consume validated `payloadJson` and reject invalid session JSON visibly.
5+
6+
## Scope
7+
- `tools/asset-browser-v2/index.js`
8+
- `tools/palette-manager-v2/index.js`
9+
- `tools/svg-asset-studio-v2/index.js`
10+
- `tools/tilemap-studio-v2/index.js`
11+
- `tools/vector-map-editor-v2/index.js`
12+
- `tests/fixtures/v2-tools/palette-manager-v2.json`
13+
14+
## Steps
15+
1. Tighten per-tool contract checks at session load boundaries.
16+
2. Enforce strict `toolId` match per tool.
17+
3. Enforce strict `payloadJson` path and reject legacy palette root fields.
18+
4. Remove palette auto-correction behavior and require explicit valid swatch shape.
19+
5. Run targeted syntax validation on changed tool entry files.
20+
6. Write tool validation report JSON and package artifacts.

tests/fixtures/v2-tools/palette-manager-v2.json

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
"sessionContext": {
44
"version": "v2",
55
"toolId": "palette-manager-v2",
6-
"paletteJson": {
7-
"name": "Fixture Palette",
8-
"colors": [
9-
"#112233",
10-
{
11-
"name": "Accent",
12-
"hex": "#FF6600"
13-
}
14-
]
6+
"payloadJson": {
7+
"paletteDocument": {
8+
"name": "Fixture Palette",
9+
"swatches": [
10+
{ "symbol": "A", "hex": "#112233", "name": "Base" },
11+
{ "symbol": "B", "hex": "#FF6600", "name": "Accent" }
12+
]
13+
}
1514
}
1615
}
1716
}

tools/asset-browser-v2/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ class AssetBrowserV2 {
166166
this.renderError(versionCheck.error);
167167
return;
168168
}
169+
if (typeof versionCheck.payload.toolId !== "string" || versionCheck.payload.toolId.trim() !== "asset-browser-v2") {
170+
this.renderError("Asset Browser V2 session data is invalid. Expected toolId 'asset-browser-v2'.");
171+
return;
172+
}
169173
if (!versionCheck.payload.payloadJson || typeof versionCheck.payload.payloadJson !== "object" || Array.isArray(versionCheck.payload.payloadJson)) {
170174
this.renderError("Asset Browser V2 session data is invalid. Expected payloadJson only.");
171175
return;

tools/palette-manager-v2/index.js

Lines changed: 59 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -158,93 +158,99 @@ class PaletteManagerV2 {
158158
loadContract(sessionContext) {
159159
console.log("[PALETTE_MANAGER_V2_CONTRACT_LOADED]");
160160
if (!sessionContext || typeof sessionContext !== "object" || Array.isArray(sessionContext)) {
161-
this.renderError("Session context is invalid. Expected an object containing paletteJson.");
161+
this.renderError("Session context is invalid. Expected an object containing payloadJson.paletteDocument.");
162162
return;
163163
}
164164
const versionCheck = this.handleSessionVersion(sessionContext);
165165
if (!versionCheck.ok) {
166166
this.renderError(versionCheck.error);
167167
return;
168168
}
169-
if (!versionCheck.payload.paletteJson || typeof versionCheck.payload.paletteJson !== "object" || Array.isArray(versionCheck.payload.paletteJson)) {
170-
this.renderError("Palette Manager V2 session data is invalid. Expected paletteJson only.");
169+
if (typeof versionCheck.payload.toolId !== "string" || versionCheck.payload.toolId.trim() !== "palette-manager-v2") {
170+
this.renderError("Palette Manager V2 session data is invalid. Expected toolId 'palette-manager-v2'.");
171171
return;
172172
}
173-
this.renderPalette(versionCheck.payload.paletteJson, versionCheck.payload);
173+
if (Object.prototype.hasOwnProperty.call(versionCheck.payload, "paletteJson")) {
174+
this.renderError("Palette Manager V2 session data is invalid. paletteJson is not supported; use payloadJson.paletteDocument.");
175+
return;
176+
}
177+
if (!Object.prototype.hasOwnProperty.call(versionCheck.payload, "payloadJson") || !versionCheck.payload.payloadJson || typeof versionCheck.payload.payloadJson !== "object" || Array.isArray(versionCheck.payload.payloadJson)) {
178+
this.renderError("Palette Manager V2 session data is invalid. Expected payloadJson only.");
179+
return;
180+
}
181+
if (!Object.prototype.hasOwnProperty.call(versionCheck.payload.payloadJson, "paletteDocument") || !versionCheck.payload.payloadJson.paletteDocument || typeof versionCheck.payload.payloadJson.paletteDocument !== "object" || Array.isArray(versionCheck.payload.payloadJson.paletteDocument)) {
182+
this.renderError("Palette Manager V2 session data is invalid. Expected payloadJson.paletteDocument.");
183+
return;
184+
}
185+
this.renderPalette(versionCheck.payload.payloadJson.paletteDocument, versionCheck.payload);
174186
}
175187

176-
renderPalette(paletteJson, sessionContext) {
177-
if (typeof paletteJson.name !== "string" || !paletteJson.name.trim()) {
178-
this.renderError("Palette Manager V2 session data is invalid. Expected paletteJson.name.");
188+
renderPalette(paletteDocument, sessionContext) {
189+
if (typeof paletteDocument.name !== "string" || !paletteDocument.name.trim()) {
190+
this.renderError("Palette Manager V2 session data is invalid. Expected paletteDocument.name.");
179191
return;
180192
}
181-
if (!Array.isArray(paletteJson.colors)) {
182-
this.renderError("Palette Manager V2 session data is invalid. Expected paletteJson.colors[].");
193+
if (!Array.isArray(paletteDocument.swatches)) {
194+
this.renderError("Palette Manager V2 session data is invalid. Expected paletteDocument.swatches[].");
183195
return;
184196
}
185197

186-
const normalizedColors = [];
187-
for (let index = 0; index < paletteJson.colors.length; index += 1) {
188-
let colorValue = "";
189-
let colorLabel = "";
190-
if (typeof paletteJson.colors[index] === "string") {
191-
colorValue = paletteJson.colors[index].trim().toUpperCase();
192-
colorLabel = colorValue;
198+
const validatedSwatches = [];
199+
for (let index = 0; index < paletteDocument.swatches.length; index += 1) {
200+
const swatch = paletteDocument.swatches[index];
201+
if (!swatch || typeof swatch !== "object" || Array.isArray(swatch)) {
202+
this.renderError("Palette Manager V2 session data is invalid. Every swatch must be an object.");
203+
return;
193204
}
194-
if (
195-
paletteJson.colors[index] &&
196-
typeof paletteJson.colors[index] === "object" &&
197-
!Array.isArray(paletteJson.colors[index]) &&
198-
typeof paletteJson.colors[index].hex === "string"
199-
) {
200-
colorValue = paletteJson.colors[index].hex.trim().toUpperCase();
201-
colorLabel = typeof paletteJson.colors[index].name === "string" && paletteJson.colors[index].name.trim()
202-
? paletteJson.colors[index].name.trim()
203-
: colorValue;
205+
const swatchKeys = Object.keys(swatch);
206+
if (swatchKeys.some((key) => key !== "symbol" && key !== "hex" && key !== "name")) {
207+
this.renderError("Palette Manager V2 session data is invalid. Swatches only allow symbol, hex, and name.");
208+
return;
204209
}
205-
if (
206-
paletteJson.colors[index] &&
207-
typeof paletteJson.colors[index] === "object" &&
208-
!Array.isArray(paletteJson.colors[index]) &&
209-
typeof paletteJson.colors[index].color === "string"
210-
) {
211-
colorValue = paletteJson.colors[index].color.trim().toUpperCase();
212-
colorLabel = typeof paletteJson.colors[index].name === "string" && paletteJson.colors[index].name.trim()
213-
? paletteJson.colors[index].name.trim()
214-
: colorValue;
210+
if (typeof swatch.symbol !== "string" || swatch.symbol.length !== 1) {
211+
this.renderError("Palette Manager V2 session data is invalid. Every swatch requires a one-character symbol.");
212+
return;
213+
}
214+
if (typeof swatch.name !== "string" || !swatch.name.trim()) {
215+
this.renderError("Palette Manager V2 session data is invalid. Every swatch requires a name.");
216+
return;
215217
}
216-
if (!/^#([0-9A-F]{6}|[0-9A-F]{8})$/.test(colorValue)) {
217-
this.renderError("Palette Manager V2 session data is invalid. Every paletteJson.colors[] entry must include #RRGGBB or #RRGGBBAA.");
218+
if (typeof swatch.hex !== "string" || !/^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(swatch.hex)) {
219+
this.renderError("Palette Manager V2 session data is invalid. Every swatch requires #RRGGBB or #RRGGBBAA hex.");
218220
return;
219221
}
220-
normalizedColors.push({ label: colorLabel, color: colorValue });
222+
validatedSwatches.push({
223+
symbol: swatch.symbol,
224+
name: swatch.name.trim(),
225+
hex: swatch.hex.toUpperCase()
226+
});
221227
}
222228

223229
document.getElementById("paletteManagerSessionReadout").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()}` : ""}`;
224-
document.getElementById("paletteManagerContractReadout").textContent = "paletteJson loaded\npaletteJson.name valid\npaletteJson.colors[] valid";
230+
document.getElementById("paletteManagerContractReadout").textContent = "payloadJson loaded\npayloadJson.paletteDocument valid\nswatches[] valid";
225231
document.getElementById("paletteManagerWorkspaceReadout").textContent = "Workspace session context was read. Workspace writes are deferred for this isolated V2 entry.";
226-
document.getElementById("paletteManagerName").textContent = paletteJson.name.trim();
227-
document.getElementById("paletteManagerCount").textContent = `${normalizedColors.length} color${normalizedColors.length === 1 ? "" : "s"}`;
228-
document.getElementById("paletteManagerState").textContent = normalizedColors.length === 0
229-
? "Palette Manager V2 loaded a valid session palette with zero colors."
232+
document.getElementById("paletteManagerName").textContent = paletteDocument.name.trim();
233+
document.getElementById("paletteManagerCount").textContent = `${validatedSwatches.length} swatch${validatedSwatches.length === 1 ? "" : "es"}`;
234+
document.getElementById("paletteManagerState").textContent = validatedSwatches.length === 0
235+
? "Palette Manager V2 loaded a valid session palette with zero swatches."
230236
: "Palette Manager V2 loaded the session palette.";
231237
document.getElementById("paletteManagerEmptyState").hidden = true;
232238
document.getElementById("paletteManagerInvalidState").hidden = true;
233239
document.getElementById("paletteManagerValidState").hidden = false;
234240

235241
document.getElementById("paletteManagerSwatches").replaceChildren();
236-
normalizedColors.forEach((entry) => {
242+
validatedSwatches.forEach((entry) => {
237243
const swatch = document.createElement("article");
238244
const chip = document.createElement("div");
239245
const swatchBody = document.createElement("div");
240246
const swatchName = document.createElement("div");
241247
const swatchColor = document.createElement("div");
242248
swatch.className = "palette-manager-v2-swatch";
243249
chip.className = "palette-manager-v2-chip";
244-
chip.style.backgroundColor = entry.color;
250+
chip.style.backgroundColor = entry.hex;
245251
swatchBody.className = "palette-manager-v2-swatch-body";
246-
swatchName.textContent = entry.label;
247-
swatchColor.textContent = entry.color;
252+
swatchName.textContent = `${entry.symbol} ${entry.name}`;
253+
swatchColor.textContent = entry.hex;
248254
swatchBody.append(swatchName, swatchColor);
249255
swatch.append(chip, swatchBody);
250256
document.getElementById("paletteManagerSwatches").appendChild(swatch);
@@ -254,10 +260,10 @@ class PaletteManagerV2 {
254260
renderMissing(message) {
255261
this.logStructuredError("EMPTY", message, { hostContextId: this.urlState.hostContextId || "" });
256262
document.getElementById("paletteManagerSessionReadout").textContent = "Session: missing";
257-
document.getElementById("paletteManagerContractReadout").textContent = "paletteJson not loaded";
263+
document.getElementById("paletteManagerContractReadout").textContent = "payloadJson.paletteDocument not loaded";
258264
document.getElementById("paletteManagerWorkspaceReadout").textContent = "Workspace session context is not available.";
259265
document.getElementById("paletteManagerName").textContent = "No palette loaded";
260-
document.getElementById("paletteManagerCount").textContent = "0 colors";
266+
document.getElementById("paletteManagerCount").textContent = "0 swatches";
261267
document.getElementById("paletteManagerEmptyState").textContent = message;
262268
document.getElementById("paletteManagerEmptyState").hidden = false;
263269
document.getElementById("paletteManagerInvalidState").hidden = true;
@@ -268,11 +274,11 @@ class PaletteManagerV2 {
268274
renderError(message) {
269275
this.logStructuredError("INVALID", message, { hostContextId: this.urlState.hostContextId || "" });
270276
document.getElementById("paletteManagerSessionReadout").textContent = "Session: read attempted";
271-
document.getElementById("paletteManagerContractReadout").textContent = "paletteJson invalid";
277+
document.getElementById("paletteManagerContractReadout").textContent = "payloadJson.paletteDocument invalid";
272278
document.getElementById("paletteManagerWorkspaceReadout").textContent = "Workspace writes are disabled for invalid Palette Manager V2 session data.";
273279
document.getElementById("paletteManagerName").textContent = "Palette Manager V2 error";
274-
document.getElementById("paletteManagerCount").textContent = "0 colors";
275-
document.getElementById("paletteManagerInvalidState").textContent = `${message} Re-open Palette Manager V2 from a host session that provides paletteJson.`;
280+
document.getElementById("paletteManagerCount").textContent = "0 swatches";
281+
document.getElementById("paletteManagerInvalidState").textContent = `${message} Re-open Palette Manager V2 from a host session that provides payloadJson.paletteDocument.`;
276282
document.getElementById("paletteManagerEmptyState").hidden = true;
277283
document.getElementById("paletteManagerInvalidState").hidden = false;
278284
document.getElementById("paletteManagerValidState").hidden = true;

tools/svg-asset-studio-v2/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ class SvgAssetStudioV2 {
155155
this.renderError(versionCheck.error);
156156
return;
157157
}
158+
if (typeof versionCheck.payload.toolId !== "string" || versionCheck.payload.toolId.trim() !== "svg-asset-studio-v2") {
159+
this.renderError("SVG Asset Studio V2 session data is invalid. Expected toolId 'svg-asset-studio-v2'.");
160+
return;
161+
}
158162
if (!versionCheck.payload.payloadJson || typeof versionCheck.payload.payloadJson !== "object" || Array.isArray(versionCheck.payload.payloadJson)) {
159163
this.renderError("SVG Asset Studio V2 session data is invalid. Expected payloadJson only.");
160164
return;

tools/tilemap-studio-v2/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ class TilemapStudioV2 {
166166
this.renderError(versionCheck.error);
167167
return;
168168
}
169+
if (typeof versionCheck.payload.toolId !== "string" || versionCheck.payload.toolId.trim() !== "tilemap-studio-v2") {
170+
this.renderError("Tilemap session data is invalid. Expected toolId 'tilemap-studio-v2'.");
171+
return;
172+
}
169173
if (!versionCheck.payload.payloadJson || typeof versionCheck.payload.payloadJson !== "object" || Array.isArray(versionCheck.payload.payloadJson)) {
170174
this.renderError("Tilemap session data is invalid. Expected payloadJson only.");
171175
return;

0 commit comments

Comments
 (0)