@zat-design/sisyphus-scene 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +44 -0
  2. package/bin/sisyphus-scene.mjs +81 -0
  3. package/mcp/sisyphus-react.json +8 -0
  4. package/package.json +36 -0
  5. package/scripts/install-mcp.mjs +158 -0
  6. package/scripts/sync-platforms.mjs +87 -0
  7. package/skills/sisyphus-scene/SKILL.md +149 -0
  8. package/skills/sisyphus-scene/agents/openai.yaml +8 -0
  9. package/skills/sisyphus-scene/assets/templates/scene-registry.json +75 -0
  10. package/skills/sisyphus-scene/evals/fixtures/invalid-antd-fallback.tsx +5 -0
  11. package/skills/sisyphus-scene/evals/fixtures/invalid-decisions.json +11 -0
  12. package/skills/sisyphus-scene/evals/fixtures/valid-decisions.json +21 -0
  13. package/skills/sisyphus-scene/evals/fixtures/valid-page.tsx +25 -0
  14. package/skills/sisyphus-scene/evals/generation-cases.yaml +36 -0
  15. package/skills/sisyphus-scene/evals/trigger-cases.yaml +13 -0
  16. package/skills/sisyphus-scene/references/approved-examples.yaml +16 -0
  17. package/skills/sisyphus-scene/references/integration-contracts.md +112 -0
  18. package/skills/sisyphus-scene/references/platform-claude.md +10 -0
  19. package/skills/sisyphus-scene/references/platform-codex.md +10 -0
  20. package/skills/sisyphus-scene/references/platform-cursor.md +10 -0
  21. package/skills/sisyphus-scene/references/scene-model.md +51 -0
  22. package/skills/sisyphus-scene/references/validation-rules.md +45 -0
  23. package/skills/sisyphus-scene/references/workflow.md +94 -0
  24. package/skills/sisyphus-scene/scripts/inspect-project.mjs +229 -0
  25. package/skills/sisyphus-scene/scripts/install.mjs +100 -0
  26. package/skills/sisyphus-scene/scripts/resolve-versions.mjs +204 -0
  27. package/skills/sisyphus-scene/scripts/validate-scaffold.mjs +158 -0
  28. package/skills/sisyphus-scene/scripts/validate-skill.mjs +48 -0
  29. package/skills/sisyphus-scene-legacy/SKILL.md +614 -0
  30. package/skills/sisyphus-scene-legacy/agents/openai.yaml +7 -0
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import process from 'node:process';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const parseArgs = (argv) => {
11
+ const args = {};
12
+ for (let index = 0; index < argv.length; index += 1) {
13
+ const token = argv[index];
14
+ if (!token.startsWith('--')) continue;
15
+ const key = token.slice(2);
16
+ const value = argv[index + 1];
17
+ if (!value || value.startsWith('--')) args[key] = true;
18
+ else {
19
+ args[key] = value;
20
+ index += 1;
21
+ }
22
+ }
23
+ return args;
24
+ };
25
+
26
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
27
+ const source = path.resolve(scriptDirectory, '..');
28
+ const args = parseArgs(process.argv.slice(2));
29
+ const platform = String(args.platform || 'all');
30
+ const scope = String(args.scope || 'user');
31
+ const mode = String(args.mode || 'copy');
32
+ const dryRun = Boolean(args['dry-run']);
33
+
34
+ if (!['claude', 'codex', 'cursor', 'all'].includes(platform)) {
35
+ throw new Error(`不支持的平台:${platform}`);
36
+ }
37
+ if (!['user', 'project'].includes(scope)) throw new Error(`不支持的 scope:${scope}`);
38
+ if (!['copy', 'link'].includes(mode)) throw new Error(`不支持的 mode:${mode}`);
39
+
40
+ const projectRoot = path.resolve(String(args.project || process.cwd()));
41
+ const platformDirectories = {
42
+ claude: scope === 'user' ? path.join(os.homedir(), '.claude', 'skills') : path.join(projectRoot, '.claude', 'skills'),
43
+ codex: scope === 'user' ? path.join(os.homedir(), '.agents', 'skills') : path.join(projectRoot, '.agents', 'skills'),
44
+ cursor: scope === 'user' ? path.join(os.homedir(), '.cursor', 'skills') : path.join(projectRoot, '.cursor', 'skills'),
45
+ };
46
+
47
+ const selectedPlatforms = platform === 'all' ? Object.keys(platformDirectories) : [platform];
48
+
49
+ const collectFiles = (directory, relative = '') => {
50
+ const files = [];
51
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
52
+ if (entry.name === '.DS_Store') continue;
53
+ const nextRelative = path.join(relative, entry.name);
54
+ const fullPath = path.join(directory, entry.name);
55
+ if (entry.isDirectory()) files.push(...collectFiles(fullPath, nextRelative));
56
+ else if (entry.isFile()) files.push(nextRelative);
57
+ }
58
+ return files.sort();
59
+ };
60
+
61
+ const fingerprint = (directory) => {
62
+ const hash = crypto.createHash('sha256');
63
+ for (const relative of collectFiles(directory)) {
64
+ hash.update(relative);
65
+ hash.update(fs.readFileSync(path.join(directory, relative)));
66
+ }
67
+ return hash.digest('hex');
68
+ };
69
+
70
+ const sourceRealPath = fs.realpathSync(source);
71
+ const results = [];
72
+
73
+ for (const name of selectedPlatforms) {
74
+ const target = path.join(platformDirectories[name], 'sisyphus-scene');
75
+ let targetRealPath;
76
+ try {
77
+ targetRealPath = fs.realpathSync(target);
78
+ } catch {
79
+ targetRealPath = undefined;
80
+ }
81
+ if (targetRealPath === sourceRealPath) {
82
+ results.push({ platform: name, target, action: 'source', fingerprint: fingerprint(source) });
83
+ continue;
84
+ }
85
+ if (!dryRun) {
86
+ fs.mkdirSync(path.dirname(target), { recursive: true });
87
+ fs.rmSync(target, { recursive: true, force: true });
88
+ if (mode === 'link') fs.symlinkSync(source, target, 'dir');
89
+ else fs.cpSync(source, target, { recursive: true });
90
+ }
91
+ results.push({
92
+ platform: name,
93
+ target,
94
+ action: dryRun ? `would-${mode}` : mode,
95
+ fingerprint: dryRun ? fingerprint(source) : fingerprint(target),
96
+ });
97
+ }
98
+
99
+ process.stdout.write(`${JSON.stringify({ source, mode, scope, dryRun, results }, null, 2)}\n`);
100
+
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+
8
+ const PACKAGES = [
9
+ { name: 'antd', major: 6 },
10
+ { name: '@zat-design/sisyphus-react', major: 4 },
11
+ ];
12
+
13
+ const parseArgs = (argv) => {
14
+ const args = {};
15
+ for (let index = 0; index < argv.length; index += 1) {
16
+ const token = argv[index];
17
+ if (!token.startsWith('--')) continue;
18
+ const key = token.slice(2);
19
+ const value = argv[index + 1];
20
+ if (!value || value.startsWith('--')) args[key] = true;
21
+ else {
22
+ args[key] = value;
23
+ index += 1;
24
+ }
25
+ }
26
+ return args;
27
+ };
28
+
29
+ const parseVersion = (value) => {
30
+ const match = String(value || '').match(/(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
31
+ if (!match) return null;
32
+ return {
33
+ raw: match[0],
34
+ major: Number(match[1]),
35
+ minor: Number(match[2]),
36
+ patch: Number(match[3]),
37
+ prerelease: match[4] || '',
38
+ };
39
+ };
40
+
41
+ const compareVersions = (leftValue, rightValue) => {
42
+ const left = parseVersion(leftValue);
43
+ const right = parseVersion(rightValue);
44
+ if (!left || !right) return 0;
45
+ for (const key of ['major', 'minor', 'patch']) {
46
+ if (left[key] !== right[key]) return left[key] - right[key];
47
+ }
48
+ if (!left.prerelease && right.prerelease) return 1;
49
+ if (left.prerelease && !right.prerelease) return -1;
50
+ return left.prerelease.localeCompare(right.prerelease, undefined, { numeric: true });
51
+ };
52
+
53
+ const readPackageJson = (root) => {
54
+ const packagePath = path.join(root, 'package.json');
55
+ if (!fs.existsSync(packagePath)) throw new Error(`未找到 package.json:${packagePath}`);
56
+ return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
57
+ };
58
+
59
+ const getDeclaredVersion = (packageJson, packageName) => {
60
+ const sections = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
61
+ for (const section of sections) {
62
+ const value = packageJson[section]?.[packageName];
63
+ if (value) return value;
64
+ }
65
+ return undefined;
66
+ };
67
+
68
+ const getPackageLockVersion = (root, packageName) => {
69
+ const packageLockPath = path.join(root, 'package-lock.json');
70
+ if (!fs.existsSync(packageLockPath)) return undefined;
71
+ try {
72
+ const lock = JSON.parse(fs.readFileSync(packageLockPath, 'utf8'));
73
+ return lock.packages?.[`node_modules/${packageName}`]?.version;
74
+ } catch {
75
+ return undefined;
76
+ }
77
+ };
78
+
79
+ const getYarnLockVersion = (root, packageName, declared) => {
80
+ const yarnLockPath = path.join(root, 'yarn.lock');
81
+ if (!fs.existsSync(yarnLockPath)) return undefined;
82
+ const content = fs.readFileSync(yarnLockPath, 'utf8');
83
+ const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
84
+ const escapedSelector = declared
85
+ ? `${escaped}@${String(declared).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`
86
+ : undefined;
87
+ for (const block of content.split(/\n{2,}/)) {
88
+ const header = block.split('\n', 1)[0];
89
+ if (escapedSelector && !new RegExp(`(?:^|["',\\s])${escapedSelector}(?:["',:]|$)`).test(header)) continue;
90
+ if (!new RegExp(`(?:^|["',\\s])${escaped}@`).test(header)) continue;
91
+ const version = block.match(/^\s{2}version\s+["']?([^"'\n]+)["']?$/m);
92
+ if (version) return version[1];
93
+ }
94
+ return undefined;
95
+ };
96
+
97
+ const getPnpmLockVersion = (root, packageName) => {
98
+ const pnpmLockPath = path.join(root, 'pnpm-lock.yaml');
99
+ if (!fs.existsSync(pnpmLockPath)) return undefined;
100
+ const content = fs.readFileSync(pnpmLockPath, 'utf8');
101
+ const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
102
+ const patterns = [
103
+ new RegExp(`(?:^|\\n)\\s{2,}['"]?${escaped}@([^:'"\\s]+)`, 'm'),
104
+ new RegExp(`(?:^|\\n)\\s{2,}/?${escaped}/([^:\\s]+):`, 'm'),
105
+ ];
106
+ for (const pattern of patterns) {
107
+ const match = content.match(pattern);
108
+ if (match) return match[1];
109
+ }
110
+ return undefined;
111
+ };
112
+
113
+ const getResolvedVersions = (root, packageName, declared) => {
114
+ const values = {
115
+ packageLock: getPackageLockVersion(root, packageName),
116
+ yarnLock: getYarnLockVersion(root, packageName, declared),
117
+ pnpmLock: getPnpmLockVersion(root, packageName),
118
+ };
119
+ const present = Object.values(values).filter(Boolean);
120
+ const unique = [...new Set(present)];
121
+ return {
122
+ values,
123
+ resolved: unique.length === 1 ? unique[0] : undefined,
124
+ conflict: unique.length > 1,
125
+ };
126
+ };
127
+
128
+ const queryVersions = (root, packageName) => {
129
+ const output = execFileSync('npm', ['view', packageName, 'versions', '--json'], {
130
+ cwd: root,
131
+ encoding: 'utf8',
132
+ stdio: ['ignore', 'pipe', 'pipe'],
133
+ });
134
+ const parsed = JSON.parse(output);
135
+ return Array.isArray(parsed) ? parsed : [parsed];
136
+ };
137
+
138
+ const chooseLatest = (versions, major, channel) =>
139
+ versions
140
+ .filter((version) => {
141
+ const parsed = parseVersion(version);
142
+ if (!parsed || parsed.major !== major) return false;
143
+ return channel === 'beta' || !parsed.prerelease;
144
+ })
145
+ .sort(compareVersions)
146
+ .at(-1);
147
+
148
+ const evaluateStatus = ({ declared, resolved, latest, major, lockConflict }) => {
149
+ if (lockConflict) return 'lock-conflict';
150
+ const active = parseVersion(resolved || declared);
151
+ if (!active) return 'unknown';
152
+ if (active.major !== major) return 'unsupported-major';
153
+ if (!latest) return 'unknown';
154
+ return compareVersions(active.raw, latest) === 0 ? 'ok' : 'outdated';
155
+ };
156
+
157
+ const args = parseArgs(process.argv.slice(2));
158
+ const root = path.resolve(String(args.root || process.cwd()));
159
+ const channel = String(args.channel || 'stable');
160
+ if (!['stable', 'beta'].includes(channel)) throw new Error(`不支持的 channel:${channel}`);
161
+ const offline = Boolean(args.offline);
162
+ const packageJson = readPackageJson(root);
163
+
164
+ const results = PACKAGES.map(({ name, major }) => {
165
+ const declared = getDeclaredVersion(packageJson, name);
166
+ const lockResolution = getResolvedVersions(root, name, declared);
167
+ let versions = [];
168
+ let registryError;
169
+ if (!offline) {
170
+ try {
171
+ versions = queryVersions(root, name);
172
+ } catch (error) {
173
+ registryError = error.stderr?.trim() || error.message;
174
+ }
175
+ }
176
+ const latest = chooseLatest(versions, major, channel);
177
+ return {
178
+ name,
179
+ requiredRange: `>=${major} <${major + 1}`,
180
+ channel,
181
+ declared,
182
+ resolved: lockResolution.resolved,
183
+ resolvedVersions: lockResolution.values,
184
+ latest,
185
+ status: evaluateStatus({
186
+ declared,
187
+ resolved: lockResolution.resolved,
188
+ latest,
189
+ major,
190
+ lockConflict: lockResolution.conflict,
191
+ }),
192
+ ...(registryError ? { registryError } : {}),
193
+ };
194
+ });
195
+
196
+ const report = {
197
+ root,
198
+ channel,
199
+ ok: results.every((item) => item.status === 'ok'),
200
+ packages: results,
201
+ };
202
+
203
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
204
+ if (!report.ok) process.exitCode = 2;
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+
7
+ const parseArgs = (argv) => {
8
+ const args = {};
9
+ for (let index = 0; index < argv.length; index += 1) {
10
+ const token = argv[index];
11
+ if (!token.startsWith('--')) continue;
12
+ const key = token.slice(2);
13
+ const value = argv[index + 1];
14
+ if (!value || value.startsWith('--')) args[key] = true;
15
+ else {
16
+ args[key] = value;
17
+ index += 1;
18
+ }
19
+ }
20
+ return args;
21
+ };
22
+
23
+ const isWithin = (parent, child) => {
24
+ const relative = path.relative(parent, child);
25
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
26
+ };
27
+
28
+ const readDecisions = (filePath) => {
29
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
30
+ return Array.isArray(parsed) ? parsed : parsed.decisions || [];
31
+ };
32
+
33
+ const extractNamedImports = (content, source) => {
34
+ const escaped = source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
35
+ const regex = new RegExp(`import\\s+(?:type\\s+)?\\{([^}]+)\\}\\s+from\\s+['"]${escaped}['"]`, 'g');
36
+ const names = [];
37
+ for (const match of content.matchAll(regex)) {
38
+ for (const token of match[1].split(',')) {
39
+ const name = token.trim().replace(/^type\s+/, '').split(/\s+as\s+/)[0].trim();
40
+ if (name) names.push(name);
41
+ }
42
+ }
43
+ return names;
44
+ };
45
+
46
+ const args = parseArgs(process.argv.slice(2));
47
+ const root = path.resolve(String(args.root || process.cwd()));
48
+ if (!args.files) throw new Error('必须传入 --files <逗号分隔文件>');
49
+ if (!args.decisions) throw new Error('必须传入 --decisions <ComponentDecision JSON>');
50
+
51
+ const files = String(args.files)
52
+ .split(',')
53
+ .map((item) => path.resolve(root, item.trim()))
54
+ .filter(Boolean);
55
+ const decisionsPath = path.resolve(root, String(args.decisions));
56
+ const decisions = readDecisions(decisionsPath);
57
+ const errors = [];
58
+ const warnings = [];
59
+ const checks = [];
60
+
61
+ const addError = (file, rule, message) => errors.push({ file, rule, message });
62
+ const addWarning = (file, rule, message) => warnings.push({ file, rule, message });
63
+
64
+ for (const decision of decisions) {
65
+ if (!decision.requirementId || !decision.requirement) {
66
+ addError(decisionsPath, 'decision-schema', '每条 decision 必须包含 requirementId 和 requirement');
67
+ }
68
+ if (['unverified', 'unresolved'].includes(decision.status) || decision.source === 'unresolved') {
69
+ addError(decisionsPath, 'unresolved-capability', `能力未解决:${decision.requirement || decision.requirementId}`);
70
+ }
71
+ if (decision.source === 'sisyphus' && (!decision.apiEvidence || !decision.demoEvidence)) {
72
+ addError(decisionsPath, 'sisyphus-evidence', `Sisyphus 决策缺少 API 或 demo 证据:${decision.requirement}`);
73
+ }
74
+ if (
75
+ decision.source === 'antd' &&
76
+ (!decision.apiEvidence || !decision.demoEvidence || !decision.fallbackReason)
77
+ ) {
78
+ addError(decisionsPath, 'antd-fallback-evidence', `Ant Design fallback 证据不完整:${decision.requirement}`);
79
+ }
80
+ if (decision.source === 'project' && (!decision.apiEvidence || !decision.demoEvidence)) {
81
+ addError(decisionsPath, 'project-evidence', `项目组件决策缺少源码或调用示例:${decision.requirement}`);
82
+ }
83
+ }
84
+
85
+ for (const filePath of files) {
86
+ if (!isWithin(root, filePath)) {
87
+ addError(filePath, 'scope', '生成文件位于项目根目录外');
88
+ continue;
89
+ }
90
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
91
+ addError(filePath, 'exists', '生成文件不存在');
92
+ continue;
93
+ }
94
+ const content = fs.readFileSync(filePath, 'utf8');
95
+ const relative = path.relative(root, filePath);
96
+
97
+ const forbiddenPatterns = [
98
+ { rule: 'antd4', pattern: /from\s+['"]antd4['"]/, message: '禁止使用 antd4' },
99
+ { rule: 'ts-nocheck', pattern: /@ts-nocheck/, message: '禁止使用 @ts-nocheck' },
100
+ { rule: 'numbered-separator', pattern: /\/\/\s*─+\s*\d/, message: '禁止序号分隔注释' },
101
+ { rule: 'empty-catch', pattern: /catch\s*\([^)]*\)\s*\{\s*\}/, message: '禁止空 catch' },
102
+ ];
103
+ for (const item of forbiddenPatterns) {
104
+ if (item.pattern.test(content)) addError(relative, item.rule, item.message);
105
+ }
106
+
107
+ if (/\bProForm\b/.test(content) && /<Form\.Item\b/.test(content)) {
108
+ addError(relative, 'proform-form-item', 'ProForm 中混入 Form.Item,必须提供明确组件证据或改用配置列');
109
+ }
110
+
111
+ const sisyphusImports = extractNamedImports(content, '@zat-design/sisyphus-react').filter((name) =>
112
+ /^Pro[A-Z]/.test(name) && !/(Type|Props)$/.test(name),
113
+ );
114
+ for (const component of sisyphusImports) {
115
+ const decision = decisions.find(
116
+ (item) => item.source === 'sisyphus' && item.component === component && item.status !== 'unverified',
117
+ );
118
+ if (!decision) {
119
+ addError(relative, 'sisyphus-component-decision', `组件 ${component} 缺少 ComponentDecision 证据`);
120
+ }
121
+ }
122
+
123
+ const structuralAntdComponents = new Set(['Tabs', 'Modal', 'Drawer', 'Table', 'Steps', 'Collapse']);
124
+ const antdImports = extractNamedImports(content, 'antd').filter((name) => structuralAntdComponents.has(name));
125
+ for (const component of antdImports) {
126
+ const decision = decisions.find(
127
+ (item) =>
128
+ item.source === 'antd' &&
129
+ item.component === component &&
130
+ item.fallbackReason &&
131
+ item.apiEvidence &&
132
+ item.demoEvidence,
133
+ );
134
+ if (!decision) {
135
+ addError(relative, 'antd-fallback-decision', `结构组件 ${component} 缺少完整 fallback 证据`);
136
+ }
137
+ }
138
+
139
+ const routerImports = ['@umijs/max', 'react-router-dom', 'next/navigation'].filter((source) =>
140
+ new RegExp(`from\\s+['"]${source.replace('/', '\\/')}['"]`).test(content),
141
+ );
142
+ if (routerImports.length > 0 && !decisions.some((item) => /导航|路由|返回|跳转/.test(item.requirement))) {
143
+ addWarning(relative, 'router-without-decision', `存在路由 import,但没有导航能力决策:${routerImports.join(', ')}`);
144
+ }
145
+
146
+ checks.push({ file: relative, status: 'checked' });
147
+ }
148
+
149
+ const report = {
150
+ root,
151
+ ok: errors.length === 0,
152
+ checks,
153
+ errors,
154
+ warnings,
155
+ };
156
+
157
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
158
+ if (!report.ok) process.exitCode = 1;
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const skillRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ const skillPath = path.join(skillRoot, 'SKILL.md');
10
+ const content = fs.readFileSync(skillPath, 'utf8');
11
+ const errors = [];
12
+
13
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---\n/);
14
+ if (!frontmatter) errors.push('SKILL.md 缺少有效 YAML frontmatter');
15
+ else {
16
+ const keys = [...frontmatter[1].matchAll(/^([A-Za-z0-9_-]+):/gm)].map((match) => match[1]);
17
+ for (const required of ['name', 'description']) {
18
+ if (!keys.includes(required)) errors.push(`frontmatter 缺少 ${required}`);
19
+ }
20
+ const extra = keys.filter((key) => !['name', 'description'].includes(key));
21
+ if (extra.length > 0) errors.push(`frontmatter 包含非共享字段:${extra.join(', ')}`);
22
+ }
23
+
24
+ if (content.split('\n').length > 500) errors.push('SKILL.md 超过 500 行');
25
+
26
+ for (const match of content.matchAll(/`((?:references|assets|scripts)\/[^`]+)`/g)) {
27
+ const resource = match[1].replace(/\s+.*$/, '');
28
+ if (resource.includes('<')) continue;
29
+ if (!fs.existsSync(path.join(skillRoot, resource))) errors.push(`引用资源不存在:${resource}`);
30
+ }
31
+
32
+ for (const required of [
33
+ 'references/workflow.md',
34
+ 'references/scene-model.md',
35
+ 'references/integration-contracts.md',
36
+ 'references/validation-rules.md',
37
+ 'assets/templates/scene-registry.json',
38
+ 'agents/openai.yaml',
39
+ ]) {
40
+ if (!fs.existsSync(path.join(skillRoot, required))) errors.push(`缺少资源:${required}`);
41
+ }
42
+
43
+ JSON.parse(fs.readFileSync(path.join(skillRoot, 'assets/templates/scene-registry.json'), 'utf8'));
44
+
45
+ const report = { skillRoot, ok: errors.length === 0, errors };
46
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
47
+ if (!report.ok) process.exitCode = 1;
48
+