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.
@@ -7,6 +7,8 @@ import { resolve } from "path";
7
7
  import { defineConfig } from "vitest/config";
8
8
 
9
9
  // src/gameplay-audit.ts
10
+ import { readdirSync, readFileSync, statSync } from "fs";
11
+ import { join } from "path";
10
12
  function normalizeList(value) {
11
13
  if (value === void 0) return [];
12
14
  const values = typeof value === "string" ? [value] : value;
@@ -40,20 +42,35 @@ function createGameplayContractMetadata(contract) {
40
42
  const scenes = normalizeSet(contract.scenes);
41
43
  return {
42
44
  id: contract.id.trim(),
45
+ kind: contract.kind,
43
46
  scenes,
44
47
  requirements: normalizeGameplayRequirements(contract, scenes),
45
48
  verified: false
46
49
  };
47
50
  }
48
- function validateGameplayAuditOptions(options) {
51
+ function validateGameplayAuditOptions(options, projectRoot2) {
49
52
  const issues = [];
50
- const scenes = normalizeSet(options.scenes);
53
+ const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
54
+ const mode = options.mode ?? "interactive";
55
+ if (mode === "non-interactive") {
56
+ const justification = options.nonInteractiveJustification?.trim() ?? "";
57
+ if (justification.length < 20) {
58
+ issues.push(
59
+ "non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
60
+ );
61
+ }
62
+ }
63
+ if (mode !== "interactive" && mode !== "non-interactive") {
64
+ issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
65
+ }
51
66
  if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
52
67
  if (scenes.length !== options.scenes.length) {
53
68
  issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
54
69
  }
55
70
  const contractIds = /* @__PURE__ */ new Set();
56
71
  const coveredScenes = /* @__PURE__ */ new Set();
72
+ const kinds = /* @__PURE__ */ new Map();
73
+ const invalidKindContracts = [];
57
74
  for (const contract of options.contracts) {
58
75
  const id = contract.id.trim();
59
76
  if (!id) {
@@ -72,6 +89,27 @@ function validateGameplayAuditOptions(options) {
72
89
  );
73
90
  }
74
91
  const contractScenes = normalizeSet(contract.scenes);
92
+ if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
93
+ invalidKindContracts.push(id || "<empty>");
94
+ } else {
95
+ const list = kinds.get(contract.kind) ?? [];
96
+ list.push(contract);
97
+ kinds.set(contract.kind, list);
98
+ }
99
+ if (contract.kind === "playthrough") {
100
+ if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
101
+ if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
102
+ if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
103
+ }
104
+ if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
105
+ issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
106
+ }
107
+ if (contract.kind === "recovery") {
108
+ if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
109
+ if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
110
+ if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
111
+ 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`);
112
+ }
75
113
  if (contractScenes.length === 0) {
76
114
  issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
77
115
  }
@@ -84,6 +122,11 @@ function validateGameplayAuditOptions(options) {
84
122
  }
85
123
  }
86
124
  }
125
+ if (invalidKindContracts.length > 0) {
126
+ issues.push(
127
+ `\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`
128
+ );
129
+ }
87
130
  for (const scene of scenes) {
88
131
  if (!coveredScenes.has(scene)) {
89
132
  issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
@@ -91,11 +134,71 @@ function validateGameplayAuditOptions(options) {
91
134
  }
92
135
  if (options.contracts.length === 0)
93
136
  issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
137
+ if (mode === "interactive") {
138
+ const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
139
+ const hasInput = options.contracts.some((contract) => contract.requireInput);
140
+ const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
141
+ if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
142
+ issues.push(
143
+ '\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'
144
+ );
145
+ }
146
+ const entryScene = scenes[0];
147
+ for (const scene of scenes.slice(1)) {
148
+ const transitioned = options.contracts.some(
149
+ (contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
150
+ );
151
+ if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
152
+ }
153
+ if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
154
+ issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
155
+ }
156
+ const restartSignal = projectRoot2 ? containsProductionRestart(projectRoot2) : false;
157
+ if (restartSignal && options.flows?.restart !== true) {
158
+ 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");
159
+ } else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
160
+ issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
161
+ } else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
162
+ issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
163
+ }
164
+ }
94
165
  return issues;
95
166
  }
167
+ function containsProductionRestart(projectRoot2) {
168
+ const sceneRoot = join(projectRoot2, "src", "scenes");
169
+ const visit = (directory) => {
170
+ let entries;
171
+ try {
172
+ entries = readdirSync(directory);
173
+ } catch {
174
+ return false;
175
+ }
176
+ for (const entry of entries) {
177
+ const path = join(directory, entry);
178
+ let stats;
179
+ try {
180
+ stats = statSync(path);
181
+ } catch {
182
+ continue;
183
+ }
184
+ if (stats.isDirectory() && visit(path)) return true;
185
+ if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
186
+ try {
187
+ const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
188
+ if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
189
+ } catch {
190
+ }
191
+ }
192
+ return false;
193
+ };
194
+ return visit(sceneRoot);
195
+ }
96
196
  function getDefinitionIssues(expected, actual) {
97
197
  const issues = [];
98
198
  const expectedMetadata = createGameplayContractMetadata(expected);
199
+ if (expectedMetadata.kind !== actual.kind) {
200
+ issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
201
+ }
99
202
  if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
100
203
  issues.push(
101
204
  `contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
@@ -150,12 +253,15 @@ function auditGameplayCollection(options, file, tests) {
150
253
  function getGameplayEvidenceIssues(id, requirements, evidence) {
151
254
  const issues = [];
152
255
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
153
- if (requirements.requireInput && inputEvents === 0)
256
+ if (requirements.requireInput && inputEvents === 0) {
154
257
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
155
- if (requirements.requireFrameAdvance && evidence.frames === 0)
258
+ }
259
+ if (requirements.requireFrameAdvance && evidence.frames === 0) {
156
260
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
157
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0)
261
+ }
262
+ if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
158
263
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
264
+ }
159
265
  for (const target of normalizeList(requirements.requireTransition)) {
160
266
  if (!evidence.transitions.some((transition) => transition.to === target)) {
161
267
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
@@ -166,21 +272,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
166
272
  issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
167
273
  }
168
274
  for (const scene of normalizeList(requirements.requireRestart)) {
169
- if (!evidence.restartedScenes.includes(scene))
275
+ if (!evidence.restartedScenes.includes(scene)) {
170
276
  issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
277
+ }
171
278
  }
172
279
  for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
173
- if (!evidence.checkpoints.includes(checkpoint))
280
+ if (!evidence.checkpoints.includes(checkpoint)) {
174
281
  issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
282
+ }
175
283
  }
176
- if (requirements.requireDestroy && !evidence.destroyed)
284
+ if (requirements.requireDestroy && !evidence.destroyed) {
177
285
  issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
286
+ }
178
287
  return issues;
179
288
  }
180
289
  function auditGameplayRun(options, tests) {
181
290
  const issues = [];
182
291
  const actualContracts = tests.filter((test) => test.metadata);
183
- const acceptedEvidence = [];
292
+ const observedEvidence = [];
184
293
  for (const expected of options.contracts) {
185
294
  const matches = actualContracts.filter(
186
295
  (test2) => test2.metadata?.id === expected.id
@@ -196,6 +305,7 @@ function auditGameplayRun(options, tests) {
196
305
  const test = matches[0];
197
306
  const metadata = test?.metadata;
198
307
  if (!test || !metadata) continue;
308
+ if (metadata.evidence) observedEvidence.push(metadata.evidence);
199
309
  if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
200
310
  issues.push(
201
311
  `contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
@@ -216,7 +326,6 @@ function auditGameplayRun(options, tests) {
216
326
  metadata.evidence
217
327
  );
218
328
  issues.push(...evidenceIssues);
219
- if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
220
329
  }
221
330
  for (const test of actualContracts) {
222
331
  const id = test.metadata?.id;
@@ -227,14 +336,15 @@ function auditGameplayRun(options, tests) {
227
336
  }
228
337
  }
229
338
  const registeredScenes = normalizeSet(
230
- acceptedEvidence.flatMap((evidence) => evidence.registeredScenes)
339
+ observedEvidence.flatMap((evidence) => evidence.registeredScenes)
231
340
  );
232
341
  const expectedScenes = normalizeSet(options.scenes);
233
342
  for (const scene of expectedScenes) {
234
- if (!registeredScenes.includes(scene))
343
+ if (!registeredScenes.includes(scene)) {
235
344
  issues.push(
236
- `\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`
237
346
  );
347
+ }
238
348
  }
239
349
  for (const scene of registeredScenes) {
240
350
  if (!expectedScenes.includes(scene))
@@ -264,11 +374,96 @@ function toAuditTest(test) {
264
374
  metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
265
375
  };
266
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
+ }
267
449
  function printIssues(stage, issues) {
268
450
  if (issues.length === 0) return;
269
451
  console.error(`
270
452
  GAMEPLAY_AUDIT: ${stage} FAILED`);
271
- 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
+ }
272
467
  }
273
468
  var GameplayAuditReporter = class {
274
469
  projectRoot;
@@ -309,28 +504,37 @@ var GameplayAuditReporter = class {
309
504
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
310
505
  );
311
506
  const result = auditGameplayRun(this.options, tests);
312
- if (!result.passed) {
313
- 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)]);
314
516
  process.exitCode = 1;
315
517
  } else {
316
518
  console.log(
317
519
  `
318
520
  GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
319
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
+ );
320
525
  }
321
526
  }
322
527
  /** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
323
528
  reportPreflightIssue(issue) {
324
529
  if (this.preflightIssues.has(issue)) return;
325
530
  this.preflightIssues.add(issue);
326
- printIssues("PREFLIGHT", [issue]);
327
531
  process.exitCode = 1;
328
532
  }
329
533
  };
330
534
 
331
535
  // src/vitest-config.ts
332
536
  function defineGameVitestConfig(options) {
333
- const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
537
+ const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
334
538
  if (auditIssues.length > 0) {
335
539
  throw new Error(
336
540
  `\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
@@ -373,7 +577,9 @@ function defineGameVitestConfig(options) {
373
577
  ...options.additionalSetupFiles ?? []
374
578
  ],
375
579
  reporters: [
376
- "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",
377
583
  new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
378
584
  ],
379
585
  restoreMocks: true,
@@ -385,7 +591,9 @@ function defineGameVitestConfig(options) {
385
591
  "src/scenes/**/*.d.ts",
386
592
  "src/scenes/**/*.{spec,test}.{ts,tsx}"
387
593
  ],
388
- 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"],
389
597
  thresholds: {
390
598
  perFile: true,
391
599
  lines: 1,
@@ -400,10 +608,13 @@ function defineGameVitestConfig(options) {
400
608
  // src/lint/vitest-config.test.ts
401
609
  var projectRoot = resolve2(import.meta.dirname, "../..");
402
610
  var gameplayAudit = {
611
+ mode: "non-interactive",
612
+ nonInteractiveJustification: "\u56FA\u5B9A\u5939\u5177\u53EA\u9A8C\u8BC1\u5171\u4EAB host \u57FA\u7EBF\uFF0C\u4E0D\u5305\u542B\u73A9\u6CD5\u8F93\u5165\u3002",
403
613
  scenes: ["InputScene"],
404
614
  contracts: [
405
615
  {
406
616
  id: "InputScene.boot",
617
+ kind: "boot",
407
618
  testFile: "tests/input.test.ts",
408
619
  scenes: ["InputScene"],
409
620
  requireFrameAdvance: true
@@ -429,7 +640,7 @@ describe("defineGameVitestConfig", () => {
429
640
  environment: "jsdom",
430
641
  environmentOptions: { jsdom: { url: "http://localhost/" } },
431
642
  setupFiles: ["miaoda-game-devkit/vitest-setup"],
432
- reporters: ["default", expect.any(Object)],
643
+ reporters: ["minimal", expect.any(Object)],
433
644
  restoreMocks: true,
434
645
  clearMocks: true,
435
646
  coverage: {
@@ -439,7 +650,7 @@ describe("defineGameVitestConfig", () => {
439
650
  "src/scenes/**/*.d.ts",
440
651
  "src/scenes/**/*.{spec,test}.{ts,tsx}"
441
652
  ],
442
- reporter: ["text"],
653
+ reporter: ["text-summary"],
443
654
  thresholds: {
444
655
  perFile: true,
445
656
  lines: 1,
@@ -476,4 +687,16 @@ describe("defineGameVitestConfig", () => {
476
687
  })
477
688
  ).toThrow(/ResultScene.*没有任何 gameplay contract/s);
478
689
  });
690
+ it("\u62D2\u7EDD\u4EA4\u4E92\u9879\u76EE\u53EA\u6709 boot smoke test", () => {
691
+ expect(
692
+ () => defineGameVitestConfig({
693
+ projectRoot,
694
+ gameplayAudit: {
695
+ mode: "interactive",
696
+ scenes: ["InputScene"],
697
+ contracts: gameplayAudit.contracts
698
+ }
699
+ })
700
+ ).toThrow(/缺少最低玩法基线/);
701
+ });
479
702
  });
@@ -1,5 +1,5 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { c as GameplayAuditOptions } from './gameplay-audit-CYqLL8gY.mjs';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-XBGq_jIV.mjs';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
@@ -1,5 +1,5 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { c as GameplayAuditOptions } from './gameplay-audit-CYqLL8gY.js';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-XBGq_jIV.js';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {