@playcraft/cli 0.0.55 → 0.0.57

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.
Files changed (170) hide show
  1. package/README.md +18 -0
  2. package/REMIX.md +115 -19
  3. package/dist/atom-tree/fusion-hints.js +8 -3
  4. package/dist/atom-tree/fusion-intent.js +62 -0
  5. package/dist/atom-tree/selection.js +3 -1
  6. package/dist/commands/create.js +299 -85
  7. package/dist/commands/image.js +79 -69
  8. package/dist/commands/levels.js +29 -0
  9. package/dist/commands/patterns.js +75 -0
  10. package/dist/commands/remix.js +93 -0
  11. package/dist/commands/skills.js +48 -17
  12. package/dist/index.js +9 -5
  13. package/dist/remix/clone-source.js +208 -0
  14. package/dist/remix/clone.js +256 -0
  15. package/dist/remix/local-git.js +56 -0
  16. package/dist/remix/pull.js +64 -0
  17. package/dist/remix/push.js +70 -0
  18. package/dist/scaffold/templates.js +1 -1
  19. package/dist/utils/agent-api-client.js +10 -3
  20. package/dist/utils/remix-image-attachments.js +4 -32
  21. package/dist/utils/sandbox-path.js +20 -0
  22. package/package.json +3 -3
  23. package/project-template/.claude/agents/creative-director.md +114 -0
  24. package/project-template/.claude/agents/developer.md +5 -2
  25. package/project-template/.claude/agents/game-designer.md +41 -20
  26. package/project-template/.claude/agents/refs/README.md +8 -0
  27. package/project-template/.claude/agents/refs/creative-director-guide.md +35 -0
  28. package/project-template/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  29. package/project-template/.claude/agents/refs/creative-discovery-guide.md +76 -0
  30. package/project-template/.claude/agents/refs/creative-north-star-examples.md +82 -0
  31. package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
  32. package/project-template/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  33. package/project-template/.claude/agents/refs/game-designer-gameplay-sufficiency.md +4 -4
  34. package/project-template/.claude/agents/reviewer.md +11 -0
  35. package/project-template/.claude/hooks/README.md +12 -12
  36. package/project-template/.claude/hooks/block-developer-preflight.mjs +49 -13
  37. package/project-template/.claude/hooks/block-ring1-tasklist.mjs +1 -1
  38. package/project-template/.claude/hooks/user-prompt.mjs +3 -13
  39. package/project-template/.claude/hooks/validate-workflow-stop.mjs +209 -12
  40. package/project-template/.claude/settings.json +2 -2
  41. package/project-template/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  42. package/project-template/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -1
  43. package/project-template/.claude/snapshot.mjs +0 -1
  44. package/project-template/.codebuddy/settings.json +1 -1
  45. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  46. package/project-template/CLAUDE.md +9 -9
  47. package/project-template/docs/project-state.md +1 -1
  48. package/project-template/docs/team/agent-conduct.md +18 -18
  49. package/project-template/docs/team/collaboration.md +19 -7
  50. package/project-template/docs/team/workflow-changelog.md +15 -0
  51. package/project-template/fusion-patterns/catalog.json +77 -0
  52. package/project-template/package.json +2 -0
  53. package/project-template/skills/_shared/scripts/create-playable.mjs +31 -11
  54. package/project-template/skills/_shared/scripts/dev-start.mjs +3 -3
  55. package/project-template/skills/_shared/scripts/disclosure-config.mjs +65 -39
  56. package/project-template/skills/_shared/scripts/harness-smoke.mjs +3 -1
  57. package/project-template/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  58. package/project-template/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  59. package/project-template/skills/_shared/scripts/slice-set.mjs +29 -2
  60. package/project-template/skills/_shared/scripts/state-get.mjs +124 -7
  61. package/project-template/skills/_shared/scripts/state-store.mjs +25 -2
  62. package/project-template/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  63. package/project-template/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  64. package/project-template/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  65. package/project-template/skills/playable-record/scripts/record-playable.mjs +2 -1
  66. package/project-template/skills/playable-record/scripts/verify-contract.mjs +4 -4
  67. package/project-template/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  68. package/project-template/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  69. package/project-template/skills/playable-record/scripts/verify-slice.mjs +2 -2
  70. package/project-template/templates/design-brief.template.md +34 -0
  71. package/project-template/templates/design-methodology.md +1 -2
  72. package/project-template/templates/design.template.md +65 -0
  73. package/project-template/templates/developer-log.template.md +12 -1
  74. package/project-template/templates/five-axis-framework.md +12 -0
  75. package/project-template/templates/game-designer-creative-process.template.md +99 -0
  76. package/project-template/templates/level-sketch.template.json +56 -0
  77. package/project-template/templates/north-star.template.md +43 -0
  78. package/project-template/templates/project-state.template.md +1 -1
  79. package/project-template/vite.config.ts +4 -5
  80. package/project-templates/phaser/.claude/agents/creative-director.md +114 -0
  81. package/project-templates/phaser/.claude/agents/game-designer.md +41 -20
  82. package/project-templates/phaser/.claude/agents/refs/creative-director-guide.md +35 -0
  83. package/project-templates/phaser/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  84. package/project-templates/phaser/.claude/agents/refs/creative-discovery-guide.md +76 -0
  85. package/project-templates/phaser/.claude/agents/refs/creative-north-star-examples.md +82 -0
  86. package/project-templates/phaser/.claude/agents/refs/developer-dev-handoff.md +1 -1
  87. package/project-templates/phaser/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  88. package/project-templates/phaser/.claude/agents/reviewer.md +11 -0
  89. package/project-templates/phaser/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  90. package/project-templates/phaser/.claude/hooks/user-prompt.mjs +17 -18
  91. package/project-templates/phaser/.claude/hooks/validate-workflow-stop.mjs +209 -12
  92. package/project-templates/phaser/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  93. package/project-templates/phaser/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  94. package/project-templates/phaser/CLAUDE.md +88 -54
  95. package/project-templates/phaser/docs/DESIGN.md +25 -0
  96. package/project-templates/phaser/docs/project-state.md +1 -1
  97. package/project-templates/phaser/docs/team/collaboration.md +19 -7
  98. package/project-templates/phaser/docs/team/workflow-changelog.md +14 -0
  99. package/project-templates/phaser/fusion-patterns/catalog.json +77 -0
  100. package/project-templates/phaser/package.json +3 -1
  101. package/project-templates/phaser/skills/_shared/scripts/dev-start.mjs +3 -3
  102. package/project-templates/phaser/skills/_shared/scripts/disclosure-config.mjs +65 -39
  103. package/project-templates/phaser/skills/_shared/scripts/harness-smoke.mjs +3 -1
  104. package/project-templates/phaser/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  105. package/project-templates/phaser/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  106. package/project-templates/phaser/skills/_shared/scripts/state-get.mjs +124 -7
  107. package/project-templates/phaser/skills/_shared/scripts/state-store.mjs +25 -2
  108. package/project-templates/phaser/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  109. package/project-templates/phaser/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  110. package/project-templates/phaser/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  111. package/project-templates/phaser/skills/playable-record/scripts/record-playable.mjs +2 -1
  112. package/project-templates/phaser/skills/playable-record/scripts/verify-contract.mjs +4 -4
  113. package/project-templates/phaser/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  114. package/project-templates/phaser/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  115. package/project-templates/phaser/skills/playable-record/scripts/verify-slice.mjs +2 -2
  116. package/project-templates/phaser/templates/design-brief.template.md +42 -11
  117. package/project-templates/phaser/templates/design-methodology.md +1 -2
  118. package/project-templates/phaser/templates/design.template.md +77 -0
  119. package/project-templates/phaser/templates/five-axis-framework.md +50 -1
  120. package/project-templates/phaser/templates/game-designer-creative-process.template.md +99 -0
  121. package/project-templates/phaser/templates/level-sketch.template.json +56 -0
  122. package/project-templates/phaser/templates/north-star.template.md +43 -0
  123. package/project-templates/phaser/templates/project-state.template.md +1 -1
  124. package/project-templates/phaser/vite.config.ts +4 -5
  125. package/project-templates/threejs/.claude/agents/creative-director.md +114 -0
  126. package/project-templates/threejs/.claude/agents/game-designer.md +41 -20
  127. package/project-templates/threejs/.claude/agents/refs/creative-director-guide.md +35 -0
  128. package/project-templates/threejs/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  129. package/project-templates/threejs/.claude/agents/refs/creative-discovery-guide.md +76 -0
  130. package/project-templates/threejs/.claude/agents/refs/creative-north-star-examples.md +82 -0
  131. package/project-templates/threejs/.claude/agents/refs/developer-dev-handoff.md +1 -1
  132. package/project-templates/threejs/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  133. package/project-templates/threejs/.claude/agents/reviewer.md +11 -0
  134. package/project-templates/threejs/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  135. package/project-templates/threejs/.claude/hooks/user-prompt.mjs +17 -18
  136. package/project-templates/threejs/.claude/hooks/validate-workflow-stop.mjs +209 -12
  137. package/project-templates/threejs/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  138. package/project-templates/threejs/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  139. package/project-templates/threejs/CLAUDE.md +88 -54
  140. package/project-templates/threejs/docs/DESIGN.md +25 -0
  141. package/project-templates/threejs/docs/project-state.md +1 -1
  142. package/project-templates/threejs/docs/team/collaboration.md +19 -7
  143. package/project-templates/threejs/docs/team/workflow-changelog.md +14 -0
  144. package/project-templates/threejs/fusion-patterns/catalog.json +77 -0
  145. package/project-templates/threejs/package.json +3 -1
  146. package/project-templates/threejs/skills/_shared/scripts/dev-start.mjs +3 -3
  147. package/project-templates/threejs/skills/_shared/scripts/disclosure-config.mjs +65 -39
  148. package/project-templates/threejs/skills/_shared/scripts/harness-smoke.mjs +3 -1
  149. package/project-templates/threejs/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  150. package/project-templates/threejs/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  151. package/project-templates/threejs/skills/_shared/scripts/state-get.mjs +124 -7
  152. package/project-templates/threejs/skills/_shared/scripts/state-store.mjs +25 -2
  153. package/project-templates/threejs/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  154. package/project-templates/threejs/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  155. package/project-templates/threejs/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  156. package/project-templates/threejs/skills/playable-record/scripts/record-playable.mjs +2 -1
  157. package/project-templates/threejs/skills/playable-record/scripts/verify-contract.mjs +4 -4
  158. package/project-templates/threejs/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  159. package/project-templates/threejs/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  160. package/project-templates/threejs/skills/playable-record/scripts/verify-slice.mjs +2 -2
  161. package/project-templates/threejs/templates/design-brief.template.md +42 -11
  162. package/project-templates/threejs/templates/design-methodology.md +1 -2
  163. package/project-templates/threejs/templates/design.template.md +77 -0
  164. package/project-templates/threejs/templates/five-axis-framework.md +50 -1
  165. package/project-templates/threejs/templates/game-designer-creative-process.template.md +99 -0
  166. package/project-templates/threejs/templates/level-sketch.template.json +56 -0
  167. package/project-templates/threejs/templates/north-star.template.md +43 -0
  168. package/project-templates/threejs/templates/project-state.template.md +1 -1
  169. package/project-templates/threejs/vite.config.ts +4 -5
  170. package/project-template/docs/DESIGN.md +0 -65
@@ -0,0 +1,728 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Semantic + consistency checks for Game Designer Ring 1 deliverables.
4
+ * Does NOT require GD to fill KPI numbers — metrics are derived.
5
+ */
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { join, resolve, basename } from 'node:path';
8
+ import { detectFusionIntent, readFusionIntentFromProject } from './lib/fusion-intent.mjs';
9
+ import { validateSkillFidelity } from './lib/skill-fidelity.mjs';
10
+ import { validateLevelSketch } from './validate-level-sketch.mjs';
11
+ import { parseDesignFusionType } from '../../playable-record/scripts/verify-fusion.mjs';
12
+
13
+ function parseArgs(argv) {
14
+ const opts = { root: process.cwd(), json: false };
15
+ for (const arg of argv) {
16
+ if (arg === '--json') opts.json = true;
17
+ else if (arg.startsWith('--root=')) opts.root = resolve(arg.slice('--root='.length));
18
+ }
19
+ return opts;
20
+ }
21
+
22
+ function readFile(root, rel) {
23
+ const p = join(root, rel);
24
+ return existsSync(p) ? readFileSync(p, 'utf8') : '';
25
+ }
26
+
27
+ /** @param {string} design */
28
+ export function validateAnimationSpine(design) {
29
+ const problems = [];
30
+ const hasSpineHeader =
31
+ /Feedback\s*&\s*Animation\s*Spine|Animation\s*Spine|Event\s*Catalog/i.test(design);
32
+ if (!hasSpineHeader) {
33
+ problems.push('DESIGN.md missing §8 Feedback & Animation Spine (Event Catalog table)');
34
+ return problems;
35
+ }
36
+ const tableRows = design.match(/^\|[^|\n]+\|/gm) ?? [];
37
+ const eventRows = tableRows.filter(
38
+ (row) =>
39
+ /slide_out|blocked|fly_to_tray|match_clear|win/i.test(row) &&
40
+ !/Event\s*\|/i.test(row) &&
41
+ !/^[-| ]+$/.test(row.trim()),
42
+ );
43
+ if (eventRows.length < 4) {
44
+ problems.push('§8 Event Catalog must include at least 4 event rows (slide_out, blocked, match_clear, …)');
45
+ }
46
+ const blob = design.toLowerCase();
47
+ for (const ev of ['slide_out', 'blocked', 'match_clear']) {
48
+ if (!blob.includes(ev)) {
49
+ problems.push(`§8 Animation Spine missing required event: ${ev}`);
50
+ }
51
+ }
52
+ return problems;
53
+ }
54
+
55
+ const GENERIC_FILLER_PATTERNS = [
56
+ /解谜.*箭头/i,
57
+ /tap.*三消/i,
58
+ /找出.*移除/i,
59
+ /像解谜一样/i,
60
+ /像.*一样找出/i,
61
+ /安全移除的箭头/i,
62
+ /普通.*三消/i,
63
+ ];
64
+
65
+ const MECHANISM_VERB_RE = /tap|click|watch|feed|slide|match|clear|remove|push|drag|点|推|消|移/i;
66
+ const MECHANISM_NOUN_RE =
67
+ /arrow|path|tray|board|tile|color|block|puzzle|elim|箭头|路径|托盘|棋盘|方块|消除/i;
68
+ const EMOTION_TENSION_RE =
69
+ /debt|tension|stakes|panic|relief|thrill|satisf|fear|hope|bet|wire|walk|钢丝|债务|压力|赌|紧张|爽|险|排雷|走钢丝/i;
70
+
71
+ /** @param {string} text */
72
+ export function isMechanismChainOneLiner(text) {
73
+ const t = String(text || '').trim();
74
+ if (t.length < 15) return false;
75
+ const hasMechanism = MECHANISM_VERB_RE.test(t) && MECHANISM_NOUN_RE.test(t);
76
+ const hasEmotion = EMOTION_TENSION_RE.test(t);
77
+ const segments = t.split(/[,,;;—–-]/).filter((s) => MECHANISM_VERB_RE.test(s));
78
+ return hasMechanism && !hasEmotion && segments.length >= 2;
79
+ }
80
+
81
+ /** @param {string} northStarMd */
82
+ function extractNorthStarOneLinerFromFile(northStarMd) {
83
+ const section = northStarMd.match(/##\s*One-liner([\s\S]*?)(?=\n##\s|$)/i)?.[1] ?? northStarMd;
84
+ const labeled = extractLabeledLine(section, 'One-liner') || extractLabeledLine(section, '一句话');
85
+ if (labeled) return labeled;
86
+ const lines = section
87
+ .split('\n')
88
+ .map((l) => l.trim())
89
+ .filter(Boolean);
90
+ return lines[0] ?? '';
91
+ }
92
+
93
+ /** @returns {boolean} */
94
+ export function isCreativeDirectorSplitEnabledForVerify() {
95
+ const raw = String(process.env.CREATIVE_DIRECTOR_SPLIT ?? '1').trim().toLowerCase();
96
+ return raw !== '0' && raw !== 'false' && raw !== 'off';
97
+ }
98
+
99
+ const NORTH_STAR_STOP_WORDS = new Set([
100
+ 'the',
101
+ 'and',
102
+ 'this',
103
+ 'that',
104
+ 'with',
105
+ 'player',
106
+ 'game',
107
+ '玩家',
108
+ '游戏',
109
+ '一个',
110
+ '进行',
111
+ '通过',
112
+ ]);
113
+
114
+ /** @returns {'off' | 'warn' | 'block'} */
115
+ export function getNorthStarMode() {
116
+ const raw = String(process.env.NORTH_STAR_MODE ?? 'warn').trim().toLowerCase();
117
+ if (raw === 'off' || raw === '0' || raw === 'false') return 'off';
118
+ if (raw === 'block' || raw === 'error') return 'block';
119
+ return 'warn';
120
+ }
121
+
122
+ /** @param {string} text */
123
+ function tokenizeMeaningful(text) {
124
+ const tokens = new Set();
125
+ const parts = text.toLowerCase().match(/[\u4e00-\u9fff]{2,}|[a-z]{3,}/g) ?? [];
126
+ for (const part of parts) {
127
+ if (!NORTH_STAR_STOP_WORDS.has(part)) tokens.add(part);
128
+ }
129
+ return tokens;
130
+ }
131
+
132
+ /** @param {string} text */
133
+ function chineseBigrams(text) {
134
+ const chars = text.replace(/[^\u4e00-\u9fff]/g, '');
135
+ const bg = new Set();
136
+ for (let i = 0; i < chars.length - 1; i += 1) {
137
+ bg.add(chars.slice(i, i + 2));
138
+ }
139
+ return bg;
140
+ }
141
+
142
+ /** @param {string} a @param {string} b */
143
+ export function keywordOverlapScore(a, b) {
144
+ const ta = tokenizeMeaningful(a);
145
+ const tb = tokenizeMeaningful(b);
146
+ let score = 0;
147
+ if (ta.size > 0 && tb.size > 0) {
148
+ let shared = 0;
149
+ for (const t of ta) {
150
+ if (tb.has(t)) shared += 1;
151
+ }
152
+ score = shared / Math.min(ta.size, tb.size);
153
+ }
154
+ const ba = chineseBigrams(a);
155
+ const bb = chineseBigrams(b);
156
+ if (ba.size > 0 && bb.size > 0) {
157
+ let sharedBg = 0;
158
+ for (const bg of ba) {
159
+ if (bb.has(bg)) sharedBg += 1;
160
+ }
161
+ score = Math.max(score, sharedBg / Math.min(ba.size, bb.size));
162
+ }
163
+ return score;
164
+ }
165
+
166
+ /** @param {string} text */
167
+ function countGenericFillerHits(text) {
168
+ return GENERIC_FILLER_PATTERNS.filter((re) => re.test(text)).length;
169
+ }
170
+
171
+ /** @param {string} design */
172
+ function extractNorthStarSection(design) {
173
+ const m = design.match(/##\s*0[^\n]*Creative North Star([\s\S]*?)(?=\n##\s|$)/i);
174
+ return m?.[1] ?? '';
175
+ }
176
+
177
+ /** @param {string} section */
178
+ function extractLabeledLine(section, label) {
179
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
180
+ const re = new RegExp(`^\\*\\*${escaped}\\*\\*\\s*:+\\s*(.+)$`, 'im');
181
+ return section.match(re)?.[1]?.trim() ?? '';
182
+ }
183
+
184
+ /** @returns {'off' | 'warn' | 'block'} */
185
+ export function getStorySpineMode() {
186
+ const raw = String(process.env.STORY_SPINE_MODE ?? 'warn').trim().toLowerCase();
187
+ if (raw === 'off' || raw === '0' || raw === 'false') return 'off';
188
+ if (raw === 'block' || raw === 'error') return 'block';
189
+ return 'warn';
190
+ }
191
+
192
+ /** @param {string} md */
193
+ function extractMicroStorySpineSection(md) {
194
+ const m = md.match(/##\s*Micro-Story\s*Spine([\s\S]*?)(?=\n##\s|$)/i);
195
+ return m?.[1] ?? '';
196
+ }
197
+
198
+ /** @param {string} md */
199
+ export function extractStorySpineFields(md) {
200
+ const section = extractMicroStorySpineSection(md);
201
+ if (!section.trim()) return null;
202
+ return {
203
+ narrativeLevel: extractLabeledLine(section, 'Narrative level'),
204
+ who: extractLabeledLine(section, 'Who'),
205
+ situation: extractLabeledLine(section, 'Situation'),
206
+ stakes: extractLabeledLine(section, 'Stakes'),
207
+ payoffImage:
208
+ extractLabeledLine(section, 'Payoff image') || extractLabeledLine(section, 'Payoff'),
209
+ emotionalArc: extractLabeledLine(section, 'Emotional arc'),
210
+ };
211
+ }
212
+
213
+ /** @param {string} raw */
214
+ function normalizeNarrativeLevel(raw) {
215
+ const t = String(raw || '').trim().toUpperCase();
216
+ const m = t.match(/\b(L[0-3])\b/);
217
+ return m?.[1] ?? '';
218
+ }
219
+
220
+ /** @param {string} text */
221
+ function storyFieldLen(text) {
222
+ const t = String(text || '').trim();
223
+ if (!t || /^n\/a$/i.test(t)) return 0;
224
+ return t.length;
225
+ }
226
+
227
+ /** @param {string} creativeProcess */
228
+ function hasL0OptOutInCreativeProcess(creativeProcess) {
229
+ if (!creativeProcess.trim()) return false;
230
+ const phase3 =
231
+ creativeProcess.match(/Phase\s*3[\s\S]*?(?=Phase\s*4|\n##\s|$)/i)?.[0] ?? creativeProcess;
232
+ return /纯机制|不要故事|no story|pure mechanic|\bL0\b|opt-out|opt out|用户.*(不要|无需|明确).*(故事|叙事)/i.test(
233
+ phase3,
234
+ );
235
+ }
236
+
237
+ /**
238
+ * Micro-Story Spine validation — independent from validateNorthStar.
239
+ * @param {string} northStarMd
240
+ * @param {string} [design]
241
+ * @param {string} [creativeProcess]
242
+ * @param {string} [oneLiner]
243
+ */
244
+ export function validateStorySpine(northStarMd, design = '', creativeProcess = '', oneLiner = '') {
245
+ const problems = [];
246
+ const warnings = [];
247
+ const metrics = {
248
+ storySpineComplete: false,
249
+ narrativeLevel: /** @type {string | null} */ (null),
250
+ spinePresent: false,
251
+ };
252
+
253
+ const nsFields = northStarMd ? extractStorySpineFields(northStarMd) : null;
254
+ metrics.spinePresent = Boolean(nsFields);
255
+
256
+ if (!nsFields) {
257
+ problems.push('docs/north-star.md missing ## Micro-Story Spine section');
258
+ return { ok: false, problems, warnings, metrics };
259
+ }
260
+
261
+ const level = normalizeNarrativeLevel(nsFields.narrativeLevel);
262
+ metrics.narrativeLevel = level || null;
263
+
264
+ if (!level || !/^L[0-3]$/.test(level)) {
265
+ problems.push('Micro-Story Spine: Narrative level must be L0, L1, or L2');
266
+ } else if (level === 'L3') {
267
+ warnings.push('Micro-Story Spine: L3 not supported — suggest downgrade to L2');
268
+ }
269
+
270
+ if (level === 'L0') {
271
+ if (storyFieldLen(nsFields.situation) < 8) {
272
+ problems.push('Micro-Story Spine L0: Situation must be ≥8 chars');
273
+ }
274
+ if (storyFieldLen(nsFields.payoffImage) < 8) {
275
+ problems.push('Micro-Story Spine L0: Payoff image must be ≥8 chars');
276
+ }
277
+ if (!hasL0OptOutInCreativeProcess(creativeProcess)) {
278
+ problems.push(
279
+ 'Micro-Story Spine L0: creative-process Phase 3 must cite user opt-out of story',
280
+ );
281
+ }
282
+ } else if (level === 'L1' || level === 'L2') {
283
+ for (const [label, val] of [
284
+ ['Who', nsFields.who],
285
+ ['Situation', nsFields.situation],
286
+ ['Stakes', nsFields.stakes],
287
+ ['Payoff image', nsFields.payoffImage],
288
+ ]) {
289
+ if (storyFieldLen(val) < 8) {
290
+ problems.push(`Micro-Story Spine ${level}: ${label} must be ≥8 chars`);
291
+ }
292
+ }
293
+ }
294
+
295
+ const designFields = design ? extractStorySpineFields(design) : null;
296
+ if (designFields) {
297
+ for (const [label, nsVal, designVal] of [
298
+ ['Narrative level', nsFields.narrativeLevel, designFields.narrativeLevel],
299
+ ['Who', nsFields.who, designFields.who],
300
+ ['Situation', nsFields.situation, designFields.situation],
301
+ ['Stakes', nsFields.stakes, designFields.stakes],
302
+ ['Payoff image', nsFields.payoffImage, designFields.payoffImage],
303
+ ['Emotional arc', nsFields.emotionalArc, designFields.emotionalArc],
304
+ ]) {
305
+ const trimmedDesign = String(designVal || '').trim();
306
+ const trimmedNs = String(nsVal || '').trim();
307
+ if (trimmedDesign && trimmedNs !== trimmedDesign) {
308
+ problems.push(`DESIGN §0 Micro-Story ${label} must match north-star.md`);
309
+ }
310
+ }
311
+ }
312
+
313
+ if ((level === 'L1' || level === 'L2') && oneLiner) {
314
+ const hasTension = EMOTION_TENSION_RE.test(oneLiner);
315
+ const hasEntity =
316
+ storyFieldLen(nsFields.who) >= 8 || storyFieldLen(nsFields.situation) >= 8;
317
+ if (hasTension && !hasEntity) {
318
+ warnings.push(
319
+ 'One-liner has tension vocabulary but lacks Who/Situation entity grounding',
320
+ );
321
+ }
322
+ }
323
+
324
+ const levelOk = level === 'L0' || level === 'L1' || level === 'L2';
325
+ metrics.storySpineComplete = problems.length === 0 && levelOk;
326
+
327
+ const mode = getStorySpineMode();
328
+ const blocking = mode === 'block' ? [...problems, ...warnings] : problems;
329
+ return {
330
+ ok: blocking.length === 0,
331
+ problems,
332
+ warnings,
333
+ metrics,
334
+ };
335
+ }
336
+
337
+ /** @param {string} design */
338
+ function extractPlayerFantasy(design) {
339
+ const m = design.match(/Player Fantasy[:\s]*([\s\S]*?)(?=\n(?:Decision Moment|First Level|##\s*\d|$))/i);
340
+ return m?.[1]?.trim().replace(/\n+/g, ' ') ?? '';
341
+ }
342
+
343
+ /** @param {string} design @param {string} brief @param {string} creativeProcess @param {string} [northStarMd] */
344
+ export function validateNorthStar(design, brief, creativeProcess, northStarMd = '') {
345
+ const mode = getNorthStarMode();
346
+ const metrics = {
347
+ northStarPresent: false,
348
+ oneLinerLength: 0,
349
+ genericFillerHit: false,
350
+ mechanismChainLikely: false,
351
+ northStarSyncOk: null,
352
+ creativePhase1Candidates: 0,
353
+ intentSyncScore: 0,
354
+ playerFantasyOverlap: 0,
355
+ mode,
356
+ };
357
+ const problems = [];
358
+ const warnings = [];
359
+
360
+ if (mode === 'off') {
361
+ return { problems, warnings, metrics, ok: true };
362
+ }
363
+
364
+ const hasHeader = /Creative North Star|创意锚点/i.test(design);
365
+ const section = extractNorthStarSection(design);
366
+ const oneLiner = extractLabeledLine(section, 'One-liner') || extractLabeledLine(section, '一句话');
367
+ metrics.oneLinerLength = oneLiner.length;
368
+ metrics.northStarPresent = hasHeader && oneLiner.length >= 15;
369
+
370
+ if (!hasHeader) problems.push('missing §0 Creative North Star section');
371
+ else if (oneLiner.length < 15) problems.push('§0 One-liner too short (need ≥15 chars)');
372
+
373
+ const playerFantasy = extractPlayerFantasy(design);
374
+ if (oneLiner && playerFantasy) {
375
+ metrics.playerFantasyOverlap = keywordOverlapScore(oneLiner, playerFantasy);
376
+ if (metrics.northStarPresent && metrics.playerFantasyOverlap < 0.15) {
377
+ problems.push('Player Fantasy shares no keywords with §0 One-liner');
378
+ }
379
+ }
380
+
381
+ const genericHits = countGenericFillerHits(`${oneLiner}\n${playerFantasy}`);
382
+ metrics.genericFillerHit = genericHits >= 2;
383
+ if (metrics.genericFillerHit) {
384
+ problems.push('generic filler patterns in One-liner + Player Fantasy (≥2 hits)');
385
+ }
386
+
387
+ const nsSpineFields = northStarMd ? extractStorySpineFields(northStarMd) : null;
388
+ const narrativeLevel = nsSpineFields ? normalizeNarrativeLevel(nsSpineFields.narrativeLevel) : '';
389
+
390
+ metrics.mechanismChainLikely = isMechanismChainOneLiner(oneLiner);
391
+ if (metrics.mechanismChainLikely && narrativeLevel !== 'L0') {
392
+ problems.push('§0 One-liner reads like a mechanism chain — add feeling/tension vocabulary');
393
+ }
394
+
395
+ const candidateCount = (creativeProcess.match(/###\s*Candidate\s+[ABC]/gi) || []).length;
396
+ metrics.creativePhase1Candidates = candidateCount;
397
+ if (creativeProcess && candidateCount < 3) {
398
+ warnings.push('creative-process Phase 1 needs ≥3 candidates (Candidate A/B/C)');
399
+ }
400
+
401
+ if (isCreativeDirectorSplitEnabledForVerify() && northStarMd.trim()) {
402
+ const nsOneLiner = extractNorthStarOneLinerFromFile(northStarMd);
403
+ metrics.northStarSyncOk = nsOneLiner.length > 0 && nsOneLiner === oneLiner;
404
+ if (oneLiner && nsOneLiner && !metrics.northStarSyncOk) {
405
+ problems.push('DESIGN §0 One-liner must verbatim match docs/north-star.md');
406
+ }
407
+ } else if (isCreativeDirectorSplitEnabledForVerify() && !northStarMd.trim() && oneLiner) {
408
+ warnings.push('docs/north-star.md missing — spec mode expects standalone north star file');
409
+ }
410
+
411
+ const designIntent =
412
+ creativeProcess.match(/\*\*Design Intent\*\*[:\s]*([^\n]+)/i)?.[1]?.trim() ?? '';
413
+ if (designIntent && oneLiner) {
414
+ metrics.intentSyncScore = keywordOverlapScore(designIntent, oneLiner);
415
+ if (metrics.intentSyncScore < 0.3) {
416
+ warnings.push('creative-process Design Intent weakly synced with §0 One-liner');
417
+ }
418
+ }
419
+
420
+ const fusionSec = design.match(/##\s*6\.?\s*Gameplay Fusion([\s\S]*?)(?=\n##\s*7|$)/i)?.[1] ?? '';
421
+ const servesIdea = /serves|服务|because|因为|This fusion serves/i.test(fusionSec);
422
+ if (metrics.northStarPresent && !servesIdea) {
423
+ problems.push('§6 Gameplay Fusion must explain how fusion serves §0 (serves/because/服务/因为)');
424
+ }
425
+
426
+ if (brief && !/Creative Angle|Idea hook|Anti-generic/i.test(brief)) {
427
+ warnings.push('design-brief missing Creative Angle section');
428
+ }
429
+
430
+ const blocking = mode === 'block' ? [...problems, ...warnings] : problems;
431
+ return {
432
+ problems: blocking,
433
+ warnings: mode === 'warn' ? [...problems, ...warnings].map((p) => `[north-star-warn] ${p}`) : [],
434
+ metrics,
435
+ ok: mode === 'warn' ? true : blocking.length === 0,
436
+ };
437
+ }
438
+
439
+ /** @param {string} content @param {string} brief @param {boolean} fusionIntent */
440
+ function validateSemantic(content, brief, fusionIntent) {
441
+ const problems = [];
442
+ if (!/Player Fantasy|Player Promise|玩家.*(幻想|承诺)/i.test(content + brief)) {
443
+ problems.push('missing Player Fantasy / Player Promise');
444
+ }
445
+ if (!/Decision Moment|决策时刻|啊哈|误点|blocked/i.test(content + brief)) {
446
+ problems.push('missing Decision Moment (describe one aha/mis-tap moment)');
447
+ }
448
+ const fusion = parseDesignFusionType(content);
449
+ if (!fusion.type) {
450
+ problems.push('DESIGN.md missing Fusion type: (## 6. Gameplay Fusion)');
451
+ } else if (fusionIntent && fusion.type === 'Single' && !fusion.isPseudoFusion) {
452
+ problems.push('fusion intent but Fusion type is Single without pseudo-fusion / downgrade reason');
453
+ }
454
+ const hasReference =
455
+ /Reference insight|Pattern picked|playcraft patterns match/i.test(brief) ||
456
+ /researchSkippedReason/i.test(brief);
457
+ if (!hasReference) {
458
+ problems.push('design-brief missing Reference insight or Pattern picked (or researchSkippedReason)');
459
+ }
460
+ return problems;
461
+ }
462
+
463
+ /** @param {string} content */
464
+ function extractFirstLevelScenario(content) {
465
+ const m = content.match(/First Level Scenario[*_\s]*:?\s*([\s\S]*?)(?=\n###|\n## |$)/i);
466
+ return m?.[1]?.trim() ?? '';
467
+ }
468
+
469
+ /** @param {string} design */
470
+ function extractDesignProseBundle(design) {
471
+ const scenario = extractFirstLevelScenario(design);
472
+ const sec7 = design.match(/##\s*7\.[\s\S]*?(?=\n##\s*\d|$)/i)?.[0] ?? '';
473
+ return `${scenario}\n${sec7}`;
474
+ }
475
+
476
+ /**
477
+ * @param {Record<string, unknown>} obj
478
+ * @param {{ pathCount?: number, openingRemovableCount?: number, hasBlockedPaths?: boolean }} [levelMetrics]
479
+ */
480
+ export function deriveSketchTopology(obj, levelMetrics) {
481
+ const arrows = Array.isArray(obj.arrows) ? obj.arrows : [];
482
+ const pathPuzzle = arrows.some((a) => Array.isArray(a) && a.length > 1);
483
+ const serialized = JSON.stringify(obj);
484
+ const hasTray =
485
+ typeof obj.trayCapacity === 'number' ||
486
+ typeof obj.matchCount === 'number' ||
487
+ /trayCapacity|matchCount|"tray"|match-3/i.test(serialized);
488
+ return {
489
+ pathPuzzle,
490
+ hasTray,
491
+ boardSize: `${obj.row ?? 0}x${obj.col ?? 0}`,
492
+ pathCount: levelMetrics?.pathCount ?? arrows.length,
493
+ openingRemovableCount: levelMetrics?.openingRemovableCount ?? 0,
494
+ hasBlockedPaths: levelMetrics?.hasBlockedPaths ?? false,
495
+ };
496
+ }
497
+
498
+ /** @param {string} text */
499
+ export function deriveProseProfile(text) {
500
+ const t = text.toLowerCase();
501
+ return {
502
+ claimsAllFree:
503
+ /all free|all paths free|zero blocking|无阻挡|均可推出|任意顺序|any tap order|all unblocked|全部.*free|无遮挡.*均可|均可.*点击/i.test(
504
+ text,
505
+ ),
506
+ claimsBlocked:
507
+ /blocked|阻挡|遮挡|dependency|依赖|unlock|解锁|puzzle|顺序依赖|blocked path|解谜|unlock sequence/i.test(
508
+ text,
509
+ ),
510
+ claimsTrayMatch: /tray|托盘|match-3|三消|match3|elimination burst|凑齐.*3/i.test(text),
511
+ claimsGridPlacement:
512
+ /tap empty|place arrow|grid.?plac|empty cell|点击空格放置|格子.*放置|convergence.*grid|highlight.*cell/i.test(
513
+ text,
514
+ ),
515
+ };
516
+ }
517
+
518
+ /** Merge prose profiles — any source claiming a trait counts. */
519
+ function mergeProseProfiles(profiles) {
520
+ return {
521
+ claimsAllFree: profiles.some((p) => p.claimsAllFree),
522
+ claimsBlocked: profiles.some((p) => p.claimsBlocked),
523
+ claimsTrayMatch: profiles.some((p) => p.claimsTrayMatch),
524
+ claimsGridPlacement: profiles.some((p) => p.claimsGridPlacement),
525
+ };
526
+ }
527
+
528
+ /**
529
+ * @param {ReturnType<typeof deriveSketchTopology>} sketch
530
+ * @param {ReturnType<typeof mergeProseProfiles>} prose
531
+ */
532
+ export function validateTopologyConsistency(sketch, prose) {
533
+ const problems = [];
534
+
535
+ if (sketch.pathPuzzle && sketch.hasBlockedPaths) {
536
+ if (prose.claimsAllFree && !prose.claimsBlocked) {
537
+ problems.push(
538
+ '[consistency] level-sketch is blocked path-puzzle but prose claims all-free / no blocking',
539
+ );
540
+ }
541
+ if (prose.claimsGridPlacement && !prose.claimsBlocked && !prose.claimsTrayMatch) {
542
+ problems.push(
543
+ '[consistency] level-sketch is path-puzzle but prose describes grid-placement without path/blocked semantics',
544
+ );
545
+ }
546
+ }
547
+
548
+ if (sketch.pathPuzzle && prose.claimsGridPlacement && !prose.claimsBlocked && !prose.claimsTrayMatch) {
549
+ problems.push(
550
+ '[consistency] level-sketch is path-puzzle but prose describes grid-placement without path puzzle semantics',
551
+ );
552
+ }
553
+
554
+ if (sketch.hasTray && !prose.claimsTrayMatch) {
555
+ problems.push('[consistency] level-sketch includes tray/match-3 but prose omits tray or match-3');
556
+ }
557
+
558
+ if (
559
+ sketch.pathCount > 1 &&
560
+ !sketch.hasBlockedPaths &&
561
+ prose.claimsBlocked &&
562
+ !prose.claimsAllFree
563
+ ) {
564
+ problems.push('[consistency] prose claims blocked/dependency but level-sketch has no blocked paths');
565
+ }
566
+
567
+ return problems;
568
+ }
569
+
570
+ /** @param {string} root */
571
+ function readHandoffSummary(root) {
572
+ const raw = readFile(root, 'docs/project-state.json');
573
+ if (!raw) return '';
574
+ try {
575
+ const state = JSON.parse(raw);
576
+ return String(state.handoffContext?.lastRole?.summary ?? '');
577
+ } catch {
578
+ return '';
579
+ }
580
+ }
581
+
582
+ export function verifyDesignConsistency(root) {
583
+ const problems = [];
584
+ const consistency = [];
585
+ const design = readFile(root, 'docs/DESIGN.md');
586
+ const brief = readFile(root, 'docs/design-brief.md');
587
+ const creativeProcess = readFile(root, 'docs/creative-process.md');
588
+ const northStarMd = readFile(root, 'docs/north-star.md');
589
+ if (!design) problems.push('docs/DESIGN.md missing');
590
+ if (!brief) problems.push('docs/design-brief.md missing');
591
+ if (problems.length) {
592
+ return { ok: false, problems, semantic: [], consistency: [], topology: { sketch: null, prose: null, consistent: false } };
593
+ }
594
+
595
+ const { intentTags, rawIntent } = readFusionIntentFromProject(root);
596
+ const fusionIntent = detectFusionIntent(intentTags, rawIntent);
597
+
598
+ const semantic = validateSemantic(design, brief, fusionIntent);
599
+ problems.push(...semantic.map((p) => `[semantic] ${p}`));
600
+ problems.push(...validateAnimationSpine(design).map((p) => `[animation-spine] ${p}`));
601
+
602
+ const northStar = validateNorthStar(design, brief, creativeProcess, northStarMd);
603
+ if (!northStar.ok) {
604
+ problems.push(...northStar.problems.map((p) => `[north-star] ${p}`));
605
+ }
606
+
607
+ const oneLinerForSpine =
608
+ extractNorthStarOneLinerFromFile(northStarMd) ||
609
+ extractLabeledLine(extractNorthStarSection(design), 'One-liner');
610
+ const storySpine = validateStorySpine(northStarMd, design, creativeProcess, oneLinerForSpine);
611
+ const storySpineMode = getStorySpineMode();
612
+ if (storySpineMode === 'block') {
613
+ if (!storySpine.ok) {
614
+ problems.push(...storySpine.problems.map((p) => `[story-spine] ${p}`));
615
+ problems.push(...storySpine.warnings.map((p) => `[story-spine] ${p}`));
616
+ }
617
+ } else if (storySpineMode === 'warn') {
618
+ // Rollout: collect metrics only; do not fail verify:design yet
619
+ } else if (!storySpine.ok) {
620
+ problems.push(...storySpine.problems.map((p) => `[story-spine] ${p}`));
621
+ }
622
+
623
+ const mechanismFidelity = validateSkillFidelity(root);
624
+ if (!mechanismFidelity.skipped && !mechanismFidelity.pass) {
625
+ for (const reason of mechanismFidelity.driftReasons) {
626
+ problems.push(`[mechanism-fidelity] ${reason}`);
627
+ }
628
+ }
629
+
630
+ let topology = { sketch: null, prose: null, consistent: true };
631
+
632
+ const sketchPath = 'docs/level-sketch.json';
633
+ if (existsSync(join(root, sketchPath))) {
634
+ const levelResult = validateLevelSketch(root, sketchPath);
635
+ if (!levelResult.ok) {
636
+ problems.push(...levelResult.problems.map((p) => `[level-sketch] ${p}`));
637
+ }
638
+
639
+ let sketchObj;
640
+ try {
641
+ sketchObj = JSON.parse(readFileSync(join(root, sketchPath), 'utf8'));
642
+ } catch (err) {
643
+ problems.push(`[level-sketch] parse error: ${err instanceof Error ? err.message : String(err)}`);
644
+ sketchObj = { arrows: [] };
645
+ }
646
+
647
+ const sketchTopo = deriveSketchTopology(sketchObj, levelResult.metrics);
648
+ const designBundle = extractDesignProseBundle(design);
649
+ const briefFirstWin = brief.match(/First[- ]?win|First Level|首关|第一关|Level 1[\s\S]{0,400}/i)?.[0] ?? brief;
650
+ const handoffSummary = readHandoffSummary(root);
651
+
652
+ const proseMerged = mergeProseProfiles([
653
+ deriveProseProfile(designBundle),
654
+ deriveProseProfile(briefFirstWin),
655
+ deriveProseProfile(handoffSummary),
656
+ ]);
657
+
658
+ const topoProblems = validateTopologyConsistency(sketchTopo, proseMerged);
659
+ consistency.push(...topoProblems);
660
+ problems.push(...topoProblems);
661
+
662
+ if (handoffSummary) {
663
+ const handoffOnly = deriveProseProfile(handoffSummary);
664
+ if (sketchTopo.hasBlockedPaths && handoffOnly.claimsAllFree && !handoffOnly.claimsBlocked) {
665
+ const msg = '[consistency] handoffContext.summary contradicts level-sketch (claims all-free but sketch has blocked paths)';
666
+ if (!problems.includes(msg)) {
667
+ problems.push(msg);
668
+ consistency.push(msg);
669
+ }
670
+ }
671
+ }
672
+
673
+ const scenario = extractFirstLevelScenario(design);
674
+ if (scenario && /blocked|阻挡|dependency|依赖|puzzle|遮挡/i.test(scenario)) {
675
+ if (levelResult.metrics && !levelResult.metrics.hasBlockedPaths) {
676
+ problems.push(
677
+ '[consistency] First Level Scenario claims blocked/dependency but level-sketch has no blocked paths',
678
+ );
679
+ }
680
+ }
681
+
682
+ topology = {
683
+ sketch: sketchTopo,
684
+ prose: proseMerged,
685
+ consistent: topoProblems.length === 0,
686
+ };
687
+ } else if (
688
+ existsSync(join(root, 'game/levels/ring1Level.ts')) ||
689
+ existsSync(join(root, 'game/levels/level1.json'))
690
+ ) {
691
+ // Dev phase: level file exists without sketch — optional warn only
692
+ } else {
693
+ problems.push('[level-sketch] docs/level-sketch.json missing for Ring 1 clarify');
694
+ topology.consistent = false;
695
+ }
696
+
697
+ return {
698
+ ok: problems.length === 0,
699
+ problems,
700
+ fusionIntent,
701
+ semantic,
702
+ consistency,
703
+ topology,
704
+ northStar: northStar.metrics,
705
+ northStarWarnings: northStar.warnings,
706
+ storySpine: storySpine.metrics,
707
+ storySpineWarnings: storySpine.warnings,
708
+ storySpineProblems: storySpine.problems,
709
+ mechanismFidelity,
710
+ };
711
+ }
712
+
713
+ function main() {
714
+ const opts = parseArgs(process.argv.slice(2));
715
+ const result = verifyDesignConsistency(opts.root);
716
+ if (opts.json) {
717
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
718
+ } else if (result.ok) {
719
+ process.stdout.write('[verify:design] OK — semantic + consistency pass\n');
720
+ } else {
721
+ process.stderr.write(`[verify:design] FAIL\n - ${result.problems.join('\n - ')}\n`);
722
+ }
723
+ process.exit(result.ok ? 0 : 2);
724
+ }
725
+
726
+ if (process.argv[1] && basename(process.argv[1]) === 'verify-design-consistency.mjs') {
727
+ main();
728
+ }