miaoda-game-devkit 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,23 +3,185 @@
3
3
 
4
4
  // src/cli/lint.ts
5
5
  var import_node_child_process = require("child_process");
6
- var import_node_fs = require("fs");
6
+ var import_node_fs2 = require("fs");
7
7
  var import_node_module = require("module");
8
8
  var import_node_os = require("os");
9
+ var import_node_path2 = require("path");
10
+
11
+ // src/cli/react-authoritative-playthrough.ts
12
+ var import_node_fs = require("fs");
9
13
  var import_node_path = require("path");
10
- var devkitRoot = (0, import_node_path.resolve)(__dirname, "../..");
14
+ var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
15
+ var REACT_RUNTIME_ENTRY = "miaoda-game-devkit/react";
16
+ var REACT_TESTING_ENTRY = "miaoda-game-devkit/react/testing";
17
+ var CLOCK_IMPORTS = /* @__PURE__ */ new Set(["browserGameClock"]);
18
+ var CONTROLLER_IMPORTS = /* @__PURE__ */ new Set([
19
+ "useGameController",
20
+ "useOwnedGameController"
21
+ ]);
22
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
23
+ function extension(path) {
24
+ const index = path.lastIndexOf(".");
25
+ return index < 0 ? "" : path.slice(index);
26
+ }
27
+ function sourceFiles(root, directory = (0, import_node_path.join)(root, "src")) {
28
+ if (!(0, import_node_fs.existsSync)(directory)) return [];
29
+ const files = [];
30
+ for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
31
+ const path = (0, import_node_path.join)(directory, entry.name);
32
+ const projectPath = (0, import_node_path.relative)(root, path).replaceAll("\\", "/");
33
+ if (entry.isDirectory()) {
34
+ if (projectPath === "src/game/example") continue;
35
+ files.push(...sourceFiles(root, path));
36
+ } else if (entry.isFile() && SOURCE_EXTENSIONS.has(extension(entry.name))) {
37
+ files.push(path);
38
+ }
39
+ }
40
+ return files;
41
+ }
42
+ function withoutComments(source) {
43
+ let output = "";
44
+ let state = "code";
45
+ for (let index = 0; index < source.length; index += 1) {
46
+ const char = source[index];
47
+ const next = source[index + 1];
48
+ if (state === "line") {
49
+ if (char === "\n") {
50
+ state = "code";
51
+ output += char;
52
+ } else {
53
+ output += " ";
54
+ }
55
+ continue;
56
+ }
57
+ if (state === "block") {
58
+ if (char === "*" && next === "/") {
59
+ output += " ";
60
+ index += 1;
61
+ state = "code";
62
+ } else {
63
+ output += char === "\n" ? "\n" : " ";
64
+ }
65
+ continue;
66
+ }
67
+ if (state === "code" && char === "/" && next === "/") {
68
+ output += " ";
69
+ index += 1;
70
+ state = "line";
71
+ continue;
72
+ }
73
+ if (state === "code" && char === "/" && next === "*") {
74
+ output += " ";
75
+ index += 1;
76
+ state = "block";
77
+ continue;
78
+ }
79
+ if (state === "code" && char === "'") state = "single";
80
+ else if (state === "code" && char === '"') state = "double";
81
+ else if (state === "code" && char === "`") state = "template";
82
+ else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
83
+ state = "code";
84
+ } else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
85
+ state = "code";
86
+ } else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
87
+ state = "code";
88
+ }
89
+ output += char;
90
+ }
91
+ return output;
92
+ }
93
+ function codePositions(source) {
94
+ const positions = Array.from({ length: source.length }, () => false);
95
+ let state = "code";
96
+ for (let index = 0; index < source.length; index += 1) {
97
+ const char = source[index];
98
+ if (state === "code") positions[index] = true;
99
+ if (state === "code" && char === "'") state = "single";
100
+ else if (state === "code" && char === '"') state = "double";
101
+ else if (state === "code" && char === "`") state = "template";
102
+ else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
103
+ state = "code";
104
+ } else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
105
+ state = "code";
106
+ } else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
107
+ state = "code";
108
+ }
109
+ }
110
+ return positions;
111
+ }
112
+ function namedImports(source, moduleName) {
113
+ const names = /* @__PURE__ */ new Set();
114
+ const clean = withoutComments(source);
115
+ const positions = codePositions(clean);
116
+ const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
117
+ const pattern = new RegExp(
118
+ `^\\s*import\\s+(?:type\\s+)?\\{([\\s\\S]*?)\\}\\s+from\\s+["']${escapedModule}["']`,
119
+ "gm"
120
+ );
121
+ for (const match of clean.matchAll(pattern)) {
122
+ if (!positions[match.index]) continue;
123
+ for (const specifier of match[1].split(",")) {
124
+ const imported = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
125
+ if (imported) names.add(imported);
126
+ }
127
+ }
128
+ return names;
129
+ }
130
+ function containsAny(values, expected) {
131
+ return [...values].some((value) => expected.has(value));
132
+ }
133
+ function declaresObserve(source) {
134
+ const clean = withoutComments(source);
135
+ const positions = codePositions(clean);
136
+ for (const match of clean.matchAll(/\bobserve\s*:/g)) {
137
+ if (positions[match.index]) return true;
138
+ }
139
+ return false;
140
+ }
141
+ function auditReactAuthoritativePlaythrough(projectRoot2) {
142
+ const clockFiles = [];
143
+ const controllerFiles = [];
144
+ for (const file of sourceFiles(projectRoot2)) {
145
+ const imports = namedImports((0, import_node_fs.readFileSync)(file, "utf8"), REACT_RUNTIME_ENTRY);
146
+ const projectPath = (0, import_node_path.relative)(projectRoot2, file).replaceAll("\\", "/");
147
+ if (containsAny(imports, CLOCK_IMPORTS)) clockFiles.push(projectPath);
148
+ if (containsAny(imports, CONTROLLER_IMPORTS)) controllerFiles.push(projectPath);
149
+ }
150
+ const issues = [];
151
+ if (clockFiles.length === 0 && controllerFiles.length === 0) {
152
+ return { ok: true, issues, clockFiles, controllerFiles };
153
+ }
154
+ const testPath = (0, import_node_path.join)(projectRoot2, PRODUCTION_PLAYTHROUGH);
155
+ const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
156
+ const testingImports = namedImports(testSource, REACT_TESTING_ENTRY);
157
+ const hasObserve = declaresObserve(testSource);
158
+ if (!hasObserve) {
159
+ issues.push(
160
+ `${PRODUCTION_PLAYTHROUGH} must declare observe because production uses the devkit clock or Controller ownership hook. Observe the same production Controller rendered by <App /> through Telemetry; DOM labels are not an authoritative gameplay boundary.`
161
+ );
162
+ }
163
+ if (clockFiles.length > 0 && !testingImports.has("ManualGameClock")) {
164
+ issues.push(
165
+ `${PRODUCTION_PLAYTHROUGH} must import ManualGameClock because production owns gameplay time in ${clockFiles.join(", ")}. Inject it through the production <App /> factory and advance it with a non-empty deterministic step.`
166
+ );
167
+ }
168
+ return { ok: issues.length === 0, issues, clockFiles, controllerFiles };
169
+ }
170
+
171
+ // src/cli/lint.ts
172
+ var devkitRoot = (0, import_node_path2.resolve)(__dirname, "../..");
11
173
  var projectRoot = process.cwd();
12
- var requireFromDevkit = (0, import_node_module.createRequire)((0, import_node_path.join)(devkitRoot, "package.json"));
13
- var projectBiomeConfig = (0, import_node_path.join)(projectRoot, "biome.json");
14
- var biomeConfig = (0, import_node_fs.existsSync)(projectBiomeConfig) ? projectBiomeConfig : (0, import_node_path.join)(devkitRoot, "biome-config.json");
174
+ var requireFromDevkit = (0, import_node_module.createRequire)((0, import_node_path2.join)(devkitRoot, "package.json"));
175
+ var projectBiomeConfig = (0, import_node_path2.join)(projectRoot, "biome.json");
176
+ var biomeConfig = (0, import_node_fs2.existsSync)(projectBiomeConfig) ? projectBiomeConfig : (0, import_node_path2.join)(devkitRoot, "biome-config.json");
15
177
  function resolveBin(packageName, binName) {
16
178
  const packageJsonPath = requireFromDevkit.resolve(`${packageName}/package.json`);
17
- const packageJson = JSON.parse((0, import_node_fs.readFileSync)(packageJsonPath, "utf8"));
179
+ const packageJson = JSON.parse((0, import_node_fs2.readFileSync)(packageJsonPath, "utf8"));
18
180
  const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
19
181
  if (!relativeBin) {
20
182
  throw new Error(`${packageName} does not expose the ${binName} executable`);
21
183
  }
22
- return (0, import_node_path.resolve)((0, import_node_path.dirname)(packageJsonPath), relativeBin);
184
+ return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(packageJsonPath), relativeBin);
23
185
  }
24
186
  function run(name, packageName, binName, args) {
25
187
  return new Promise((resolveResult) => {
@@ -41,21 +203,21 @@ function run(name, packageName, binName, args) {
41
203
  });
42
204
  }
43
205
  function readProjectSource(path, checkName) {
44
- const absolutePath = (0, import_node_path.join)(projectRoot, path);
45
- if (!(0, import_node_fs.existsSync)(absolutePath)) {
206
+ const absolutePath = (0, import_node_path2.join)(projectRoot, path);
207
+ if (!(0, import_node_fs2.existsSync)(absolutePath)) {
46
208
  console.error(`[${checkName}] \u7F3A\u5C11\u6A21\u677F\u5FC5\u9700\u7684 ${path}\u3002\u8BF7\u6062\u590D\u6A21\u677F\u539F\u6587\u4EF6\u540E\u91CD\u65B0\u8FD0\u884C pnpm lint\u3002`);
47
209
  return void 0;
48
210
  }
49
- return (0, import_node_fs.readFileSync)(absolutePath, "utf8");
211
+ return (0, import_node_fs2.readFileSync)(absolutePath, "utf8");
50
212
  }
51
213
  function readProjectManifest() {
52
- const manifestPath = (0, import_node_path.join)(projectRoot, "package.json");
53
- if (!(0, import_node_fs.existsSync)(manifestPath)) {
214
+ const manifestPath = (0, import_node_path2.join)(projectRoot, "package.json");
215
+ if (!(0, import_node_fs2.existsSync)(manifestPath)) {
54
216
  console.error("[phaser-version] \u7F3A\u5C11\u9879\u76EE package.json\u3002");
55
217
  return void 0;
56
218
  }
57
219
  try {
58
- return JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
220
+ return JSON.parse((0, import_node_fs2.readFileSync)(manifestPath, "utf8"));
59
221
  } catch (error) {
60
222
  console.error(`[phaser-version] \u65E0\u6CD5\u89E3\u6790 package.json\uFF1A${String(error)}`);
61
223
  return void 0;
@@ -113,9 +275,15 @@ function checkVitestConfig(target) {
113
275
  }
114
276
  return { name, ok: true };
115
277
  }
278
+ function checkReactAuthoritativePlaythrough() {
279
+ const name = "react-authoritative-playthrough";
280
+ const audit = auditReactAuthoritativePlaythrough(projectRoot);
281
+ for (const issue of audit.issues) console.error(`[${name}] ${issue}`);
282
+ return { name, ok: audit.ok };
283
+ }
116
284
  async function runAllChecks(target) {
117
- const targetChecks = target === "react" ? [checkVitestConfig("react")] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
118
- const lintRoots = target === "react" && (0, import_node_fs.existsSync)((0, import_node_path.join)(projectRoot, "tests")) ? ["src", "tests"] : ["src"];
285
+ const targetChecks = target === "react" ? [checkVitestConfig("react"), checkReactAuthoritativePlaythrough()] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
286
+ const lintRoots = target === "react" && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(projectRoot, "tests")) ? ["src", "tests"] : ["src"];
119
287
  const [tsgo, biome] = await Promise.all([
120
288
  run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
121
289
  run("biome", "@biomejs/biome", "biome", [
@@ -125,17 +293,17 @@ async function runAllChecks(target) {
125
293
  ...lintRoots
126
294
  ])
127
295
  ]);
128
- const tailwindOutput = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
296
+ const tailwindOutput = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
129
297
  const tailwind = await run("tailwind", "tailwindcss", "tailwindcss", [
130
298
  "-i",
131
- (0, import_node_path.join)(projectRoot, "src/index.css"),
299
+ (0, import_node_path2.join)(projectRoot, "src/index.css"),
132
300
  "-o",
133
301
  tailwindOutput
134
302
  ]);
135
- (0, import_node_fs.rmSync)(tailwindOutput, { force: true });
303
+ (0, import_node_fs2.rmSync)(tailwindOutput, { force: true });
136
304
  const oxlint = await run("oxlint", "oxlint", "oxlint", [
137
305
  "-c",
138
- (0, import_node_path.join)(devkitRoot, "oxlint-config.json"),
306
+ (0, import_node_path2.join)(devkitRoot, "oxlint-config.json"),
139
307
  ...lintRoots
140
308
  ]);
141
309
  return [...targetChecks, tsgo, biome, tailwind, oxlint];
@@ -3,23 +3,185 @@
3
3
 
4
4
  // src/cli/lint.ts
5
5
  var import_node_child_process = require("child_process");
6
- var import_node_fs = require("fs");
6
+ var import_node_fs2 = require("fs");
7
7
  var import_node_module = require("module");
8
8
  var import_node_os = require("os");
9
+ var import_node_path2 = require("path");
10
+
11
+ // src/cli/react-authoritative-playthrough.ts
12
+ var import_node_fs = require("fs");
9
13
  var import_node_path = require("path");
10
- var devkitRoot = (0, import_node_path.resolve)(__dirname, "../..");
14
+ var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
15
+ var REACT_RUNTIME_ENTRY = "miaoda-game-devkit/react";
16
+ var REACT_TESTING_ENTRY = "miaoda-game-devkit/react/testing";
17
+ var CLOCK_IMPORTS = /* @__PURE__ */ new Set(["browserGameClock"]);
18
+ var CONTROLLER_IMPORTS = /* @__PURE__ */ new Set([
19
+ "useGameController",
20
+ "useOwnedGameController"
21
+ ]);
22
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
23
+ function extension(path) {
24
+ const index = path.lastIndexOf(".");
25
+ return index < 0 ? "" : path.slice(index);
26
+ }
27
+ function sourceFiles(root, directory = (0, import_node_path.join)(root, "src")) {
28
+ if (!(0, import_node_fs.existsSync)(directory)) return [];
29
+ const files = [];
30
+ for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
31
+ const path = (0, import_node_path.join)(directory, entry.name);
32
+ const projectPath = (0, import_node_path.relative)(root, path).replaceAll("\\", "/");
33
+ if (entry.isDirectory()) {
34
+ if (projectPath === "src/game/example") continue;
35
+ files.push(...sourceFiles(root, path));
36
+ } else if (entry.isFile() && SOURCE_EXTENSIONS.has(extension(entry.name))) {
37
+ files.push(path);
38
+ }
39
+ }
40
+ return files;
41
+ }
42
+ function withoutComments(source) {
43
+ let output = "";
44
+ let state = "code";
45
+ for (let index = 0; index < source.length; index += 1) {
46
+ const char = source[index];
47
+ const next = source[index + 1];
48
+ if (state === "line") {
49
+ if (char === "\n") {
50
+ state = "code";
51
+ output += char;
52
+ } else {
53
+ output += " ";
54
+ }
55
+ continue;
56
+ }
57
+ if (state === "block") {
58
+ if (char === "*" && next === "/") {
59
+ output += " ";
60
+ index += 1;
61
+ state = "code";
62
+ } else {
63
+ output += char === "\n" ? "\n" : " ";
64
+ }
65
+ continue;
66
+ }
67
+ if (state === "code" && char === "/" && next === "/") {
68
+ output += " ";
69
+ index += 1;
70
+ state = "line";
71
+ continue;
72
+ }
73
+ if (state === "code" && char === "/" && next === "*") {
74
+ output += " ";
75
+ index += 1;
76
+ state = "block";
77
+ continue;
78
+ }
79
+ if (state === "code" && char === "'") state = "single";
80
+ else if (state === "code" && char === '"') state = "double";
81
+ else if (state === "code" && char === "`") state = "template";
82
+ else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
83
+ state = "code";
84
+ } else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
85
+ state = "code";
86
+ } else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
87
+ state = "code";
88
+ }
89
+ output += char;
90
+ }
91
+ return output;
92
+ }
93
+ function codePositions(source) {
94
+ const positions = Array.from({ length: source.length }, () => false);
95
+ let state = "code";
96
+ for (let index = 0; index < source.length; index += 1) {
97
+ const char = source[index];
98
+ if (state === "code") positions[index] = true;
99
+ if (state === "code" && char === "'") state = "single";
100
+ else if (state === "code" && char === '"') state = "double";
101
+ else if (state === "code" && char === "`") state = "template";
102
+ else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
103
+ state = "code";
104
+ } else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
105
+ state = "code";
106
+ } else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
107
+ state = "code";
108
+ }
109
+ }
110
+ return positions;
111
+ }
112
+ function namedImports(source, moduleName) {
113
+ const names = /* @__PURE__ */ new Set();
114
+ const clean = withoutComments(source);
115
+ const positions = codePositions(clean);
116
+ const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
117
+ const pattern = new RegExp(
118
+ `^\\s*import\\s+(?:type\\s+)?\\{([\\s\\S]*?)\\}\\s+from\\s+["']${escapedModule}["']`,
119
+ "gm"
120
+ );
121
+ for (const match of clean.matchAll(pattern)) {
122
+ if (!positions[match.index]) continue;
123
+ for (const specifier of match[1].split(",")) {
124
+ const imported = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
125
+ if (imported) names.add(imported);
126
+ }
127
+ }
128
+ return names;
129
+ }
130
+ function containsAny(values, expected) {
131
+ return [...values].some((value) => expected.has(value));
132
+ }
133
+ function declaresObserve(source) {
134
+ const clean = withoutComments(source);
135
+ const positions = codePositions(clean);
136
+ for (const match of clean.matchAll(/\bobserve\s*:/g)) {
137
+ if (positions[match.index]) return true;
138
+ }
139
+ return false;
140
+ }
141
+ function auditReactAuthoritativePlaythrough(projectRoot2) {
142
+ const clockFiles = [];
143
+ const controllerFiles = [];
144
+ for (const file of sourceFiles(projectRoot2)) {
145
+ const imports = namedImports((0, import_node_fs.readFileSync)(file, "utf8"), REACT_RUNTIME_ENTRY);
146
+ const projectPath = (0, import_node_path.relative)(projectRoot2, file).replaceAll("\\", "/");
147
+ if (containsAny(imports, CLOCK_IMPORTS)) clockFiles.push(projectPath);
148
+ if (containsAny(imports, CONTROLLER_IMPORTS)) controllerFiles.push(projectPath);
149
+ }
150
+ const issues = [];
151
+ if (clockFiles.length === 0 && controllerFiles.length === 0) {
152
+ return { ok: true, issues, clockFiles, controllerFiles };
153
+ }
154
+ const testPath = (0, import_node_path.join)(projectRoot2, PRODUCTION_PLAYTHROUGH);
155
+ const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
156
+ const testingImports = namedImports(testSource, REACT_TESTING_ENTRY);
157
+ const hasObserve = declaresObserve(testSource);
158
+ if (!hasObserve) {
159
+ issues.push(
160
+ `${PRODUCTION_PLAYTHROUGH} must declare observe because production uses the devkit clock or Controller ownership hook. Observe the same production Controller rendered by <App /> through Telemetry; DOM labels are not an authoritative gameplay boundary.`
161
+ );
162
+ }
163
+ if (clockFiles.length > 0 && !testingImports.has("ManualGameClock")) {
164
+ issues.push(
165
+ `${PRODUCTION_PLAYTHROUGH} must import ManualGameClock because production owns gameplay time in ${clockFiles.join(", ")}. Inject it through the production <App /> factory and advance it with a non-empty deterministic step.`
166
+ );
167
+ }
168
+ return { ok: issues.length === 0, issues, clockFiles, controllerFiles };
169
+ }
170
+
171
+ // src/cli/lint.ts
172
+ var devkitRoot = (0, import_node_path2.resolve)(__dirname, "../..");
11
173
  var projectRoot = process.cwd();
12
- var requireFromDevkit = (0, import_node_module.createRequire)((0, import_node_path.join)(devkitRoot, "package.json"));
13
- var projectBiomeConfig = (0, import_node_path.join)(projectRoot, "biome.json");
14
- var biomeConfig = (0, import_node_fs.existsSync)(projectBiomeConfig) ? projectBiomeConfig : (0, import_node_path.join)(devkitRoot, "biome-config.json");
174
+ var requireFromDevkit = (0, import_node_module.createRequire)((0, import_node_path2.join)(devkitRoot, "package.json"));
175
+ var projectBiomeConfig = (0, import_node_path2.join)(projectRoot, "biome.json");
176
+ var biomeConfig = (0, import_node_fs2.existsSync)(projectBiomeConfig) ? projectBiomeConfig : (0, import_node_path2.join)(devkitRoot, "biome-config.json");
15
177
  function resolveBin(packageName, binName) {
16
178
  const packageJsonPath = requireFromDevkit.resolve(`${packageName}/package.json`);
17
- const packageJson = JSON.parse((0, import_node_fs.readFileSync)(packageJsonPath, "utf8"));
179
+ const packageJson = JSON.parse((0, import_node_fs2.readFileSync)(packageJsonPath, "utf8"));
18
180
  const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
19
181
  if (!relativeBin) {
20
182
  throw new Error(`${packageName} does not expose the ${binName} executable`);
21
183
  }
22
- return (0, import_node_path.resolve)((0, import_node_path.dirname)(packageJsonPath), relativeBin);
184
+ return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(packageJsonPath), relativeBin);
23
185
  }
24
186
  function run(name, packageName, binName, args) {
25
187
  return new Promise((resolveResult) => {
@@ -41,21 +203,21 @@ function run(name, packageName, binName, args) {
41
203
  });
42
204
  }
43
205
  function readProjectSource(path, checkName) {
44
- const absolutePath = (0, import_node_path.join)(projectRoot, path);
45
- if (!(0, import_node_fs.existsSync)(absolutePath)) {
206
+ const absolutePath = (0, import_node_path2.join)(projectRoot, path);
207
+ if (!(0, import_node_fs2.existsSync)(absolutePath)) {
46
208
  console.error(`[${checkName}] \u7F3A\u5C11\u6A21\u677F\u5FC5\u9700\u7684 ${path}\u3002\u8BF7\u6062\u590D\u6A21\u677F\u539F\u6587\u4EF6\u540E\u91CD\u65B0\u8FD0\u884C pnpm lint\u3002`);
47
209
  return void 0;
48
210
  }
49
- return (0, import_node_fs.readFileSync)(absolutePath, "utf8");
211
+ return (0, import_node_fs2.readFileSync)(absolutePath, "utf8");
50
212
  }
51
213
  function readProjectManifest() {
52
- const manifestPath = (0, import_node_path.join)(projectRoot, "package.json");
53
- if (!(0, import_node_fs.existsSync)(manifestPath)) {
214
+ const manifestPath = (0, import_node_path2.join)(projectRoot, "package.json");
215
+ if (!(0, import_node_fs2.existsSync)(manifestPath)) {
54
216
  console.error("[phaser-version] \u7F3A\u5C11\u9879\u76EE package.json\u3002");
55
217
  return void 0;
56
218
  }
57
219
  try {
58
- return JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
220
+ return JSON.parse((0, import_node_fs2.readFileSync)(manifestPath, "utf8"));
59
221
  } catch (error) {
60
222
  console.error(`[phaser-version] \u65E0\u6CD5\u89E3\u6790 package.json\uFF1A${String(error)}`);
61
223
  return void 0;
@@ -113,9 +275,15 @@ function checkVitestConfig(target) {
113
275
  }
114
276
  return { name, ok: true };
115
277
  }
278
+ function checkReactAuthoritativePlaythrough() {
279
+ const name = "react-authoritative-playthrough";
280
+ const audit = auditReactAuthoritativePlaythrough(projectRoot);
281
+ for (const issue of audit.issues) console.error(`[${name}] ${issue}`);
282
+ return { name, ok: audit.ok };
283
+ }
116
284
  async function runAllChecks(target) {
117
- const targetChecks = target === "react" ? [checkVitestConfig("react")] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
118
- const lintRoots = target === "react" && (0, import_node_fs.existsSync)((0, import_node_path.join)(projectRoot, "tests")) ? ["src", "tests"] : ["src"];
285
+ const targetChecks = target === "react" ? [checkVitestConfig("react"), checkReactAuthoritativePlaythrough()] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
286
+ const lintRoots = target === "react" && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(projectRoot, "tests")) ? ["src", "tests"] : ["src"];
119
287
  const [tsgo, biome] = await Promise.all([
120
288
  run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
121
289
  run("biome", "@biomejs/biome", "biome", [
@@ -125,17 +293,17 @@ async function runAllChecks(target) {
125
293
  ...lintRoots
126
294
  ])
127
295
  ]);
128
- const tailwindOutput = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
296
+ const tailwindOutput = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
129
297
  const tailwind = await run("tailwind", "tailwindcss", "tailwindcss", [
130
298
  "-i",
131
- (0, import_node_path.join)(projectRoot, "src/index.css"),
299
+ (0, import_node_path2.join)(projectRoot, "src/index.css"),
132
300
  "-o",
133
301
  tailwindOutput
134
302
  ]);
135
- (0, import_node_fs.rmSync)(tailwindOutput, { force: true });
303
+ (0, import_node_fs2.rmSync)(tailwindOutput, { force: true });
136
304
  const oxlint = await run("oxlint", "oxlint", "oxlint", [
137
305
  "-c",
138
- (0, import_node_path.join)(devkitRoot, "oxlint-config.json"),
306
+ (0, import_node_path2.join)(devkitRoot, "oxlint-config.json"),
139
307
  ...lintRoots
140
308
  ]);
141
309
  return [...targetChecks, tsgo, biome, tailwind, oxlint];
@@ -54,6 +54,15 @@ function useOwnedGameController(createController) {
54
54
 
55
55
  // src/react/use-game-controller.ts
56
56
  var import_react2 = require("react");
57
+
58
+ // src/react/react-error-diagnostics.ts
59
+ function codedError(code, message) {
60
+ const error = new Error(message);
61
+ error.code = code;
62
+ return error;
63
+ }
64
+
65
+ // src/react/use-game-controller.ts
57
66
  function useGameController(createController) {
58
67
  const game = useOwnedGameController(createController);
59
68
  useSnapshotIntegrityCheck(game);
@@ -91,7 +100,8 @@ function useSnapshotIntegrityCheck(game) {
91
100
  }
92
101
  if (previousJson !== void 0 && nextJson !== previousJson) {
93
102
  reported.current = true;
94
- throw new Error(
103
+ throw codedError(
104
+ "GAME_SNAPSHOT_REFERENCE_REUSED",
95
105
  "Game state changed while snapshot() returned the same reference. React compares snapshots by reference and skips the render when Object.is(previous, next) is true, so the UI freezes. Publish a new top-level snapshot before each notification, for example cachedSnapshot = { ...state }, and never expose a mutable internal object as the snapshot."
96
106
  );
97
107
  }
@@ -26,6 +26,15 @@ function useOwnedGameController(createController) {
26
26
 
27
27
  // src/react/use-game-controller.ts
28
28
  import { useEffect as useEffect2, useRef as useRef2, useSyncExternalStore } from "react";
29
+
30
+ // src/react/react-error-diagnostics.ts
31
+ function codedError(code, message) {
32
+ const error = new Error(message);
33
+ error.code = code;
34
+ return error;
35
+ }
36
+
37
+ // src/react/use-game-controller.ts
29
38
  function useGameController(createController) {
30
39
  const game = useOwnedGameController(createController);
31
40
  useSnapshotIntegrityCheck(game);
@@ -63,7 +72,8 @@ function useSnapshotIntegrityCheck(game) {
63
72
  }
64
73
  if (previousJson !== void 0 && nextJson !== previousJson) {
65
74
  reported.current = true;
66
- throw new Error(
75
+ throw codedError(
76
+ "GAME_SNAPSHOT_REFERENCE_REUSED",
67
77
  "Game state changed while snapshot() returned the same reference. React compares snapshots by reference and skips the render when Object.is(previous, next) is true, so the UI freezes. Publish a new top-level snapshot before each notification, for example cachedSnapshot = { ...state }, and never expose a mutable internal object as the snapshot."
68
78
  );
69
79
  }
@@ -22,6 +22,15 @@ declare class ManualGameClock implements GameClock {
22
22
  pendingTimerCount(): number;
23
23
  }
24
24
 
25
+ interface ReactFailureEntry {
26
+ code: string;
27
+ message: string;
28
+ }
29
+ interface ReactFailureDiagnostic {
30
+ source: "playthrough" | "test-runtime";
31
+ entries: ReactFailureEntry[];
32
+ }
33
+
25
34
  /** 有界推进的通用配置,不绑定任何特定 Core、phase 或时钟实现。 */
26
35
  interface StepUntilOptions {
27
36
  /** 条件仍不成立时允许执行的最大生产步数,默认 120。 */
@@ -57,13 +66,21 @@ interface ReactPlaythroughEvidence {
57
66
  }
58
67
  /** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
59
68
  interface ReactPlaythroughMetadata {
60
- version: 4;
69
+ version: 5;
61
70
  waiverReason?: string;
62
71
  trace?: string;
72
+ failure?: ReactFailureDiagnostic;
63
73
  evidence: ReactPlaythroughEvidence;
64
74
  }
65
75
  interface ReactPlaythroughOptions {
66
- /** Canvas/Controller flows expose one stable authoritative production read. */
76
+ /**
77
+ * Read authoritative production gameplay state when the DOM cannot prove the
78
+ * result, such as Canvas, Controller, time, or physics flows. The harness
79
+ * samples this state before and after each stage, not after every step.
80
+ * Individual steps may leave it unchanged, but the completed stage must
81
+ * produce a new gameplay result. Observation supplements real production
82
+ * input; it does not replace it.
83
+ */
67
84
  observe: () => unknown;
68
85
  }
69
86
  interface ReactPlaythroughAssertContext {
@@ -22,6 +22,15 @@ declare class ManualGameClock implements GameClock {
22
22
  pendingTimerCount(): number;
23
23
  }
24
24
 
25
+ interface ReactFailureEntry {
26
+ code: string;
27
+ message: string;
28
+ }
29
+ interface ReactFailureDiagnostic {
30
+ source: "playthrough" | "test-runtime";
31
+ entries: ReactFailureEntry[];
32
+ }
33
+
25
34
  /** 有界推进的通用配置,不绑定任何特定 Core、phase 或时钟实现。 */
26
35
  interface StepUntilOptions {
27
36
  /** 条件仍不成立时允许执行的最大生产步数,默认 120。 */
@@ -57,13 +66,21 @@ interface ReactPlaythroughEvidence {
57
66
  }
58
67
  /** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
59
68
  interface ReactPlaythroughMetadata {
60
- version: 4;
69
+ version: 5;
61
70
  waiverReason?: string;
62
71
  trace?: string;
72
+ failure?: ReactFailureDiagnostic;
63
73
  evidence: ReactPlaythroughEvidence;
64
74
  }
65
75
  interface ReactPlaythroughOptions {
66
- /** Canvas/Controller flows expose one stable authoritative production read. */
76
+ /**
77
+ * Read authoritative production gameplay state when the DOM cannot prove the
78
+ * result, such as Canvas, Controller, time, or physics flows. The harness
79
+ * samples this state before and after each stage, not after every step.
80
+ * Individual steps may leave it unchanged, but the completed stage must
81
+ * produce a new gameplay result. Observation supplements real production
82
+ * input; it does not replace it.
83
+ */
67
84
  observe: () => unknown;
68
85
  }
69
86
  interface ReactPlaythroughAssertContext {