Skip to content

Commit 7ed64eb

Browse files
author
DavidQ
committed
Introduce session version migration hook pattern with strict version gate and no auto-migration - PR 11.227
1 parent a175ccc commit 7ed64eb

7 files changed

Lines changed: 295 additions & 24 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# PR_11_227 Report - V2 Session Migration Hook (Version Gate)
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/V2SessionMigration.test.mjs`
10+
- `docs/dev/reports/PR_11_227_report.md`
11+
12+
## Migration Hook Presence
13+
Each V2 tool now contains the hook pattern:
14+
15+
```js
16+
handleSessionVersion(payload) {
17+
if (payload && payload.version === "v2") return { ok: true, payload };
18+
return {
19+
ok: false,
20+
error: "Unsupported session version",
21+
code: "UNSUPPORTED_VERSION"
22+
};
23+
}
24+
```
25+
26+
Enforcement pattern in each tool:
27+
- calls `handleSessionVersion(sessionContext)` before payload contract/render checks
28+
- when `ok: false`, routes to INVALID state via existing `renderError(...)`
29+
- uses error message:
30+
- `Unsupported session version`
31+
32+
## Validation Results
33+
Commands run:
34+
1. `node --check tests/runtime/V2SessionMigration.test.mjs`
35+
Result: **PASS**
36+
2. `node tests/runtime/V2SessionMigration.test.mjs`
37+
Result: **PASS** (writes `tmp/v2-session-migration-results.json`)
38+
3. `node --check tools/*-v2/index.js`
39+
Result: **FAIL** on Windows/Node wildcard resolution (`MODULE_NOT_FOUND` for literal `tools\\*-v2\\index.js`)
40+
4. Equivalent explicit per-file `node --check` sweep for `tools/*-v2/index.js`
41+
Result: **PASS** for all detected V2 tool JS files
42+
43+
Runtime test cases passed for all five V2 tools:
44+
- `version: "v2"` -> `VALID`
45+
- `version: "v3"` -> `INVALID` with `code: "UNSUPPORTED_VERSION"`
46+
- missing `version` -> `INVALID` with `code: "UNSUPPORTED_VERSION"`
47+
48+
## No Fallback / No Migration Execution Confirmation
49+
- No fallback paths introduced.
50+
- No auto-upgrade paths introduced.
51+
- No migration logic executed; hook explicitly returns unsupported for non-v2 versions.
52+
- Payload structure unchanged.
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 fixturesRoot = path.join(repoRoot, "tests", "fixtures", "v2-tools");
12+
const resultsPath = path.join(repoRoot, "tmp", "v2-session-migration-results.json");
13+
14+
const TOOLS = [
15+
"asset-browser-v2",
16+
"palette-manager-v2",
17+
"svg-asset-studio-v2",
18+
"tilemap-studio-v2",
19+
"vector-map-editor-v2"
20+
];
21+
22+
function readText(filePath) {
23+
return fs.readFileSync(filePath, "utf8");
24+
}
25+
26+
function readJson(filePath) {
27+
return JSON.parse(readText(filePath));
28+
}
29+
30+
function cloneJson(value) {
31+
return JSON.parse(JSON.stringify(value));
32+
}
33+
34+
function checkJsSyntax(jsPath) {
35+
try {
36+
execFileSync(process.execPath, ["--check", jsPath], {
37+
cwd: repoRoot,
38+
stdio: ["ignore", "pipe", "pipe"]
39+
});
40+
return { syntaxValid: true, syntaxError: "" };
41+
} catch (error) {
42+
return {
43+
syntaxValid: false,
44+
syntaxError: (error?.stderr || error?.stdout || error?.message || "").toString().trim()
45+
};
46+
}
47+
}
48+
49+
function handleSessionVersion(payload) {
50+
if (payload && payload.version === "v2") return { ok: true, payload };
51+
return {
52+
ok: false,
53+
error: "Unsupported session version",
54+
code: "UNSUPPORTED_VERSION"
55+
};
56+
}
57+
58+
function classifyVersionState(payload) {
59+
const versionResult = handleSessionVersion(payload);
60+
if (!versionResult.ok) {
61+
return { state: "INVALID", result: versionResult };
62+
}
63+
return { state: "VALID", result: versionResult };
64+
}
65+
66+
function validateTool(toolId) {
67+
const fixturePath = path.join(fixturesRoot, `${toolId}.json`);
68+
const jsPath = path.join(toolsRoot, toolId, "index.js");
69+
const failures = [];
70+
const fixtureExists = fs.existsSync(fixturePath);
71+
const jsExists = fs.existsSync(jsPath);
72+
const jsText = jsExists ? readText(jsPath) : "";
73+
const { syntaxValid, syntaxError } = checkJsSyntax(jsPath);
74+
75+
const hasHookMethod = jsText.includes("handleSessionVersion(payload)");
76+
const hasHookPattern = jsText.includes('if (payload && payload.version === "v2") return { ok: true, payload };') &&
77+
jsText.includes('code: "UNSUPPORTED_VERSION"');
78+
const hasHookEnforcement = jsText.includes("const versionCheck = this.handleSessionVersion(sessionContext);") &&
79+
jsText.includes("if (!versionCheck.ok)");
80+
81+
if (!jsExists) failures.push("Missing tool index.js.");
82+
if (!syntaxValid) failures.push("Tool index.js failed syntax check.");
83+
if (!hasHookMethod) failures.push("Missing handleSessionVersion(payload) hook method.");
84+
if (!hasHookPattern) failures.push("Missing expected hook return pattern for v2/unsupported versions.");
85+
if (!hasHookEnforcement) failures.push("Tool does not enforce handleSessionVersion(sessionContext) before render.");
86+
87+
let fixtureValid = false;
88+
let sessionContext = null;
89+
if (!fixtureExists) {
90+
failures.push("Missing fixture file.");
91+
} else {
92+
try {
93+
const fixture = readJson(fixturePath);
94+
fixtureValid = true;
95+
sessionContext = fixture.sessionContext;
96+
} catch {
97+
fixtureValid = false;
98+
}
99+
}
100+
if (!fixtureValid) failures.push("Fixture JSON is invalid.");
101+
102+
let validVersionCase = { state: "INVALID", result: { ok: false, error: "fixture missing", code: "UNSUPPORTED_VERSION" } };
103+
let wrongVersionCase = { state: "INVALID", result: { ok: false, error: "fixture missing", code: "UNSUPPORTED_VERSION" } };
104+
let missingVersionCase = { state: "INVALID", result: { ok: false, error: "fixture missing", code: "UNSUPPORTED_VERSION" } };
105+
106+
if (sessionContext) {
107+
validVersionCase = classifyVersionState(sessionContext);
108+
const wrongVersionContext = cloneJson(sessionContext);
109+
wrongVersionContext.version = "v3";
110+
wrongVersionCase = classifyVersionState(wrongVersionContext);
111+
const missingVersionContext = cloneJson(sessionContext);
112+
delete missingVersionContext.version;
113+
missingVersionCase = classifyVersionState(missingVersionContext);
114+
}
115+
116+
if (validVersionCase.state !== "VALID") failures.push(`Expected VALID for version v2, got ${validVersionCase.state}.`);
117+
if (wrongVersionCase.state !== "INVALID") failures.push(`Expected INVALID for version v3, got ${wrongVersionCase.state}.`);
118+
if (missingVersionCase.state !== "INVALID") failures.push(`Expected INVALID for missing version, got ${missingVersionCase.state}.`);
119+
if (wrongVersionCase.result.code !== "UNSUPPORTED_VERSION") failures.push("Wrong-version case did not return code UNSUPPORTED_VERSION.");
120+
if (missingVersionCase.result.code !== "UNSUPPORTED_VERSION") failures.push("Missing-version case did not return code UNSUPPORTED_VERSION.");
121+
if (wrongVersionCase.result.error !== "Unsupported session version") failures.push("Wrong-version case did not return error 'Unsupported session version'.");
122+
if (missingVersionCase.result.error !== "Unsupported session version") failures.push("Missing-version case did not return error 'Unsupported session version'.");
123+
124+
return {
125+
tool: toolId,
126+
fixturePath: path.relative(repoRoot, fixturePath).replace(/\\/g, "/"),
127+
jsPath: path.relative(repoRoot, jsPath).replace(/\\/g, "/"),
128+
jsExists,
129+
fixtureExists,
130+
fixtureValid,
131+
syntaxValid,
132+
syntaxError,
133+
hasHookMethod,
134+
hasHookPattern,
135+
hasHookEnforcement,
136+
cases: {
137+
versionV2: validVersionCase,
138+
versionV3: wrongVersionCase,
139+
missingVersion: missingVersionCase
140+
},
141+
failures
142+
};
143+
}
144+
145+
export function run() {
146+
const rows = TOOLS.map(validateTool);
147+
const failures = rows.flatMap((row) => row.failures.map((entry) => `${row.tool}: ${entry}`));
148+
149+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
150+
fs.writeFileSync(resultsPath, `${JSON.stringify({
151+
generatedAt: new Date().toISOString(),
152+
failures,
153+
rows
154+
}, null, 2)}\n`, "utf8");
155+
156+
console.log(`v2 session migration results: ${resultsPath}`);
157+
assert.equal(failures.length, 0, `V2 session migration failures: ${failures.join(" | ")}`);
158+
return { 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: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ class AssetBrowserV2 {
8282
return urlStateParts.join(", ");
8383
}
8484

85+
handleSessionVersion(payload) {
86+
if (payload && payload.version === "v2") return { ok: true, payload };
87+
return {
88+
ok: false,
89+
error: "Unsupported session version",
90+
code: "UNSUPPORTED_VERSION"
91+
};
92+
}
93+
8594
logStructuredError(type, message, details) {
8695
console.error({
8796
tool: "asset-browser-v2",
@@ -127,19 +136,20 @@ class AssetBrowserV2 {
127136
this.renderError("Session context is invalid. Expected an object containing payloadJson.assetCatalog.");
128137
return;
129138
}
130-
if (sessionContext.version !== "v2") {
131-
this.renderError("Unsupported session version");
139+
const versionCheck = this.handleSessionVersion(sessionContext);
140+
if (!versionCheck.ok) {
141+
this.renderError(versionCheck.error);
132142
return;
133143
}
134-
if (!sessionContext.payloadJson || typeof sessionContext.payloadJson !== "object" || Array.isArray(sessionContext.payloadJson)) {
144+
if (!versionCheck.payload.payloadJson || typeof versionCheck.payload.payloadJson !== "object" || Array.isArray(versionCheck.payload.payloadJson)) {
135145
this.renderError("Asset Browser V2 session data is invalid. Expected payloadJson only.");
136146
return;
137147
}
138-
if (!sessionContext.payloadJson.assetCatalog || typeof sessionContext.payloadJson.assetCatalog !== "object" || Array.isArray(sessionContext.payloadJson.assetCatalog)) {
148+
if (!versionCheck.payload.payloadJson.assetCatalog || typeof versionCheck.payload.payloadJson.assetCatalog !== "object" || Array.isArray(versionCheck.payload.payloadJson.assetCatalog)) {
139149
this.renderError("Asset Browser V2 session data is invalid. Expected payloadJson.assetCatalog.");
140150
return;
141151
}
142-
this.renderCatalog(sessionContext.payloadJson.assetCatalog, sessionContext);
152+
this.renderCatalog(versionCheck.payload.payloadJson.assetCatalog, versionCheck.payload);
143153
}
144154

145155
renderCatalog(assetCatalog, sessionContext) {

tools/palette-manager-v2/index.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ class PaletteManagerV2 {
8282
return urlStateParts.join(", ");
8383
}
8484

85+
handleSessionVersion(payload) {
86+
if (payload && payload.version === "v2") return { ok: true, payload };
87+
return {
88+
ok: false,
89+
error: "Unsupported session version",
90+
code: "UNSUPPORTED_VERSION"
91+
};
92+
}
93+
8594
logStructuredError(type, message, details) {
8695
console.error({
8796
tool: "palette-manager-v2",
@@ -127,15 +136,16 @@ class PaletteManagerV2 {
127136
this.renderError("Session context is invalid. Expected an object containing paletteJson.");
128137
return;
129138
}
130-
if (sessionContext.version !== "v2") {
131-
this.renderError("Unsupported session version");
139+
const versionCheck = this.handleSessionVersion(sessionContext);
140+
if (!versionCheck.ok) {
141+
this.renderError(versionCheck.error);
132142
return;
133143
}
134-
if (!sessionContext.paletteJson || typeof sessionContext.paletteJson !== "object" || Array.isArray(sessionContext.paletteJson)) {
144+
if (!versionCheck.payload.paletteJson || typeof versionCheck.payload.paletteJson !== "object" || Array.isArray(versionCheck.payload.paletteJson)) {
135145
this.renderError("Palette Manager V2 session data is invalid. Expected paletteJson only.");
136146
return;
137147
}
138-
this.renderPalette(sessionContext.paletteJson, sessionContext);
148+
this.renderPalette(versionCheck.payload.paletteJson, versionCheck.payload);
139149
}
140150

141151
renderPalette(paletteJson, sessionContext) {

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ class SvgAssetStudioV2 {
7171
return urlStateParts.join(", ");
7272
}
7373

74+
handleSessionVersion(payload) {
75+
if (payload && payload.version === "v2") return { ok: true, payload };
76+
return {
77+
ok: false,
78+
error: "Unsupported session version",
79+
code: "UNSUPPORTED_VERSION"
80+
};
81+
}
82+
7483
logStructuredError(type, message, details) {
7584
console.error({
7685
tool: "svg-asset-studio-v2",
@@ -116,19 +125,20 @@ class SvgAssetStudioV2 {
116125
this.renderError("Session context is invalid. Expected an object containing payloadJson.vectorAssetDocument.");
117126
return;
118127
}
119-
if (sessionContext.version !== "v2") {
120-
this.renderError("Unsupported session version");
128+
const versionCheck = this.handleSessionVersion(sessionContext);
129+
if (!versionCheck.ok) {
130+
this.renderError(versionCheck.error);
121131
return;
122132
}
123-
if (!sessionContext.payloadJson || typeof sessionContext.payloadJson !== "object" || Array.isArray(sessionContext.payloadJson)) {
133+
if (!versionCheck.payload.payloadJson || typeof versionCheck.payload.payloadJson !== "object" || Array.isArray(versionCheck.payload.payloadJson)) {
124134
this.renderError("SVG Asset Studio V2 session data is invalid. Expected payloadJson only.");
125135
return;
126136
}
127-
if (!sessionContext.payloadJson.vectorAssetDocument || typeof sessionContext.payloadJson.vectorAssetDocument !== "object" || Array.isArray(sessionContext.payloadJson.vectorAssetDocument)) {
137+
if (!versionCheck.payload.payloadJson.vectorAssetDocument || typeof versionCheck.payload.payloadJson.vectorAssetDocument !== "object" || Array.isArray(versionCheck.payload.payloadJson.vectorAssetDocument)) {
128138
this.renderError("SVG Asset Studio V2 session data is invalid. Expected payloadJson.vectorAssetDocument.");
129139
return;
130140
}
131-
this.renderSvg(sessionContext.payloadJson.vectorAssetDocument, sessionContext);
141+
this.renderSvg(versionCheck.payload.payloadJson.vectorAssetDocument, versionCheck.payload);
132142
}
133143

134144
renderSvg(vectorAssetDocument, sessionContext) {

tools/tilemap-studio-v2/index.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ class TilemapStudioV2 {
8282
return urlStateParts.join(", ");
8383
}
8484

85+
handleSessionVersion(payload) {
86+
if (payload && payload.version === "v2") return { ok: true, payload };
87+
return {
88+
ok: false,
89+
error: "Unsupported session version",
90+
code: "UNSUPPORTED_VERSION"
91+
};
92+
}
93+
8594
logStructuredError(type, message, details) {
8695
console.error({
8796
tool: "tilemap-studio-v2",
@@ -127,19 +136,20 @@ class TilemapStudioV2 {
127136
this.renderError("Session context is invalid. Expected an object containing payloadJson.tileMapDocument.");
128137
return;
129138
}
130-
if (sessionContext.version !== "v2") {
131-
this.renderError("Unsupported session version");
139+
const versionCheck = this.handleSessionVersion(sessionContext);
140+
if (!versionCheck.ok) {
141+
this.renderError(versionCheck.error);
132142
return;
133143
}
134-
if (!sessionContext.payloadJson || typeof sessionContext.payloadJson !== "object" || Array.isArray(sessionContext.payloadJson)) {
144+
if (!versionCheck.payload.payloadJson || typeof versionCheck.payload.payloadJson !== "object" || Array.isArray(versionCheck.payload.payloadJson)) {
135145
this.renderError("Tilemap session data is invalid. Expected payloadJson only.");
136146
return;
137147
}
138-
if (!sessionContext.payloadJson.tileMapDocument || typeof sessionContext.payloadJson.tileMapDocument !== "object" || Array.isArray(sessionContext.payloadJson.tileMapDocument)) {
148+
if (!versionCheck.payload.payloadJson.tileMapDocument || typeof versionCheck.payload.payloadJson.tileMapDocument !== "object" || Array.isArray(versionCheck.payload.payloadJson.tileMapDocument)) {
139149
this.renderError("Tilemap session data is invalid. Expected payloadJson.tileMapDocument.");
140150
return;
141151
}
142-
this.renderTilemap(sessionContext.payloadJson.tileMapDocument, sessionContext);
152+
this.renderTilemap(versionCheck.payload.payloadJson.tileMapDocument, versionCheck.payload);
143153
}
144154

145155
renderTilemap(tileMapDocument, sessionContext) {

0 commit comments

Comments
 (0)