miaoda-game-devkit 0.6.4 → 0.6.6

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.
@@ -222,12 +222,12 @@ function auditReactAuthoritativePlaythrough(projectRoot2) {
222
222
  const hasObserve = declaresObserve(testSource);
223
223
  if (!hasObserve) {
224
224
  issues.push(
225
- `${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.`
225
+ `${PRODUCTION_PLAYTHROUGH} must declare observe because production uses the devkit clock or Controller ownership hook. Pass { observe: () => telemetry.read.session() } to playthroughTest, using Telemetry backed by the same production Controller rendered by <App />; DOM labels are not an authoritative gameplay boundary.`
226
226
  );
227
227
  }
228
228
  if (clockFiles.length > 0 && !testingImports.has("ManualGameClock")) {
229
229
  issues.push(
230
- `${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.`
230
+ `${PRODUCTION_PLAYTHROUGH} must import ManualGameClock because production owns gameplay time in ${clockFiles.join(", ")}. Add this import to the test: import { ManualGameClock } from "miaoda-game-devkit/react/testing"; Then create one, inject it through the production <App /> factory, and advance it with step: () => clock.stepFrame().`
231
231
  );
232
232
  }
233
233
  return {
@@ -222,12 +222,12 @@ function auditReactAuthoritativePlaythrough(projectRoot2) {
222
222
  const hasObserve = declaresObserve(testSource);
223
223
  if (!hasObserve) {
224
224
  issues.push(
225
- `${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.`
225
+ `${PRODUCTION_PLAYTHROUGH} must declare observe because production uses the devkit clock or Controller ownership hook. Pass { observe: () => telemetry.read.session() } to playthroughTest, using Telemetry backed by the same production Controller rendered by <App />; DOM labels are not an authoritative gameplay boundary.`
226
226
  );
227
227
  }
228
228
  if (clockFiles.length > 0 && !testingImports.has("ManualGameClock")) {
229
229
  issues.push(
230
- `${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.`
230
+ `${PRODUCTION_PLAYTHROUGH} must import ManualGameClock because production owns gameplay time in ${clockFiles.join(", ")}. Add this import to the test: import { ManualGameClock } from "miaoda-game-devkit/react/testing"; Then create one, inject it through the production <App /> factory, and advance it with step: () => clock.stepFrame().`
231
231
  );
232
232
  }
233
233
  return {
@@ -25,7 +25,18 @@ declare class ManualGameClock implements GameClock {
25
25
  interface ReactFailureEntry {
26
26
  code: string;
27
27
  message: string;
28
+ errorName?: string;
29
+ actual?: string;
30
+ expected?: string;
31
+ origin?: ReactFailureOrigin;
32
+ layer?: ReactFailureLayer;
28
33
  }
34
+ interface ReactFailureOrigin {
35
+ file: string;
36
+ line: number;
37
+ column: number;
38
+ }
39
+ type ReactFailureLayer = "product" | "test" | "harness" | "dependency" | "unknown";
29
40
  interface ReactFailureDiagnostic {
30
41
  source: "playthrough" | "test-runtime";
31
42
  entries: ReactFailureEntry[];
@@ -126,6 +137,7 @@ interface ReactPlaythroughTest {
126
137
  interface ReactPlaythroughAuditInput {
127
138
  name: string;
128
139
  state: "passed" | "failed" | "skipped" | "pending";
140
+ mode?: "run" | "only" | "skip" | "todo";
129
141
  metadata?: ReactPlaythroughMetadata;
130
142
  }
131
143
  interface ReactPlaythroughAuditResult {
@@ -25,7 +25,18 @@ declare class ManualGameClock implements GameClock {
25
25
  interface ReactFailureEntry {
26
26
  code: string;
27
27
  message: string;
28
+ errorName?: string;
29
+ actual?: string;
30
+ expected?: string;
31
+ origin?: ReactFailureOrigin;
32
+ layer?: ReactFailureLayer;
28
33
  }
34
+ interface ReactFailureOrigin {
35
+ file: string;
36
+ line: number;
37
+ column: number;
38
+ }
39
+ type ReactFailureLayer = "product" | "test" | "harness" | "dependency" | "unknown";
29
40
  interface ReactFailureDiagnostic {
30
41
  source: "playthrough" | "test-runtime";
31
42
  entries: ReactFailureEntry[];
@@ -126,6 +137,7 @@ interface ReactPlaythroughTest {
126
137
  interface ReactPlaythroughAuditInput {
127
138
  name: string;
128
139
  state: "passed" | "failed" | "skipped" | "pending";
140
+ mode?: "run" | "only" | "skip" | "todo";
129
141
  metadata?: ReactPlaythroughMetadata;
130
142
  }
131
143
  interface ReactPlaythroughAuditResult {
@@ -122,47 +122,145 @@ function safeJson(value) {
122
122
  return void 0;
123
123
  }
124
124
  }
125
- function collectEntries(value, fallbackCode, seen) {
125
+ function diagnosticValue(value) {
126
+ if (value === void 0) return void 0;
126
127
  if (typeof value === "string") {
127
- return value.trim() ? [{ code: fallbackCode, message: truncate(value) }] : [];
128
+ return truncate(value.replace(/\s+/g, " ").trim());
129
+ }
130
+ if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
131
+ return String(value);
132
+ }
133
+ if (typeof value === "symbol") return value.toString();
134
+ if (typeof value === "function") {
135
+ return `Function<${value.name || "anonymous"}>`;
136
+ }
137
+ const json = safeJson(value);
138
+ return truncate(json ?? String(value));
139
+ }
140
+ function normalizeFile(value) {
141
+ return value.replaceAll("\\", "/").replace(/^file:\/\//, "");
142
+ }
143
+ function failureLayer(file) {
144
+ const normalized = normalizeFile(file);
145
+ if (normalized.includes("/miaoda-game-devkit/") || normalized.includes("/packages/game-devkit/")) {
146
+ return "harness";
147
+ }
148
+ if (normalized.includes("/node_modules/")) return "dependency";
149
+ if (/(?:^|\/)tests?\//.test(normalized) || /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(normalized)) {
150
+ return "test";
151
+ }
152
+ if (/(?:^|\/)src\//.test(normalized)) return "product";
153
+ return "unknown";
154
+ }
155
+ function parsedOrigin(value) {
156
+ if (!value || typeof value !== "object") return void 0;
157
+ const frame = value;
158
+ if (typeof frame.file !== "string" || typeof frame.line !== "number" || typeof frame.column !== "number") {
159
+ return void 0;
160
+ }
161
+ return {
162
+ file: normalizeFile(frame.file),
163
+ line: frame.line,
164
+ column: frame.column
165
+ };
166
+ }
167
+ function stackOrigins(record) {
168
+ if (Array.isArray(record.stacks)) {
169
+ const parsed = record.stacks.map(parsedOrigin).filter((origin) => Boolean(origin));
170
+ if (parsed.length > 0) return parsed;
171
+ }
172
+ if (typeof record.stack !== "string") return [];
173
+ const origins = [];
174
+ const pattern = /(?:at\s+.*?\()?((?:file:\/\/)?[^()\s]+):(\d+):(\d+)\)?/g;
175
+ for (const match of record.stack.matchAll(pattern)) {
176
+ origins.push({
177
+ file: normalizeFile(match[1]),
178
+ line: Number(match[2]),
179
+ column: Number(match[3])
180
+ });
181
+ }
182
+ return origins;
183
+ }
184
+ function selectOrigin(record) {
185
+ const origins = stackOrigins(record);
186
+ return origins.find((origin) => {
187
+ const layer = failureLayer(origin.file);
188
+ return layer === "product" || layer === "test";
189
+ }) ?? origins.find((origin) => failureLayer(origin.file) !== "dependency");
190
+ }
191
+ function fallbackCode(record, origin, defaultCode) {
192
+ if (typeof record.code === "string" && record.code.trim()) {
193
+ return record.code.trim();
194
+ }
195
+ if (record.name === "AssertionError" && record.actual !== void 0 && record.expected !== void 0) {
196
+ return "EXPECTATION_MISMATCH";
197
+ }
198
+ if (record.name === "TypeError" && origin) {
199
+ const layer = failureLayer(origin.file);
200
+ if (layer === "product") return "PRODUCT_RUNTIME_TYPE_ERROR";
201
+ if (layer === "test") return "TEST_API_MISMATCH";
202
+ }
203
+ return defaultCode;
204
+ }
205
+ function structuredEntry(record, message, defaultCode) {
206
+ const origin = selectOrigin(record);
207
+ return {
208
+ code: fallbackCode(record, origin, defaultCode),
209
+ message: truncate(message),
210
+ errorName: typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0,
211
+ actual: diagnosticValue(record.actual),
212
+ expected: diagnosticValue(record.expected),
213
+ origin,
214
+ layer: origin ? failureLayer(origin.file) : void 0
215
+ };
216
+ }
217
+ function collectEntries(value, fallbackCode2, seen) {
218
+ if (typeof value === "string") {
219
+ return value.trim() ? [{ code: fallbackCode2, message: truncate(value) }] : [];
128
220
  }
129
221
  if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") {
130
- return [{ code: fallbackCode, message: String(value) }];
222
+ return [{ code: fallbackCode2, message: String(value) }];
131
223
  }
132
224
  if (typeof value === "function") {
133
225
  return [
134
- { code: fallbackCode, message: `Function<${value.name || "anonymous"}>` }
226
+ { code: fallbackCode2, message: `Function<${value.name || "anonymous"}>` }
135
227
  ];
136
228
  }
137
229
  if (seen.has(value)) return [];
138
230
  seen.add(value);
139
231
  if (Array.isArray(value)) {
140
- return value.flatMap((item) => collectEntries(item, fallbackCode, seen));
232
+ return value.flatMap((item) => collectEntries(item, fallbackCode2, seen));
141
233
  }
142
234
  const record = value;
143
- const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : fallbackCode;
144
235
  const entries = [];
145
236
  if (typeof record.message === "string" && record.message.trim()) {
146
- entries.push({ code, message: truncate(record.message) });
237
+ entries.push(structuredEntry(record, record.message, fallbackCode2));
147
238
  }
148
239
  if (record.cause !== void 0) {
149
- entries.push(...collectEntries(record.cause, fallbackCode, seen));
240
+ entries.push(...collectEntries(record.cause, fallbackCode2, seen));
150
241
  }
151
242
  if (Array.isArray(record.errors)) {
152
- entries.push(...collectEntries(record.errors, fallbackCode, seen));
243
+ entries.push(...collectEntries(record.errors, fallbackCode2, seen));
153
244
  }
154
245
  if (entries.length > 0) return entries;
155
246
  if (typeof record.stack === "string" && record.stack.trim()) {
156
- return [{ code, message: truncate(record.stack) }];
247
+ return [structuredEntry(record, record.stack, fallbackCode2)];
157
248
  }
158
249
  const json = safeJson(value);
159
- return json && json !== "{}" ? [{ code, message: truncate(json) }] : [];
250
+ return json && json !== "{}" ? [structuredEntry(record, json, fallbackCode2)] : [];
160
251
  }
161
- function extractFailureEntries(value, fallbackCode = "TEST_FAILURE") {
162
- const entries = collectEntries(value, fallbackCode, /* @__PURE__ */ new WeakSet());
252
+ function extractFailureEntries(value, fallbackCode2 = "TEST_FAILURE") {
253
+ const entries = collectEntries(value, fallbackCode2, /* @__PURE__ */ new WeakSet());
163
254
  const keys = /* @__PURE__ */ new Set();
164
255
  return entries.filter((entry) => {
165
- const key = `${entry.code}\0${entry.message}`;
256
+ const key = [
257
+ entry.code,
258
+ entry.message,
259
+ entry.errorName,
260
+ entry.actual,
261
+ entry.expected,
262
+ entry.origin ? `${entry.origin.file}:${entry.origin.line}:${entry.origin.column}` : void 0
263
+ ].join("\0");
166
264
  if (keys.has(key)) return false;
167
265
  keys.add(key);
168
266
  return true;
@@ -84,47 +84,145 @@ function safeJson(value) {
84
84
  return void 0;
85
85
  }
86
86
  }
87
- function collectEntries(value, fallbackCode, seen) {
87
+ function diagnosticValue(value) {
88
+ if (value === void 0) return void 0;
88
89
  if (typeof value === "string") {
89
- return value.trim() ? [{ code: fallbackCode, message: truncate(value) }] : [];
90
+ return truncate(value.replace(/\s+/g, " ").trim());
91
+ }
92
+ if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
93
+ return String(value);
94
+ }
95
+ if (typeof value === "symbol") return value.toString();
96
+ if (typeof value === "function") {
97
+ return `Function<${value.name || "anonymous"}>`;
98
+ }
99
+ const json = safeJson(value);
100
+ return truncate(json ?? String(value));
101
+ }
102
+ function normalizeFile(value) {
103
+ return value.replaceAll("\\", "/").replace(/^file:\/\//, "");
104
+ }
105
+ function failureLayer(file) {
106
+ const normalized = normalizeFile(file);
107
+ if (normalized.includes("/miaoda-game-devkit/") || normalized.includes("/packages/game-devkit/")) {
108
+ return "harness";
109
+ }
110
+ if (normalized.includes("/node_modules/")) return "dependency";
111
+ if (/(?:^|\/)tests?\//.test(normalized) || /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(normalized)) {
112
+ return "test";
113
+ }
114
+ if (/(?:^|\/)src\//.test(normalized)) return "product";
115
+ return "unknown";
116
+ }
117
+ function parsedOrigin(value) {
118
+ if (!value || typeof value !== "object") return void 0;
119
+ const frame = value;
120
+ if (typeof frame.file !== "string" || typeof frame.line !== "number" || typeof frame.column !== "number") {
121
+ return void 0;
122
+ }
123
+ return {
124
+ file: normalizeFile(frame.file),
125
+ line: frame.line,
126
+ column: frame.column
127
+ };
128
+ }
129
+ function stackOrigins(record) {
130
+ if (Array.isArray(record.stacks)) {
131
+ const parsed = record.stacks.map(parsedOrigin).filter((origin) => Boolean(origin));
132
+ if (parsed.length > 0) return parsed;
133
+ }
134
+ if (typeof record.stack !== "string") return [];
135
+ const origins = [];
136
+ const pattern = /(?:at\s+.*?\()?((?:file:\/\/)?[^()\s]+):(\d+):(\d+)\)?/g;
137
+ for (const match of record.stack.matchAll(pattern)) {
138
+ origins.push({
139
+ file: normalizeFile(match[1]),
140
+ line: Number(match[2]),
141
+ column: Number(match[3])
142
+ });
143
+ }
144
+ return origins;
145
+ }
146
+ function selectOrigin(record) {
147
+ const origins = stackOrigins(record);
148
+ return origins.find((origin) => {
149
+ const layer = failureLayer(origin.file);
150
+ return layer === "product" || layer === "test";
151
+ }) ?? origins.find((origin) => failureLayer(origin.file) !== "dependency");
152
+ }
153
+ function fallbackCode(record, origin, defaultCode) {
154
+ if (typeof record.code === "string" && record.code.trim()) {
155
+ return record.code.trim();
156
+ }
157
+ if (record.name === "AssertionError" && record.actual !== void 0 && record.expected !== void 0) {
158
+ return "EXPECTATION_MISMATCH";
159
+ }
160
+ if (record.name === "TypeError" && origin) {
161
+ const layer = failureLayer(origin.file);
162
+ if (layer === "product") return "PRODUCT_RUNTIME_TYPE_ERROR";
163
+ if (layer === "test") return "TEST_API_MISMATCH";
164
+ }
165
+ return defaultCode;
166
+ }
167
+ function structuredEntry(record, message, defaultCode) {
168
+ const origin = selectOrigin(record);
169
+ return {
170
+ code: fallbackCode(record, origin, defaultCode),
171
+ message: truncate(message),
172
+ errorName: typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0,
173
+ actual: diagnosticValue(record.actual),
174
+ expected: diagnosticValue(record.expected),
175
+ origin,
176
+ layer: origin ? failureLayer(origin.file) : void 0
177
+ };
178
+ }
179
+ function collectEntries(value, fallbackCode2, seen) {
180
+ if (typeof value === "string") {
181
+ return value.trim() ? [{ code: fallbackCode2, message: truncate(value) }] : [];
90
182
  }
91
183
  if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") {
92
- return [{ code: fallbackCode, message: String(value) }];
184
+ return [{ code: fallbackCode2, message: String(value) }];
93
185
  }
94
186
  if (typeof value === "function") {
95
187
  return [
96
- { code: fallbackCode, message: `Function<${value.name || "anonymous"}>` }
188
+ { code: fallbackCode2, message: `Function<${value.name || "anonymous"}>` }
97
189
  ];
98
190
  }
99
191
  if (seen.has(value)) return [];
100
192
  seen.add(value);
101
193
  if (Array.isArray(value)) {
102
- return value.flatMap((item) => collectEntries(item, fallbackCode, seen));
194
+ return value.flatMap((item) => collectEntries(item, fallbackCode2, seen));
103
195
  }
104
196
  const record = value;
105
- const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : fallbackCode;
106
197
  const entries = [];
107
198
  if (typeof record.message === "string" && record.message.trim()) {
108
- entries.push({ code, message: truncate(record.message) });
199
+ entries.push(structuredEntry(record, record.message, fallbackCode2));
109
200
  }
110
201
  if (record.cause !== void 0) {
111
- entries.push(...collectEntries(record.cause, fallbackCode, seen));
202
+ entries.push(...collectEntries(record.cause, fallbackCode2, seen));
112
203
  }
113
204
  if (Array.isArray(record.errors)) {
114
- entries.push(...collectEntries(record.errors, fallbackCode, seen));
205
+ entries.push(...collectEntries(record.errors, fallbackCode2, seen));
115
206
  }
116
207
  if (entries.length > 0) return entries;
117
208
  if (typeof record.stack === "string" && record.stack.trim()) {
118
- return [{ code, message: truncate(record.stack) }];
209
+ return [structuredEntry(record, record.stack, fallbackCode2)];
119
210
  }
120
211
  const json = safeJson(value);
121
- return json && json !== "{}" ? [{ code, message: truncate(json) }] : [];
212
+ return json && json !== "{}" ? [structuredEntry(record, json, fallbackCode2)] : [];
122
213
  }
123
- function extractFailureEntries(value, fallbackCode = "TEST_FAILURE") {
124
- const entries = collectEntries(value, fallbackCode, /* @__PURE__ */ new WeakSet());
214
+ function extractFailureEntries(value, fallbackCode2 = "TEST_FAILURE") {
215
+ const entries = collectEntries(value, fallbackCode2, /* @__PURE__ */ new WeakSet());
125
216
  const keys = /* @__PURE__ */ new Set();
126
217
  return entries.filter((entry) => {
127
- const key = `${entry.code}\0${entry.message}`;
218
+ const key = [
219
+ entry.code,
220
+ entry.message,
221
+ entry.errorName,
222
+ entry.actual,
223
+ entry.expected,
224
+ entry.origin ? `${entry.origin.file}:${entry.origin.line}:${entry.origin.column}` : void 0
225
+ ].join("\0");
128
226
  if (keys.has(key)) return false;
129
227
  keys.add(key);
130
228
  return true;