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.
@@ -3,6 +3,8 @@ import { resolve } from "path";
3
3
  import { defineConfig } from "vitest/config";
4
4
 
5
5
  // src/gameplay-audit.ts
6
+ import { readdirSync, readFileSync, statSync } from "fs";
7
+ import { join } from "path";
6
8
  function normalizeList(value) {
7
9
  if (value === void 0) return [];
8
10
  const values = typeof value === "string" ? [value] : value;
@@ -36,20 +38,35 @@ function createGameplayContractMetadata(contract) {
36
38
  const scenes = normalizeSet(contract.scenes);
37
39
  return {
38
40
  id: contract.id.trim(),
41
+ kind: contract.kind,
39
42
  scenes,
40
43
  requirements: normalizeGameplayRequirements(contract, scenes),
41
44
  verified: false
42
45
  };
43
46
  }
44
- function validateGameplayAuditOptions(options) {
47
+ function validateGameplayAuditOptions(options, projectRoot) {
45
48
  const issues = [];
46
- const scenes = normalizeSet(options.scenes);
49
+ const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
50
+ const mode = options.mode ?? "interactive";
51
+ if (mode === "non-interactive") {
52
+ const justification = options.nonInteractiveJustification?.trim() ?? "";
53
+ if (justification.length < 20) {
54
+ issues.push(
55
+ "non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
56
+ );
57
+ }
58
+ }
59
+ if (mode !== "interactive" && mode !== "non-interactive") {
60
+ issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
61
+ }
47
62
  if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
48
63
  if (scenes.length !== options.scenes.length) {
49
64
  issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
50
65
  }
51
66
  const contractIds = /* @__PURE__ */ new Set();
52
67
  const coveredScenes = /* @__PURE__ */ new Set();
68
+ const kinds = /* @__PURE__ */ new Map();
69
+ const invalidKindContracts = [];
53
70
  for (const contract of options.contracts) {
54
71
  const id = contract.id.trim();
55
72
  if (!id) {
@@ -68,6 +85,27 @@ function validateGameplayAuditOptions(options) {
68
85
  );
69
86
  }
70
87
  const contractScenes = normalizeSet(contract.scenes);
88
+ if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
89
+ invalidKindContracts.push(id || "<empty>");
90
+ } else {
91
+ const list = kinds.get(contract.kind) ?? [];
92
+ list.push(contract);
93
+ kinds.set(contract.kind, list);
94
+ }
95
+ if (contract.kind === "playthrough") {
96
+ if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
97
+ if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
98
+ if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
99
+ }
100
+ if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
101
+ issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
102
+ }
103
+ if (contract.kind === "recovery") {
104
+ if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
105
+ if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
106
+ if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
107
+ 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`);
108
+ }
71
109
  if (contractScenes.length === 0) {
72
110
  issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
73
111
  }
@@ -80,6 +118,11 @@ function validateGameplayAuditOptions(options) {
80
118
  }
81
119
  }
82
120
  }
121
+ if (invalidKindContracts.length > 0) {
122
+ issues.push(
123
+ `\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`
124
+ );
125
+ }
83
126
  for (const scene of scenes) {
84
127
  if (!coveredScenes.has(scene)) {
85
128
  issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
@@ -87,11 +130,71 @@ function validateGameplayAuditOptions(options) {
87
130
  }
88
131
  if (options.contracts.length === 0)
89
132
  issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
133
+ if (mode === "interactive") {
134
+ const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
135
+ const hasInput = options.contracts.some((contract) => contract.requireInput);
136
+ const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
137
+ if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
138
+ issues.push(
139
+ '\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'
140
+ );
141
+ }
142
+ const entryScene = scenes[0];
143
+ for (const scene of scenes.slice(1)) {
144
+ const transitioned = options.contracts.some(
145
+ (contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
146
+ );
147
+ if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
148
+ }
149
+ if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
150
+ issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
151
+ }
152
+ const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
153
+ if (restartSignal && options.flows?.restart !== true) {
154
+ 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");
155
+ } else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
156
+ issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
157
+ } else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
158
+ issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
159
+ }
160
+ }
90
161
  return issues;
91
162
  }
163
+ function containsProductionRestart(projectRoot) {
164
+ const sceneRoot = join(projectRoot, "src", "scenes");
165
+ const visit = (directory) => {
166
+ let entries;
167
+ try {
168
+ entries = readdirSync(directory);
169
+ } catch {
170
+ return false;
171
+ }
172
+ for (const entry of entries) {
173
+ const path = join(directory, entry);
174
+ let stats;
175
+ try {
176
+ stats = statSync(path);
177
+ } catch {
178
+ continue;
179
+ }
180
+ if (stats.isDirectory() && visit(path)) return true;
181
+ if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
182
+ try {
183
+ const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
184
+ if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
185
+ } catch {
186
+ }
187
+ }
188
+ return false;
189
+ };
190
+ return visit(sceneRoot);
191
+ }
92
192
  function getDefinitionIssues(expected, actual) {
93
193
  const issues = [];
94
194
  const expectedMetadata = createGameplayContractMetadata(expected);
195
+ if (expectedMetadata.kind !== actual.kind) {
196
+ issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
197
+ }
95
198
  if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
96
199
  issues.push(
97
200
  `contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
@@ -146,12 +249,15 @@ function auditGameplayCollection(options, file, tests) {
146
249
  function getGameplayEvidenceIssues(id, requirements, evidence) {
147
250
  const issues = [];
148
251
  const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
149
- if (requirements.requireInput && inputEvents === 0)
252
+ if (requirements.requireInput && inputEvents === 0) {
150
253
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
151
- if (requirements.requireFrameAdvance && evidence.frames === 0)
254
+ }
255
+ if (requirements.requireFrameAdvance && evidence.frames === 0) {
152
256
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
153
- if (requirements.requirePhysicsStep && evidence.physicsSteps === 0)
257
+ }
258
+ if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
154
259
  issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
260
+ }
155
261
  for (const target of normalizeList(requirements.requireTransition)) {
156
262
  if (!evidence.transitions.some((transition) => transition.to === target)) {
157
263
  issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
@@ -162,21 +268,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
162
268
  issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
163
269
  }
164
270
  for (const scene of normalizeList(requirements.requireRestart)) {
165
- if (!evidence.restartedScenes.includes(scene))
271
+ if (!evidence.restartedScenes.includes(scene)) {
166
272
  issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
273
+ }
167
274
  }
168
275
  for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
169
- if (!evidence.checkpoints.includes(checkpoint))
276
+ if (!evidence.checkpoints.includes(checkpoint)) {
170
277
  issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
278
+ }
171
279
  }
172
- if (requirements.requireDestroy && !evidence.destroyed)
280
+ if (requirements.requireDestroy && !evidence.destroyed) {
173
281
  issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
282
+ }
174
283
  return issues;
175
284
  }
176
285
  function auditGameplayRun(options, tests) {
177
286
  const issues = [];
178
287
  const actualContracts = tests.filter((test) => test.metadata);
179
- const acceptedEvidence = [];
288
+ const observedEvidence = [];
180
289
  for (const expected of options.contracts) {
181
290
  const matches = actualContracts.filter(
182
291
  (test2) => test2.metadata?.id === expected.id
@@ -192,6 +301,7 @@ function auditGameplayRun(options, tests) {
192
301
  const test = matches[0];
193
302
  const metadata = test?.metadata;
194
303
  if (!test || !metadata) continue;
304
+ if (metadata.evidence) observedEvidence.push(metadata.evidence);
195
305
  if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
196
306
  issues.push(
197
307
  `contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
@@ -212,7 +322,6 @@ function auditGameplayRun(options, tests) {
212
322
  metadata.evidence
213
323
  );
214
324
  issues.push(...evidenceIssues);
215
- if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
216
325
  }
217
326
  for (const test of actualContracts) {
218
327
  const id = test.metadata?.id;
@@ -223,14 +332,15 @@ function auditGameplayRun(options, tests) {
223
332
  }
224
333
  }
225
334
  const registeredScenes = normalizeSet(
226
- acceptedEvidence.flatMap((evidence) => evidence.registeredScenes)
335
+ observedEvidence.flatMap((evidence) => evidence.registeredScenes)
227
336
  );
228
337
  const expectedScenes = normalizeSet(options.scenes);
229
338
  for (const scene of expectedScenes) {
230
- if (!registeredScenes.includes(scene))
339
+ if (!registeredScenes.includes(scene)) {
231
340
  issues.push(
232
- `\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`
233
342
  );
343
+ }
234
344
  }
235
345
  for (const scene of registeredScenes) {
236
346
  if (!expectedScenes.includes(scene))
@@ -260,11 +370,96 @@ function toAuditTest(test) {
260
370
  metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
261
371
  };
262
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
+ }
263
445
  function printIssues(stage, issues) {
264
446
  if (issues.length === 0) return;
265
447
  console.error(`
266
448
  GAMEPLAY_AUDIT: ${stage} FAILED`);
267
- 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
+ }
268
463
  }
269
464
  var GameplayAuditReporter = class {
270
465
  projectRoot;
@@ -305,28 +500,37 @@ var GameplayAuditReporter = class {
305
500
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
306
501
  );
307
502
  const result = auditGameplayRun(this.options, tests);
308
- if (!result.passed) {
309
- 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)]);
310
512
  process.exitCode = 1;
311
513
  } else {
312
514
  console.log(
313
515
  `
314
516
  GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
315
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
+ );
316
521
  }
317
522
  }
318
523
  /** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
319
524
  reportPreflightIssue(issue) {
320
525
  if (this.preflightIssues.has(issue)) return;
321
526
  this.preflightIssues.add(issue);
322
- printIssues("PREFLIGHT", [issue]);
323
527
  process.exitCode = 1;
324
528
  }
325
529
  };
326
530
 
327
531
  // src/vitest-config.ts
328
532
  function defineGameVitestConfig(options) {
329
- const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
533
+ const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
330
534
  if (auditIssues.length > 0) {
331
535
  throw new Error(
332
536
  `\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
@@ -369,7 +573,9 @@ function defineGameVitestConfig(options) {
369
573
  ...options.additionalSetupFiles ?? []
370
574
  ],
371
575
  reporters: [
372
- "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",
373
579
  new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
374
580
  ],
375
581
  restoreMocks: true,
@@ -381,7 +587,9 @@ function defineGameVitestConfig(options) {
381
587
  "src/scenes/**/*.d.ts",
382
588
  "src/scenes/**/*.{spec,test}.{ts,tsx}"
383
589
  ],
384
- 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"],
385
593
  thresholds: {
386
594
  perFile: true,
387
595
  lines: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.2.2",
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",