@playcraft/cli 0.0.42 → 0.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atom-plan/validate-asr-coverage.js +317 -0
- package/dist/commands/build.js +6 -6
- package/dist/commands/remix.js +4 -2
- package/dist/commands/skills.js +24 -0
- package/dist/index.js +0 -0
- package/package.json +3 -3
- package/project-template/.claude/agents/designer.md +26 -22
- package/project-template/.claude/agents/developer.md +2 -0
- package/project-template/.claude/agents/pm.md +3 -1
- package/project-template/.claude/agents/refs/designer-deliverable-spec.md +46 -7
- package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +21 -13
- package/project-template/.claude/agents/refs/designer-style-exploration-flow.md +39 -9
- package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
- package/project-template/.claude/agents/refs/pm-workflow-detail.md +18 -2
- package/project-template/.claude/agents/refs/reviewer-convergence-eval.md +17 -5
- package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +42 -6
- package/project-template/.claude/agents/reviewer.md +8 -5
- package/project-template/.claude/agents/technical-artist.md +2 -0
- package/project-template/.claude/hooks/README.md +34 -6
- package/project-template/.claude/hooks/asr-coverage-validate.mjs +381 -0
- package/project-template/.claude/hooks/validate-workflow-stop.mjs +113 -7
- package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +76 -22
- package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +19 -0
- package/project-template/docs/team/agent-runtime-matrix.md +71 -39
- package/project-template/docs/team/atom-plan-format.md +68 -0
- package/project-template/docs/team/core-model.md +20 -19
- package/project-template/docs/team/workflow-consistency-checklist.md +52 -0
- package/project-template/templates/atom-plan.template.json +18 -0
- package/project-template/templates/designer-log.template.md +78 -5
- package/project-template/templates/layout-spec.template.md +48 -8
- package/project-template/templates/ta-log.template.md +50 -22
- package/dist/playable/base-builder.js +0 -265
- package/dist/playable/builder.js +0 -1462
- package/dist/playable/converter.js +0 -150
- package/dist/playable/index.js +0 -3
- package/dist/playable/platforms/base.js +0 -12
- package/dist/playable/platforms/facebook.js +0 -37
- package/dist/playable/platforms/index.js +0 -24
- package/dist/playable/platforms/snapchat.js +0 -59
- package/dist/playable/playable-builder.js +0 -521
- package/dist/playable/types.js +0 -1
- package/dist/playable/vite/config-builder.js +0 -136
- package/dist/playable/vite/platform-configs.js +0 -102
- package/dist/playable/vite/plugin-model-compression.js +0 -63
- package/dist/playable/vite/plugin-platform.js +0 -65
- package/dist/playable/vite/plugin-playcanvas.js +0 -454
- package/dist/playable/vite-builder.js +0 -125
- package/project-template/.claude/settings.local.json +0 -4
- package/project-template/logs/.gitkeep +0 -0
- package/project-template/ta-workspace/logs/.gitkeep +0 -0
- package/project-template/ta-workspace/tmp/.gitkeep +0 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASR Coverage validation — shared by validate-workflow-stop.mjs and playcraft skills validate-asr-coverage.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const PLACEHOLDER_RE = /\{\{[^}]+\}\}/;
|
|
9
|
+
const MIN_TAKEAWAY_LEN = 8;
|
|
10
|
+
|
|
11
|
+
export const IMAGE_PRODUCTION_PREFLIGHT_HEADING = '## Image Production Preflight';
|
|
12
|
+
export const ASR_COVERAGE_HEADING = '## ASR Coverage Matrix';
|
|
13
|
+
export const REQUIRED_IMAGE_SKILLS = [
|
|
14
|
+
'playcraft-storyboard',
|
|
15
|
+
'playcraft-asset-state-sheet',
|
|
16
|
+
'playcraft-image-generation',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} content
|
|
21
|
+
*/
|
|
22
|
+
export function readSelectedMcOption(content) {
|
|
23
|
+
const m =
|
|
24
|
+
content.match(/selectedMcOption:\s*["']?([ABC])["']?/i) ||
|
|
25
|
+
content.match(/selectedMcOption\s*=\s*([ABC])/i);
|
|
26
|
+
return m ? m[1].toUpperCase() : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} layoutSpec
|
|
31
|
+
* @returns {{ elementIds: string[], staticTextIds: string[] }}
|
|
32
|
+
*/
|
|
33
|
+
export function parseLayoutSpecContractIds(layoutSpec) {
|
|
34
|
+
/** @type {string[]} */
|
|
35
|
+
const elementIds = [];
|
|
36
|
+
/** @type {string[]} */
|
|
37
|
+
const staticTextIds = [];
|
|
38
|
+
|
|
39
|
+
const assetSection = extractSection(layoutSpec, '## Asset Mapping');
|
|
40
|
+
if (assetSection) {
|
|
41
|
+
for (const line of assetSection.split('\n')) {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
if (!trimmed.startsWith('|')) continue;
|
|
44
|
+
if (/^\|\s*---/.test(trimmed)) continue;
|
|
45
|
+
if (/^\|\s*elementId/i.test(trimmed)) continue;
|
|
46
|
+
|
|
47
|
+
const parts = trimmed
|
|
48
|
+
.split('|')
|
|
49
|
+
.map((p) => p.trim())
|
|
50
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
51
|
+
if (parts.length < 1) continue;
|
|
52
|
+
const id = parts[0].replace(/`/g, '');
|
|
53
|
+
if (!id || id === '—' || id === '-' || PLACEHOLDER_RE.test(id)) continue;
|
|
54
|
+
elementIds.push(id);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const textSection = extractSection(layoutSpec, '### Static Text');
|
|
59
|
+
if (textSection) {
|
|
60
|
+
for (const line of textSection.split('\n')) {
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (!trimmed.startsWith('|')) continue;
|
|
63
|
+
if (/^\|\s*---/.test(trimmed)) continue;
|
|
64
|
+
if (/^\|\s*id/i.test(trimmed)) continue;
|
|
65
|
+
|
|
66
|
+
const parts = trimmed
|
|
67
|
+
.split('|')
|
|
68
|
+
.map((p) => p.trim())
|
|
69
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
70
|
+
if (parts.length < 1) continue;
|
|
71
|
+
const id = parts[0].replace(/`/g, '');
|
|
72
|
+
if (!id || id === '—' || PLACEHOLDER_RE.test(id)) continue;
|
|
73
|
+
staticTextIds.push(id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { elementIds, staticTextIds };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} content
|
|
82
|
+
* @param {string} heading
|
|
83
|
+
*/
|
|
84
|
+
function extractSection(content, heading) {
|
|
85
|
+
const start = content.indexOf(heading);
|
|
86
|
+
if (start === -1) return '';
|
|
87
|
+
const after = content.slice(start + heading.length);
|
|
88
|
+
const next = after.search(/\n##\s+/);
|
|
89
|
+
return next === -1 ? after : after.slice(0, next);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} cell
|
|
94
|
+
*/
|
|
95
|
+
function isReadChecked(cell) {
|
|
96
|
+
const t = cell.trim();
|
|
97
|
+
if (/[✓✔☑]/.test(t)) return true;
|
|
98
|
+
if (/\[x\]/i.test(t)) return true;
|
|
99
|
+
if (/^(yes|done|true|read)$/i.test(t)) return true;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} content
|
|
105
|
+
*/
|
|
106
|
+
export function validateImageProductionPreflight(content) {
|
|
107
|
+
const errors = [];
|
|
108
|
+
const start = content.indexOf(IMAGE_PRODUCTION_PREFLIGHT_HEADING);
|
|
109
|
+
if (start === -1) {
|
|
110
|
+
return [`Missing "${IMAGE_PRODUCTION_PREFLIGHT_HEADING}" section`];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const after = content.slice(start + IMAGE_PRODUCTION_PREFLIGHT_HEADING.length);
|
|
114
|
+
const nextHeading = after.search(/\n##\s+/);
|
|
115
|
+
const section = nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
116
|
+
|
|
117
|
+
/** @type {{ skill: string, read: string, summary: string }[]} */
|
|
118
|
+
const rows = [];
|
|
119
|
+
for (const line of section.split('\n')) {
|
|
120
|
+
const trimmed = line.trim();
|
|
121
|
+
if (!trimmed.startsWith('|')) continue;
|
|
122
|
+
if (/^\|\s*---/.test(trimmed)) continue;
|
|
123
|
+
if (/^\|\s*Skill/i.test(trimmed)) continue;
|
|
124
|
+
|
|
125
|
+
const parts = trimmed
|
|
126
|
+
.split('|')
|
|
127
|
+
.map((p) => p.trim())
|
|
128
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
129
|
+
if (parts.length < 3) continue;
|
|
130
|
+
rows.push({ skill: parts[0], read: parts[1], summary: parts[2] });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (rows.length === 0) {
|
|
134
|
+
errors.push(`${IMAGE_PRODUCTION_PREFLIGHT_HEADING}: table has no data rows`);
|
|
135
|
+
return errors;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const required of REQUIRED_IMAGE_SKILLS) {
|
|
139
|
+
const row = rows.find((r) => r.skill.includes(required));
|
|
140
|
+
if (!row) {
|
|
141
|
+
errors.push(`${IMAGE_PRODUCTION_PREFLIGHT_HEADING}: missing row for ${required}`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!isReadChecked(row.read)) {
|
|
145
|
+
errors.push(`${IMAGE_PRODUCTION_PREFLIGHT_HEADING}: "${required}" — mark 已读 (✓)`);
|
|
146
|
+
}
|
|
147
|
+
if (!row.summary || PLACEHOLDER_RE.test(row.summary) || row.summary.length < MIN_TAKEAWAY_LEN) {
|
|
148
|
+
errors.push(
|
|
149
|
+
`${IMAGE_PRODUCTION_PREFLIGHT_HEADING}: "${required}" — add decision summary (≥${MIN_TAKEAWAY_LEN} chars)`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return errors;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {string} content
|
|
159
|
+
*/
|
|
160
|
+
export function parseCoverageMatrixRows(content) {
|
|
161
|
+
const section = extractSection(content, ASR_COVERAGE_HEADING);
|
|
162
|
+
const coverageStart = section.indexOf('### Coverage');
|
|
163
|
+
const coverageBlock = coverageStart === -1 ? section : section.slice(coverageStart);
|
|
164
|
+
|
|
165
|
+
/** @type {{ contractId: string, elementType: string, coverageLayer: string }[]} */
|
|
166
|
+
const rows = [];
|
|
167
|
+
for (const line of coverageBlock.split('\n')) {
|
|
168
|
+
const trimmed = line.trim();
|
|
169
|
+
if (!trimmed.startsWith('|')) continue;
|
|
170
|
+
if (/^\|\s*---/.test(trimmed)) continue;
|
|
171
|
+
if (/^\|\s*Contract id/i.test(trimmed)) continue;
|
|
172
|
+
|
|
173
|
+
const parts = trimmed
|
|
174
|
+
.split('|')
|
|
175
|
+
.map((p) => p.trim())
|
|
176
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
177
|
+
if (parts.length < 4) continue;
|
|
178
|
+
|
|
179
|
+
const contractId = parts[0].replace(/`/g, '');
|
|
180
|
+
if (!contractId || PLACEHOLDER_RE.test(contractId)) continue;
|
|
181
|
+
|
|
182
|
+
const typeValues = new Set(['visual', 'audio', 'text', 'digit', 'sfx']);
|
|
183
|
+
let elementType = '';
|
|
184
|
+
let coverageLayer = '';
|
|
185
|
+
|
|
186
|
+
if (parts.length >= 4 && typeValues.has(parts[2]?.toLowerCase())) {
|
|
187
|
+
elementType = parts[1];
|
|
188
|
+
coverageLayer = parts[3];
|
|
189
|
+
} else if (parts.length >= 3 && typeValues.has(parts[1]?.toLowerCase())) {
|
|
190
|
+
coverageLayer = parts[2];
|
|
191
|
+
} else {
|
|
192
|
+
elementType = parts.length >= 6 ? parts[1] : '';
|
|
193
|
+
coverageLayer = parts.length >= 4 ? parts[3] : parts[1] || '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
rows.push({
|
|
197
|
+
contractId,
|
|
198
|
+
elementType: elementType.replace(/`/g, ''),
|
|
199
|
+
coverageLayer: coverageLayer.toLowerCase(),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return rows;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} content
|
|
207
|
+
*/
|
|
208
|
+
export function validateSheetGridMetadata(content) {
|
|
209
|
+
const errors = [];
|
|
210
|
+
const section = extractSection(content, ASR_COVERAGE_HEADING);
|
|
211
|
+
if (!section.includes('### Sheet grid metadata')) {
|
|
212
|
+
errors.push('ASR Coverage Matrix: missing ### Sheet grid metadata table');
|
|
213
|
+
return errors;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const gridStart = section.indexOf('### Sheet grid metadata');
|
|
217
|
+
const gridBlock = section.slice(gridStart);
|
|
218
|
+
const gridEnd = gridBlock.search(/\n###\s+/);
|
|
219
|
+
const gridSection = gridEnd === -1 ? gridBlock : gridBlock.slice(0, gridEnd);
|
|
220
|
+
|
|
221
|
+
if (PLACEHOLDER_RE.test(gridSection)) {
|
|
222
|
+
errors.push('Sheet grid metadata: contains {{placeholders}} — fill real rows/cols/cell dimensions');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let dataRows = 0;
|
|
226
|
+
for (const line of gridSection.split('\n')) {
|
|
227
|
+
const trimmed = line.trim();
|
|
228
|
+
if (!trimmed.startsWith('|')) continue;
|
|
229
|
+
if (/^\|\s*---/.test(trimmed)) continue;
|
|
230
|
+
if (/^\|\s*Sheet/i.test(trimmed)) continue;
|
|
231
|
+
dataRows++;
|
|
232
|
+
}
|
|
233
|
+
if (dataRows < 2) {
|
|
234
|
+
errors.push('Sheet grid metadata: need at least ui + element sheet rows');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return errors;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {string} designerLog
|
|
242
|
+
* @param {string} layoutSpec
|
|
243
|
+
*/
|
|
244
|
+
export function validateCoverageRowCount(designerLog, layoutSpec) {
|
|
245
|
+
const errors = [];
|
|
246
|
+
const { elementIds, staticTextIds } = parseLayoutSpecContractIds(layoutSpec);
|
|
247
|
+
const expected = elementIds.length + staticTextIds.length;
|
|
248
|
+
const matrixRows = parseCoverageMatrixRows(designerLog);
|
|
249
|
+
|
|
250
|
+
if (expected === 0) {
|
|
251
|
+
errors.push('layout-spec: could not parse assetMapping/static text ids — fill Asset Mapping table');
|
|
252
|
+
return errors;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (matrixRows.length < expected) {
|
|
256
|
+
errors.push(
|
|
257
|
+
`Coverage Matrix: ${matrixRows.length} rows but layout-spec expects ${expected} (assetMapping + static text)`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const contractSet = new Set([...elementIds, ...staticTextIds]);
|
|
262
|
+
const matrixIds = new Set(matrixRows.map((r) => r.contractId));
|
|
263
|
+
for (const id of contractSet) {
|
|
264
|
+
if (!matrixIds.has(id)) {
|
|
265
|
+
errors.push(`Coverage Matrix: missing row for contract id "${id}"`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return errors;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {string} designerLog
|
|
274
|
+
*/
|
|
275
|
+
export function validateTypeRepresentatives(designerLog) {
|
|
276
|
+
const errors = [];
|
|
277
|
+
const rows = parseCoverageMatrixRows(designerLog);
|
|
278
|
+
/** @type {Map<string, Set<string>>} */
|
|
279
|
+
const typeLayers = new Map();
|
|
280
|
+
|
|
281
|
+
for (const row of rows) {
|
|
282
|
+
const type = row.elementType && row.elementType !== '—' ? row.elementType : row.contractId.split('_')[0];
|
|
283
|
+
if (!type) continue;
|
|
284
|
+
if (!typeLayers.has(type)) typeLayers.set(type, new Set());
|
|
285
|
+
typeLayers.get(type).add(row.coverageLayer);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const [type, layers] of typeLayers) {
|
|
289
|
+
const hasRep =
|
|
290
|
+
[...layers].some((l) => l.includes('on-asr')) || [...layers].some((l) => l.includes('mc-crop'));
|
|
291
|
+
if (!hasRep && layers.size > 0) {
|
|
292
|
+
const allExtends = [...layers].every((l) => l.includes('ta-extends') || l.includes('ta extends'));
|
|
293
|
+
if (allExtends) {
|
|
294
|
+
errors.push(
|
|
295
|
+
`Coverage Matrix: elementType "${type}" has no on-asr or mc-crop representative`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return errors;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @param {string} projectDir
|
|
306
|
+
* @param {string} option
|
|
307
|
+
* @param {string} [designerLog]
|
|
308
|
+
*/
|
|
309
|
+
export function validateAsrPngFiles(projectDir, option, designerLog) {
|
|
310
|
+
const errors = [];
|
|
311
|
+
const ui = path.join(projectDir, `assets/images/reference/ui_state_sheet_${option}.png`);
|
|
312
|
+
const el = path.join(projectDir, `assets/images/reference/element_state_sheet_${option}.png`);
|
|
313
|
+
const mc = path.join(
|
|
314
|
+
projectDir,
|
|
315
|
+
`assets/images/storyboard/master_composite_option_${option}.png`,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (!fs.existsSync(mc)) {
|
|
319
|
+
errors.push(`Missing MC: assets/images/storyboard/master_composite_option_${option}.png`);
|
|
320
|
+
}
|
|
321
|
+
if (!fs.existsSync(ui)) {
|
|
322
|
+
errors.push(`Missing ASR: assets/images/reference/ui_state_sheet_${option}.png`);
|
|
323
|
+
}
|
|
324
|
+
if (!fs.existsSync(el)) {
|
|
325
|
+
errors.push(`Missing ASR: assets/images/reference/element_state_sheet_${option}.png`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (designerLog && /element_2|element_state_sheet_\w+_2\.png/i.test(designerLog)) {
|
|
329
|
+
const el2 = path.join(
|
|
330
|
+
projectDir,
|
|
331
|
+
`assets/images/reference/element_state_sheet_${option}_2.png`,
|
|
332
|
+
);
|
|
333
|
+
if (!fs.existsSync(el2)) {
|
|
334
|
+
errors.push(
|
|
335
|
+
`Missing ASR overflow board: assets/images/reference/element_state_sheet_${option}_2.png`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return errors;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {string} projectDir
|
|
345
|
+
* @returns {string[]}
|
|
346
|
+
*/
|
|
347
|
+
export function validateDesignerGate2b(projectDir) {
|
|
348
|
+
const errors = [];
|
|
349
|
+
const logPath = path.join(projectDir, 'logs/designer-log.md');
|
|
350
|
+
const layoutPath = path.join(projectDir, 'docs/layout-spec.md');
|
|
351
|
+
const statePath = path.join(projectDir, 'docs/project-state.md');
|
|
352
|
+
|
|
353
|
+
if (!fs.existsSync(logPath)) {
|
|
354
|
+
return ['Create logs/designer-log.md before Gate #2b STOP'];
|
|
355
|
+
}
|
|
356
|
+
if (!fs.existsSync(layoutPath)) {
|
|
357
|
+
return ['Missing docs/layout-spec.md'];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const designerLog = fs.readFileSync(logPath, 'utf8');
|
|
361
|
+
const layoutSpec = fs.readFileSync(layoutPath, 'utf8');
|
|
362
|
+
|
|
363
|
+
errors.push(...validateImageProductionPreflight(designerLog));
|
|
364
|
+
|
|
365
|
+
if (!designerLog.includes(ASR_COVERAGE_HEADING)) {
|
|
366
|
+
errors.push(`Missing "${ASR_COVERAGE_HEADING}" section`);
|
|
367
|
+
} else {
|
|
368
|
+
errors.push(...validateSheetGridMetadata(designerLog));
|
|
369
|
+
errors.push(...validateCoverageRowCount(designerLog, layoutSpec));
|
|
370
|
+
errors.push(...validateTypeRepresentatives(designerLog));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let option = 'A';
|
|
374
|
+
if (fs.existsSync(statePath)) {
|
|
375
|
+
const opt = readSelectedMcOption(fs.readFileSync(statePath, 'utf8'));
|
|
376
|
+
if (opt) option = opt;
|
|
377
|
+
}
|
|
378
|
+
errors.push(...validateAsrPngFiles(projectDir, option, designerLog));
|
|
379
|
+
|
|
380
|
+
return errors;
|
|
381
|
+
}
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { validateDesignerGate2b } from './asr-coverage-validate.mjs';
|
|
14
15
|
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
|
|
17
18
|
const INTAKE_HEADING = '## Upstream Intake';
|
|
19
|
+
const SKILL_PREFLIGHT_HEADING = '## Skill Preflight';
|
|
18
20
|
const PLAN_HEADING = {
|
|
19
21
|
'technical-artist': '## Production Plan',
|
|
20
22
|
developer: '## UI Pass Plan', // default; gameplay_pass uses ## Gameplay Pass Plan
|
|
@@ -49,9 +51,13 @@ const TA_REQUIRED_DOCS = [
|
|
|
49
51
|
|
|
50
52
|
const DEV_REQUIRED_DOCS = [...TA_REQUIRED_DOCS, 'logs/ta-log.md'];
|
|
51
53
|
|
|
54
|
+
// Designer does not perform Upstream Intake (Designer is the upstream itself). Only Skill Preflight is enforced in Phase 2 (stage = production).
|
|
55
|
+
const DESIGNER_REQUIRED_DOCS = [];
|
|
56
|
+
|
|
52
57
|
const ROLE_LOG = {
|
|
53
58
|
'technical-artist': { log: 'logs/ta-log.md', docs: TA_REQUIRED_DOCS, label: 'Technical Artist' },
|
|
54
59
|
developer: { log: 'logs/developer-log.md', docs: DEV_REQUIRED_DOCS, label: 'Developer' },
|
|
60
|
+
designer: { log: 'logs/designer-log.md', docs: DESIGNER_REQUIRED_DOCS, label: 'Designer' },
|
|
55
61
|
};
|
|
56
62
|
|
|
57
63
|
/** @param {string} raw */
|
|
@@ -74,7 +80,7 @@ function normalizeRole(s) {
|
|
|
74
80
|
|
|
75
81
|
/**
|
|
76
82
|
* @param {Record<string, unknown>} input
|
|
77
|
-
* @returns {'technical-artist' | 'developer' | null}
|
|
83
|
+
* @returns {'technical-artist' | 'developer' | 'designer' | null}
|
|
78
84
|
*/
|
|
79
85
|
function detectRole(input) {
|
|
80
86
|
const candidates = [
|
|
@@ -90,6 +96,7 @@ function detectRole(input) {
|
|
|
90
96
|
const n = normalizeRole(String(c));
|
|
91
97
|
if (n.includes('technical-artist') || n === 'ta') return 'technical-artist';
|
|
92
98
|
if (n === 'developer' || n === 'dev') return 'developer';
|
|
99
|
+
if (n === 'designer') return 'designer';
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
const blobs = [
|
|
@@ -235,20 +242,93 @@ function validatePlan(content, role, stage) {
|
|
|
235
242
|
return errors;
|
|
236
243
|
}
|
|
237
244
|
|
|
238
|
-
|
|
245
|
+
// All convergence-v1 stages
|
|
246
|
+
const STAGE_NAMES =
|
|
247
|
+
'(pm|style_exploration|production|ui_pass|ui_review|ui_rework|gameplay_pass|done)';
|
|
248
|
+
const STAGE_MARKDOWN_BOLD_RE = new RegExp(`\\*\\*${STAGE_NAMES}\\*\\*`, 'i');
|
|
249
|
+
const STAGE_BACKTICK_RE = new RegExp(`stage[=:\\s]+\`${STAGE_NAMES}\``, 'i');
|
|
250
|
+
// YAML form inside ```yaml ... ``` block: `stage: ui_pass` (no backticks around value)
|
|
251
|
+
const STAGE_YAML_RE = new RegExp(`^\\s*stage:\\s*${STAGE_NAMES}\\s*$`, 'im');
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Extract current `stage` value from `docs/project-state.md`. Returns null if not found.
|
|
255
|
+
* Tries three forms (in order):
|
|
256
|
+
* 1. Markdown bold: `**ui_pass**` — used in Current Stage header
|
|
257
|
+
* 2. Backtick form: `` stage: `ui_pass` `` — used in prose
|
|
258
|
+
* 3. YAML form: `stage: ui_pass` — used in `## Agent handoff` YAML fence (most authoritative)
|
|
259
|
+
* @param {string} projectDir
|
|
260
|
+
*/
|
|
239
261
|
function readStage(projectDir) {
|
|
240
262
|
const statePath = path.join(projectDir, 'docs/project-state.md');
|
|
241
263
|
if (!fs.existsSync(statePath)) return null;
|
|
242
264
|
const content = fs.readFileSync(statePath, 'utf8');
|
|
243
265
|
const m =
|
|
244
|
-
content.match(
|
|
245
|
-
content.match(
|
|
266
|
+
content.match(STAGE_YAML_RE) ||
|
|
267
|
+
content.match(STAGE_MARKDOWN_BOLD_RE) ||
|
|
268
|
+
content.match(STAGE_BACKTICK_RE);
|
|
269
|
+
return m ? m[1].toLowerCase() : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function readGatePending(projectDir) {
|
|
273
|
+
const statePath = path.join(projectDir, 'docs/project-state.md');
|
|
274
|
+
if (!fs.existsSync(statePath)) return null;
|
|
275
|
+
const content = fs.readFileSync(statePath, 'utf8');
|
|
276
|
+
const m =
|
|
277
|
+
content.match(/gate_pending:\s*["']?(1|2a|2b|3)["']?/i) ||
|
|
278
|
+
content.match(/gate_pending:\s*(1|2a|2b|3)/i);
|
|
246
279
|
return m ? m[1].toLowerCase() : null;
|
|
247
280
|
}
|
|
248
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Validate Designer Skill Preflight section (Phase 2 / production only).
|
|
284
|
+
* @param {string} content
|
|
285
|
+
*/
|
|
286
|
+
function validateDesignerSkillPreflight(content) {
|
|
287
|
+
const errors = [];
|
|
288
|
+
const start = content.indexOf(SKILL_PREFLIGHT_HEADING);
|
|
289
|
+
if (start === -1) {
|
|
290
|
+
return [`Missing "${SKILL_PREFLIGHT_HEADING}" section — fill before Phase 2 STOP`];
|
|
291
|
+
}
|
|
292
|
+
const after = content.slice(start + SKILL_PREFLIGHT_HEADING.length);
|
|
293
|
+
const nextHeading = after.search(/\n##\s+/);
|
|
294
|
+
const section = nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
295
|
+
|
|
296
|
+
// Each non-header data row: | Skill | 已读 | 关键决策摘要 |
|
|
297
|
+
const rows = [];
|
|
298
|
+
for (const line of section.split('\n')) {
|
|
299
|
+
const trimmed = line.trim();
|
|
300
|
+
if (!trimmed.startsWith('|')) continue;
|
|
301
|
+
if (/^\|\s*---/.test(trimmed)) continue;
|
|
302
|
+
if (/^\|\s*Skill/i.test(trimmed)) continue;
|
|
303
|
+
|
|
304
|
+
const parts = trimmed
|
|
305
|
+
.split('|')
|
|
306
|
+
.map((p) => p.trim())
|
|
307
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
308
|
+
if (parts.length < 3) continue;
|
|
309
|
+
rows.push({ skill: parts[0], read: parts[1], summary: parts[2] });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (rows.length === 0) {
|
|
313
|
+
errors.push(`${SKILL_PREFLIGHT_HEADING}: table has no data rows`);
|
|
314
|
+
return errors;
|
|
315
|
+
}
|
|
316
|
+
for (const row of rows) {
|
|
317
|
+
if (!isReadChecked(row.read)) {
|
|
318
|
+
errors.push(`${SKILL_PREFLIGHT_HEADING}: "${row.skill}" — mark Read column (✓) after reading`);
|
|
319
|
+
}
|
|
320
|
+
if (!row.summary || PLACEHOLDER_RE.test(row.summary) || row.summary.length < MIN_TAKEAWAY_LEN) {
|
|
321
|
+
errors.push(
|
|
322
|
+
`${SKILL_PREFLIGHT_HEADING}: "${row.skill}" — add a concrete decision summary (≥${MIN_TAKEAWAY_LEN} chars, no {{placeholders}})`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return errors;
|
|
327
|
+
}
|
|
328
|
+
|
|
249
329
|
/**
|
|
250
330
|
* @param {string} projectDir
|
|
251
|
-
* @param {'technical-artist' | 'developer'} role
|
|
331
|
+
* @param {'technical-artist' | 'developer' | 'designer'} role
|
|
252
332
|
*/
|
|
253
333
|
function validateRole(projectDir, role) {
|
|
254
334
|
const cfg = ROLE_LOG[role];
|
|
@@ -256,11 +336,26 @@ function validateRole(projectDir, role) {
|
|
|
256
336
|
|
|
257
337
|
if (!fs.existsSync(logPath)) {
|
|
258
338
|
return [
|
|
259
|
-
`${cfg.label}: create ${cfg.log} from templates/${path.basename(cfg.log).replace('.md', '.template.md')}
|
|
339
|
+
`${cfg.label}: create ${cfg.log} from templates/${path.basename(cfg.log).replace('.md', '.template.md')} before STOP.`,
|
|
260
340
|
];
|
|
261
341
|
}
|
|
262
342
|
|
|
263
343
|
const content = fs.readFileSync(logPath, 'utf8');
|
|
344
|
+
|
|
345
|
+
if (role === 'designer') {
|
|
346
|
+
const stage = readStage(projectDir);
|
|
347
|
+
if (stage === 'production') {
|
|
348
|
+
return validateDesignerSkillPreflight(content);
|
|
349
|
+
}
|
|
350
|
+
if (stage === 'style_exploration') {
|
|
351
|
+
const gatePending = readGatePending(projectDir);
|
|
352
|
+
if (gatePending === '2b') {
|
|
353
|
+
return validateDesignerGate2b(projectDir);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
|
|
264
359
|
const intakeErrors = validateIntakeContent(content, cfg.docs);
|
|
265
360
|
const stage = role === 'developer' ? readStage(projectDir) : undefined;
|
|
266
361
|
const planErrors = validatePlan(content, role, stage ?? undefined);
|
|
@@ -296,12 +391,16 @@ export {
|
|
|
296
391
|
parseUpstreamIntake,
|
|
297
392
|
validateIntakeContent,
|
|
298
393
|
validatePlan,
|
|
394
|
+
validateDesignerSkillPreflight,
|
|
299
395
|
validateRole,
|
|
300
396
|
readStage,
|
|
397
|
+
readGatePending,
|
|
301
398
|
TA_REQUIRED_DOCS,
|
|
302
399
|
DEV_REQUIRED_DOCS,
|
|
400
|
+
DESIGNER_REQUIRED_DOCS,
|
|
303
401
|
PLAN_HEADING,
|
|
304
402
|
PLAN_REQUIRED_SUBS,
|
|
403
|
+
ROLE_LOG,
|
|
305
404
|
};
|
|
306
405
|
|
|
307
406
|
async function main() {
|
|
@@ -325,7 +424,14 @@ async function main() {
|
|
|
325
424
|
|
|
326
425
|
const errors = validateRole(projectDir, role);
|
|
327
426
|
if (errors.length > 0) {
|
|
328
|
-
const
|
|
427
|
+
const logPath = ROLE_LOG[role].log;
|
|
428
|
+
const section =
|
|
429
|
+
role === 'designer'
|
|
430
|
+
? readStage(projectDir) === 'style_exploration'
|
|
431
|
+
? '§ Image Production Preflight / ASR Coverage Matrix'
|
|
432
|
+
: '§ Skill Preflight'
|
|
433
|
+
: '§ Upstream Intake / Plan';
|
|
434
|
+
const reason = `${ROLE_LOG[role].label} STOP blocked — fix ${logPath} ${section}:\n- ${errors.join('\n- ')}`;
|
|
329
435
|
emitBlock(reason);
|
|
330
436
|
}
|
|
331
437
|
|