Skip to content

Commit 8f6c801

Browse files
author
DavidQ
committed
Add structured error logging across V2 tools with executable validation - PR 11.221
1 parent 02e388a commit 8f6c801

7 files changed

Lines changed: 320 additions & 5 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# PR_11_221 Report - V2 Error Logging (Structured)
2+
3+
## Files Changed
4+
- `tools/asset-browser-v2/index.js`
5+
- `tools/palette-manager-v2/index.js`
6+
- `tools/svg-asset-studio-v2/index.js`
7+
- `tools/tilemap-studio-v2/index.js`
8+
- `tools/vector-map-editor-v2/index.js`
9+
- `tests/runtime/V2ErrorLogging.test.mjs`
10+
- `docs/dev/reports/PR_11_221_report.md`
11+
12+
## Structured Logging Update
13+
Added consistent structured error logging in each V2 tool class:
14+
15+
```js
16+
console.error({
17+
tool: "<tool-id>",
18+
type: "EMPTY | INVALID | RUNTIME",
19+
message: "<human-readable>",
20+
details: { hostContextId: "<id-or-empty>" }
21+
});
22+
```
23+
24+
Triggers now covered per tool:
25+
- `EMPTY`: emitted in `renderMissing(...)`
26+
- `INVALID`: emitted in `renderError(...)`
27+
- `RUNTIME`: emitted in `readSession()` catch path before routing to existing invalid UI state
28+
29+
## Log Samples
30+
Example EMPTY:
31+
```json
32+
{
33+
"tool": "asset-browser-v2",
34+
"type": "EMPTY",
35+
"message": "No hostContextId was provided for this tool.",
36+
"details": { "hostContextId": "" }
37+
}
38+
```
39+
40+
Example INVALID:
41+
```json
42+
{
43+
"tool": "tilemap-studio-v2",
44+
"type": "INVALID",
45+
"message": "Session payload is invalid for this tool.",
46+
"details": { "hostContextId": "tilemap-studio-v2-invalid" }
47+
}
48+
```
49+
50+
Example RUNTIME:
51+
```json
52+
{
53+
"tool": "vector-map-editor-v2",
54+
"type": "RUNTIME",
55+
"message": "Unable to read session context: runtime-test-injection",
56+
"details": { "hostContextId": "vector-map-editor-v2-runtime" }
57+
}
58+
```
59+
60+
## Validation Results
61+
Commands run:
62+
1. `node --check tests/runtime/V2ErrorLogging.test.mjs`
63+
Result: **PASS**
64+
2. `node tests/runtime/V2ErrorLogging.test.mjs`
65+
Result: **PASS** (writes `tmp/v2-error-logging-results.json`)
66+
3. `node --check tools/*-v2/index.js`
67+
Result: **FAIL** on Windows/Node wildcard resolution (`MODULE_NOT_FOUND` for literal `tools\\*-v2\\index.js`)
68+
4. Explicit equivalent per-file syntax sweep for `tools/*-v2/index.js`
69+
Result: **PASS** for all detected V2 tool JS files
70+
71+
Runtime test assertions passed for all five target tools:
72+
- structured logger method present
73+
- object log shape present
74+
- EMPTY / INVALID / RUNTIME trigger calls present
75+
- simulated captured logs are machine-readable and schema-valid
76+
77+
## No UI/Fallback Behavior Change Confirmation
78+
- No UI rendering logic was modified.
79+
- Existing EMPTY/INVALID/VALID state rendering remains unchanged.
80+
- No fallback/default/demo data introduced.
81+
- No non-scope files (schemas/samples/games/workspace-v1/platformShell/tools/shared) were modified.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import { execFileSync } from "node:child_process";
5+
import { fileURLToPath, pathToFileURL } from "node:url";
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
const repoRoot = path.resolve(__dirname, "..", "..");
10+
const toolsRoot = path.join(repoRoot, "tools");
11+
const resultsPath = path.join(repoRoot, "tmp", "v2-error-logging-results.json");
12+
13+
const TOOLS = [
14+
"asset-browser-v2",
15+
"palette-manager-v2",
16+
"svg-asset-studio-v2",
17+
"tilemap-studio-v2",
18+
"vector-map-editor-v2"
19+
];
20+
21+
const VALID_TYPES = new Set(["EMPTY", "INVALID", "RUNTIME"]);
22+
23+
function readText(filePath) {
24+
return fs.readFileSync(filePath, "utf8");
25+
}
26+
27+
function checkJsSyntax(jsPath) {
28+
try {
29+
execFileSync(process.execPath, ["--check", jsPath], {
30+
cwd: repoRoot,
31+
stdio: ["ignore", "pipe", "pipe"]
32+
});
33+
return { syntaxValid: true, syntaxError: "" };
34+
} catch (error) {
35+
return {
36+
syntaxValid: false,
37+
syntaxError: (error?.stderr || error?.stdout || error?.message || "").toString().trim()
38+
};
39+
}
40+
}
41+
42+
function validateLogEntry(logEntry, expectedTool, expectedType) {
43+
const issues = [];
44+
if (!logEntry || typeof logEntry !== "object" || Array.isArray(logEntry)) {
45+
issues.push("Log entry must be an object.");
46+
return issues;
47+
}
48+
if (logEntry.tool !== expectedTool) {
49+
issues.push(`Expected tool "${expectedTool}", got "${typeof logEntry.tool === "string" ? logEntry.tool : "missing"}".`);
50+
}
51+
if (logEntry.type !== expectedType) {
52+
issues.push(`Expected type "${expectedType}", got "${typeof logEntry.type === "string" ? logEntry.type : "missing"}".`);
53+
}
54+
if (!VALID_TYPES.has(logEntry.type)) {
55+
issues.push(`Unexpected type value "${typeof logEntry.type === "string" ? logEntry.type : "missing"}".`);
56+
}
57+
if (typeof logEntry.message !== "string" || !logEntry.message.trim()) {
58+
issues.push("Log message must be a non-empty string.");
59+
}
60+
if (!Object.prototype.hasOwnProperty.call(logEntry, "details")) {
61+
issues.push("Log details field is missing.");
62+
} else if (!logEntry.details || typeof logEntry.details !== "object" || Array.isArray(logEntry.details)) {
63+
issues.push("Log details must be an object.");
64+
}
65+
return issues;
66+
}
67+
68+
function simulateStructuredLogs(toolId) {
69+
const capturedLogs = [];
70+
const writeLog = (logEntry) => capturedLogs.push(logEntry);
71+
const emitStructuredLog = (type, message, details) => {
72+
writeLog({
73+
tool: toolId,
74+
type,
75+
message,
76+
details: details && typeof details === "object" ? details : {}
77+
});
78+
};
79+
80+
emitStructuredLog("EMPTY", "No hostContextId was provided for this tool.", { hostContextId: "" });
81+
emitStructuredLog("INVALID", "Session payload is invalid for this tool.", { hostContextId: `${toolId}-invalid` });
82+
try {
83+
throw new Error("runtime-test-injection");
84+
} catch (error) {
85+
emitStructuredLog("RUNTIME", `Unable to read session context: ${error instanceof Error ? error.message : "unknown error"}`, { hostContextId: `${toolId}-runtime` });
86+
}
87+
88+
return capturedLogs;
89+
}
90+
91+
function validateTool(toolId) {
92+
const jsPath = path.join(toolsRoot, toolId, "index.js");
93+
const jsExists = fs.existsSync(jsPath);
94+
const jsText = jsExists ? readText(jsPath) : "";
95+
const { syntaxValid, syntaxError } = checkJsSyntax(jsPath);
96+
const failures = [];
97+
98+
const hasStructuredLoggerMethod = jsText.includes("logStructuredError(type, message, details)");
99+
const hasObjectLogShape = jsText.includes("console.error({") &&
100+
jsText.includes(`tool: "${toolId}"`) &&
101+
jsText.includes("type,") &&
102+
jsText.includes("message,") &&
103+
jsText.includes("details:");
104+
const hasEmptyTrigger = jsText.includes('this.logStructuredError("EMPTY",');
105+
const hasInvalidTrigger = jsText.includes('this.logStructuredError("INVALID",');
106+
const hasRuntimeTrigger = jsText.includes('this.logStructuredError("RUNTIME",');
107+
108+
if (!jsExists) failures.push("Missing tool index.js.");
109+
if (!syntaxValid) failures.push("Tool index.js failed syntax check.");
110+
if (!hasStructuredLoggerMethod) failures.push("Missing logStructuredError(type, message, details) method.");
111+
if (!hasObjectLogShape) failures.push("Structured log object shape is missing or inconsistent.");
112+
if (!hasEmptyTrigger) failures.push("Missing EMPTY structured log trigger.");
113+
if (!hasInvalidTrigger) failures.push("Missing INVALID structured log trigger.");
114+
if (!hasRuntimeTrigger) failures.push("Missing RUNTIME structured log trigger.");
115+
116+
const simulatedLogs = simulateStructuredLogs(toolId);
117+
if (simulatedLogs.length !== 3) {
118+
failures.push(`Expected 3 simulated logs, got ${simulatedLogs.length}.`);
119+
}
120+
121+
const emptyIssues = validateLogEntry(simulatedLogs[0], toolId, "EMPTY");
122+
const invalidIssues = validateLogEntry(simulatedLogs[1], toolId, "INVALID");
123+
const runtimeIssues = validateLogEntry(simulatedLogs[2], toolId, "RUNTIME");
124+
emptyIssues.forEach((entry) => failures.push(`EMPTY log: ${entry}`));
125+
invalidIssues.forEach((entry) => failures.push(`INVALID log: ${entry}`));
126+
runtimeIssues.forEach((entry) => failures.push(`RUNTIME log: ${entry}`));
127+
128+
return {
129+
tool: toolId,
130+
jsPath: path.relative(repoRoot, jsPath).replace(/\\/g, "/"),
131+
jsExists,
132+
syntaxValid,
133+
syntaxError,
134+
hasStructuredLoggerMethod,
135+
hasObjectLogShape,
136+
hasEmptyTrigger,
137+
hasInvalidTrigger,
138+
hasRuntimeTrigger,
139+
simulatedLogs,
140+
failures
141+
};
142+
}
143+
144+
export function run() {
145+
const rows = TOOLS.map(validateTool);
146+
const failures = rows.flatMap((row) => row.failures.map((entry) => `${row.tool}: ${entry}`));
147+
148+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
149+
fs.writeFileSync(resultsPath, `${JSON.stringify({
150+
generatedAt: new Date().toISOString(),
151+
toolCount: rows.length,
152+
failures,
153+
rows
154+
}, null, 2)}\n`, "utf8");
155+
156+
console.log(`v2 error logging results: ${resultsPath}`);
157+
assert.equal(failures.length, 0, `V2 error logging failures: ${failures.join(" | ")}`);
158+
return { toolCount: rows.length, failures, rows };
159+
}
160+
161+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
162+
try {
163+
const summary = run();
164+
console.log(JSON.stringify(summary, null, 2));
165+
} catch (error) {
166+
console.error(error);
167+
process.exitCode = 1;
168+
}
169+
}

tools/asset-browser-v2/index.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ class AssetBrowserV2 {
8181
return urlStateParts.join(", ");
8282
}
8383

84+
logStructuredError(type, message, details) {
85+
console.error({
86+
tool: "asset-browser-v2",
87+
type,
88+
message,
89+
details: details && typeof details === "object" ? details : {}
90+
});
91+
}
92+
8493
readSession() {
8594
console.log("[SESSION_CONTEXT_READ]");
8695
try {
@@ -104,7 +113,9 @@ class AssetBrowserV2 {
104113
)
105114
);
106115
} catch (error) {
107-
this.renderError(`Unable to read Asset Browser V2 session context: ${error instanceof Error ? error.message : "unknown error"}`);
116+
const runtimeMessage = `Unable to read Asset Browser V2 session context: ${error instanceof Error ? error.message : "unknown error"}`;
117+
this.logStructuredError("RUNTIME", runtimeMessage, { hostContextId: this.urlState.hostContextId || "" });
118+
this.renderError(runtimeMessage);
108119
}
109120
}
110121

@@ -196,6 +207,7 @@ class AssetBrowserV2 {
196207
}
197208

198209
renderMissing(message) {
210+
this.logStructuredError("EMPTY", message, { hostContextId: this.urlState.hostContextId || "" });
199211
document.getElementById("assetBrowserV2SessionReadout").textContent = "Session: missing";
200212
document.getElementById("assetBrowserV2ContractReadout").textContent = "payloadJson.assetCatalog not loaded";
201213
document.getElementById("assetBrowserV2WorkspaceReadout").textContent = "Workspace session context is not available.";
@@ -210,6 +222,7 @@ class AssetBrowserV2 {
210222
}
211223

212224
renderError(message) {
225+
this.logStructuredError("INVALID", message, { hostContextId: this.urlState.hostContextId || "" });
213226
document.getElementById("assetBrowserV2SessionReadout").textContent = "Session: read attempted";
214227
document.getElementById("assetBrowserV2ContractReadout").textContent = "payloadJson.assetCatalog invalid";
215228
document.getElementById("assetBrowserV2WorkspaceReadout").textContent = "Workspace writes are disabled for invalid Asset Browser V2 session data.";

tools/palette-manager-v2/index.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ class PaletteManagerV2 {
8181
return urlStateParts.join(", ");
8282
}
8383

84+
logStructuredError(type, message, details) {
85+
console.error({
86+
tool: "palette-manager-v2",
87+
type,
88+
message,
89+
details: details && typeof details === "object" ? details : {}
90+
});
91+
}
92+
8493
readSession() {
8594
console.log("[SESSION_CONTEXT_READ]");
8695
try {
@@ -104,7 +113,9 @@ class PaletteManagerV2 {
104113
)
105114
);
106115
} catch (error) {
107-
this.renderError(`Unable to read Palette Manager V2 session context: ${error instanceof Error ? error.message : "unknown error"}`);
116+
const runtimeMessage = `Unable to read Palette Manager V2 session context: ${error instanceof Error ? error.message : "unknown error"}`;
117+
this.logStructuredError("RUNTIME", runtimeMessage, { hostContextId: this.urlState.hostContextId || "" });
118+
this.renderError(runtimeMessage);
108119
}
109120
}
110121

@@ -200,6 +211,7 @@ class PaletteManagerV2 {
200211
}
201212

202213
renderMissing(message) {
214+
this.logStructuredError("EMPTY", message, { hostContextId: this.urlState.hostContextId || "" });
203215
document.getElementById("paletteManagerSessionReadout").textContent = "Session: missing";
204216
document.getElementById("paletteManagerContractReadout").textContent = "paletteJson not loaded";
205217
document.getElementById("paletteManagerWorkspaceReadout").textContent = "Workspace session context is not available.";
@@ -213,6 +225,7 @@ class PaletteManagerV2 {
213225
}
214226

215227
renderError(message) {
228+
this.logStructuredError("INVALID", message, { hostContextId: this.urlState.hostContextId || "" });
216229
document.getElementById("paletteManagerSessionReadout").textContent = "Session: read attempted";
217230
document.getElementById("paletteManagerContractReadout").textContent = "paletteJson invalid";
218231
document.getElementById("paletteManagerWorkspaceReadout").textContent = "Workspace writes are disabled for invalid Palette Manager V2 session data.";

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ class SvgAssetStudioV2 {
7070
return urlStateParts.join(", ");
7171
}
7272

73+
logStructuredError(type, message, details) {
74+
console.error({
75+
tool: "svg-asset-studio-v2",
76+
type,
77+
message,
78+
details: details && typeof details === "object" ? details : {}
79+
});
80+
}
81+
7382
readSession() {
7483
console.log("[SESSION_CONTEXT_READ]");
7584
try {
@@ -93,7 +102,9 @@ class SvgAssetStudioV2 {
93102
)
94103
);
95104
} catch (error) {
96-
this.renderError(`Unable to read SVG Asset Studio V2 session context: ${error instanceof Error ? error.message : "unknown error"}`);
105+
const runtimeMessage = `Unable to read SVG Asset Studio V2 session context: ${error instanceof Error ? error.message : "unknown error"}`;
106+
this.logStructuredError("RUNTIME", runtimeMessage, { hostContextId: this.urlState.hostContextId || "" });
107+
this.renderError(runtimeMessage);
97108
}
98109
}
99110

@@ -151,6 +162,7 @@ class SvgAssetStudioV2 {
151162
}
152163

153164
renderMissing(message) {
165+
this.logStructuredError("EMPTY", message, { hostContextId: this.urlState.hostContextId || "" });
154166
if (this.previewObjectUrl) {
155167
URL.revokeObjectURL(this.previewObjectUrl);
156168
this.previewObjectUrl = "";
@@ -168,6 +180,7 @@ class SvgAssetStudioV2 {
168180
}
169181

170182
renderError(message) {
183+
this.logStructuredError("INVALID", message, { hostContextId: this.urlState.hostContextId || "" });
171184
if (this.previewObjectUrl) {
172185
URL.revokeObjectURL(this.previewObjectUrl);
173186
this.previewObjectUrl = "";

0 commit comments

Comments
 (0)