miaoda-game-devkit 0.2.2 → 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.
@@ -23,10 +23,12 @@ __export(vitest_config_exports, {
23
23
  defineGameVitestConfig: () => defineGameVitestConfig
24
24
  });
25
25
  module.exports = __toCommonJS(vitest_config_exports);
26
- var import_node_path2 = require("path");
26
+ var import_node_path3 = require("path");
27
27
  var import_config = require("vitest/config");
28
28
 
29
29
  // src/gameplay-audit.ts
30
+ var import_node_fs = require("fs");
31
+ var import_node_path = require("path");
30
32
  function normalizeList(value) {
31
33
  if (value === void 0) return [];
32
34
  const values = typeof value === "string" ? [value] : value;
@@ -60,20 +62,35 @@ function createGameplayContractMetadata(contract) {
60
62
  const scenes = normalizeSet(contract.scenes);
61
63
  return {
62
64
  id: contract.id.trim(),
65
+ kind: contract.kind,
63
66
  scenes,
64
67
  requirements: normalizeGameplayRequirements(contract, scenes),
65
68
  verified: false
66
69
  };
67
70
  }
68
- function validateGameplayAuditOptions(options) {
71
+ function validateGameplayAuditOptions(options, projectRoot) {
69
72
  const issues = [];
70
- const scenes = normalizeSet(options.scenes);
73
+ const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
74
+ const mode = options.mode ?? "interactive";
75
+ if (mode === "non-interactive") {
76
+ const justification = options.nonInteractiveJustification?.trim() ?? "";
77
+ if (justification.length < 20) {
78
+ issues.push(
79
+ "non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
80
+ );
81
+ }
82
+ }
83
+ if (mode !== "interactive" && mode !== "non-interactive") {
84
+ issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
85
+ }
71
86
  if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
72
87
  if (scenes.length !== options.scenes.length) {
73
88
  issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
74
89
  }
75
90
  const contractIds = /* @__PURE__ */ new Set();
76
91
  const coveredScenes = /* @__PURE__ */ new Set();
92
+ const kinds = /* @__PURE__ */ new Map();
93
+ const invalidKindContracts = [];
77
94
  for (const contract of options.contracts) {
78
95
  const id = contract.id.trim();
79
96
  if (!id) {
@@ -92,6 +109,27 @@ function validateGameplayAuditOptions(options) {
92
109
  );
93
110
  }
94
111
  const contractScenes = normalizeSet(contract.scenes);
112
+ if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
113
+ invalidKindContracts.push(id || "<empty>");
114
+ } else {
115
+ const list = kinds.get(contract.kind) ?? [];
116
+ list.push(contract);
117
+ kinds.set(contract.kind, list);
118
+ }
119
+ if (contract.kind === "playthrough") {
120
+ if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
121
+ if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
122
+ if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
123
+ }
124
+ if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
125
+ issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
126
+ }
127
+ if (contract.kind === "recovery") {
128
+ if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
129
+ if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
130
+ if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
131
+ if (normalizeList(contract.requireCheckpoint).length < 2) issues.push(`recovery contract "${id}" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002`);
132
+ }
95
133
  if (contractScenes.length === 0) {
96
134
  issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
97
135
  }
@@ -104,6 +142,11 @@ function validateGameplayAuditOptions(options) {
104
142
  }
105
143
  }
106
144
  }
145
+ if (invalidKindContracts.length > 0) {
146
+ issues.push(
147
+ `\u4EE5\u4E0B contract \u7F3A\u5C11\u6709\u6548 kind\uFF1A${invalidKindContracts.map((id) => `"${id}"`).join("\u3001")}\u3002\u8BF7\u4E3A\u6BCF\u4E2A contract \u8BBE\u7F6E kind: "boot"\u3001"playthrough"\u3001"transition" \u6216 "recovery"\u3002`
148
+ );
149
+ }
107
150
  for (const scene of scenes) {
108
151
  if (!coveredScenes.has(scene)) {
109
152
  issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
@@ -111,11 +154,71 @@ function validateGameplayAuditOptions(options) {
111
154
  }
112
155
  if (options.contracts.length === 0)
113
156
  issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
157
+ if (mode === "interactive") {
158
+ const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
159
+ const hasInput = options.contracts.some((contract) => contract.requireInput);
160
+ const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
161
+ if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
162
+ issues.push(
163
+ '\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A kind: "playthrough" contract \u5FC5\u987B\u540C\u65F6\u58F0\u660E requireInput: true\u3001requireFrameAdvance: true \u548C requireCheckpoint: ["progress"]\u3002'
164
+ );
165
+ }
166
+ const entryScene = scenes[0];
167
+ for (const scene of scenes.slice(1)) {
168
+ const transitioned = options.contracts.some(
169
+ (contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
170
+ );
171
+ if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
172
+ }
173
+ if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
174
+ issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
175
+ }
176
+ const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
177
+ if (restartSignal && options.flows?.restart !== true) {
178
+ issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF1B\u5FC5\u987B\u58F0\u660E flows.restart: true \u5E76\u63D0\u4F9B kind: recovery contract\u3002");
179
+ } else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
180
+ issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
181
+ } else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
182
+ issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
183
+ }
184
+ }
114
185
  return issues;
115
186
  }
187
+ function containsProductionRestart(projectRoot) {
188
+ const sceneRoot = (0, import_node_path.join)(projectRoot, "src", "scenes");
189
+ const visit = (directory) => {
190
+ let entries;
191
+ try {
192
+ entries = (0, import_node_fs.readdirSync)(directory);
193
+ } catch {
194
+ return false;
195
+ }
196
+ for (const entry of entries) {
197
+ const path = (0, import_node_path.join)(directory, entry);
198
+ let stats;
199
+ try {
200
+ stats = (0, import_node_fs.statSync)(path);
201
+ } catch {
202
+ continue;
203
+ }
204
+ if (stats.isDirectory() && visit(path)) return true;
205
+ if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
206
+ try {
207
+ const source = (0, import_node_fs.readFileSync)(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
208
+ if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
209
+ } catch {
210
+ }
211
+ }
212
+ return false;
213
+ };
214
+ return visit(sceneRoot);
215
+ }
116
216
  function getDefinitionIssues(expected, actual) {
117
217
  const issues = [];
118
218
  const expectedMetadata = createGameplayContractMetadata(expected);
219
+ if (expectedMetadata.kind !== actual.kind) {
220
+ issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
221
+ }
119
222
  if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
120
223
  issues.push(
121
224
  `contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
@@ -170,12 +273,15 @@ function auditGameplayCollection(options, file, tests) {
170
273
  function getGameplayEvidenceIssues(id, requirements, evidence) {
171
274
  const issues = [];
172
275
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
173
- if (requirements.requireInput && inputEvents === 0)
276
+ if (requirements.requireInput && inputEvents === 0) {
174
277
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
175
- if (requirements.requireFrameAdvance && evidence.frames === 0)
278
+ }
279
+ if (requirements.requireFrameAdvance && evidence.frames === 0) {
176
280
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
177
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0)
281
+ }
282
+ if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
178
283
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
284
+ }
179
285
  for (const target of normalizeList(requirements.requireTransition)) {
180
286
  if (!evidence.transitions.some((transition) => transition.to === target)) {
181
287
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
@@ -186,21 +292,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
186
292
  issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
187
293
  }
188
294
  for (const scene of normalizeList(requirements.requireRestart)) {
189
- if (!evidence.restartedScenes.includes(scene))
295
+ if (!evidence.restartedScenes.includes(scene)) {
190
296
  issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
297
+ }
191
298
  }
192
299
  for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
193
- if (!evidence.checkpoints.includes(checkpoint))
300
+ if (!evidence.checkpoints.includes(checkpoint)) {
194
301
  issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
302
+ }
195
303
  }
196
- if (requirements.requireDestroy && !evidence.destroyed)
304
+ if (requirements.requireDestroy && !evidence.destroyed) {
197
305
  issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
306
+ }
198
307
  return issues;
199
308
  }
200
309
  function auditGameplayRun(options, tests) {
201
310
  const issues = [];
202
311
  const actualContracts = tests.filter((test) => test.metadata);
203
- const acceptedEvidence = [];
312
+ const observedEvidence = [];
204
313
  for (const expected of options.contracts) {
205
314
  const matches = actualContracts.filter(
206
315
  (test2) => test2.metadata?.id === expected.id
@@ -216,6 +325,7 @@ function auditGameplayRun(options, tests) {
216
325
  const test = matches[0];
217
326
  const metadata = test?.metadata;
218
327
  if (!test || !metadata) continue;
328
+ if (metadata.evidence) observedEvidence.push(metadata.evidence);
219
329
  if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
220
330
  issues.push(
221
331
  `contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
@@ -236,7 +346,6 @@ function auditGameplayRun(options, tests) {
236
346
  metadata.evidence
237
347
  );
238
348
  issues.push(...evidenceIssues);
239
- if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
240
349
  }
241
350
  for (const test of actualContracts) {
242
351
  const id = test.metadata?.id;
@@ -247,14 +356,15 @@ function auditGameplayRun(options, tests) {
247
356
  }
248
357
  }
249
358
  const registeredScenes = normalizeSet(
250
- acceptedEvidence.flatMap((evidence) => evidence.registeredScenes)
359
+ observedEvidence.flatMap((evidence) => evidence.registeredScenes)
251
360
  );
252
361
  const expectedScenes = normalizeSet(options.scenes);
253
362
  for (const scene of expectedScenes) {
254
- if (!registeredScenes.includes(scene))
363
+ if (!registeredScenes.includes(scene)) {
255
364
  issues.push(
256
- `\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`
257
366
  );
367
+ }
258
368
  }
259
369
  for (const scene of registeredScenes) {
260
370
  if (!expectedScenes.includes(scene))
@@ -266,7 +376,7 @@ function auditGameplayRun(options, tests) {
266
376
  }
267
377
 
268
378
  // src/gameplay-audit-reporter.ts
269
- var import_node_path = require("path");
379
+ var import_node_path2 = require("path");
270
380
  function normalizePath(path) {
271
381
  return path.replaceAll("\\", "/").replace(/^\.\//, "");
272
382
  }
@@ -284,11 +394,96 @@ function toAuditTest(test) {
284
394
  metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
285
395
  };
286
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
+ }
287
469
  function printIssues(stage, issues) {
288
470
  if (issues.length === 0) return;
289
471
  console.error(`
290
472
  GAMEPLAY_AUDIT: ${stage} FAILED`);
291
- 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
+ }
292
487
  }
293
488
  var GameplayAuditReporter = class {
294
489
  projectRoot;
@@ -304,7 +499,7 @@ var GameplayAuditReporter = class {
304
499
  this.preflightIssues.clear();
305
500
  const scheduledFiles = new Set(
306
501
  specifications.map(
307
- (specification) => normalizePath((0, import_node_path.relative)(this.projectRoot, specification.moduleId))
502
+ (specification) => normalizePath((0, import_node_path2.relative)(this.projectRoot, specification.moduleId))
308
503
  )
309
504
  );
310
505
  for (const contract of this.options.contracts) {
@@ -329,28 +524,37 @@ var GameplayAuditReporter = class {
329
524
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
330
525
  );
331
526
  const result = auditGameplayRun(this.options, tests);
332
- if (!result.passed) {
333
- 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)]);
334
536
  process.exitCode = 1;
335
537
  } else {
336
538
  console.log(
337
539
  `
338
540
  GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
339
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
+ );
340
545
  }
341
546
  }
342
547
  /** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
343
548
  reportPreflightIssue(issue) {
344
549
  if (this.preflightIssues.has(issue)) return;
345
550
  this.preflightIssues.add(issue);
346
- printIssues("PREFLIGHT", [issue]);
347
551
  process.exitCode = 1;
348
552
  }
349
553
  };
350
554
 
351
555
  // src/vitest-config.ts
352
556
  function defineGameVitestConfig(options) {
353
- const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
557
+ const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
354
558
  if (auditIssues.length > 0) {
355
559
  throw new Error(
356
560
  `\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
@@ -361,7 +565,7 @@ function defineGameVitestConfig(options) {
361
565
  resolve: {
362
566
  alias: {
363
567
  ...options.aliases,
364
- "@": (0, import_node_path2.resolve)(options.projectRoot, "src")
568
+ "@": (0, import_node_path3.resolve)(options.projectRoot, "src")
365
569
  }
366
570
  },
367
571
  // devkit 作为 external ESM 加载时,应用测试也必须 externalize Phaser,
@@ -393,7 +597,9 @@ function defineGameVitestConfig(options) {
393
597
  ...options.additionalSetupFiles ?? []
394
598
  ],
395
599
  reporters: [
396
- "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",
397
603
  new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
398
604
  ],
399
605
  restoreMocks: true,
@@ -405,7 +611,9 @@ function defineGameVitestConfig(options) {
405
611
  "src/scenes/**/*.d.ts",
406
612
  "src/scenes/**/*.{spec,test}.{ts,tsx}"
407
613
  ],
408
- 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"],
409
617
  thresholds: {
410
618
  perFile: true,
411
619
  lines: 1,