@playcraft/cli 0.0.41 → 0.0.43
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/prad.js +61 -0
- package/dist/commands/remix.js +4 -2
- package/dist/commands/skills.js +24 -0
- package/dist/commands/tools-generation.js +2 -4
- package/dist/commands/tools-utils.js +19 -0
- package/dist/prad/atom-ref.js +23 -0
- package/dist/prad/check.js +377 -0
- package/dist/prad/check.test.js +27 -0
- package/dist/prad/explain.js +109 -0
- package/dist/prad/load-spec.js +23 -0
- package/dist/prad/paths.js +83 -0
- package/dist/prad/skills-index.js +60 -0
- package/dist/utils/version-checker.js +8 -11
- package/package.json +3 -3
- package/project-template/.claude/agents/designer.md +34 -26
- package/project-template/.claude/agents/developer.md +55 -62
- package/project-template/.claude/agents/pm.md +3 -1
- package/project-template/.claude/agents/refs/README.md +21 -15
- package/project-template/.claude/agents/refs/designer-deliverable-spec.md +70 -7
- package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +21 -13
- package/project-template/.claude/agents/refs/designer-master-composite-recipes.md +20 -28
- 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/developer-phase1-flow.md +81 -156
- package/project-template/.claude/agents/refs/pm-workflow-detail.md +24 -2
- package/project-template/.claude/agents/refs/reviewer-convergence-eval.md +142 -0
- package/project-template/.claude/agents/refs/reviewer-six-dimension-eval.md +4 -284
- package/project-template/.claude/agents/refs/ta-atlas-deliverable-standard.md +27 -6
- package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +474 -29
- package/project-template/.claude/agents/reviewer.md +65 -38
- package/project-template/.claude/agents/technical-artist.md +38 -25
- package/project-template/.claude/hooks/README.md +40 -4
- package/project-template/.claude/hooks/asr-coverage-validate.mjs +381 -0
- package/project-template/.claude/hooks/validate-workflow-stop.mjs +196 -5
- package/project-template/.claude/settings.json +4 -0
- package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +76 -22
- package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +84 -15
- package/project-template/.claude/skills/playcraft-storyboard/SKILL.md +26 -7
- package/project-template/.claude/skills/playcraft-workflow/SKILL.md +104 -15
- package/project-template/.claude/skills/playwright-cli/SKILL.md +390 -0
- package/project-template/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
- package/project-template/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/project-template/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
- package/project-template/.claude/skills/playwright-cli/references/running-code.md +240 -0
- package/project-template/.claude/skills/playwright-cli/references/session-management.md +226 -0
- package/project-template/.claude/skills/playwright-cli/references/spec-driven-testing.md +312 -0
- package/project-template/.claude/skills/playwright-cli/references/storage-state.md +275 -0
- package/project-template/.claude/skills/playwright-cli/references/test-generation.md +138 -0
- package/project-template/.claude/skills/playwright-cli/references/tracing.md +142 -0
- package/project-template/.claude/skills/playwright-cli/references/video-recording.md +157 -0
- package/project-template/.cursor/rules/playcraft-orchestrator.mdc +74 -24
- package/project-template/.cursor/rules/playcraft-subagent-boundary.mdc +1 -1
- package/project-template/CLAUDE.md +99 -59
- package/project-template/docs/team/agent-conduct.md +42 -26
- package/project-template/docs/team/agent-runtime-matrix.md +71 -39
- package/project-template/docs/team/atom-plan-format.md +101 -2
- package/project-template/docs/team/collaboration.md +57 -48
- package/project-template/docs/team/core-model.md +20 -19
- package/project-template/docs/team/workflow-changelog.md +28 -14
- package/project-template/docs/team/workflow-consistency-checklist.md +64 -0
- package/project-template/templates/atom-plan.template.json +18 -0
- package/project-template/templates/atom-plan.template.md +35 -3
- package/project-template/templates/designer-log.template.md +94 -5
- package/project-template/templates/developer-log.template.md +95 -101
- package/project-template/templates/layout-spec.template.md +62 -8
- package/project-template/templates/project-state.template.md +51 -33
- package/project-template/templates/review-report.template.md +76 -151
- package/project-template/templates/ta-log.template.md +180 -14
|
@@ -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,31 @@
|
|
|
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';
|
|
20
|
+
const PLAN_HEADING = {
|
|
21
|
+
'technical-artist': '## Production Plan',
|
|
22
|
+
developer: '## UI Pass Plan', // default; gameplay_pass uses ## Gameplay Pass Plan
|
|
23
|
+
};
|
|
24
|
+
const PLAN_HEADING_BY_STAGE = {
|
|
25
|
+
developer: {
|
|
26
|
+
ui_pass: '## UI Pass Plan',
|
|
27
|
+
ui_rework: '## UI Pass Plan',
|
|
28
|
+
gameplay_pass: '## Gameplay Pass Plan',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const PLAN_REQUIRED_SUBS = {
|
|
32
|
+
developer: {
|
|
33
|
+
ui_pass: ['### Scene-Asset Binding Plan', '### Scene navigation', '### Risk Checklist'],
|
|
34
|
+
ui_rework: ['### Scene-Asset Binding Plan', '### Scene navigation', '### Risk Checklist'],
|
|
35
|
+
gameplay_pass: ['### PGS Strategy', '### Risk Checklist'],
|
|
36
|
+
},
|
|
37
|
+
'technical-artist': ['### Coverage Plan', '### Atlas Assembly Plan', '### Risk Checklist'],
|
|
38
|
+
};
|
|
18
39
|
const PLACEHOLDER_RE = /\{\{[^}]+\}\}/;
|
|
19
40
|
const MIN_TAKEAWAY_LEN = 8;
|
|
20
41
|
|
|
@@ -30,9 +51,13 @@ const TA_REQUIRED_DOCS = [
|
|
|
30
51
|
|
|
31
52
|
const DEV_REQUIRED_DOCS = [...TA_REQUIRED_DOCS, 'logs/ta-log.md'];
|
|
32
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
|
+
|
|
33
57
|
const ROLE_LOG = {
|
|
34
58
|
'technical-artist': { log: 'logs/ta-log.md', docs: TA_REQUIRED_DOCS, label: 'Technical Artist' },
|
|
35
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' },
|
|
36
61
|
};
|
|
37
62
|
|
|
38
63
|
/** @param {string} raw */
|
|
@@ -55,7 +80,7 @@ function normalizeRole(s) {
|
|
|
55
80
|
|
|
56
81
|
/**
|
|
57
82
|
* @param {Record<string, unknown>} input
|
|
58
|
-
* @returns {'technical-artist' | 'developer' | null}
|
|
83
|
+
* @returns {'technical-artist' | 'developer' | 'designer' | null}
|
|
59
84
|
*/
|
|
60
85
|
function detectRole(input) {
|
|
61
86
|
const candidates = [
|
|
@@ -71,6 +96,7 @@ function detectRole(input) {
|
|
|
71
96
|
const n = normalizeRole(String(c));
|
|
72
97
|
if (n.includes('technical-artist') || n === 'ta') return 'technical-artist';
|
|
73
98
|
if (n === 'developer' || n === 'dev') return 'developer';
|
|
99
|
+
if (n === 'designer') return 'designer';
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
const blobs = [
|
|
@@ -169,8 +195,140 @@ function validateIntakeContent(content, requiredDocs) {
|
|
|
169
195
|
}
|
|
170
196
|
|
|
171
197
|
/**
|
|
172
|
-
* @param {string}
|
|
198
|
+
* @param {string} content
|
|
173
199
|
* @param {'technical-artist' | 'developer'} role
|
|
200
|
+
* @param {string} [stage]
|
|
201
|
+
*/
|
|
202
|
+
function validatePlan(content, role, stage) {
|
|
203
|
+
let heading = PLAN_HEADING[role];
|
|
204
|
+
/** @type {string[]} */
|
|
205
|
+
let requiredSubs = [];
|
|
206
|
+
|
|
207
|
+
if (role === 'developer') {
|
|
208
|
+
if (stage && PLAN_HEADING_BY_STAGE.developer[stage]) {
|
|
209
|
+
heading = PLAN_HEADING_BY_STAGE.developer[stage];
|
|
210
|
+
requiredSubs = PLAN_REQUIRED_SUBS.developer[stage] ?? [];
|
|
211
|
+
}
|
|
212
|
+
} else if (role === 'technical-artist') {
|
|
213
|
+
requiredSubs = PLAN_REQUIRED_SUBS['technical-artist'] ?? [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const errors = [];
|
|
217
|
+
const start = content.indexOf(heading);
|
|
218
|
+
if (start === -1) {
|
|
219
|
+
return [`Missing "${heading}" section — write plan before STOP (stage: ${stage || 'unknown'})`];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const after = content.slice(start + heading.length);
|
|
223
|
+
const nextHeading = after.search(/\n##\s+/);
|
|
224
|
+
const section = nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
225
|
+
|
|
226
|
+
for (const sub of requiredSubs) {
|
|
227
|
+
if (!section.includes(sub)) {
|
|
228
|
+
errors.push(`${heading}: missing ${sub}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const checkboxes = section.match(/- \[[ xX]\]/gi) || [];
|
|
233
|
+
const unchecked = checkboxes.filter((c) => /\[ \]/i.test(c));
|
|
234
|
+
if (checkboxes.length > 0 && unchecked.length > 0) {
|
|
235
|
+
errors.push(`${heading} Risk Checklist: ${unchecked.length} unchecked item(s)`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (PLACEHOLDER_RE.test(section)) {
|
|
239
|
+
errors.push(`${heading}: contains {{placeholders}} — fill with real data`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return errors;
|
|
243
|
+
}
|
|
244
|
+
|
|
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
|
+
*/
|
|
261
|
+
function readStage(projectDir) {
|
|
262
|
+
const statePath = path.join(projectDir, 'docs/project-state.md');
|
|
263
|
+
if (!fs.existsSync(statePath)) return null;
|
|
264
|
+
const content = fs.readFileSync(statePath, 'utf8');
|
|
265
|
+
const m =
|
|
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);
|
|
279
|
+
return m ? m[1].toLowerCase() : null;
|
|
280
|
+
}
|
|
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
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @param {string} projectDir
|
|
331
|
+
* @param {'technical-artist' | 'developer' | 'designer'} role
|
|
174
332
|
*/
|
|
175
333
|
function validateRole(projectDir, role) {
|
|
176
334
|
const cfg = ROLE_LOG[role];
|
|
@@ -178,12 +336,30 @@ function validateRole(projectDir, role) {
|
|
|
178
336
|
|
|
179
337
|
if (!fs.existsSync(logPath)) {
|
|
180
338
|
return [
|
|
181
|
-
`${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.`,
|
|
182
340
|
];
|
|
183
341
|
}
|
|
184
342
|
|
|
185
343
|
const content = fs.readFileSync(logPath, 'utf8');
|
|
186
|
-
|
|
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
|
+
|
|
359
|
+
const intakeErrors = validateIntakeContent(content, cfg.docs);
|
|
360
|
+
const stage = role === 'developer' ? readStage(projectDir) : undefined;
|
|
361
|
+
const planErrors = validatePlan(content, role, stage ?? undefined);
|
|
362
|
+
return [...intakeErrors, ...planErrors];
|
|
187
363
|
}
|
|
188
364
|
|
|
189
365
|
/** @param {string} projectDir */
|
|
@@ -214,9 +390,17 @@ export {
|
|
|
214
390
|
detectRole,
|
|
215
391
|
parseUpstreamIntake,
|
|
216
392
|
validateIntakeContent,
|
|
393
|
+
validatePlan,
|
|
394
|
+
validateDesignerSkillPreflight,
|
|
217
395
|
validateRole,
|
|
396
|
+
readStage,
|
|
397
|
+
readGatePending,
|
|
218
398
|
TA_REQUIRED_DOCS,
|
|
219
399
|
DEV_REQUIRED_DOCS,
|
|
400
|
+
DESIGNER_REQUIRED_DOCS,
|
|
401
|
+
PLAN_HEADING,
|
|
402
|
+
PLAN_REQUIRED_SUBS,
|
|
403
|
+
ROLE_LOG,
|
|
220
404
|
};
|
|
221
405
|
|
|
222
406
|
async function main() {
|
|
@@ -240,7 +424,14 @@ async function main() {
|
|
|
240
424
|
|
|
241
425
|
const errors = validateRole(projectDir, role);
|
|
242
426
|
if (errors.length > 0) {
|
|
243
|
-
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- ')}`;
|
|
244
435
|
emitBlock(reason);
|
|
245
436
|
}
|
|
246
437
|
|