miaoda-game-devkit 0.10.1 → 0.10.2

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.
@@ -50,7 +50,10 @@ interface StepUntilOptions {
50
50
  step?: (step: number) => void | Promise<void>;
51
51
  /**
52
52
  * Optional read-only state used to make bounded failures actionable.
53
- * The value is sampled only when the bound is exhausted.
53
+ *
54
+ * The value is sampled when the bound is exhausted, and additionally at a fixed
55
+ * step interval so an exhausted bound can distinguish a run that is still
56
+ * progressing from one whose state already settled.
54
57
  */
55
58
  diagnostics?: () => unknown;
56
59
  /** 测试超时或运行取消时停止继续推进,通常由 playthroughTest 自动传入。 */
@@ -50,7 +50,10 @@ interface StepUntilOptions {
50
50
  step?: (step: number) => void | Promise<void>;
51
51
  /**
52
52
  * Optional read-only state used to make bounded failures actionable.
53
- * The value is sampled only when the bound is exhausted.
53
+ *
54
+ * The value is sampled when the bound is exhausted, and additionally at a fixed
55
+ * step interval so an exhausted bound can distinguish a run that is still
56
+ * progressing from one whose state already settled.
54
57
  */
55
58
  diagnostics?: () => unknown;
56
59
  /** 测试超时或运行取消时停止继续推进,通常由 playthroughTest 自动传入。 */
@@ -311,6 +311,39 @@ function formatDiagnostics(read) {
311
311
  return `diagnostics() threw: ${String(error)}`;
312
312
  }
313
313
  }
314
+ var STAGNATION_SAMPLE_INTERVAL = 32;
315
+ var STAGNATION_SAMPLE_MINIMUM = 3;
316
+ var StagnationProbe = class {
317
+ constructor(read) {
318
+ this.read = read;
319
+ this.disabled = !read;
320
+ }
321
+ read;
322
+ fingerprint;
323
+ unchangedSamples = 0;
324
+ disabled = false;
325
+ settledAtStep;
326
+ sample(step) {
327
+ if (this.disabled || step % STAGNATION_SAMPLE_INTERVAL !== 0) return;
328
+ let next;
329
+ try {
330
+ next = JSON.stringify(this.read?.()) ?? "undefined";
331
+ } catch {
332
+ this.disabled = true;
333
+ return;
334
+ }
335
+ if (this.fingerprint === next) {
336
+ this.unchangedSamples += 1;
337
+ return;
338
+ }
339
+ this.fingerprint = next;
340
+ this.unchangedSamples = 1;
341
+ this.settledAtStep = step;
342
+ }
343
+ get settled() {
344
+ return !this.disabled && this.unchangedSamples >= STAGNATION_SAMPLE_MINIMUM;
345
+ }
346
+ };
314
347
  function normalizePlaythroughWaiverReason(waiverReason) {
315
348
  if (waiverReason === void 0) return void 0;
316
349
  const reason = waiverReason.trim();
@@ -330,6 +363,7 @@ async function runBoundedUntil(condition, options = {}) {
330
363
  "stepUntil maxSteps must be a safe integer between 0 and 10000."
331
364
  );
332
365
  }
366
+ const probe = new StagnationProbe(options.diagnostics);
333
367
  for (let step = 0; step <= maxSteps; step += 1) {
334
368
  throwIfAborted(options.signal);
335
369
  if (condition()) return step;
@@ -338,11 +372,18 @@ async function runBoundedUntil(condition, options = {}) {
338
372
  await options.step?.(step + 1);
339
373
  });
340
374
  throwIfAborted(options.signal);
375
+ probe.sample(step + 1);
341
376
  }
342
377
  }
343
378
  const diagnostics = formatDiagnostics(options.diagnostics);
344
- const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
345
379
  const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
380
+ if (probe.settled) {
381
+ throw codedError(
382
+ "PLAYTHROUGH_STATE_STAGNANT",
383
+ `Playthrough state stopped changing at step ${probe.settledAtStep} of ${maxSteps} and stayed identical until the bound. The run is already settled, so raising maxSteps cannot help: compare the settled state against the outcome this stage asserts. A settled state usually means the game reached a terminal result other than the asserted one, or that the stage never advances the production Controller at all.${suffix}`
384
+ );
385
+ }
386
+ const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
346
387
  throw codedError(
347
388
  options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
348
389
  `Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
@@ -273,6 +273,39 @@ function formatDiagnostics(read) {
273
273
  return `diagnostics() threw: ${String(error)}`;
274
274
  }
275
275
  }
276
+ var STAGNATION_SAMPLE_INTERVAL = 32;
277
+ var STAGNATION_SAMPLE_MINIMUM = 3;
278
+ var StagnationProbe = class {
279
+ constructor(read) {
280
+ this.read = read;
281
+ this.disabled = !read;
282
+ }
283
+ read;
284
+ fingerprint;
285
+ unchangedSamples = 0;
286
+ disabled = false;
287
+ settledAtStep;
288
+ sample(step) {
289
+ if (this.disabled || step % STAGNATION_SAMPLE_INTERVAL !== 0) return;
290
+ let next;
291
+ try {
292
+ next = JSON.stringify(this.read?.()) ?? "undefined";
293
+ } catch {
294
+ this.disabled = true;
295
+ return;
296
+ }
297
+ if (this.fingerprint === next) {
298
+ this.unchangedSamples += 1;
299
+ return;
300
+ }
301
+ this.fingerprint = next;
302
+ this.unchangedSamples = 1;
303
+ this.settledAtStep = step;
304
+ }
305
+ get settled() {
306
+ return !this.disabled && this.unchangedSamples >= STAGNATION_SAMPLE_MINIMUM;
307
+ }
308
+ };
276
309
  function normalizePlaythroughWaiverReason(waiverReason) {
277
310
  if (waiverReason === void 0) return void 0;
278
311
  const reason = waiverReason.trim();
@@ -292,6 +325,7 @@ async function runBoundedUntil(condition, options = {}) {
292
325
  "stepUntil maxSteps must be a safe integer between 0 and 10000."
293
326
  );
294
327
  }
328
+ const probe = new StagnationProbe(options.diagnostics);
295
329
  for (let step = 0; step <= maxSteps; step += 1) {
296
330
  throwIfAborted(options.signal);
297
331
  if (condition()) return step;
@@ -300,11 +334,18 @@ async function runBoundedUntil(condition, options = {}) {
300
334
  await options.step?.(step + 1);
301
335
  });
302
336
  throwIfAborted(options.signal);
337
+ probe.sample(step + 1);
303
338
  }
304
339
  }
305
340
  const diagnostics = formatDiagnostics(options.diagnostics);
306
- const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
307
341
  const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
342
+ if (probe.settled) {
343
+ throw codedError(
344
+ "PLAYTHROUGH_STATE_STAGNANT",
345
+ `Playthrough state stopped changing at step ${probe.settledAtStep} of ${maxSteps} and stayed identical until the bound. The run is already settled, so raising maxSteps cannot help: compare the settled state against the outcome this stage asserts. A settled state usually means the game reached a terminal result other than the asserted one, or that the stage never advances the production Controller at all.${suffix}`
346
+ );
347
+ }
348
+ const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
308
349
  throw codedError(
309
350
  options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
310
351
  `Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
@@ -265,6 +265,39 @@ function formatDiagnostics(read) {
265
265
  return `diagnostics() threw: ${String(error)}`;
266
266
  }
267
267
  }
268
+ var STAGNATION_SAMPLE_INTERVAL = 32;
269
+ var STAGNATION_SAMPLE_MINIMUM = 3;
270
+ var StagnationProbe = class {
271
+ constructor(read) {
272
+ this.read = read;
273
+ this.disabled = !read;
274
+ }
275
+ read;
276
+ fingerprint;
277
+ unchangedSamples = 0;
278
+ disabled = false;
279
+ settledAtStep;
280
+ sample(step) {
281
+ if (this.disabled || step % STAGNATION_SAMPLE_INTERVAL !== 0) return;
282
+ let next;
283
+ try {
284
+ next = JSON.stringify(this.read?.()) ?? "undefined";
285
+ } catch {
286
+ this.disabled = true;
287
+ return;
288
+ }
289
+ if (this.fingerprint === next) {
290
+ this.unchangedSamples += 1;
291
+ return;
292
+ }
293
+ this.fingerprint = next;
294
+ this.unchangedSamples = 1;
295
+ this.settledAtStep = step;
296
+ }
297
+ get settled() {
298
+ return !this.disabled && this.unchangedSamples >= STAGNATION_SAMPLE_MINIMUM;
299
+ }
300
+ };
268
301
  function normalizePlaythroughWaiverReason(waiverReason) {
269
302
  if (waiverReason === void 0) return void 0;
270
303
  const reason = waiverReason.trim();
@@ -284,6 +317,7 @@ async function runBoundedUntil(condition, options = {}) {
284
317
  "stepUntil maxSteps must be a safe integer between 0 and 10000."
285
318
  );
286
319
  }
320
+ const probe = new StagnationProbe(options.diagnostics);
287
321
  for (let step = 0; step <= maxSteps; step += 1) {
288
322
  throwIfAborted(options.signal);
289
323
  if (condition()) return step;
@@ -292,11 +326,18 @@ async function runBoundedUntil(condition, options = {}) {
292
326
  await options.step?.(step + 1);
293
327
  });
294
328
  throwIfAborted(options.signal);
329
+ probe.sample(step + 1);
295
330
  }
296
331
  }
297
332
  const diagnostics = formatDiagnostics(options.diagnostics);
298
- const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
299
333
  const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
334
+ if (probe.settled) {
335
+ throw codedError(
336
+ "PLAYTHROUGH_STATE_STAGNANT",
337
+ `Playthrough state stopped changing at step ${probe.settledAtStep} of ${maxSteps} and stayed identical until the bound. The run is already settled, so raising maxSteps cannot help: compare the settled state against the outcome this stage asserts. A settled state usually means the game reached a terminal result other than the asserted one, or that the stage never advances the production Controller at all.${suffix}`
338
+ );
339
+ }
340
+ const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
300
341
  throw codedError(
301
342
  options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
302
343
  `Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
@@ -1266,7 +1307,9 @@ function repairGuidance(code) {
1266
1307
  case "STAGE_STATE_UNCHANGED":
1267
1308
  return "The stage ran and asserted, but its observable state matched the previous milestone. Preserve the intended gameplay result and observe the same production Controller rendered by <App />. Do not substitute arbitrary labels or a weaker state change.";
1268
1309
  case "PLAYTHROUGH_BOUND_EXHAUSTED":
1269
- return "Keep the intended outcome unchanged. The stage driver ran, but gameplay did not reach it within the bound. Inspect TRACE and Last diagnostics, then confirm that each deterministic step advances the same production Controller rendered by <App />. If state remains unchanged, inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Do not replace the outcome with navigation, an intermediate phase, a no-op step, or a weaker assertion.";
1310
+ return "First read the phase or result field of Last diagnostics. If the run already reached a terminal result other than the one asserted, the game is over and the bound is irrelevant: assert the result the shipped constants actually produce (loss, draw, and ranking are legitimate terminal results), or fix the mechanic that was supposed to make the intended objective reachable. Raising maxSteps or rewriting the in-test play order cannot pass a finished game. If the run is still mid-game, keep the intended outcome unchanged and confirm that each deterministic step advances the same production Controller rendered by <App />; if state remains unchanged, inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Never replace the outcome with navigation, an intermediate phase, a no-op step, or a weaker assertion, and never retune the production constants that define the challenge.";
1311
+ case "PLAYTHROUGH_STATE_STAGNANT":
1312
+ return "The observed state stopped changing well before the bound, so the bound is not the problem and raising maxSteps cannot help. Two causes: the run already reached a terminal result other than the one asserted \u2014 assert the result the shipped constants actually produce, or fix the mechanic meant to make the intended objective reachable \u2014 or the stage never advances the production Controller at all, in which case inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Do not retune production constants and do not weaken the assertion.";
1270
1313
  case "PLAYTHROUGH_OUTCOME_NOT_REACHED":
1271
1314
  return "The stage action completed, but its until condition never became true. Inspect TRACE and Last diagnostics, then confirm that the production DOM input reaches the rendered App, until describes the result caused by this stage, and observe reads the matching authoritative state when used. Add a deterministic step only if the gameplay is actually driven by time or frames; do not add a no-op step or weaken the intended outcome.";
1272
1315
  case "PLAYTHROUGH_CLOCK_NOT_ADVANCED":
@@ -231,6 +231,39 @@ function formatDiagnostics(read) {
231
231
  return `diagnostics() threw: ${String(error)}`;
232
232
  }
233
233
  }
234
+ var STAGNATION_SAMPLE_INTERVAL = 32;
235
+ var STAGNATION_SAMPLE_MINIMUM = 3;
236
+ var StagnationProbe = class {
237
+ constructor(read) {
238
+ this.read = read;
239
+ this.disabled = !read;
240
+ }
241
+ read;
242
+ fingerprint;
243
+ unchangedSamples = 0;
244
+ disabled = false;
245
+ settledAtStep;
246
+ sample(step) {
247
+ if (this.disabled || step % STAGNATION_SAMPLE_INTERVAL !== 0) return;
248
+ let next;
249
+ try {
250
+ next = JSON.stringify(this.read?.()) ?? "undefined";
251
+ } catch {
252
+ this.disabled = true;
253
+ return;
254
+ }
255
+ if (this.fingerprint === next) {
256
+ this.unchangedSamples += 1;
257
+ return;
258
+ }
259
+ this.fingerprint = next;
260
+ this.unchangedSamples = 1;
261
+ this.settledAtStep = step;
262
+ }
263
+ get settled() {
264
+ return !this.disabled && this.unchangedSamples >= STAGNATION_SAMPLE_MINIMUM;
265
+ }
266
+ };
234
267
  function normalizePlaythroughWaiverReason(waiverReason) {
235
268
  if (waiverReason === void 0) return void 0;
236
269
  const reason = waiverReason.trim();
@@ -250,6 +283,7 @@ async function runBoundedUntil(condition, options = {}) {
250
283
  "stepUntil maxSteps must be a safe integer between 0 and 10000."
251
284
  );
252
285
  }
286
+ const probe = new StagnationProbe(options.diagnostics);
253
287
  for (let step = 0; step <= maxSteps; step += 1) {
254
288
  throwIfAborted(options.signal);
255
289
  if (condition()) return step;
@@ -258,11 +292,18 @@ async function runBoundedUntil(condition, options = {}) {
258
292
  await options.step?.(step + 1);
259
293
  });
260
294
  throwIfAborted(options.signal);
295
+ probe.sample(step + 1);
261
296
  }
262
297
  }
263
298
  const diagnostics = formatDiagnostics(options.diagnostics);
264
- const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
265
299
  const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
300
+ if (probe.settled) {
301
+ throw codedError(
302
+ "PLAYTHROUGH_STATE_STAGNANT",
303
+ `Playthrough state stopped changing at step ${probe.settledAtStep} of ${maxSteps} and stayed identical until the bound. The run is already settled, so raising maxSteps cannot help: compare the settled state against the outcome this stage asserts. A settled state usually means the game reached a terminal result other than the asserted one, or that the stage never advances the production Controller at all.${suffix}`
304
+ );
305
+ }
306
+ const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
266
307
  throw codedError(
267
308
  options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
268
309
  `Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
@@ -1232,7 +1273,9 @@ function repairGuidance(code) {
1232
1273
  case "STAGE_STATE_UNCHANGED":
1233
1274
  return "The stage ran and asserted, but its observable state matched the previous milestone. Preserve the intended gameplay result and observe the same production Controller rendered by <App />. Do not substitute arbitrary labels or a weaker state change.";
1234
1275
  case "PLAYTHROUGH_BOUND_EXHAUSTED":
1235
- return "Keep the intended outcome unchanged. The stage driver ran, but gameplay did not reach it within the bound. Inspect TRACE and Last diagnostics, then confirm that each deterministic step advances the same production Controller rendered by <App />. If state remains unchanged, inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Do not replace the outcome with navigation, an intermediate phase, a no-op step, or a weaker assertion.";
1276
+ return "First read the phase or result field of Last diagnostics. If the run already reached a terminal result other than the one asserted, the game is over and the bound is irrelevant: assert the result the shipped constants actually produce (loss, draw, and ranking are legitimate terminal results), or fix the mechanic that was supposed to make the intended objective reachable. Raising maxSteps or rewriting the in-test play order cannot pass a finished game. If the run is still mid-game, keep the intended outcome unchanged and confirm that each deterministic step advances the same production Controller rendered by <App />; if state remains unchanged, inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Never replace the outcome with navigation, an intermediate phase, a no-op step, or a weaker assertion, and never retune the production constants that define the challenge.";
1277
+ case "PLAYTHROUGH_STATE_STAGNANT":
1278
+ return "The observed state stopped changing well before the bound, so the bound is not the problem and raising maxSteps cannot help. Two causes: the run already reached a terminal result other than the one asserted \u2014 assert the result the shipped constants actually produce, or fix the mechanic meant to make the intended objective reachable \u2014 or the stage never advances the production Controller at all, in which case inject ManualGameClock through the production App factory and observe that Controller through Telemetry. Do not retune production constants and do not weaken the assertion.";
1236
1279
  case "PLAYTHROUGH_OUTCOME_NOT_REACHED":
1237
1280
  return "The stage action completed, but its until condition never became true. Inspect TRACE and Last diagnostics, then confirm that the production DOM input reaches the rendered App, until describes the result caused by this stage, and observe reads the matching authoritative state when used. Add a deterministic step only if the gameplay is actually driven by time or frames; do not add a no-op step or weaken the intended outcome.";
1238
1281
  case "PLAYTHROUGH_CLOCK_NOT_ADVANCED":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Shared React and Phaser game lint plus deterministic testing tools for Miaoda games",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",