miaoda-game-devkit 0.2.3 → 0.2.4

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.
package/README.md CHANGED
@@ -17,10 +17,14 @@ Scene 命令、restart、业务 checkpoint 和销毁。玩法测试可以调用
17
17
  `host.assertGameplayEvidence()` 把这些客观事实设为门禁;它不推断业务语义,
18
18
  authoritative state 仍必须由测试显式断言。
19
19
 
20
- 玩法测试应优先使用 `gameplayTest`。它在 Vitest 收集阶段写入静态 contract metadata,
21
- 在执行阶段绑定 HEADLESS host,并在 `onTestFinished` 校验 Ledger。`gameplayAudit` reporter
22
- 会在测试执行前报告未声明或重复的 contract,在测试结束后汇总缺失、失败、证据不足、
23
- Scene 未访问和 registry 不一致。`registerGameplayContract` 保留为不参加执行前预检的低层接口。
20
+ 玩法测试应优先使用 `gameplayTest`。它在 Vitest 收集阶段写入测试声明,
21
+ 在执行阶段绑定 HEADLESS host,并在 `onTestFinished` 校验 Ledger。配置加载阶段会先校验
22
+ 交互项目的最低基线:至少一个 `kind: "playthrough"` contract 必须要求真实输入、完整帧和
23
+ 语义 checkpoint,每个非入口 Scene 必须有生产转场覆盖;检测到 `scene.restart()` 时还必须
24
+ 声明 `flows.restart: true` 和 `kind: "recovery"` contract。配置预检失败时测试和 coverage
25
+ 都不会运行。配置通过后,`gameplayAudit` reporter 才会检查测试声明,并在结束时汇总
26
+ 缺失、失败、证据不足、Scene 未访问和 registry 不一致。`registerGameplayContract` 保留为
27
+ 不参加执行前预检的低层接口。
24
28
 
25
29
  模板直接调用开发工具包提供的命令行程序:
26
30
 
@@ -83,7 +87,12 @@ DOM 到 Phaser 的输入、命中测试和游戏状态变化,不验证 Canvas/
83
87
  断言 authoritative state 后调用 `host.checkpoint("progress")`、
84
88
  `host.checkpoint("recovery")` 等稳定检查点,并在清单中通过 `requireCheckpoint` 声明。
85
89
  运行结束会输出稳定的 `GAMEPLAY_AUDIT: ALL CONTRACTS PASSED` 或
86
- `GAMEPLAY_AUDIT: ... FAILED` 标识。
90
+ `GAMEPLAY_AUDIT: ... FAILED` 标识。该标识只表示玩法证据审计;`pnpm test` 的整体结果
91
+ 必须以最后的 `TEST_RESULT: PASS` 或 `TEST_RESULT: FAIL` 为准。失败报告会合并重复的问题,
92
+ 并给出按类别去重的修复建议,先处理列出的原始测试错误,再重新运行 `pnpm test`。
93
+
94
+ 共享 Vitest 配置使用 `minimal` reporter 和 `text-summary` coverage reporter,保留失败
95
+ 定位、Scene 覆盖摘要和最终机器标识,同时避免输出完整测试列表和逐文件噪声。
87
96
 
88
97
  ## 发布顺序
89
98
 
@@ -248,12 +248,15 @@ function auditGameplayCollection(options2, file, tests) {
248
248
  function getGameplayEvidenceIssues(id, requirements, evidence) {
249
249
  const issues = [];
250
250
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
251
- if (requirements.requireInput && inputEvents === 0)
251
+ if (requirements.requireInput && inputEvents === 0) {
252
252
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
253
- if (requirements.requireFrameAdvance && evidence.frames === 0)
253
+ }
254
+ if (requirements.requireFrameAdvance && evidence.frames === 0) {
254
255
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
255
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0)
256
+ }
257
+ if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
256
258
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
259
+ }
257
260
  for (const target of normalizeList(requirements.requireTransition)) {
258
261
  if (!evidence.transitions.some((transition) => transition.to === target)) {
259
262
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
@@ -264,21 +267,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
264
267
  issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
265
268
  }
266
269
  for (const scene of normalizeList(requirements.requireRestart)) {
267
- if (!evidence.restartedScenes.includes(scene))
270
+ if (!evidence.restartedScenes.includes(scene)) {
268
271
  issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
272
+ }
269
273
  }
270
274
  for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
271
- if (!evidence.checkpoints.includes(checkpoint))
275
+ if (!evidence.checkpoints.includes(checkpoint)) {
272
276
  issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
277
+ }
273
278
  }
274
- if (requirements.requireDestroy && !evidence.destroyed)
279
+ if (requirements.requireDestroy && !evidence.destroyed) {
275
280
  issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
281
+ }
276
282
  return issues;
277
283
  }
278
284
  function auditGameplayRun(options2, tests) {
279
285
  const issues = [];
280
286
  const actualContracts = tests.filter((test) => test.metadata);
281
- const acceptedEvidence = [];
287
+ const observedEvidence = [];
282
288
  for (const expected of options2.contracts) {
283
289
  const matches = actualContracts.filter(
284
290
  (test2) => test2.metadata?.id === expected.id
@@ -294,6 +300,7 @@ function auditGameplayRun(options2, tests) {
294
300
  const test = matches[0];
295
301
  const metadata = test?.metadata;
296
302
  if (!test || !metadata) continue;
303
+ if (metadata.evidence) observedEvidence.push(metadata.evidence);
297
304
  if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
298
305
  issues.push(
299
306
  `contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
@@ -314,7 +321,6 @@ function auditGameplayRun(options2, tests) {
314
321
  metadata.evidence
315
322
  );
316
323
  issues.push(...evidenceIssues);
317
- if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
318
324
  }
319
325
  for (const test of actualContracts) {
320
326
  const id = test.metadata?.id;
@@ -325,14 +331,15 @@ function auditGameplayRun(options2, tests) {
325
331
  }
326
332
  }
327
333
  const registeredScenes = normalizeSet(
328
- acceptedEvidence.flatMap((evidence) => evidence.registeredScenes)
334
+ observedEvidence.flatMap((evidence) => evidence.registeredScenes)
329
335
  );
330
336
  const expectedScenes = normalizeSet(options2.scenes);
331
337
  for (const scene of expectedScenes) {
332
- if (!registeredScenes.includes(scene))
338
+ if (!registeredScenes.includes(scene)) {
333
339
  issues.push(
334
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u4EFB\u4F55\u901A\u8FC7\u5BA1\u8BA1\u7684 host registry \u4E2D\u3002`
340
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
335
341
  );
342
+ }
336
343
  }
337
344
  for (const scene of registeredScenes) {
338
345
  if (!expectedScenes.includes(scene))
@@ -343,6 +350,81 @@ function auditGameplayRun(options2, tests) {
343
350
  return { passed: issues.length === 0, issues: [...new Set(issues)] };
344
351
  }
345
352
 
353
+ // src/gameplay-audit-reporter.ts
354
+ import { relative } from "path";
355
+ function formatGameplayIssues(issues) {
356
+ const groups = /* @__PURE__ */ new Map();
357
+ const repairKeys = /* @__PURE__ */ new Set();
358
+ const repairs = [];
359
+ const repairText = {
360
+ input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
361
+ frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
362
+ physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
363
+ transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
364
+ visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
365
+ restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
366
+ checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
367
+ destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
368
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
369
+ missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
370
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
371
+ };
372
+ const getCategory = (issue) => {
373
+ if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
374
+ if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
375
+ if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
376
+ if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
377
+ const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
378
+ return { category: "transition", key: `transition:${target}` };
379
+ }
380
+ if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
381
+ const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
382
+ return { category: "visit", key: `visit:${scene}` };
383
+ }
384
+ if (issue.includes("\u672A\u91CD\u542F Scene")) {
385
+ const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
386
+ return { category: "restart", key: `restart:${scene}` };
387
+ }
388
+ if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
389
+ if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
390
+ if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
391
+ const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
392
+ return { category: "registry", key: `registry:${scene}` };
393
+ }
394
+ if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
395
+ if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
396
+ return { category: "other", key: `other:${issue}` };
397
+ };
398
+ for (const rawIssue of [...new Set(issues)]) {
399
+ const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
400
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
401
+ const id = compact.match(/contract "([^"]+)"/)?.[1];
402
+ const { category, key } = getCategory(compact);
403
+ const group = groups.get(key);
404
+ if (group) {
405
+ if (id && !group.ids.includes(id)) group.ids.push(id);
406
+ } else {
407
+ groups.set(key, { text: readable, ids: id ? [id] : [], key });
408
+ }
409
+ if (repairText[category] && !repairKeys.has(category)) {
410
+ repairKeys.add(category);
411
+ repairs.push(repairText[category]);
412
+ }
413
+ }
414
+ const grouped = [...groups.values()];
415
+ const visible = grouped.slice(0, 8).map((group) => {
416
+ if (group.ids.length <= 1) return group.text;
417
+ return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
418
+ });
419
+ return {
420
+ issues: visible.map(
421
+ (issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
422
+ ),
423
+ repairs: repairs.slice(0, 8),
424
+ omitted: Math.max(0, grouped.length - visible.length)
425
+ };
426
+ }
427
+
346
428
  // src/lint/gameplay-audit.test.ts
347
429
  var options = {
348
430
  mode: "interactive",
@@ -602,6 +684,7 @@ describe("gameplay audit", () => {
602
684
  evidence.destroyed = false;
603
685
  evidence.registeredScenes.push("DebugScene");
604
686
  const test = createPassingTest();
687
+ if (!test.metadata) throw new Error("expected gameplay metadata");
605
688
  test.metadata.evidence = evidence;
606
689
  const result = auditGameplayRun(options, [test]);
607
690
  expect(result.passed).toBe(false);
@@ -613,8 +696,24 @@ describe("gameplay audit", () => {
613
696
  ])
614
697
  );
615
698
  });
699
+ it("\u4FDD\u7559\u5931\u8D25 host \u5DF2\u6CE8\u518C\u7684 Scene\uFF0C\u907F\u514D checkpoint \u7F3A\u53E3\u7EA7\u8054\u6210\u6CE8\u518C\u9519\u8BEF", () => {
700
+ const test = createPassingTest();
701
+ if (!test.metadata?.evidence) throw new Error("expected gameplay evidence");
702
+ test.metadata.evidence.checkpoints = ["progress"];
703
+ const result = auditGameplayRun(options, [test]);
704
+ expect(result.issues).toContain(
705
+ 'contract "gameplay.main-flow" \u7F3A\u5C11 checkpoint "recovery"\u3002'
706
+ );
707
+ expect(result.issues).not.toContain(
708
+ '\u751F\u4EA7 Scene "IndexScene" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002'
709
+ );
710
+ expect(result.issues).not.toContain(
711
+ '\u751F\u4EA7 Scene "BattleScene" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002'
712
+ );
713
+ });
616
714
  it("\u62D2\u7EDD\u901A\u8FC7\u5951\u7EA6\u7684 host registry \u4E2D\u51FA\u73B0\u6E05\u5355\u5916 Scene", () => {
617
715
  const test = createPassingTest();
716
+ if (!test.metadata?.evidence) throw new Error("expected gameplay evidence");
618
717
  test.metadata.evidence.registeredScenes.push("DebugScene");
619
718
  expect(auditGameplayRun(options, [test]).issues).toContain(
620
719
  'host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "DebugScene"\u3002'
@@ -647,4 +746,28 @@ describe("gameplay audit", () => {
647
746
  )
648
747
  ).toContain('contract "arcade-physics" \u672A\u63A8\u8FDB Arcade Physics\u3002');
649
748
  });
749
+ it("\u5408\u5E76\u76F8\u540C\u4FEE\u590D\u7C7B\u578B\u4F46\u4FDD\u7559\u4E0D\u540C Scene \u8F6C\u573A\u76EE\u6807", () => {
750
+ const result = formatGameplayIssues([
751
+ 'contract "Menu.start" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002',
752
+ 'contract "Battle.start" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002',
753
+ 'contract "Menu.to-battle" \u672A\u8BB0\u5F55\u5230 BattleScene \u7684 Scene \u8F6C\u573A\u3002',
754
+ 'contract "Battle.to-result" \u672A\u8BB0\u5F55\u5230 ResultScene \u7684 Scene \u8F6C\u573A\u3002',
755
+ 'contract "Menu.visit-result" \u672A\u8BBF\u95EE Scene "ResultScene"\u3002',
756
+ 'contract "Battle.visit-result" \u672A\u8BBF\u95EE Scene "ResultScene"\u3002'
757
+ ]);
758
+ expect(result.issues).toHaveLength(4);
759
+ expect(result.repairs).toHaveLength(3);
760
+ expect(result.repairs[0]).toContain("\u8F93\u5165");
761
+ expect(result.repairs[1]).toContain("\u8F6C\u573A");
762
+ });
763
+ it("\u5C06\u591A\u68C0\u67E5\u9879\u7684\u7F3A\u53E3\u9650\u5236\u4E3A\u95EE\u9898\u7EC4\u800C\u4E0D\u662F\u539F\u59CB\u65E5\u5FD7\u884C", () => {
764
+ const issues = Array.from(
765
+ { length: 20 },
766
+ (_, index) => `contract "Scene${index}.boot" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`
767
+ );
768
+ const result = formatGameplayIssues(issues);
769
+ expect(result.issues).toHaveLength(1);
770
+ expect(result.repairs).toHaveLength(1);
771
+ expect(result.omitted).toBe(0);
772
+ });
650
773
  });
@@ -1188,6 +1188,7 @@ describe("gameplay contract lifecycle integration", () => {
1188
1188
  const { callbacks, context } = createTestContext();
1189
1189
  const contract = registerGameplayContract(context, {
1190
1190
  id: "InputScene.hit",
1191
+ kind: "playthrough",
1191
1192
  requireInput: true,
1192
1193
  requireFrameAdvance: true
1193
1194
  });
@@ -1216,6 +1217,7 @@ describe("gameplay contract lifecycle integration", () => {
1216
1217
  const { callbacks, context } = createTestContext();
1217
1218
  const contract = registerGameplayContract(context, {
1218
1219
  id: "InputScene.missing-input",
1220
+ kind: "playthrough",
1219
1221
  requireInput: true
1220
1222
  });
1221
1223
  const host = await createHeadlessGame(new InputScene());
@@ -253,12 +253,15 @@ function auditGameplayCollection(options, file, tests) {
253
253
  function getGameplayEvidenceIssues(id, requirements, evidence) {
254
254
  const issues = [];
255
255
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
256
- if (requirements.requireInput && inputEvents === 0)
256
+ if (requirements.requireInput && inputEvents === 0) {
257
257
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
258
- if (requirements.requireFrameAdvance && evidence.frames === 0)
258
+ }
259
+ if (requirements.requireFrameAdvance && evidence.frames === 0) {
259
260
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
260
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0)
261
+ }
262
+ if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
261
263
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
264
+ }
262
265
  for (const target of normalizeList(requirements.requireTransition)) {
263
266
  if (!evidence.transitions.some((transition) => transition.to === target)) {
264
267
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
@@ -269,21 +272,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
269
272
  issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
270
273
  }
271
274
  for (const scene of normalizeList(requirements.requireRestart)) {
272
- if (!evidence.restartedScenes.includes(scene))
275
+ if (!evidence.restartedScenes.includes(scene)) {
273
276
  issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
277
+ }
274
278
  }
275
279
  for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
276
- if (!evidence.checkpoints.includes(checkpoint))
280
+ if (!evidence.checkpoints.includes(checkpoint)) {
277
281
  issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
282
+ }
278
283
  }
279
- if (requirements.requireDestroy && !evidence.destroyed)
284
+ if (requirements.requireDestroy && !evidence.destroyed) {
280
285
  issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
286
+ }
281
287
  return issues;
282
288
  }
283
289
  function auditGameplayRun(options, tests) {
284
290
  const issues = [];
285
291
  const actualContracts = tests.filter((test) => test.metadata);
286
- const acceptedEvidence = [];
292
+ const observedEvidence = [];
287
293
  for (const expected of options.contracts) {
288
294
  const matches = actualContracts.filter(
289
295
  (test2) => test2.metadata?.id === expected.id
@@ -299,6 +305,7 @@ function auditGameplayRun(options, tests) {
299
305
  const test = matches[0];
300
306
  const metadata = test?.metadata;
301
307
  if (!test || !metadata) continue;
308
+ if (metadata.evidence) observedEvidence.push(metadata.evidence);
302
309
  if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
303
310
  issues.push(
304
311
  `contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
@@ -319,7 +326,6 @@ function auditGameplayRun(options, tests) {
319
326
  metadata.evidence
320
327
  );
321
328
  issues.push(...evidenceIssues);
322
- if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
323
329
  }
324
330
  for (const test of actualContracts) {
325
331
  const id = test.metadata?.id;
@@ -330,14 +336,15 @@ function auditGameplayRun(options, tests) {
330
336
  }
331
337
  }
332
338
  const registeredScenes = normalizeSet(
333
- acceptedEvidence.flatMap((evidence) => evidence.registeredScenes)
339
+ observedEvidence.flatMap((evidence) => evidence.registeredScenes)
334
340
  );
335
341
  const expectedScenes = normalizeSet(options.scenes);
336
342
  for (const scene of expectedScenes) {
337
- if (!registeredScenes.includes(scene))
343
+ if (!registeredScenes.includes(scene)) {
338
344
  issues.push(
339
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u4EFB\u4F55\u901A\u8FC7\u5BA1\u8BA1\u7684 host registry \u4E2D\u3002`
345
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
340
346
  );
347
+ }
341
348
  }
342
349
  for (const scene of registeredScenes) {
343
350
  if (!expectedScenes.includes(scene))
@@ -367,11 +374,96 @@ function toAuditTest(test) {
367
374
  metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
368
375
  };
369
376
  }
377
+ function formatGameplayIssues(issues) {
378
+ const groups = /* @__PURE__ */ new Map();
379
+ const repairKeys = /* @__PURE__ */ new Set();
380
+ const repairs = [];
381
+ const repairText = {
382
+ input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
383
+ frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
384
+ physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
385
+ transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
386
+ visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
387
+ restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
388
+ checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
389
+ destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
390
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
391
+ missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
392
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
393
+ };
394
+ const getCategory = (issue) => {
395
+ if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
396
+ if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
397
+ if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
398
+ if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
399
+ const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
400
+ return { category: "transition", key: `transition:${target}` };
401
+ }
402
+ if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
403
+ const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
404
+ return { category: "visit", key: `visit:${scene}` };
405
+ }
406
+ if (issue.includes("\u672A\u91CD\u542F Scene")) {
407
+ const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
408
+ return { category: "restart", key: `restart:${scene}` };
409
+ }
410
+ if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
411
+ if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
412
+ if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
413
+ const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
414
+ return { category: "registry", key: `registry:${scene}` };
415
+ }
416
+ if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
417
+ if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
418
+ return { category: "other", key: `other:${issue}` };
419
+ };
420
+ for (const rawIssue of [...new Set(issues)]) {
421
+ const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
422
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
423
+ const id = compact.match(/contract "([^"]+)"/)?.[1];
424
+ const { category, key } = getCategory(compact);
425
+ const group = groups.get(key);
426
+ if (group) {
427
+ if (id && !group.ids.includes(id)) group.ids.push(id);
428
+ } else {
429
+ groups.set(key, { text: readable, ids: id ? [id] : [], key });
430
+ }
431
+ if (repairText[category] && !repairKeys.has(category)) {
432
+ repairKeys.add(category);
433
+ repairs.push(repairText[category]);
434
+ }
435
+ }
436
+ const grouped = [...groups.values()];
437
+ const visible = grouped.slice(0, 8).map((group) => {
438
+ if (group.ids.length <= 1) return group.text;
439
+ return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
440
+ });
441
+ return {
442
+ issues: visible.map(
443
+ (issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
444
+ ),
445
+ repairs: repairs.slice(0, 8),
446
+ omitted: Math.max(0, grouped.length - visible.length)
447
+ };
448
+ }
370
449
  function printIssues(stage, issues) {
371
450
  if (issues.length === 0) return;
372
451
  console.error(`
373
452
  GAMEPLAY_AUDIT: ${stage} FAILED`);
374
- for (const issue of issues) console.error(`- ${issue}`);
453
+ console.error(
454
+ "\u73A9\u6CD5\u68C0\u67E5\u5931\u8D25\uFF1A\u4EE5\u4E0B\u6BCF\u4E2A\u68C0\u67E5\u9879\u5BF9\u5E94\u4E00\u4E2A\u9700\u8981\u6D4B\u8BD5\u8BC1\u660E\u7684\u6E38\u620F\u884C\u4E3A\u3002"
455
+ );
456
+ const formatted = formatGameplayIssues(issues);
457
+ for (const issue of formatted.issues) console.error(`- ${issue}`);
458
+ if (formatted.repairs.length > 0) {
459
+ console.error("\u4FEE\u590D\u5EFA\u8BAE\uFF08\u540C\u7C7B\u95EE\u9898\u53EA\u5217\u4E00\u6B21\uFF09\uFF1A");
460
+ for (const repair of formatted.repairs) console.error(`- ${repair}`);
461
+ }
462
+ if (formatted.omitted > 0) {
463
+ console.error(
464
+ `- \u5176\u4F59 ${formatted.omitted} \u7C7B\u95EE\u9898\u5DF2\u7701\u7565\uFF1B\u5148\u4FEE\u590D\u4EE5\u4E0A\u95EE\u9898\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002`
465
+ );
466
+ }
375
467
  }
376
468
  var GameplayAuditReporter = class {
377
469
  projectRoot;
@@ -412,21 +504,30 @@ var GameplayAuditReporter = class {
412
504
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
413
505
  );
414
506
  const result = auditGameplayRun(this.options, tests);
415
- if (!result.passed) {
416
- printIssues("FINAL", result.issues);
507
+ const combinedIssues = [
508
+ ...this.preflightIssues,
509
+ ...result.issues
510
+ ];
511
+ if (combinedIssues.length > 0) {
512
+ if (this.preflightIssues.size > 0) {
513
+ console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
514
+ }
515
+ printIssues("FINAL", [...new Set(combinedIssues)]);
417
516
  process.exitCode = 1;
418
517
  } else {
419
518
  console.log(
420
519
  `
421
520
  GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
422
521
  );
522
+ console.log(
523
+ "GAMEPLAY_AUDIT_NOTE: \u73A9\u6CD5\u68C0\u67E5\u5DF2\u901A\u8FC7\uFF1B\u4ECD\u9700\u4EE5\u6700\u540E\u7684 TEST_RESULT \u5224\u65AD\u6574\u4F53\u6210\u529F\u3002"
524
+ );
423
525
  }
424
526
  }
425
527
  /** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
426
528
  reportPreflightIssue(issue) {
427
529
  if (this.preflightIssues.has(issue)) return;
428
530
  this.preflightIssues.add(issue);
429
- printIssues("PREFLIGHT", [issue]);
430
531
  process.exitCode = 1;
431
532
  }
432
533
  };
@@ -476,7 +577,9 @@ function defineGameVitestConfig(options) {
476
577
  ...options.additionalSetupFiles ?? []
477
578
  ],
478
579
  reporters: [
479
- "default",
580
+ // Vitest's minimal reporter is optimized for AI output: passing tests
581
+ // stay quiet while failed tests retain their useful error message.
582
+ "minimal",
480
583
  new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
481
584
  ],
482
585
  restoreMocks: true,
@@ -488,7 +591,9 @@ function defineGameVitestConfig(options) {
488
591
  "src/scenes/**/*.d.ts",
489
592
  "src/scenes/**/*.{spec,test}.{ts,tsx}"
490
593
  ],
491
- reporter: ["text"],
594
+ // Per-file threshold errors still identify omitted Scene files; the
595
+ // summary avoids printing a large coverage table on every successful run.
596
+ reporter: ["text-summary"],
492
597
  thresholds: {
493
598
  perFile: true,
494
599
  lines: 1,
@@ -535,7 +640,7 @@ describe("defineGameVitestConfig", () => {
535
640
  environment: "jsdom",
536
641
  environmentOptions: { jsdom: { url: "http://localhost/" } },
537
642
  setupFiles: ["miaoda-game-devkit/vitest-setup"],
538
- reporters: ["default", expect.any(Object)],
643
+ reporters: ["minimal", expect.any(Object)],
539
644
  restoreMocks: true,
540
645
  clearMocks: true,
541
646
  coverage: {
@@ -545,7 +650,7 @@ describe("defineGameVitestConfig", () => {
545
650
  "src/scenes/**/*.d.ts",
546
651
  "src/scenes/**/*.{spec,test}.{ts,tsx}"
547
652
  ],
548
- reporter: ["text"],
653
+ reporter: ["text-summary"],
549
654
  thresholds: {
550
655
  perFile: true,
551
656
  lines: 1,
@@ -273,12 +273,15 @@ function auditGameplayCollection(options, file, tests) {
273
273
  function getGameplayEvidenceIssues(id, requirements, evidence) {
274
274
  const issues = [];
275
275
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
276
- if (requirements.requireInput && inputEvents === 0)
276
+ if (requirements.requireInput && inputEvents === 0) {
277
277
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
278
- if (requirements.requireFrameAdvance && evidence.frames === 0)
278
+ }
279
+ if (requirements.requireFrameAdvance && evidence.frames === 0) {
279
280
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
280
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0)
281
+ }
282
+ if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
281
283
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
284
+ }
282
285
  for (const target of normalizeList(requirements.requireTransition)) {
283
286
  if (!evidence.transitions.some((transition) => transition.to === target)) {
284
287
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
@@ -289,21 +292,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
289
292
  issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
290
293
  }
291
294
  for (const scene of normalizeList(requirements.requireRestart)) {
292
- if (!evidence.restartedScenes.includes(scene))
295
+ if (!evidence.restartedScenes.includes(scene)) {
293
296
  issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
297
+ }
294
298
  }
295
299
  for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
296
- if (!evidence.checkpoints.includes(checkpoint))
300
+ if (!evidence.checkpoints.includes(checkpoint)) {
297
301
  issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
302
+ }
298
303
  }
299
- if (requirements.requireDestroy && !evidence.destroyed)
304
+ if (requirements.requireDestroy && !evidence.destroyed) {
300
305
  issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
306
+ }
301
307
  return issues;
302
308
  }
303
309
  function auditGameplayRun(options, tests) {
304
310
  const issues = [];
305
311
  const actualContracts = tests.filter((test) => test.metadata);
306
- const acceptedEvidence = [];
312
+ const observedEvidence = [];
307
313
  for (const expected of options.contracts) {
308
314
  const matches = actualContracts.filter(
309
315
  (test2) => test2.metadata?.id === expected.id
@@ -319,6 +325,7 @@ function auditGameplayRun(options, tests) {
319
325
  const test = matches[0];
320
326
  const metadata = test?.metadata;
321
327
  if (!test || !metadata) continue;
328
+ if (metadata.evidence) observedEvidence.push(metadata.evidence);
322
329
  if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
323
330
  issues.push(
324
331
  `contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
@@ -339,7 +346,6 @@ function auditGameplayRun(options, tests) {
339
346
  metadata.evidence
340
347
  );
341
348
  issues.push(...evidenceIssues);
342
- if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
343
349
  }
344
350
  for (const test of actualContracts) {
345
351
  const id = test.metadata?.id;
@@ -350,14 +356,15 @@ function auditGameplayRun(options, tests) {
350
356
  }
351
357
  }
352
358
  const registeredScenes = normalizeSet(
353
- acceptedEvidence.flatMap((evidence) => evidence.registeredScenes)
359
+ observedEvidence.flatMap((evidence) => evidence.registeredScenes)
354
360
  );
355
361
  const expectedScenes = normalizeSet(options.scenes);
356
362
  for (const scene of expectedScenes) {
357
- if (!registeredScenes.includes(scene))
363
+ if (!registeredScenes.includes(scene)) {
358
364
  issues.push(
359
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u4EFB\u4F55\u901A\u8FC7\u5BA1\u8BA1\u7684 host registry \u4E2D\u3002`
365
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
360
366
  );
367
+ }
361
368
  }
362
369
  for (const scene of registeredScenes) {
363
370
  if (!expectedScenes.includes(scene))
@@ -387,11 +394,96 @@ function toAuditTest(test) {
387
394
  metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
388
395
  };
389
396
  }
397
+ function formatGameplayIssues(issues) {
398
+ const groups = /* @__PURE__ */ new Map();
399
+ const repairKeys = /* @__PURE__ */ new Set();
400
+ const repairs = [];
401
+ const repairText = {
402
+ input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
403
+ frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
404
+ physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
405
+ transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
406
+ visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
407
+ restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
408
+ checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
409
+ destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
410
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
411
+ missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
412
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
413
+ };
414
+ const getCategory = (issue) => {
415
+ if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
416
+ if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
417
+ if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
418
+ if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
419
+ const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
420
+ return { category: "transition", key: `transition:${target}` };
421
+ }
422
+ if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
423
+ const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
424
+ return { category: "visit", key: `visit:${scene}` };
425
+ }
426
+ if (issue.includes("\u672A\u91CD\u542F Scene")) {
427
+ const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
428
+ return { category: "restart", key: `restart:${scene}` };
429
+ }
430
+ if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
431
+ if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
432
+ if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
433
+ const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
434
+ return { category: "registry", key: `registry:${scene}` };
435
+ }
436
+ if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
437
+ if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
438
+ return { category: "other", key: `other:${issue}` };
439
+ };
440
+ for (const rawIssue of [...new Set(issues)]) {
441
+ const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
442
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
443
+ const id = compact.match(/contract "([^"]+)"/)?.[1];
444
+ const { category, key } = getCategory(compact);
445
+ const group = groups.get(key);
446
+ if (group) {
447
+ if (id && !group.ids.includes(id)) group.ids.push(id);
448
+ } else {
449
+ groups.set(key, { text: readable, ids: id ? [id] : [], key });
450
+ }
451
+ if (repairText[category] && !repairKeys.has(category)) {
452
+ repairKeys.add(category);
453
+ repairs.push(repairText[category]);
454
+ }
455
+ }
456
+ const grouped = [...groups.values()];
457
+ const visible = grouped.slice(0, 8).map((group) => {
458
+ if (group.ids.length <= 1) return group.text;
459
+ return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
460
+ });
461
+ return {
462
+ issues: visible.map(
463
+ (issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
464
+ ),
465
+ repairs: repairs.slice(0, 8),
466
+ omitted: Math.max(0, grouped.length - visible.length)
467
+ };
468
+ }
390
469
  function printIssues(stage, issues) {
391
470
  if (issues.length === 0) return;
392
471
  console.error(`
393
472
  GAMEPLAY_AUDIT: ${stage} FAILED`);
394
- for (const issue of issues) console.error(`- ${issue}`);
473
+ console.error(
474
+ "\u73A9\u6CD5\u68C0\u67E5\u5931\u8D25\uFF1A\u4EE5\u4E0B\u6BCF\u4E2A\u68C0\u67E5\u9879\u5BF9\u5E94\u4E00\u4E2A\u9700\u8981\u6D4B\u8BD5\u8BC1\u660E\u7684\u6E38\u620F\u884C\u4E3A\u3002"
475
+ );
476
+ const formatted = formatGameplayIssues(issues);
477
+ for (const issue of formatted.issues) console.error(`- ${issue}`);
478
+ if (formatted.repairs.length > 0) {
479
+ console.error("\u4FEE\u590D\u5EFA\u8BAE\uFF08\u540C\u7C7B\u95EE\u9898\u53EA\u5217\u4E00\u6B21\uFF09\uFF1A");
480
+ for (const repair of formatted.repairs) console.error(`- ${repair}`);
481
+ }
482
+ if (formatted.omitted > 0) {
483
+ console.error(
484
+ `- \u5176\u4F59 ${formatted.omitted} \u7C7B\u95EE\u9898\u5DF2\u7701\u7565\uFF1B\u5148\u4FEE\u590D\u4EE5\u4E0A\u95EE\u9898\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002`
485
+ );
486
+ }
395
487
  }
396
488
  var GameplayAuditReporter = class {
397
489
  projectRoot;
@@ -432,21 +524,30 @@ var GameplayAuditReporter = class {
432
524
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
433
525
  );
434
526
  const result = auditGameplayRun(this.options, tests);
435
- if (!result.passed) {
436
- printIssues("FINAL", result.issues);
527
+ const combinedIssues = [
528
+ ...this.preflightIssues,
529
+ ...result.issues
530
+ ];
531
+ if (combinedIssues.length > 0) {
532
+ if (this.preflightIssues.size > 0) {
533
+ console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
534
+ }
535
+ printIssues("FINAL", [...new Set(combinedIssues)]);
437
536
  process.exitCode = 1;
438
537
  } else {
439
538
  console.log(
440
539
  `
441
540
  GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
442
541
  );
542
+ console.log(
543
+ "GAMEPLAY_AUDIT_NOTE: \u73A9\u6CD5\u68C0\u67E5\u5DF2\u901A\u8FC7\uFF1B\u4ECD\u9700\u4EE5\u6700\u540E\u7684 TEST_RESULT \u5224\u65AD\u6574\u4F53\u6210\u529F\u3002"
544
+ );
443
545
  }
444
546
  }
445
547
  /** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
446
548
  reportPreflightIssue(issue) {
447
549
  if (this.preflightIssues.has(issue)) return;
448
550
  this.preflightIssues.add(issue);
449
- printIssues("PREFLIGHT", [issue]);
450
551
  process.exitCode = 1;
451
552
  }
452
553
  };
@@ -496,7 +597,9 @@ function defineGameVitestConfig(options) {
496
597
  ...options.additionalSetupFiles ?? []
497
598
  ],
498
599
  reporters: [
499
- "default",
600
+ // Vitest's minimal reporter is optimized for AI output: passing tests
601
+ // stay quiet while failed tests retain their useful error message.
602
+ "minimal",
500
603
  new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
501
604
  ],
502
605
  restoreMocks: true,
@@ -508,7 +611,9 @@ function defineGameVitestConfig(options) {
508
611
  "src/scenes/**/*.d.ts",
509
612
  "src/scenes/**/*.{spec,test}.{ts,tsx}"
510
613
  ],
511
- reporter: ["text"],
614
+ // Per-file threshold errors still identify omitted Scene files; the
615
+ // summary avoids printing a large coverage table on every successful run.
616
+ reporter: ["text-summary"],
512
617
  thresholds: {
513
618
  perFile: true,
514
619
  lines: 1,
@@ -249,12 +249,15 @@ function auditGameplayCollection(options, file, tests) {
249
249
  function getGameplayEvidenceIssues(id, requirements, evidence) {
250
250
  const issues = [];
251
251
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
252
- if (requirements.requireInput && inputEvents === 0)
252
+ if (requirements.requireInput && inputEvents === 0) {
253
253
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
254
- if (requirements.requireFrameAdvance && evidence.frames === 0)
254
+ }
255
+ if (requirements.requireFrameAdvance && evidence.frames === 0) {
255
256
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
256
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0)
257
+ }
258
+ if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
257
259
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
260
+ }
258
261
  for (const target of normalizeList(requirements.requireTransition)) {
259
262
  if (!evidence.transitions.some((transition) => transition.to === target)) {
260
263
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
@@ -265,21 +268,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
265
268
  issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
266
269
  }
267
270
  for (const scene of normalizeList(requirements.requireRestart)) {
268
- if (!evidence.restartedScenes.includes(scene))
271
+ if (!evidence.restartedScenes.includes(scene)) {
269
272
  issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
273
+ }
270
274
  }
271
275
  for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
272
- if (!evidence.checkpoints.includes(checkpoint))
276
+ if (!evidence.checkpoints.includes(checkpoint)) {
273
277
  issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
278
+ }
274
279
  }
275
- if (requirements.requireDestroy && !evidence.destroyed)
280
+ if (requirements.requireDestroy && !evidence.destroyed) {
276
281
  issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
282
+ }
277
283
  return issues;
278
284
  }
279
285
  function auditGameplayRun(options, tests) {
280
286
  const issues = [];
281
287
  const actualContracts = tests.filter((test) => test.metadata);
282
- const acceptedEvidence = [];
288
+ const observedEvidence = [];
283
289
  for (const expected of options.contracts) {
284
290
  const matches = actualContracts.filter(
285
291
  (test2) => test2.metadata?.id === expected.id
@@ -295,6 +301,7 @@ function auditGameplayRun(options, tests) {
295
301
  const test = matches[0];
296
302
  const metadata = test?.metadata;
297
303
  if (!test || !metadata) continue;
304
+ if (metadata.evidence) observedEvidence.push(metadata.evidence);
298
305
  if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
299
306
  issues.push(
300
307
  `contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
@@ -315,7 +322,6 @@ function auditGameplayRun(options, tests) {
315
322
  metadata.evidence
316
323
  );
317
324
  issues.push(...evidenceIssues);
318
- if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
319
325
  }
320
326
  for (const test of actualContracts) {
321
327
  const id = test.metadata?.id;
@@ -326,14 +332,15 @@ function auditGameplayRun(options, tests) {
326
332
  }
327
333
  }
328
334
  const registeredScenes = normalizeSet(
329
- acceptedEvidence.flatMap((evidence) => evidence.registeredScenes)
335
+ observedEvidence.flatMap((evidence) => evidence.registeredScenes)
330
336
  );
331
337
  const expectedScenes = normalizeSet(options.scenes);
332
338
  for (const scene of expectedScenes) {
333
- if (!registeredScenes.includes(scene))
339
+ if (!registeredScenes.includes(scene)) {
334
340
  issues.push(
335
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u4EFB\u4F55\u901A\u8FC7\u5BA1\u8BA1\u7684 host registry \u4E2D\u3002`
341
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
336
342
  );
343
+ }
337
344
  }
338
345
  for (const scene of registeredScenes) {
339
346
  if (!expectedScenes.includes(scene))
@@ -363,11 +370,96 @@ function toAuditTest(test) {
363
370
  metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
364
371
  };
365
372
  }
373
+ function formatGameplayIssues(issues) {
374
+ const groups = /* @__PURE__ */ new Map();
375
+ const repairKeys = /* @__PURE__ */ new Set();
376
+ const repairs = [];
377
+ const repairText = {
378
+ input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
379
+ frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
380
+ physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
381
+ transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
382
+ visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
383
+ restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
384
+ checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
385
+ destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
386
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
387
+ missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
388
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
389
+ };
390
+ const getCategory = (issue) => {
391
+ if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
392
+ if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
393
+ if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
394
+ if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
395
+ const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
396
+ return { category: "transition", key: `transition:${target}` };
397
+ }
398
+ if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
399
+ const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
400
+ return { category: "visit", key: `visit:${scene}` };
401
+ }
402
+ if (issue.includes("\u672A\u91CD\u542F Scene")) {
403
+ const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
404
+ return { category: "restart", key: `restart:${scene}` };
405
+ }
406
+ if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
407
+ if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
408
+ if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
409
+ const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
410
+ return { category: "registry", key: `registry:${scene}` };
411
+ }
412
+ if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
413
+ if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
414
+ return { category: "other", key: `other:${issue}` };
415
+ };
416
+ for (const rawIssue of [...new Set(issues)]) {
417
+ const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
418
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
419
+ const id = compact.match(/contract "([^"]+)"/)?.[1];
420
+ const { category, key } = getCategory(compact);
421
+ const group = groups.get(key);
422
+ if (group) {
423
+ if (id && !group.ids.includes(id)) group.ids.push(id);
424
+ } else {
425
+ groups.set(key, { text: readable, ids: id ? [id] : [], key });
426
+ }
427
+ if (repairText[category] && !repairKeys.has(category)) {
428
+ repairKeys.add(category);
429
+ repairs.push(repairText[category]);
430
+ }
431
+ }
432
+ const grouped = [...groups.values()];
433
+ const visible = grouped.slice(0, 8).map((group) => {
434
+ if (group.ids.length <= 1) return group.text;
435
+ return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
436
+ });
437
+ return {
438
+ issues: visible.map(
439
+ (issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
440
+ ),
441
+ repairs: repairs.slice(0, 8),
442
+ omitted: Math.max(0, grouped.length - visible.length)
443
+ };
444
+ }
366
445
  function printIssues(stage, issues) {
367
446
  if (issues.length === 0) return;
368
447
  console.error(`
369
448
  GAMEPLAY_AUDIT: ${stage} FAILED`);
370
- for (const issue of issues) console.error(`- ${issue}`);
449
+ console.error(
450
+ "\u73A9\u6CD5\u68C0\u67E5\u5931\u8D25\uFF1A\u4EE5\u4E0B\u6BCF\u4E2A\u68C0\u67E5\u9879\u5BF9\u5E94\u4E00\u4E2A\u9700\u8981\u6D4B\u8BD5\u8BC1\u660E\u7684\u6E38\u620F\u884C\u4E3A\u3002"
451
+ );
452
+ const formatted = formatGameplayIssues(issues);
453
+ for (const issue of formatted.issues) console.error(`- ${issue}`);
454
+ if (formatted.repairs.length > 0) {
455
+ console.error("\u4FEE\u590D\u5EFA\u8BAE\uFF08\u540C\u7C7B\u95EE\u9898\u53EA\u5217\u4E00\u6B21\uFF09\uFF1A");
456
+ for (const repair of formatted.repairs) console.error(`- ${repair}`);
457
+ }
458
+ if (formatted.omitted > 0) {
459
+ console.error(
460
+ `- \u5176\u4F59 ${formatted.omitted} \u7C7B\u95EE\u9898\u5DF2\u7701\u7565\uFF1B\u5148\u4FEE\u590D\u4EE5\u4E0A\u95EE\u9898\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002`
461
+ );
462
+ }
371
463
  }
372
464
  var GameplayAuditReporter = class {
373
465
  projectRoot;
@@ -408,21 +500,30 @@ var GameplayAuditReporter = class {
408
500
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
409
501
  );
410
502
  const result = auditGameplayRun(this.options, tests);
411
- if (!result.passed) {
412
- printIssues("FINAL", result.issues);
503
+ const combinedIssues = [
504
+ ...this.preflightIssues,
505
+ ...result.issues
506
+ ];
507
+ if (combinedIssues.length > 0) {
508
+ if (this.preflightIssues.size > 0) {
509
+ console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
510
+ }
511
+ printIssues("FINAL", [...new Set(combinedIssues)]);
413
512
  process.exitCode = 1;
414
513
  } else {
415
514
  console.log(
416
515
  `
417
516
  GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
418
517
  );
518
+ console.log(
519
+ "GAMEPLAY_AUDIT_NOTE: \u73A9\u6CD5\u68C0\u67E5\u5DF2\u901A\u8FC7\uFF1B\u4ECD\u9700\u4EE5\u6700\u540E\u7684 TEST_RESULT \u5224\u65AD\u6574\u4F53\u6210\u529F\u3002"
520
+ );
419
521
  }
420
522
  }
421
523
  /** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
422
524
  reportPreflightIssue(issue) {
423
525
  if (this.preflightIssues.has(issue)) return;
424
526
  this.preflightIssues.add(issue);
425
- printIssues("PREFLIGHT", [issue]);
426
527
  process.exitCode = 1;
427
528
  }
428
529
  };
@@ -472,7 +573,9 @@ function defineGameVitestConfig(options) {
472
573
  ...options.additionalSetupFiles ?? []
473
574
  ],
474
575
  reporters: [
475
- "default",
576
+ // Vitest's minimal reporter is optimized for AI output: passing tests
577
+ // stay quiet while failed tests retain their useful error message.
578
+ "minimal",
476
579
  new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
477
580
  ],
478
581
  restoreMocks: true,
@@ -484,7 +587,9 @@ function defineGameVitestConfig(options) {
484
587
  "src/scenes/**/*.d.ts",
485
588
  "src/scenes/**/*.{spec,test}.{ts,tsx}"
486
589
  ],
487
- reporter: ["text"],
590
+ // Per-file threshold errors still identify omitted Scene files; the
591
+ // summary avoids printing a large coverage table on every successful run.
592
+ reporter: ["text-summary"],
488
593
  thresholds: {
489
594
  perFile: true,
490
595
  lines: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Shared lint and deterministic Phaser HEADLESS testing tools for Miaoda games",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -43,18 +43,6 @@
43
43
  "!dist/**/*.map",
44
44
  "!dist/lint/**/*.test.*"
45
45
  ],
46
- "scripts": {
47
- "build": "tsup",
48
- "prepack": "pnpm run build",
49
- "release": "pnpm run test && clean-publish",
50
- "release:dry-run": "pnpm run test && clean-publish --dry-run",
51
- "release:beta": "pnpm run test && npm version prerelease --preid=beta && clean-publish --tag beta",
52
- "release:patch": "pnpm run test && npm version patch && clean-publish",
53
- "release:minor": "pnpm run test && npm version minor && clean-publish",
54
- "release:major": "pnpm run test && npm version major && clean-publish",
55
- "typecheck": "tsc --noEmit",
56
- "test": "pnpm run build && ./bin/miaoda-game-lint.js --contracts-only"
57
- },
58
46
  "nx": {
59
47
  "tags": [
60
48
  "type:package",
@@ -94,23 +82,8 @@
94
82
  "peerDependencies": {
95
83
  "phaser": ">=4.0.0 <5"
96
84
  },
97
- "devDependencies": {
98
- "@types/eslint": "^9.6.1",
99
- "@types/estree": "^1.0.9",
100
- "@types/node": "^24.13.3",
101
- "clean-publish": "^6.0.5",
102
- "phaser": "4.0.0",
103
- "tsup": "^8.5.1",
104
- "typescript": "^5.9.3"
105
- },
106
85
  "publishConfig": {
107
86
  "registry": "https://registry.npmjs.org/",
108
87
  "access": "public"
109
- },
110
- "clean-publish": {
111
- "fields": [
112
- "devDependencies",
113
- "scripts"
114
- ]
115
88
  }
116
89
  }