@playcraft/cli 0.0.42 → 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/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/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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
const PLACEHOLDER_RE = /\{\{[^}]+\}\}/;
|
|
4
|
+
const MIN_TAKEAWAY_LEN = 8;
|
|
5
|
+
export const IMAGE_PRODUCTION_PREFLIGHT_HEADING = '## Image Production Preflight';
|
|
6
|
+
export const ASR_COVERAGE_HEADING = '## ASR Coverage Matrix';
|
|
7
|
+
export const REQUIRED_IMAGE_SKILLS = [
|
|
8
|
+
'playcraft-storyboard',
|
|
9
|
+
'playcraft-asset-state-sheet',
|
|
10
|
+
'playcraft-image-generation',
|
|
11
|
+
];
|
|
12
|
+
export function readSelectedMcOption(content) {
|
|
13
|
+
const m = content.match(/selectedMcOption:\s*["']?([ABC])["']?/i) ||
|
|
14
|
+
content.match(/selectedMcOption\s*=\s*([ABC])/i);
|
|
15
|
+
return m ? m[1].toUpperCase() : null;
|
|
16
|
+
}
|
|
17
|
+
function extractSection(content, heading) {
|
|
18
|
+
const start = content.indexOf(heading);
|
|
19
|
+
if (start === -1)
|
|
20
|
+
return '';
|
|
21
|
+
const after = content.slice(start + heading.length);
|
|
22
|
+
const next = after.search(/\n##\s+/);
|
|
23
|
+
return next === -1 ? after : after.slice(0, next);
|
|
24
|
+
}
|
|
25
|
+
export function parseLayoutSpecContractIds(layoutSpec) {
|
|
26
|
+
const elementIds = [];
|
|
27
|
+
const staticTextIds = [];
|
|
28
|
+
const assetSection = extractSection(layoutSpec, '## Asset Mapping');
|
|
29
|
+
for (const line of assetSection.split('\n')) {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (!trimmed.startsWith('|'))
|
|
32
|
+
continue;
|
|
33
|
+
if (/^\|\s*---/.test(trimmed))
|
|
34
|
+
continue;
|
|
35
|
+
if (/^\|\s*elementId/i.test(trimmed))
|
|
36
|
+
continue;
|
|
37
|
+
const parts = trimmed
|
|
38
|
+
.split('|')
|
|
39
|
+
.map((p) => p.trim())
|
|
40
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
41
|
+
if (parts.length < 1)
|
|
42
|
+
continue;
|
|
43
|
+
const id = parts[0].replace(/`/g, '');
|
|
44
|
+
if (!id || id === '—' || id === '-' || PLACEHOLDER_RE.test(id))
|
|
45
|
+
continue;
|
|
46
|
+
elementIds.push(id);
|
|
47
|
+
}
|
|
48
|
+
const textSection = extractSection(layoutSpec, '### Static Text');
|
|
49
|
+
for (const line of textSection.split('\n')) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (!trimmed.startsWith('|'))
|
|
52
|
+
continue;
|
|
53
|
+
if (/^\|\s*---/.test(trimmed))
|
|
54
|
+
continue;
|
|
55
|
+
if (/^\|\s*id/i.test(trimmed))
|
|
56
|
+
continue;
|
|
57
|
+
const parts = trimmed
|
|
58
|
+
.split('|')
|
|
59
|
+
.map((p) => p.trim())
|
|
60
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
61
|
+
if (parts.length < 1)
|
|
62
|
+
continue;
|
|
63
|
+
const id = parts[0].replace(/`/g, '');
|
|
64
|
+
if (!id || id === '—' || PLACEHOLDER_RE.test(id))
|
|
65
|
+
continue;
|
|
66
|
+
staticTextIds.push(id);
|
|
67
|
+
}
|
|
68
|
+
return { elementIds, staticTextIds };
|
|
69
|
+
}
|
|
70
|
+
function isReadChecked(cell) {
|
|
71
|
+
const t = cell.trim();
|
|
72
|
+
if (/[✓✔☑]/.test(t))
|
|
73
|
+
return true;
|
|
74
|
+
if (/\[x\]/i.test(t))
|
|
75
|
+
return true;
|
|
76
|
+
if (/^(yes|done|true|read)$/i.test(t))
|
|
77
|
+
return true;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
export function validateImageProductionPreflight(content) {
|
|
81
|
+
const errors = [];
|
|
82
|
+
const start = content.indexOf(IMAGE_PRODUCTION_PREFLIGHT_HEADING);
|
|
83
|
+
if (start === -1) {
|
|
84
|
+
return [`Missing "${IMAGE_PRODUCTION_PREFLIGHT_HEADING}" section`];
|
|
85
|
+
}
|
|
86
|
+
const after = content.slice(start + IMAGE_PRODUCTION_PREFLIGHT_HEADING.length);
|
|
87
|
+
const nextHeading = after.search(/\n##\s+/);
|
|
88
|
+
const section = nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
89
|
+
const rows = [];
|
|
90
|
+
for (const line of section.split('\n')) {
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
if (!trimmed.startsWith('|'))
|
|
93
|
+
continue;
|
|
94
|
+
if (/^\|\s*---/.test(trimmed))
|
|
95
|
+
continue;
|
|
96
|
+
if (/^\|\s*Skill/i.test(trimmed))
|
|
97
|
+
continue;
|
|
98
|
+
const parts = trimmed
|
|
99
|
+
.split('|')
|
|
100
|
+
.map((p) => p.trim())
|
|
101
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
102
|
+
if (parts.length < 3)
|
|
103
|
+
continue;
|
|
104
|
+
rows.push({ skill: parts[0], read: parts[1], summary: parts[2] });
|
|
105
|
+
}
|
|
106
|
+
if (rows.length === 0) {
|
|
107
|
+
errors.push(`${IMAGE_PRODUCTION_PREFLIGHT_HEADING}: table has no data rows`);
|
|
108
|
+
return errors;
|
|
109
|
+
}
|
|
110
|
+
for (const required of REQUIRED_IMAGE_SKILLS) {
|
|
111
|
+
const row = rows.find((r) => r.skill.includes(required));
|
|
112
|
+
if (!row) {
|
|
113
|
+
errors.push(`${IMAGE_PRODUCTION_PREFLIGHT_HEADING}: missing row for ${required}`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!isReadChecked(row.read)) {
|
|
117
|
+
errors.push(`${IMAGE_PRODUCTION_PREFLIGHT_HEADING}: "${required}" — mark 已读 (✓)`);
|
|
118
|
+
}
|
|
119
|
+
if (!row.summary || PLACEHOLDER_RE.test(row.summary) || row.summary.length < MIN_TAKEAWAY_LEN) {
|
|
120
|
+
errors.push(`${IMAGE_PRODUCTION_PREFLIGHT_HEADING}: "${required}" — add decision summary (≥${MIN_TAKEAWAY_LEN} chars)`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return errors;
|
|
124
|
+
}
|
|
125
|
+
export function parseCoverageMatrixRows(content) {
|
|
126
|
+
const section = extractSection(content, ASR_COVERAGE_HEADING);
|
|
127
|
+
const coverageStart = section.indexOf('### Coverage');
|
|
128
|
+
const coverageBlock = coverageStart === -1 ? section : section.slice(coverageStart);
|
|
129
|
+
const rows = [];
|
|
130
|
+
for (const line of coverageBlock.split('\n')) {
|
|
131
|
+
const trimmed = line.trim();
|
|
132
|
+
if (!trimmed.startsWith('|'))
|
|
133
|
+
continue;
|
|
134
|
+
if (/^\|\s*---/.test(trimmed))
|
|
135
|
+
continue;
|
|
136
|
+
if (/^\|\s*Contract id/i.test(trimmed))
|
|
137
|
+
continue;
|
|
138
|
+
const parts = trimmed
|
|
139
|
+
.split('|')
|
|
140
|
+
.map((p) => p.trim())
|
|
141
|
+
.filter((_, i, arr) => i > 0 && i < arr.length - 1);
|
|
142
|
+
if (parts.length < 4)
|
|
143
|
+
continue;
|
|
144
|
+
const contractId = parts[0].replace(/`/g, '');
|
|
145
|
+
if (!contractId || PLACEHOLDER_RE.test(contractId))
|
|
146
|
+
continue;
|
|
147
|
+
const typeValues = new Set(['visual', 'audio', 'text', 'digit', 'sfx']);
|
|
148
|
+
let elementType = '';
|
|
149
|
+
let coverageLayer = '';
|
|
150
|
+
if (parts.length >= 4 && typeValues.has(parts[2]?.toLowerCase())) {
|
|
151
|
+
// id | elementType | Type | CoverageLayer | ...
|
|
152
|
+
elementType = parts[1];
|
|
153
|
+
coverageLayer = parts[3];
|
|
154
|
+
}
|
|
155
|
+
else if (parts.length >= 3 && typeValues.has(parts[1]?.toLowerCase())) {
|
|
156
|
+
// Legacy: id | Type | CoverageLayer | ...
|
|
157
|
+
coverageLayer = parts[2];
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
elementType = parts.length >= 6 ? parts[1] : '';
|
|
161
|
+
coverageLayer = parts.length >= 4 ? parts[3] : parts[1] || '';
|
|
162
|
+
}
|
|
163
|
+
rows.push({
|
|
164
|
+
contractId,
|
|
165
|
+
elementType: elementType.replace(/`/g, ''),
|
|
166
|
+
coverageLayer: coverageLayer.toLowerCase(),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return rows;
|
|
170
|
+
}
|
|
171
|
+
export function validateSheetGridMetadata(content) {
|
|
172
|
+
const errors = [];
|
|
173
|
+
const section = extractSection(content, ASR_COVERAGE_HEADING);
|
|
174
|
+
if (!section.includes('### Sheet grid metadata')) {
|
|
175
|
+
errors.push('ASR Coverage Matrix: missing ### Sheet grid metadata table');
|
|
176
|
+
return errors;
|
|
177
|
+
}
|
|
178
|
+
const gridStart = section.indexOf('### Sheet grid metadata');
|
|
179
|
+
const gridBlock = section.slice(gridStart);
|
|
180
|
+
const gridEnd = gridBlock.search(/\n###\s+/);
|
|
181
|
+
const gridSection = gridEnd === -1 ? gridBlock : gridBlock.slice(0, gridEnd);
|
|
182
|
+
if (PLACEHOLDER_RE.test(gridSection)) {
|
|
183
|
+
errors.push('Sheet grid metadata: contains {{placeholders}} — fill real rows/cols/cell dimensions');
|
|
184
|
+
}
|
|
185
|
+
let dataRows = 0;
|
|
186
|
+
for (const line of gridSection.split('\n')) {
|
|
187
|
+
const trimmed = line.trim();
|
|
188
|
+
if (!trimmed.startsWith('|'))
|
|
189
|
+
continue;
|
|
190
|
+
if (/^\|\s*---/.test(trimmed))
|
|
191
|
+
continue;
|
|
192
|
+
if (/^\|\s*Sheet/i.test(trimmed))
|
|
193
|
+
continue;
|
|
194
|
+
dataRows++;
|
|
195
|
+
}
|
|
196
|
+
if (dataRows < 2) {
|
|
197
|
+
errors.push('Sheet grid metadata: need at least ui + element sheet rows');
|
|
198
|
+
}
|
|
199
|
+
return errors;
|
|
200
|
+
}
|
|
201
|
+
export function validateCoverageRowCount(designerLog, layoutSpec) {
|
|
202
|
+
const errors = [];
|
|
203
|
+
const { elementIds, staticTextIds } = parseLayoutSpecContractIds(layoutSpec);
|
|
204
|
+
const expected = elementIds.length + staticTextIds.length;
|
|
205
|
+
const matrixRows = parseCoverageMatrixRows(designerLog);
|
|
206
|
+
if (expected === 0) {
|
|
207
|
+
errors.push('layout-spec: could not parse assetMapping/static text ids — fill Asset Mapping table');
|
|
208
|
+
return errors;
|
|
209
|
+
}
|
|
210
|
+
if (matrixRows.length < expected) {
|
|
211
|
+
errors.push(`Coverage Matrix: ${matrixRows.length} rows but layout-spec expects ${expected} (assetMapping + static text)`);
|
|
212
|
+
}
|
|
213
|
+
const contractSet = new Set([...elementIds, ...staticTextIds]);
|
|
214
|
+
const matrixIds = new Set(matrixRows.map((r) => r.contractId));
|
|
215
|
+
for (const id of contractSet) {
|
|
216
|
+
if (!matrixIds.has(id)) {
|
|
217
|
+
errors.push(`Coverage Matrix: missing row for contract id "${id}"`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return errors;
|
|
221
|
+
}
|
|
222
|
+
export function validateTypeRepresentatives(designerLog) {
|
|
223
|
+
const errors = [];
|
|
224
|
+
const rows = parseCoverageMatrixRows(designerLog);
|
|
225
|
+
const typeLayers = new Map();
|
|
226
|
+
for (const row of rows) {
|
|
227
|
+
const type = row.elementType && row.elementType !== '—' ? row.elementType : row.contractId.split('_')[0];
|
|
228
|
+
if (!type)
|
|
229
|
+
continue;
|
|
230
|
+
if (!typeLayers.has(type))
|
|
231
|
+
typeLayers.set(type, new Set());
|
|
232
|
+
typeLayers.get(type).add(row.coverageLayer);
|
|
233
|
+
}
|
|
234
|
+
for (const [type, layers] of typeLayers) {
|
|
235
|
+
const hasRep = [...layers].some((l) => l.includes('on-asr')) || [...layers].some((l) => l.includes('mc-crop'));
|
|
236
|
+
if (!hasRep && layers.size > 0) {
|
|
237
|
+
const allExtends = [...layers].every((l) => l.includes('ta-extends') || l.includes('ta extends'));
|
|
238
|
+
if (allExtends) {
|
|
239
|
+
errors.push(`Coverage Matrix: elementType "${type}" has no on-asr or mc-crop representative`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return errors;
|
|
244
|
+
}
|
|
245
|
+
export function validateAsrPngFiles(projectDir, option, designerLog) {
|
|
246
|
+
const errors = [];
|
|
247
|
+
const ui = path.join(projectDir, `assets/images/reference/ui_state_sheet_${option}.png`);
|
|
248
|
+
const el = path.join(projectDir, `assets/images/reference/element_state_sheet_${option}.png`);
|
|
249
|
+
const mc = path.join(projectDir, `assets/images/storyboard/master_composite_option_${option}.png`);
|
|
250
|
+
if (!fs.existsSync(mc)) {
|
|
251
|
+
errors.push(`Missing MC: assets/images/storyboard/master_composite_option_${option}.png`);
|
|
252
|
+
}
|
|
253
|
+
if (!fs.existsSync(ui)) {
|
|
254
|
+
errors.push(`Missing ASR: assets/images/reference/ui_state_sheet_${option}.png`);
|
|
255
|
+
}
|
|
256
|
+
if (!fs.existsSync(el)) {
|
|
257
|
+
errors.push(`Missing ASR: assets/images/reference/element_state_sheet_${option}.png`);
|
|
258
|
+
}
|
|
259
|
+
if (designerLog && /element_2|element_state_sheet_\w+_2\.png/i.test(designerLog)) {
|
|
260
|
+
const el2 = path.join(projectDir, `assets/images/reference/element_state_sheet_${option}_2.png`);
|
|
261
|
+
if (!fs.existsSync(el2)) {
|
|
262
|
+
errors.push(`Missing ASR overflow board: assets/images/reference/element_state_sheet_${option}_2.png`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return errors;
|
|
266
|
+
}
|
|
267
|
+
export function validateDesignerGate2b(projectDir) {
|
|
268
|
+
const errors = [];
|
|
269
|
+
const logPath = path.join(projectDir, 'logs/designer-log.md');
|
|
270
|
+
const layoutPath = path.join(projectDir, 'docs/layout-spec.md');
|
|
271
|
+
const statePath = path.join(projectDir, 'docs/project-state.md');
|
|
272
|
+
if (!fs.existsSync(logPath)) {
|
|
273
|
+
return ['Create logs/designer-log.md before Gate #2b STOP'];
|
|
274
|
+
}
|
|
275
|
+
if (!fs.existsSync(layoutPath)) {
|
|
276
|
+
return ['Missing docs/layout-spec.md'];
|
|
277
|
+
}
|
|
278
|
+
const designerLog = fs.readFileSync(logPath, 'utf8');
|
|
279
|
+
const layoutSpec = fs.readFileSync(layoutPath, 'utf8');
|
|
280
|
+
errors.push(...validateImageProductionPreflight(designerLog));
|
|
281
|
+
if (!designerLog.includes(ASR_COVERAGE_HEADING)) {
|
|
282
|
+
errors.push(`Missing "${ASR_COVERAGE_HEADING}" section`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
errors.push(...validateSheetGridMetadata(designerLog));
|
|
286
|
+
errors.push(...validateCoverageRowCount(designerLog, layoutSpec));
|
|
287
|
+
errors.push(...validateTypeRepresentatives(designerLog));
|
|
288
|
+
}
|
|
289
|
+
let option = 'A';
|
|
290
|
+
if (fs.existsSync(statePath)) {
|
|
291
|
+
const opt = readSelectedMcOption(fs.readFileSync(statePath, 'utf8'));
|
|
292
|
+
if (opt)
|
|
293
|
+
option = opt;
|
|
294
|
+
}
|
|
295
|
+
errors.push(...validateAsrPngFiles(projectDir, option, designerLog));
|
|
296
|
+
return errors;
|
|
297
|
+
}
|
|
298
|
+
export function validateAsrCoverageProject(projectDir) {
|
|
299
|
+
const layoutPath = path.join(projectDir, 'docs/layout-spec.md');
|
|
300
|
+
const logPath = path.join(projectDir, 'logs/designer-log.md');
|
|
301
|
+
let expectedContractRows = 0;
|
|
302
|
+
let matrixRows = 0;
|
|
303
|
+
if (fs.existsSync(layoutPath)) {
|
|
304
|
+
const ids = parseLayoutSpecContractIds(fs.readFileSync(layoutPath, 'utf8'));
|
|
305
|
+
expectedContractRows = ids.elementIds.length + ids.staticTextIds.length;
|
|
306
|
+
}
|
|
307
|
+
if (fs.existsSync(logPath)) {
|
|
308
|
+
matrixRows = parseCoverageMatrixRows(fs.readFileSync(logPath, 'utf8')).length;
|
|
309
|
+
}
|
|
310
|
+
const errors = validateDesignerGate2b(projectDir);
|
|
311
|
+
return {
|
|
312
|
+
ok: errors.length === 0,
|
|
313
|
+
errors,
|
|
314
|
+
expectedContractRows,
|
|
315
|
+
matrixRows,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { checkPradFile } from '../prad/check.js';
|
|
4
|
+
import { explainPradPath, formatExplain } from '../prad/explain.js';
|
|
5
|
+
export function registerPradCommands(program) {
|
|
6
|
+
const prad = program.command('prad').description('PRAD (PlayCraft Remix Assembly DSL) 校验与调试');
|
|
7
|
+
prad
|
|
8
|
+
.command('check')
|
|
9
|
+
.description('静态校验 atom-tree.json(PRAD 文档)')
|
|
10
|
+
.argument('[file]', 'PRAD JSON 文件', 'atom-tree.json')
|
|
11
|
+
.option('--gate <n>', 'Gate 1–4', (v) => parseInt(v, 10))
|
|
12
|
+
.option('--strict-drafts', 'draft atom 视为 error')
|
|
13
|
+
.option('--legacy', '允许缺失 pradVersion')
|
|
14
|
+
.option('--spec-dir <path>', 'PRAD spec 目录(含 spine-registry)')
|
|
15
|
+
.option('--skills-dir <path>', 'Skills 库目录')
|
|
16
|
+
.action((file, options) => {
|
|
17
|
+
if (!fs.existsSync(file)) {
|
|
18
|
+
console.error(pc.red(`File not found: ${file}`));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const result = checkPradFile(file, {
|
|
22
|
+
gate: options.gate,
|
|
23
|
+
strictDrafts: options.strictDrafts,
|
|
24
|
+
legacy: options.legacy,
|
|
25
|
+
specDir: options.specDir,
|
|
26
|
+
skillsDir: options.skillsDir,
|
|
27
|
+
});
|
|
28
|
+
for (const w of result.warnings) {
|
|
29
|
+
console.log(pc.yellow(`${w.code} [${w.path}] ${w.message}`));
|
|
30
|
+
}
|
|
31
|
+
for (const e of result.errors) {
|
|
32
|
+
console.error(pc.red(`${e.code} [${e.path}] ${e.message}`));
|
|
33
|
+
}
|
|
34
|
+
if (result.ok) {
|
|
35
|
+
console.log(pc.green('prad check: pass'));
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
console.error(pc.red(`prad check: fail (${result.errors.length} errors)`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
prad
|
|
42
|
+
.command('explain')
|
|
43
|
+
.description('解释 PRAD 节点 path 的上下文(slot / atom / bindAs)')
|
|
44
|
+
.argument('[file]', 'PRAD JSON 文件', 'atom-tree.json')
|
|
45
|
+
.requiredOption('--path <logicalPath>', '逻辑 path,如 scenes.entry.ui_layer.play_btn')
|
|
46
|
+
.option('--spec-dir <path>', 'PRAD spec 目录')
|
|
47
|
+
.option('--skills-dir <path>', 'Skills 库目录')
|
|
48
|
+
.action((file, options) => {
|
|
49
|
+
if (!fs.existsSync(file)) {
|
|
50
|
+
console.error(pc.red(`File not found: ${file}`));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const doc = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
54
|
+
const result = explainPradPath(doc, options.path, {
|
|
55
|
+
specDir: options.specDir,
|
|
56
|
+
skillsDir: options.skillsDir,
|
|
57
|
+
});
|
|
58
|
+
console.log(formatExplain(result));
|
|
59
|
+
process.exit(result.found ? 0 : 1);
|
|
60
|
+
});
|
|
61
|
+
}
|
package/dist/commands/remix.js
CHANGED
|
@@ -120,7 +120,8 @@ async function remixEnsureStream(options) {
|
|
|
120
120
|
throw new Error(event.message || 'ensure-stream 返回 error 事件');
|
|
121
121
|
}
|
|
122
122
|
else if (event.type === 'capacity_exceeded') {
|
|
123
|
-
|
|
123
|
+
const selectableCandidates = event.candidates.filter((c) => c.evictable !== false);
|
|
124
|
+
if (!selectableCandidates.length) {
|
|
124
125
|
throw new Error(`容量已满 (${event.current}/${event.max}),但服务端未返回可驱逐候选`);
|
|
125
126
|
}
|
|
126
127
|
const selected = await inquirer.prompt([
|
|
@@ -129,8 +130,9 @@ async function remixEnsureStream(options) {
|
|
|
129
130
|
name: 'slotId',
|
|
130
131
|
message: `沙箱容量已满 (${event.current}/${event.max}),请选择一个候选项继续驱逐:`,
|
|
131
132
|
choices: event.candidates.map((c) => ({
|
|
132
|
-
name: `${c.slotId} (${c.projectId}/${c.branchName}) idle=${formatIdleSec(c.idleSec)}${c.hasUnpushedChanges ? ' [unpushed]' : ''}`,
|
|
133
|
+
name: `${c.slotId} (${c.projectId}/${c.branchName}) status=${c.agentStatus ?? 'unknown'} idle=${formatIdleSec(c.idleSec)}${c.hasUnpushedChanges ? ' [unpushed]' : ''}`,
|
|
133
134
|
value: c.slotId,
|
|
135
|
+
disabled: c.evictable === false ? (c.disabledReason ?? 'not evictable') : false,
|
|
134
136
|
})),
|
|
135
137
|
},
|
|
136
138
|
]);
|
package/dist/commands/skills.js
CHANGED
|
@@ -20,6 +20,7 @@ import * as fs from 'node:fs';
|
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { parseSkillRefsFromAtomPlanProject, validateAtomPlanProject } from '../atom-plan/validate-atom-plan.js';
|
|
23
|
+
import { validateAsrCoverageProject } from '../atom-plan/validate-asr-coverage.js';
|
|
23
24
|
import { loadConfig } from '../config.js';
|
|
24
25
|
const KNOWN_ENGINES = ['phaser', 'threejs'];
|
|
25
26
|
const SCORE_THRESHOLD_FULL = 8;
|
|
@@ -1352,6 +1353,29 @@ export function registerSkillsCommands(program) {
|
|
|
1352
1353
|
if (!result.ok)
|
|
1353
1354
|
process.exit(1);
|
|
1354
1355
|
});
|
|
1356
|
+
// ── validate-asr-coverage ─────────────────────────────────────────────────
|
|
1357
|
+
addSkillsDirOption(skills
|
|
1358
|
+
.command('validate-asr-coverage')
|
|
1359
|
+
.description('校验 Gate #2b ASR Coverage Matrix、Image Production Preflight 与 ASR/MC PNG 文件')
|
|
1360
|
+
.option('--project-dir <path>', '项目根目录', process.cwd())
|
|
1361
|
+
.option('--json', '输出 JSON 格式')).action(async (opts) => {
|
|
1362
|
+
void opts.skillsDir;
|
|
1363
|
+
const result = validateAsrCoverageProject(opts.projectDir);
|
|
1364
|
+
if (opts.json) {
|
|
1365
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
1366
|
+
}
|
|
1367
|
+
else if (result.ok) {
|
|
1368
|
+
console.log(`\n✓ ASR coverage 校验通过(${result.matrixRows}/${result.expectedContractRows} Matrix rows)\n`);
|
|
1369
|
+
}
|
|
1370
|
+
else {
|
|
1371
|
+
console.error('\n✗ ASR coverage 校验失败\n');
|
|
1372
|
+
for (const e of result.errors)
|
|
1373
|
+
console.error(` - ${e}`);
|
|
1374
|
+
console.error();
|
|
1375
|
+
}
|
|
1376
|
+
if (!result.ok)
|
|
1377
|
+
process.exit(1);
|
|
1378
|
+
});
|
|
1355
1379
|
// ── read ──────────────────────────────────────────────────────────────────
|
|
1356
1380
|
addSkillsDirOption(skills
|
|
1357
1381
|
.command('read <atomId>')
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Parse entity/engine atom field (string or object). */
|
|
2
|
+
export function parseAtomRef(raw) {
|
|
3
|
+
if (typeof raw === 'string') {
|
|
4
|
+
return { id: raw, resolve: 'registry' };
|
|
5
|
+
}
|
|
6
|
+
if (raw && typeof raw === 'object' && 'id' in raw) {
|
|
7
|
+
const o = raw;
|
|
8
|
+
if (typeof o.id !== 'string')
|
|
9
|
+
return null;
|
|
10
|
+
return {
|
|
11
|
+
id: o.id,
|
|
12
|
+
resolve: o.resolve ?? 'registry',
|
|
13
|
+
substitute: o.substitute,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
export function atomIdFromInstance(raw) {
|
|
19
|
+
if (raw && typeof raw === 'object' && 'atom' in raw) {
|
|
20
|
+
return parseAtomRef(raw.atom)?.id ?? null;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|