@playcraft/cli 0.0.43 → 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/commands/build.js +6 -6
- package/dist/index.js +0 -0
- package/package.json +3 -3
- package/dist/commands/prad.js +0 -61
- 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/dist/prad/atom-ref.js +0 -23
- package/dist/prad/check.js +0 -377
- package/dist/prad/check.test.js +0 -27
- package/dist/prad/explain.js +0 -109
- package/dist/prad/load-spec.js +0 -23
- package/dist/prad/paths.js +0 -83
- package/dist/prad/skills-index.js +0 -60
- 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
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { build as viteBuild } from 'vite';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { ViteConfigBuilder } from './vite/config-builder.js';
|
|
5
|
-
import { PLATFORM_CONFIGS } from './vite/platform-configs.js';
|
|
6
|
-
/**
|
|
7
|
-
* Vite 构建器 - 使用 Vite 构建 Playable Ads
|
|
8
|
-
*
|
|
9
|
-
* 职责:
|
|
10
|
-
* 1. 验证输入是有效的基础构建
|
|
11
|
-
* 2. 创建 Vite 配置
|
|
12
|
-
* 3. 执行 Vite 构建
|
|
13
|
-
* 4. 验证输出大小
|
|
14
|
-
* 5. 生成报告
|
|
15
|
-
*/
|
|
16
|
-
export class ViteBuilder {
|
|
17
|
-
baseBuildDir;
|
|
18
|
-
options;
|
|
19
|
-
sizeReport;
|
|
20
|
-
constructor(baseBuildDir, options) {
|
|
21
|
-
this.baseBuildDir = baseBuildDir;
|
|
22
|
-
this.options = options;
|
|
23
|
-
const platformConfig = PLATFORM_CONFIGS[options.platform];
|
|
24
|
-
this.sizeReport = {
|
|
25
|
-
engine: 0,
|
|
26
|
-
assets: {},
|
|
27
|
-
total: 0,
|
|
28
|
-
limit: platformConfig.sizeLimit,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* 执行构建
|
|
33
|
-
*/
|
|
34
|
-
async build() {
|
|
35
|
-
// 1. 验证输入
|
|
36
|
-
await this.validateBaseBuild();
|
|
37
|
-
// 2. 创建 Vite 配置
|
|
38
|
-
const configBuilder = new ViteConfigBuilder(this.baseBuildDir, this.options.platform, this.options);
|
|
39
|
-
const viteConfig = configBuilder.create();
|
|
40
|
-
// 3. 执行 Vite 构建
|
|
41
|
-
await viteBuild(viteConfig);
|
|
42
|
-
// 4. 验证输出大小
|
|
43
|
-
const outputPath = this.getOutputPath();
|
|
44
|
-
await this.validateSize(outputPath);
|
|
45
|
-
// 5. 生成报告
|
|
46
|
-
this.generateReport(outputPath);
|
|
47
|
-
return outputPath;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* 验证基础构建
|
|
51
|
-
*/
|
|
52
|
-
async validateBaseBuild() {
|
|
53
|
-
const requiredFiles = [
|
|
54
|
-
'index.html',
|
|
55
|
-
'config.json',
|
|
56
|
-
'__start__.js',
|
|
57
|
-
];
|
|
58
|
-
const missingFiles = [];
|
|
59
|
-
for (const file of requiredFiles) {
|
|
60
|
-
try {
|
|
61
|
-
await fs.access(path.join(this.baseBuildDir, file));
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
missingFiles.push(file);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (missingFiles.length > 0) {
|
|
68
|
-
throw new Error(`基础构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
|
|
69
|
-
`请确保输入目录包含完整的多文件构建产物。`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* 获取输出路径
|
|
74
|
-
*/
|
|
75
|
-
getOutputPath() {
|
|
76
|
-
const platformConfig = PLATFORM_CONFIGS[this.options.platform];
|
|
77
|
-
const outputDir = this.options.outputDir || './dist';
|
|
78
|
-
if (platformConfig.outputFormat === 'zip') {
|
|
79
|
-
return path.join(outputDir, 'playable.zip');
|
|
80
|
-
}
|
|
81
|
-
return path.join(outputDir, platformConfig.outputFileName);
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* 验证输出大小
|
|
85
|
-
*/
|
|
86
|
-
async validateSize(outputPath) {
|
|
87
|
-
try {
|
|
88
|
-
const stats = await fs.stat(outputPath);
|
|
89
|
-
this.sizeReport.total = stats.size;
|
|
90
|
-
this.sizeReport.assets[path.basename(outputPath)] = stats.size;
|
|
91
|
-
if (stats.size > this.sizeReport.limit) {
|
|
92
|
-
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
93
|
-
const limitMB = (this.sizeReport.limit / 1024 / 1024).toFixed(2);
|
|
94
|
-
console.warn(`⚠️ 警告: 文件大小 ${sizeMB} MB 超过限制 ${limitMB} MB`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
console.warn(`警告: 无法读取输出文件: ${outputPath}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* 生成报告
|
|
103
|
-
*/
|
|
104
|
-
generateReport(outputPath) {
|
|
105
|
-
// 报告已在 validateSize 中生成
|
|
106
|
-
// 这里可以添加额外的报告逻辑
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* 获取大小报告
|
|
110
|
-
*/
|
|
111
|
-
getSizeReport() {
|
|
112
|
-
return this.sizeReport;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* 格式化字节数
|
|
116
|
-
*/
|
|
117
|
-
formatBytes(bytes) {
|
|
118
|
-
if (bytes === 0)
|
|
119
|
-
return '0 Bytes';
|
|
120
|
-
const k = 1024;
|
|
121
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
122
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
123
|
-
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
124
|
-
}
|
|
125
|
-
}
|
package/dist/prad/atom-ref.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/prad/check.js
DELETED
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import { atomIdFromInstance, parseAtomRef } from './atom-ref.js';
|
|
3
|
-
import { loadGateRequirements, loadSpineRegistry } from './load-spec.js';
|
|
4
|
-
import { walkPradDocument, parentEntityPath, resolvePath } from './paths.js';
|
|
5
|
-
import { effectiveAtomId, loadManifestImports, loadRegistryAtomIds, } from './skills-index.js';
|
|
6
|
-
function pushIssue(list, issue) {
|
|
7
|
-
list.push(issue);
|
|
8
|
-
}
|
|
9
|
-
function collectAtomRefsFromDoc(doc) {
|
|
10
|
-
const map = new Map();
|
|
11
|
-
const engine = doc.engine;
|
|
12
|
-
const engineRef = typeof engine === 'string' ? parseAtomRef(engine) : parseAtomRef(engine?.atom);
|
|
13
|
-
if (engineRef)
|
|
14
|
-
map.set(engineRef.id, engineRef);
|
|
15
|
-
const remixSpec = doc.meta?.remixSpec;
|
|
16
|
-
const remixAtom = atomIdFromInstance(remixSpec);
|
|
17
|
-
if (remixAtom) {
|
|
18
|
-
const ref = parseAtomRef(remixSpec.atom);
|
|
19
|
-
if (ref)
|
|
20
|
-
map.set(ref.id, ref);
|
|
21
|
-
}
|
|
22
|
-
for (const visit of walkPradDocument(doc)) {
|
|
23
|
-
if (visit.kind === 'entity' && visit.entity) {
|
|
24
|
-
const ref = parseAtomRef(visit.entity.atom);
|
|
25
|
-
if (ref)
|
|
26
|
-
map.set(ref.id, ref);
|
|
27
|
-
}
|
|
28
|
-
if (visit.mediaInstance) {
|
|
29
|
-
const ref = parseAtomRef(visit.mediaInstance.atom);
|
|
30
|
-
if (ref)
|
|
31
|
-
map.set(ref.id, ref);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return map;
|
|
35
|
-
}
|
|
36
|
-
function parseLedger(doc) {
|
|
37
|
-
const ledger = new Map();
|
|
38
|
-
const meta = doc.meta;
|
|
39
|
-
for (const entry of meta?.atomLedger ?? []) {
|
|
40
|
-
if (entry?.id)
|
|
41
|
-
ledger.set(entry.id, entry);
|
|
42
|
-
}
|
|
43
|
-
return ledger;
|
|
44
|
-
}
|
|
45
|
-
export function checkPradDocument(doc, options = {}) {
|
|
46
|
-
const errors = [];
|
|
47
|
-
const warnings = [];
|
|
48
|
-
const gate = options.gate ?? doc.meta?.gate ?? 4;
|
|
49
|
-
const meta = doc.meta;
|
|
50
|
-
const pradVersion = meta?.pradVersion;
|
|
51
|
-
if (!pradVersion && !options.legacy) {
|
|
52
|
-
pushIssue(warnings, {
|
|
53
|
-
code: 'PRAD-W001',
|
|
54
|
-
severity: 'warning',
|
|
55
|
-
path: 'meta.pradVersion',
|
|
56
|
-
message: 'Missing pradVersion; treat as legacy v0.1',
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
const isV02Plus = pradVersion === '0.2' || pradVersion === '0.2.1';
|
|
60
|
-
const isV021 = pradVersion === '0.2.1';
|
|
61
|
-
const registry = loadRegistryAtomIds(options.skillsDir);
|
|
62
|
-
const spineReg = loadSpineRegistry(options.specDir);
|
|
63
|
-
const gateReq = loadGateRequirements(options.specDir);
|
|
64
|
-
const gateConfig = gateReq.gates[String(gate)];
|
|
65
|
-
const docAtoms = collectAtomRefsFromDoc(doc);
|
|
66
|
-
const ledger = parseLedger(doc);
|
|
67
|
-
// S2: atom resolve
|
|
68
|
-
for (const [atomId, ref] of docAtoms) {
|
|
69
|
-
if (ref.resolve === 'draft') {
|
|
70
|
-
const issue = {
|
|
71
|
-
code: options.strictDrafts ? 'PRAD-E020' : 'PRAD-W020',
|
|
72
|
-
severity: options.strictDrafts ? 'error' : 'warning',
|
|
73
|
-
path: atomId,
|
|
74
|
-
message: `Draft atom "${atomId}"${ref.substitute ? ` (substitute: ${ref.substitute})` : ''}`,
|
|
75
|
-
};
|
|
76
|
-
if (ref.substitute && !registry.has(ref.substitute)) {
|
|
77
|
-
pushIssue(errors, {
|
|
78
|
-
code: 'PRAD-E023',
|
|
79
|
-
severity: 'error',
|
|
80
|
-
path: atomId,
|
|
81
|
-
message: `Draft substitute "${ref.substitute}" not in Skills registry`,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
else if (!ref.substitute) {
|
|
85
|
-
pushIssue(errors, {
|
|
86
|
-
code: 'PRAD-E023',
|
|
87
|
-
severity: 'error',
|
|
88
|
-
path: atomId,
|
|
89
|
-
message: `Draft atom "${atomId}" missing substitute`,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
pushIssue(options.strictDrafts ? errors : warnings, issue);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
else if (!registry.has(atomId)) {
|
|
97
|
-
pushIssue(errors, {
|
|
98
|
-
code: 'PRAD-E020',
|
|
99
|
-
severity: 'error',
|
|
100
|
-
path: atomId,
|
|
101
|
-
message: `Unknown registry atom "${atomId}"`,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// S2b: atomLedger consistency
|
|
106
|
-
if (isV02Plus && gate >= 4) {
|
|
107
|
-
if (!meta?.atomLedger?.length) {
|
|
108
|
-
pushIssue(errors, {
|
|
109
|
-
code: 'PRAD-E022',
|
|
110
|
-
severity: 'error',
|
|
111
|
-
path: 'meta.atomLedger',
|
|
112
|
-
message: 'Gate 4 requires meta.atomLedger',
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
for (const [atomId, ref] of docAtoms) {
|
|
117
|
-
const ledgerEntry = ledger.get(atomId);
|
|
118
|
-
if (isV02Plus && gate >= 2 && !ledgerEntry) {
|
|
119
|
-
pushIssue(warnings, {
|
|
120
|
-
code: 'PRAD-E022',
|
|
121
|
-
severity: 'warning',
|
|
122
|
-
path: 'meta.atomLedger',
|
|
123
|
-
message: `Atom "${atomId}" used in document but missing from atomLedger`,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
if (ledgerEntry && ledgerEntry.resolve !== ref.resolve) {
|
|
127
|
-
pushIssue(errors, {
|
|
128
|
-
code: 'PRAD-E022',
|
|
129
|
-
severity: 'error',
|
|
130
|
-
path: 'meta.atomLedger',
|
|
131
|
-
message: `Ledger resolve for "${atomId}" (${ledgerEntry.resolve}) != node (${ref.resolve})`,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
for (const [atomId, entry] of ledger) {
|
|
136
|
-
if (entry.resolve === 'draft' && !entry.substitute) {
|
|
137
|
-
pushIssue(errors, {
|
|
138
|
-
code: 'PRAD-E023',
|
|
139
|
-
severity: 'error',
|
|
140
|
-
path: 'meta.atomLedger',
|
|
141
|
-
message: `Ledger draft "${atomId}" missing substitute`,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
if (entry.resolve === 'draft' && entry.substitute && !registry.has(entry.substitute)) {
|
|
145
|
-
pushIssue(errors, {
|
|
146
|
-
code: 'PRAD-E023',
|
|
147
|
-
severity: 'error',
|
|
148
|
-
path: 'meta.atomLedger',
|
|
149
|
-
message: `Ledger substitute "${entry.substitute}" for "${atomId}" not in registry`,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (isV02Plus && gate >= 4 && gateConfig?.requiredMeta?.includes('atomLedger')) {
|
|
154
|
-
for (const [atomId, ref] of docAtoms) {
|
|
155
|
-
if (ref.resolve === 'draft' && gate >= 4) {
|
|
156
|
-
pushIssue(warnings, {
|
|
157
|
-
code: 'PRAD-W020',
|
|
158
|
-
severity: 'warning',
|
|
159
|
-
path: atomId,
|
|
160
|
-
message: `Gate 4 document still contains draft atom "${atomId}" (compile uses substitute)`,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// S5: slot vs spine-registry
|
|
166
|
-
const presentSpines = new Set();
|
|
167
|
-
if (isV02Plus && gateConfig?.entitySlotRequired) {
|
|
168
|
-
for (const visit of walkPradDocument(doc)) {
|
|
169
|
-
if (visit.kind !== 'entity' || !visit.entity)
|
|
170
|
-
continue;
|
|
171
|
-
const slot = visit.entity.slot;
|
|
172
|
-
const sinceGate = visit.entity.sinceGate;
|
|
173
|
-
if (!slot?.spine) {
|
|
174
|
-
pushIssue(errors, {
|
|
175
|
-
code: 'PRAD-E025',
|
|
176
|
-
severity: 'error',
|
|
177
|
-
path: visit.path,
|
|
178
|
-
message: 'Entity missing slot.spine (required Gate 3+)',
|
|
179
|
-
});
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (!spineReg.spines[slot.spine]) {
|
|
183
|
-
pushIssue(errors, {
|
|
184
|
-
code: 'PRAD-E025',
|
|
185
|
-
severity: 'error',
|
|
186
|
-
path: visit.path,
|
|
187
|
-
message: `Unknown spine "${slot.spine}"`,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
presentSpines.add(slot.spine);
|
|
192
|
-
}
|
|
193
|
-
if (sinceGate != null && meta?.gate != null && sinceGate > meta.gate) {
|
|
194
|
-
pushIssue(errors, {
|
|
195
|
-
code: 'PRAD-E027',
|
|
196
|
-
severity: 'error',
|
|
197
|
-
path: visit.path,
|
|
198
|
-
message: `sinceGate ${sinceGate} > meta.gate ${meta.gate}`,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
if (sinceGate != null && sinceGate > gate) {
|
|
202
|
-
pushIssue(errors, {
|
|
203
|
-
code: 'PRAD-E027',
|
|
204
|
-
severity: 'error',
|
|
205
|
-
path: visit.path,
|
|
206
|
-
message: `sinceGate ${sinceGate} > check gate ${gate}`,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
for (const req of gateConfig?.requiredSpines ?? []) {
|
|
212
|
-
if (req.criticality !== 'required')
|
|
213
|
-
continue;
|
|
214
|
-
const spine = spineReg.spines[req.spine];
|
|
215
|
-
if (spine?.globalsKey) {
|
|
216
|
-
const globals = doc.globals;
|
|
217
|
-
if (!globals?.[spine.globalsKey]) {
|
|
218
|
-
pushIssue(errors, {
|
|
219
|
-
code: 'PRAD-E040',
|
|
220
|
-
severity: 'error',
|
|
221
|
-
path: `globals.${spine.globalsKey}`,
|
|
222
|
-
message: `Required spine ${req.spine} (globals.${spine.globalsKey}) missing`,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (spine?.topLevelKey) {
|
|
228
|
-
if (!(spine.topLevelKey in doc)) {
|
|
229
|
-
pushIssue(errors, {
|
|
230
|
-
code: 'PRAD-E040',
|
|
231
|
-
severity: 'error',
|
|
232
|
-
path: spine.topLevelKey,
|
|
233
|
-
message: `Required spine ${req.spine} (${spine.topLevelKey}) missing`,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (spine?.metaKey) {
|
|
239
|
-
const metaObj = doc.meta;
|
|
240
|
-
if (!metaObj?.[spine.metaKey]) {
|
|
241
|
-
pushIssue(errors, {
|
|
242
|
-
code: 'PRAD-E040',
|
|
243
|
-
severity: 'error',
|
|
244
|
-
path: `meta.${spine.metaKey}`,
|
|
245
|
-
message: `Required spine ${req.spine} (meta.${spine.metaKey}) missing`,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
if (!presentSpines.has(req.spine)) {
|
|
251
|
-
pushIssue(errors, {
|
|
252
|
-
code: 'PRAD-E040',
|
|
253
|
-
severity: 'error',
|
|
254
|
-
path: req.spine,
|
|
255
|
-
message: `Required spine ${req.spine} not present on any entity`,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
// S3b: bindAs on media (Gate 2+ when enabled)
|
|
260
|
-
if (isV02Plus && gate >= 2 && gateConfig?.bindAsRequiredOnMedia !== false) {
|
|
261
|
-
for (const visit of walkPradDocument(doc)) {
|
|
262
|
-
if (visit.kind === 'global') {
|
|
263
|
-
const inst = visit.mediaInstance;
|
|
264
|
-
if (!inst?.bindAs) {
|
|
265
|
-
pushIssue(gate >= 4 ? errors : warnings, {
|
|
266
|
-
code: 'PRAD-E026',
|
|
267
|
-
severity: gate >= 4 ? 'error' : 'warning',
|
|
268
|
-
path: visit.path,
|
|
269
|
-
message: 'Global atom instance missing bindAs',
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
if (visit.kind !== 'media' || !visit.mediaInstance || !visit.parentEntityAtom)
|
|
275
|
-
continue;
|
|
276
|
-
const bindAs = visit.mediaInstance.bindAs;
|
|
277
|
-
if (!bindAs) {
|
|
278
|
-
pushIssue(gate >= 4 ? errors : warnings, {
|
|
279
|
-
code: 'PRAD-E026',
|
|
280
|
-
severity: gate >= 4 ? 'error' : 'warning',
|
|
281
|
-
path: visit.path,
|
|
282
|
-
message: 'Media instance missing bindAs',
|
|
283
|
-
});
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
if (bindAs.startsWith('globals.'))
|
|
287
|
-
continue;
|
|
288
|
-
const bindTargetRef = visit.mediaInstance.bindTargetRef;
|
|
289
|
-
let targetEntityPath;
|
|
290
|
-
let targetEntityAtom;
|
|
291
|
-
if (bindTargetRef) {
|
|
292
|
-
if (!isV021) {
|
|
293
|
-
pushIssue(warnings, {
|
|
294
|
-
code: 'PRAD-W021',
|
|
295
|
-
severity: 'warning',
|
|
296
|
-
path: visit.path,
|
|
297
|
-
message: 'bindTargetRef ignored on pradVersion < 0.2.1',
|
|
298
|
-
});
|
|
299
|
-
targetEntityPath = parentEntityPath(visit.path);
|
|
300
|
-
targetEntityAtom = visit.parentEntityAtom;
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
const targetVisit = resolvePath(doc, bindTargetRef);
|
|
304
|
-
if (!targetVisit || targetVisit.kind !== 'entity' || !targetVisit.entity) {
|
|
305
|
-
pushIssue(errors, {
|
|
306
|
-
code: 'PRAD-E028',
|
|
307
|
-
severity: 'error',
|
|
308
|
-
path: visit.path,
|
|
309
|
-
message: `bindTargetRef "${bindTargetRef}" is not a valid entity path`,
|
|
310
|
-
});
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
targetEntityPath = bindTargetRef;
|
|
314
|
-
targetEntityAtom = parseAtomRef(targetVisit.entity.atom) ?? undefined;
|
|
315
|
-
if (!targetEntityAtom)
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
targetEntityPath = parentEntityPath(visit.path);
|
|
321
|
-
targetEntityAtom = visit.parentEntityAtom;
|
|
322
|
-
}
|
|
323
|
-
const targetEffective = effectiveAtomId(targetEntityAtom);
|
|
324
|
-
const imports = loadManifestImports(targetEffective, options.skillsDir);
|
|
325
|
-
const e026Severity = isV021 && gate >= 4 ? 'error' : 'warning';
|
|
326
|
-
const e026List = isV021 && gate >= 4 ? errors : warnings;
|
|
327
|
-
if (!imports || imports.bindAsSet.size === 0) {
|
|
328
|
-
if (isV021 && gate >= 4) {
|
|
329
|
-
pushIssue(e026List, {
|
|
330
|
-
code: 'PRAD-E026',
|
|
331
|
-
severity: e026Severity,
|
|
332
|
-
path: visit.path,
|
|
333
|
-
message: `bindAs "${bindAs}" targets ${targetEffective} which has no manifest imports`,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
else if (imports && imports.bindAsSet.size > 0) {
|
|
337
|
-
// unreachable
|
|
338
|
-
}
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
if (!imports.bindAsSet.has(bindAs)) {
|
|
342
|
-
pushIssue(e026List, {
|
|
343
|
-
code: 'PRAD-E026',
|
|
344
|
-
severity: e026Severity,
|
|
345
|
-
path: visit.path,
|
|
346
|
-
message: `bindAs "${bindAs}" not in ${targetEffective} manifest imports (known: ${[...imports.bindAsSet].join(', ')})`,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
// Gate 4 review
|
|
352
|
-
if (gate >= 4) {
|
|
353
|
-
const review = doc.review;
|
|
354
|
-
if (!review) {
|
|
355
|
-
pushIssue(errors, { code: 'PRAD-E050', severity: 'error', path: 'review', message: 'Missing review block' });
|
|
356
|
-
}
|
|
357
|
-
else if (review.pass !== true) {
|
|
358
|
-
pushIssue(errors, { code: 'PRAD-E050', severity: 'error', path: 'review.pass', message: 'review.pass must be true' });
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
return { ok: errors.length === 0, errors, warnings };
|
|
362
|
-
}
|
|
363
|
-
export function checkPradFile(filePath, options = {}) {
|
|
364
|
-
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
365
|
-
let doc;
|
|
366
|
-
try {
|
|
367
|
-
doc = JSON.parse(raw);
|
|
368
|
-
}
|
|
369
|
-
catch {
|
|
370
|
-
return {
|
|
371
|
-
ok: false,
|
|
372
|
-
errors: [{ code: 'PRAD-E001', severity: 'error', path: filePath, message: 'Invalid JSON' }],
|
|
373
|
-
warnings: [],
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
return checkPradDocument(doc, options);
|
|
377
|
-
}
|
package/dist/prad/check.test.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { checkPradFile } from './check.js';
|
|
6
|
-
import { explainPradPath, formatExplain } from './explain.js';
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const mahjongSample = path.resolve(__dirname, '..', '..', '..', '..', 'docs', 'prad', 'samples', 'mahjong-gate4.atom-tree.json');
|
|
9
|
-
describe('prad check', () => {
|
|
10
|
-
it('mahjong gate4 v0.2 passes with warnings for draft atoms', () => {
|
|
11
|
-
if (!fs.existsSync(mahjongSample))
|
|
12
|
-
return;
|
|
13
|
-
const result = checkPradFile(mahjongSample, { gate: 4 });
|
|
14
|
-
expect(result.errors).toEqual([]);
|
|
15
|
-
expect(result.warnings.some((w) => w.code === 'PRAD-W020')).toBe(true);
|
|
16
|
-
expect(result.ok).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
it('explain play_btn path', () => {
|
|
19
|
-
if (!fs.existsSync(mahjongSample))
|
|
20
|
-
return;
|
|
21
|
-
const doc = JSON.parse(fs.readFileSync(mahjongSample, 'utf-8'));
|
|
22
|
-
const result = explainPradPath(doc, 'scenes.entry.ui_layer.play_btn');
|
|
23
|
-
expect(result.found).toBe(true);
|
|
24
|
-
expect(result.slot?.spine).toBe('medium.hook.play');
|
|
25
|
-
expect(formatExplain(result)).toContain('medium.hook.play');
|
|
26
|
-
});
|
|
27
|
-
});
|
package/dist/prad/explain.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { parseAtomRef } from './atom-ref.js';
|
|
2
|
-
import { loadSpineRegistry } from './load-spec.js';
|
|
3
|
-
import { resolvePath } from './paths.js';
|
|
4
|
-
import { effectiveAtomId, loadManifestImports, loadRegistryAtomIds } from './skills-index.js';
|
|
5
|
-
export function explainPradPath(doc, logicalPath, options = {}) {
|
|
6
|
-
const visit = resolvePath(doc, logicalPath);
|
|
7
|
-
if (!visit) {
|
|
8
|
-
return { path: logicalPath, found: false };
|
|
9
|
-
}
|
|
10
|
-
const registry = loadRegistryAtomIds(options.skillsDir);
|
|
11
|
-
const spineReg = loadSpineRegistry(options.specDir);
|
|
12
|
-
const result = { path: logicalPath, found: true, kind: visit.kind };
|
|
13
|
-
if (visit.kind === 'entity' && visit.entity) {
|
|
14
|
-
const atom = parseAtomRef(visit.entity.atom);
|
|
15
|
-
result.atom = atom ?? undefined;
|
|
16
|
-
result.effectiveAtomId = atom ? effectiveAtomId(atom) : undefined;
|
|
17
|
-
result.registryHit = atom ? registry.has(atom.id) : undefined;
|
|
18
|
-
if (atom?.resolve === 'draft' && atom.substitute) {
|
|
19
|
-
result.registryHit = registry.has(atom.substitute);
|
|
20
|
-
}
|
|
21
|
-
result.slot = visit.entity.slot;
|
|
22
|
-
result.sinceGate = visit.entity.sinceGate;
|
|
23
|
-
const spineId = visit.entity.slot?.spine;
|
|
24
|
-
if (spineId && spineReg.spines[spineId]) {
|
|
25
|
-
result.spineDescription = spineReg.spines[spineId].description;
|
|
26
|
-
}
|
|
27
|
-
const props = visit.entity.props;
|
|
28
|
-
if (props) {
|
|
29
|
-
const refs = [];
|
|
30
|
-
for (const [k, v] of Object.entries(props)) {
|
|
31
|
-
if (v && typeof v === 'object' && 'ref' in v)
|
|
32
|
-
refs.push(`${k} → ${v.ref}`);
|
|
33
|
-
if (typeof v === 'string' && k.endsWith('Ref'))
|
|
34
|
-
refs.push(`${k} → ${v}`);
|
|
35
|
-
if (k === 'binding' && typeof v === 'string')
|
|
36
|
-
refs.push(`binding → ${v}`);
|
|
37
|
-
}
|
|
38
|
-
if (refs.length)
|
|
39
|
-
result.refs = refs;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (visit.kind === 'media' && visit.mediaInstance) {
|
|
43
|
-
const atom = parseAtomRef(visit.mediaInstance.atom);
|
|
44
|
-
result.atom = atom ?? undefined;
|
|
45
|
-
result.effectiveAtomId = atom ? effectiveAtomId(atom) : undefined;
|
|
46
|
-
result.bindAs = visit.mediaInstance.bindAs;
|
|
47
|
-
const bindTargetRef = visit.mediaInstance.bindTargetRef;
|
|
48
|
-
if (bindTargetRef) {
|
|
49
|
-
result.bindTargetRef = bindTargetRef;
|
|
50
|
-
const targetVisit = resolvePath(doc, bindTargetRef);
|
|
51
|
-
if (targetVisit?.entity) {
|
|
52
|
-
const targetAtom = parseAtomRef(targetVisit.entity.atom);
|
|
53
|
-
if (targetAtom) {
|
|
54
|
-
const targetEff = effectiveAtomId(targetAtom);
|
|
55
|
-
result.bindTargetEffectiveAtom = targetEff;
|
|
56
|
-
const imports = loadManifestImports(targetEff, options.skillsDir);
|
|
57
|
-
if (imports)
|
|
58
|
-
result.manifestImportSlots = [...imports.bindAsSet];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
else if (visit.parentEntityAtom) {
|
|
63
|
-
const parentEff = effectiveAtomId(visit.parentEntityAtom);
|
|
64
|
-
result.parentEffectiveAtom = parentEff;
|
|
65
|
-
const imports = loadManifestImports(parentEff, options.skillsDir);
|
|
66
|
-
if (imports)
|
|
67
|
-
result.manifestImportSlots = [...imports.bindAsSet];
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (visit.kind === 'global' && visit.mediaInstance) {
|
|
71
|
-
result.atom = parseAtomRef(visit.mediaInstance.atom) ?? undefined;
|
|
72
|
-
result.bindAs = visit.mediaInstance.bindAs;
|
|
73
|
-
}
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
export function formatExplain(result) {
|
|
77
|
-
const lines = [`path: ${result.path}`, `found: ${result.found}`];
|
|
78
|
-
if (!result.found)
|
|
79
|
-
return lines.join('\n');
|
|
80
|
-
if (result.kind)
|
|
81
|
-
lines.push(`kind: ${result.kind}`);
|
|
82
|
-
if (result.slot)
|
|
83
|
-
lines.push(`slot: ${JSON.stringify(result.slot)}`);
|
|
84
|
-
if (result.sinceGate != null)
|
|
85
|
-
lines.push(`sinceGate: ${result.sinceGate}`);
|
|
86
|
-
if (result.atom)
|
|
87
|
-
lines.push(`atom: ${JSON.stringify(result.atom)}`);
|
|
88
|
-
if (result.effectiveAtomId)
|
|
89
|
-
lines.push(`effectiveAtomId: ${result.effectiveAtomId}`);
|
|
90
|
-
if (result.registryHit != null)
|
|
91
|
-
lines.push(`registryHit: ${result.registryHit}`);
|
|
92
|
-
if (result.spineDescription) {
|
|
93
|
-
lines.push(`spineDescription: ${result.spineDescription.zh ?? result.spineDescription.en ?? ''}`);
|
|
94
|
-
}
|
|
95
|
-
if (result.bindAs)
|
|
96
|
-
lines.push(`bindAs: ${result.bindAs}`);
|
|
97
|
-
if (result.bindTargetRef)
|
|
98
|
-
lines.push(`bindTargetRef: ${result.bindTargetRef}`);
|
|
99
|
-
if (result.parentEffectiveAtom)
|
|
100
|
-
lines.push(`parentEffectiveAtom: ${result.parentEffectiveAtom}`);
|
|
101
|
-
if (result.bindTargetEffectiveAtom)
|
|
102
|
-
lines.push(`bindTargetEffectiveAtom: ${result.bindTargetEffectiveAtom}`);
|
|
103
|
-
if (result.manifestImportSlots?.length) {
|
|
104
|
-
lines.push(`manifestImportSlots: ${result.manifestImportSlots.join(', ')}`);
|
|
105
|
-
}
|
|
106
|
-
if (result.refs?.length)
|
|
107
|
-
lines.push(`refs:\n ${result.refs.join('\n ')}`);
|
|
108
|
-
return lines.join('\n');
|
|
109
|
-
}
|
package/dist/prad/load-spec.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
-
export function resolvePradSpecDir(customDir) {
|
|
6
|
-
if (customDir)
|
|
7
|
-
return path.resolve(customDir);
|
|
8
|
-
const mono = path.resolve(__dirname, '..', '..', '..', '..', 'docs', 'prad', 'spec');
|
|
9
|
-
if (fs.existsSync(path.join(mono, 'spine-registry.v1.json')))
|
|
10
|
-
return mono;
|
|
11
|
-
const cwd = path.join(process.cwd(), 'docs', 'prad', 'spec');
|
|
12
|
-
if (fs.existsSync(path.join(cwd, 'spine-registry.v1.json')))
|
|
13
|
-
return cwd;
|
|
14
|
-
return mono;
|
|
15
|
-
}
|
|
16
|
-
export function loadSpineRegistry(specDir) {
|
|
17
|
-
const file = path.join(resolvePradSpecDir(specDir), 'spine-registry.v1.json');
|
|
18
|
-
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
19
|
-
}
|
|
20
|
-
export function loadGateRequirements(specDir) {
|
|
21
|
-
const file = path.join(resolvePradSpecDir(specDir), 'gate-requirements.v1.json');
|
|
22
|
-
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
23
|
-
}
|