retiam 1.0.0 → 1.0.1
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/bin/cli.js +213 -209
- package/bin/export.js +322 -322
- package/bin/init.js +425 -425
- package/bin/lint.js +354 -354
- package/bin/render-config.js +87 -0
- package/bin/render.js +517 -600
- package/bin/snapshot.js +231 -231
- package/dist/{UTIF-pBEih-vH.js → UTIF-CuM_xgsi.js} +1 -1
- package/dist/assets/roboto-msdf.json +3642 -3642
- package/dist/{index-CnM0BhUR.js → index-C7UzgTiw.js} +4728 -4728
- package/dist/{main-Bw2LTrnR.js → main-CCBHkqdA.js} +1 -1
- package/dist/retiam.es.js +1 -1
- package/package.json +106 -116
- package/readme.md +155 -143
package/bin/lint.js
CHANGED
|
@@ -1,354 +1,354 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// =========================================================================
|
|
4
|
-
// File: bin/lint.js - Retiam Agent-Native Scene Linter & Static Analyzer
|
|
5
|
-
// =========================================================================
|
|
6
|
-
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import ts from 'typescript';
|
|
10
|
-
|
|
11
|
-
const args = process.argv.slice(2);
|
|
12
|
-
let targetFile = null;
|
|
13
|
-
let jsonOutput = false;
|
|
14
|
-
|
|
15
|
-
const isHelp = args.includes('--help') || args.includes('-h') || args.includes('help');
|
|
16
|
-
if (isHelp) {
|
|
17
|
-
console.log(`
|
|
18
|
-
🔍 Retiam WebGPU Engine - Scene Linter & Static Analyzer
|
|
19
|
-
|
|
20
|
-
Usage:
|
|
21
|
-
npx retiam lint <path/to/Scene.ts> [options]
|
|
22
|
-
|
|
23
|
-
Arguments:
|
|
24
|
-
<file> Target scene file to statically analyze (e.g. src/scenes/AStarScene.ts)
|
|
25
|
-
|
|
26
|
-
Options:
|
|
27
|
-
--json Output structured machine-readable JSON for agents and CI
|
|
28
|
-
--help, -h Show this help message and exit
|
|
29
|
-
|
|
30
|
-
Checks:
|
|
31
|
-
- Validates Scene class structure and async run() contract
|
|
32
|
-
- Detects un-awaited this.play() or this.wait() timing races
|
|
33
|
-
- Warns about dangling entities not attached to scene graph
|
|
34
|
-
- Catches Three.js / Python Manim hallucinations
|
|
35
|
-
- Flags extreme world coordinates exceeding standard camera frustum
|
|
36
|
-
|
|
37
|
-
Examples:
|
|
38
|
-
npx retiam lint src/scenes/AStarScene.ts
|
|
39
|
-
npx retiam lint src/scenes/AStarScene.ts --json
|
|
40
|
-
`);
|
|
41
|
-
process.exit(0);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
for (let i = 0; i < args.length; i++) {
|
|
45
|
-
if (args[i] === '--json') {
|
|
46
|
-
jsonOutput = true;
|
|
47
|
-
} else if (!args[i].startsWith('-') && !targetFile) {
|
|
48
|
-
targetFile = args[i];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!targetFile) {
|
|
53
|
-
if (jsonOutput) {
|
|
54
|
-
console.log(JSON.stringify({ status: 'error', message: 'No target scene file specified.' }));
|
|
55
|
-
} else {
|
|
56
|
-
console.error('❌ Error: No target scene file specified.');
|
|
57
|
-
console.log('Usage: npx retiam lint <path/to/Scene.ts> [--json]');
|
|
58
|
-
}
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const resolvedPath = path.resolve(process.cwd(), targetFile);
|
|
63
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
64
|
-
if (jsonOutput) {
|
|
65
|
-
console.log(JSON.stringify({ status: 'error', message: `File not found: ${targetFile}` }));
|
|
66
|
-
} else {
|
|
67
|
-
console.error(`❌ Error: File not found: ${resolvedPath}`);
|
|
68
|
-
}
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const sourceCode = fs.readFileSync(resolvedPath, 'utf8');
|
|
73
|
-
const sourceFile = ts.createSourceFile(
|
|
74
|
-
resolvedPath,
|
|
75
|
-
sourceCode,
|
|
76
|
-
ts.ScriptTarget.Latest,
|
|
77
|
-
true
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const diagnostics = [];
|
|
81
|
-
const lines = sourceCode.split('\n');
|
|
82
|
-
|
|
83
|
-
function addDiagnostic(type, rule, line, col, message, suggestion, codeSnippet) {
|
|
84
|
-
diagnostics.push({
|
|
85
|
-
type, // 'error' | 'warning'
|
|
86
|
-
rule,
|
|
87
|
-
line,
|
|
88
|
-
col,
|
|
89
|
-
message,
|
|
90
|
-
suggestion,
|
|
91
|
-
codeSnippet: codeSnippet || (lines[line - 1] ? lines[line - 1].trim() : '')
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function getLineAndCol(node) {
|
|
96
|
-
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
97
|
-
return { line: line + 1, col: character + 1 };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Known Retiam Entity classes
|
|
101
|
-
const ENTITY_CLASSES = new Set([
|
|
102
|
-
'Circle', 'Rect', 'Line', 'Arrow', 'Polygon', 'RegularPolygon',
|
|
103
|
-
'Arc', 'Sector', 'Annulus', 'Dot', 'GlowDot', 'Path', 'SVGShape', 'SVG',
|
|
104
|
-
'SDFText', 'Tex', 'Code', 'Axes', 'NumberLine', 'Graph', 'BarChart',
|
|
105
|
-
'PieChart', 'Model', 'Sphere', 'Box3D', 'Cylinder', 'Cone', 'Plane',
|
|
106
|
-
'Mesh', 'MorphModel', 'TracePath', 'VBox', 'HBox', 'Group'
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
// Track instantiated entities to check if they are added
|
|
110
|
-
const instantiatedEntities = new Map(); // name -> { line, col, className }
|
|
111
|
-
const referencedIdentifiers = new Set();
|
|
112
|
-
let hasSceneClass = false;
|
|
113
|
-
let hasRunMethod = false;
|
|
114
|
-
let hasCameraSetup = false;
|
|
115
|
-
|
|
116
|
-
function checkNode(node) {
|
|
117
|
-
// 1. Check for Scene Class & run() method
|
|
118
|
-
if (ts.isClassDeclaration(node)) {
|
|
119
|
-
if (node.heritageClauses) {
|
|
120
|
-
for (const clause of node.heritageClauses) {
|
|
121
|
-
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
122
|
-
for (const type of clause.types) {
|
|
123
|
-
if (type.expression.getText(sourceFile) === 'Scene') {
|
|
124
|
-
hasSceneClass = true;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Check for run() method
|
|
132
|
-
for (const member of node.members) {
|
|
133
|
-
if (ts.isMethodDeclaration(member)) {
|
|
134
|
-
const name = member.name.getText(sourceFile);
|
|
135
|
-
if (name === 'run') {
|
|
136
|
-
hasRunMethod = true;
|
|
137
|
-
const isAsync = member.modifiers && member.modifiers.some(m => m.kind === ts.SyntaxKind.AsyncKeyword);
|
|
138
|
-
if (!isAsync) {
|
|
139
|
-
const { line, col } = getLineAndCol(member);
|
|
140
|
-
addDiagnostic('error', 'retiam/async-run-required', line, col,
|
|
141
|
-
'The run() method of a Scene must be async.',
|
|
142
|
-
'Change signature to: public async run(): Promise<void>'
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 2. Check for missing await on this.play() or this.wait()
|
|
151
|
-
if (ts.isCallExpression(node)) {
|
|
152
|
-
const exprText = node.expression.getText(sourceFile);
|
|
153
|
-
const isPlayCall = exprText === 'this.play' || exprText === 'scene.play' || exprText.endsWith('.play');
|
|
154
|
-
const isWaitCall = exprText === 'this.wait' || exprText === 'scene.wait' || exprText.endsWith('.wait');
|
|
155
|
-
|
|
156
|
-
if (isPlayCall || isWaitCall) {
|
|
157
|
-
// Check if parent is an AwaitExpression
|
|
158
|
-
let parent = node.parent;
|
|
159
|
-
// Handle parentheses or expressions
|
|
160
|
-
while (parent && ts.isParenthesizedExpression(parent)) {
|
|
161
|
-
parent = parent.parent;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!parent || !ts.isAwaitExpression(parent)) {
|
|
165
|
-
// If it's part of a Promise.all or array, it might be fine, but standalone call without await causes sync race
|
|
166
|
-
const isInPromiseAll = parent && ts.isArrayLiteralExpression(parent) && parent.parent &&
|
|
167
|
-
ts.isCallExpression(parent.parent) && parent.parent.expression.getText(sourceFile).includes('Promise.all');
|
|
168
|
-
|
|
169
|
-
if (!isInPromiseAll) {
|
|
170
|
-
const { line, col } = getLineAndCol(node);
|
|
171
|
-
const callName = isPlayCall ? 'play()' : 'wait()';
|
|
172
|
-
addDiagnostic(
|
|
173
|
-
'error',
|
|
174
|
-
'retiam/missing-await',
|
|
175
|
-
line,
|
|
176
|
-
col,
|
|
177
|
-
`Calling ${callName} without 'await'. Animation steps will execute concurrently out of sequence.`,
|
|
178
|
-
`Add 'await' before ${node.getText(sourceFile)}`,
|
|
179
|
-
node.getText(sourceFile)
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Check camera setup
|
|
186
|
-
if (exprText.includes('camera.position') || exprText.includes('this.camera')) {
|
|
187
|
-
hasCameraSetup = true;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Track entities added to scene or containers
|
|
191
|
-
if (exprText === 'this.add' || exprText === 'scene.add' || exprText.endsWith('.add') || exprText.endsWith('.append')) {
|
|
192
|
-
for (const arg of node.arguments) {
|
|
193
|
-
referencedIdentifiers.add(arg.getText(sourceFile));
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 3. Track variable assignments with `new EntityClass(...)`
|
|
199
|
-
if (ts.isVariableDeclaration(node) && node.initializer && ts.isNewExpression(node.initializer)) {
|
|
200
|
-
const className = node.initializer.expression.getText(sourceFile);
|
|
201
|
-
if (ENTITY_CLASSES.has(className) && ts.isIdentifier(node.name)) {
|
|
202
|
-
const varName = node.name.text;
|
|
203
|
-
const { line, col } = getLineAndCol(node);
|
|
204
|
-
instantiatedEntities.set(varName, { line, col, className });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// 4. Check for Three.js or Python Manim hallucinations
|
|
209
|
-
if (ts.isIdentifier(node)) {
|
|
210
|
-
const text = node.text;
|
|
211
|
-
if (text === 'THREE') {
|
|
212
|
-
const { line, col } = getLineAndCol(node);
|
|
213
|
-
addDiagnostic(
|
|
214
|
-
'error',
|
|
215
|
-
'retiam/no-threejs',
|
|
216
|
-
line,
|
|
217
|
-
col,
|
|
218
|
-
'Detected Three.js reference. Retiam uses native WebGPU entities (e.g. Model, Sphere, Box3D).',
|
|
219
|
-
'Use Retiam 3D primitives: import { Model, Sphere, Box3D } from "retiam"'
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// 5. Check for extreme coordinates in Vector arrays: e.g. [150, 200, 0]
|
|
225
|
-
if (ts.isArrayLiteralExpression(node) && node.elements.length >= 2 && node.elements.length <= 4) {
|
|
226
|
-
const parentExpr = node.parent ? node.parent.getText(sourceFile) : '';
|
|
227
|
-
if (parentExpr.includes('position') || parentExpr.includes('shift') || parentExpr.includes('set')) {
|
|
228
|
-
for (const elem of node.elements) {
|
|
229
|
-
if (ts.isNumericLiteral(elem)) {
|
|
230
|
-
const val = parseFloat(elem.text);
|
|
231
|
-
if (Math.abs(val) > 40) {
|
|
232
|
-
const { line, col } = getLineAndCol(elem);
|
|
233
|
-
addDiagnostic(
|
|
234
|
-
'warning',
|
|
235
|
-
'retiam/extreme-coordinate',
|
|
236
|
-
line,
|
|
237
|
-
col,
|
|
238
|
-
`Coordinate value ${val} is very large. Standard Retiam camera view is typically between -10 and 10 world units.`,
|
|
239
|
-
'Verify camera distance or use auto-layout/nextTo for positioning.'
|
|
240
|
-
);
|
|
241
|
-
break;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Continue recursive AST traversal
|
|
249
|
-
ts.forEachChild(node, checkNode);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Traverse the AST
|
|
253
|
-
checkNode(sourceFile);
|
|
254
|
-
|
|
255
|
-
// Check if instantiated entities were ever added to the scene graph
|
|
256
|
-
for (const [varName, info] of instantiatedEntities.entries()) {
|
|
257
|
-
// If not referenced in an .add() call or as child
|
|
258
|
-
if (!referencedIdentifiers.has(varName)) {
|
|
259
|
-
// Also check if used in animations like `new Create(circle)`
|
|
260
|
-
const regex = new RegExp(`\\b${varName}\\b`, 'g');
|
|
261
|
-
const matches = (sourceCode.match(regex) || []).length;
|
|
262
|
-
// If it only appeared once (the declaration) or twice without being in this.add
|
|
263
|
-
if (matches <= 2) {
|
|
264
|
-
addDiagnostic(
|
|
265
|
-
'warning',
|
|
266
|
-
'retiam/dangling-entity',
|
|
267
|
-
info.line,
|
|
268
|
-
info.col,
|
|
269
|
-
`Entity '${varName}' (${info.className}) is created but might not be added to the scene.`,
|
|
270
|
-
`Ensure you call 'this.add(${varName})' or add it to a container (VBox/HBox).`
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Check Scene class presence
|
|
277
|
-
if (!hasSceneClass) {
|
|
278
|
-
addDiagnostic(
|
|
279
|
-
'error',
|
|
280
|
-
'retiam/missing-scene-class',
|
|
281
|
-
1,
|
|
282
|
-
1,
|
|
283
|
-
'No class extending "Scene" found in file.',
|
|
284
|
-
'Export a Scene class: export default class MyScene extends Scene { ... }'
|
|
285
|
-
);
|
|
286
|
-
} else if (!hasRunMethod) {
|
|
287
|
-
addDiagnostic(
|
|
288
|
-
'error',
|
|
289
|
-
'retiam/missing-run-method',
|
|
290
|
-
1,
|
|
291
|
-
1,
|
|
292
|
-
'The Scene class does not implement a run() method.',
|
|
293
|
-
'Implement: public async run(): Promise<void> { ... }'
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (!hasCameraSetup && hasSceneClass) {
|
|
298
|
-
addDiagnostic(
|
|
299
|
-
'warning',
|
|
300
|
-
'retiam/no-camera-setup',
|
|
301
|
-
1,
|
|
302
|
-
1,
|
|
303
|
-
'No camera position configuration detected in the scene.',
|
|
304
|
-
'Initialize camera: this.camera.position.set([0, 0, 14]);'
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Compute results
|
|
309
|
-
const errors = diagnostics.filter(d => d.type === 'error');
|
|
310
|
-
const warnings = diagnostics.filter(d => d.type === 'warning');
|
|
311
|
-
const isValid = errors.length === 0;
|
|
312
|
-
|
|
313
|
-
if (jsonOutput) {
|
|
314
|
-
const result = {
|
|
315
|
-
file: targetFile,
|
|
316
|
-
valid: isValid,
|
|
317
|
-
summary: {
|
|
318
|
-
errors: errors.length,
|
|
319
|
-
warnings: warnings.length,
|
|
320
|
-
total: diagnostics.length
|
|
321
|
-
},
|
|
322
|
-
diagnostics
|
|
323
|
-
};
|
|
324
|
-
console.log(JSON.stringify(result, null, 2));
|
|
325
|
-
} else {
|
|
326
|
-
console.log(`\n🔍 Retiam Scene Linter: ${targetFile}`);
|
|
327
|
-
console.log('='.repeat(60));
|
|
328
|
-
|
|
329
|
-
if (diagnostics.length === 0) {
|
|
330
|
-
console.log('✅ All checks passed! Scene is syntactically and structurally sound.\n');
|
|
331
|
-
} else {
|
|
332
|
-
for (const diag of diagnostics) {
|
|
333
|
-
const icon = diag.type === 'error' ? '❌ [ERROR]' : '⚠️ [WARN]';
|
|
334
|
-
console.log(`\n${icon} Line ${diag.line}:${diag.col} (${diag.rule})`);
|
|
335
|
-
console.log(` Message: ${diag.message}`);
|
|
336
|
-
if (diag.codeSnippet) {
|
|
337
|
-
console.log(` Snippet: ${diag.codeSnippet}`);
|
|
338
|
-
}
|
|
339
|
-
if (diag.suggestion) {
|
|
340
|
-
console.log(` 💡 Fix: ${diag.suggestion}`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
console.log('\n' + '-'.repeat(60));
|
|
345
|
-
console.log(`Found ${errors.length} error(s), ${warnings.length} warning(s).`);
|
|
346
|
-
if (!isValid) {
|
|
347
|
-
console.log('❌ Scene has critical errors that may prevent proper animation playback.\n');
|
|
348
|
-
} else {
|
|
349
|
-
console.log('✨ No critical errors found. Please review the warnings above.\n');
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
process.exit(isValid ? 0 : 1);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// =========================================================================
|
|
4
|
+
// File: bin/lint.js - Retiam Agent-Native Scene Linter & Static Analyzer
|
|
5
|
+
// =========================================================================
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import ts from 'typescript';
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
let targetFile = null;
|
|
13
|
+
let jsonOutput = false;
|
|
14
|
+
|
|
15
|
+
const isHelp = args.includes('--help') || args.includes('-h') || args.includes('help');
|
|
16
|
+
if (isHelp) {
|
|
17
|
+
console.log(`
|
|
18
|
+
🔍 Retiam WebGPU Engine - Scene Linter & Static Analyzer
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
npx retiam lint <path/to/Scene.ts> [options]
|
|
22
|
+
|
|
23
|
+
Arguments:
|
|
24
|
+
<file> Target scene file to statically analyze (e.g. src/scenes/AStarScene.ts)
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--json Output structured machine-readable JSON for agents and CI
|
|
28
|
+
--help, -h Show this help message and exit
|
|
29
|
+
|
|
30
|
+
Checks:
|
|
31
|
+
- Validates Scene class structure and async run() contract
|
|
32
|
+
- Detects un-awaited this.play() or this.wait() timing races
|
|
33
|
+
- Warns about dangling entities not attached to scene graph
|
|
34
|
+
- Catches Three.js / Python Manim hallucinations
|
|
35
|
+
- Flags extreme world coordinates exceeding standard camera frustum
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
npx retiam lint src/scenes/AStarScene.ts
|
|
39
|
+
npx retiam lint src/scenes/AStarScene.ts --json
|
|
40
|
+
`);
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
if (args[i] === '--json') {
|
|
46
|
+
jsonOutput = true;
|
|
47
|
+
} else if (!args[i].startsWith('-') && !targetFile) {
|
|
48
|
+
targetFile = args[i];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!targetFile) {
|
|
53
|
+
if (jsonOutput) {
|
|
54
|
+
console.log(JSON.stringify({ status: 'error', message: 'No target scene file specified.' }));
|
|
55
|
+
} else {
|
|
56
|
+
console.error('❌ Error: No target scene file specified.');
|
|
57
|
+
console.log('Usage: npx retiam lint <path/to/Scene.ts> [--json]');
|
|
58
|
+
}
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const resolvedPath = path.resolve(process.cwd(), targetFile);
|
|
63
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
64
|
+
if (jsonOutput) {
|
|
65
|
+
console.log(JSON.stringify({ status: 'error', message: `File not found: ${targetFile}` }));
|
|
66
|
+
} else {
|
|
67
|
+
console.error(`❌ Error: File not found: ${resolvedPath}`);
|
|
68
|
+
}
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const sourceCode = fs.readFileSync(resolvedPath, 'utf8');
|
|
73
|
+
const sourceFile = ts.createSourceFile(
|
|
74
|
+
resolvedPath,
|
|
75
|
+
sourceCode,
|
|
76
|
+
ts.ScriptTarget.Latest,
|
|
77
|
+
true
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const diagnostics = [];
|
|
81
|
+
const lines = sourceCode.split('\n');
|
|
82
|
+
|
|
83
|
+
function addDiagnostic(type, rule, line, col, message, suggestion, codeSnippet) {
|
|
84
|
+
diagnostics.push({
|
|
85
|
+
type, // 'error' | 'warning'
|
|
86
|
+
rule,
|
|
87
|
+
line,
|
|
88
|
+
col,
|
|
89
|
+
message,
|
|
90
|
+
suggestion,
|
|
91
|
+
codeSnippet: codeSnippet || (lines[line - 1] ? lines[line - 1].trim() : '')
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getLineAndCol(node) {
|
|
96
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
97
|
+
return { line: line + 1, col: character + 1 };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Known Retiam Entity classes
|
|
101
|
+
const ENTITY_CLASSES = new Set([
|
|
102
|
+
'Circle', 'Rect', 'Line', 'Arrow', 'Polygon', 'RegularPolygon',
|
|
103
|
+
'Arc', 'Sector', 'Annulus', 'Dot', 'GlowDot', 'Path', 'SVGShape', 'SVG',
|
|
104
|
+
'SDFText', 'Tex', 'Code', 'Axes', 'NumberLine', 'Graph', 'BarChart',
|
|
105
|
+
'PieChart', 'Model', 'Sphere', 'Box3D', 'Cylinder', 'Cone', 'Plane',
|
|
106
|
+
'Mesh', 'MorphModel', 'TracePath', 'VBox', 'HBox', 'Group'
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
// Track instantiated entities to check if they are added
|
|
110
|
+
const instantiatedEntities = new Map(); // name -> { line, col, className }
|
|
111
|
+
const referencedIdentifiers = new Set();
|
|
112
|
+
let hasSceneClass = false;
|
|
113
|
+
let hasRunMethod = false;
|
|
114
|
+
let hasCameraSetup = false;
|
|
115
|
+
|
|
116
|
+
function checkNode(node) {
|
|
117
|
+
// 1. Check for Scene Class & run() method
|
|
118
|
+
if (ts.isClassDeclaration(node)) {
|
|
119
|
+
if (node.heritageClauses) {
|
|
120
|
+
for (const clause of node.heritageClauses) {
|
|
121
|
+
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
122
|
+
for (const type of clause.types) {
|
|
123
|
+
if (type.expression.getText(sourceFile) === 'Scene') {
|
|
124
|
+
hasSceneClass = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check for run() method
|
|
132
|
+
for (const member of node.members) {
|
|
133
|
+
if (ts.isMethodDeclaration(member)) {
|
|
134
|
+
const name = member.name.getText(sourceFile);
|
|
135
|
+
if (name === 'run') {
|
|
136
|
+
hasRunMethod = true;
|
|
137
|
+
const isAsync = member.modifiers && member.modifiers.some(m => m.kind === ts.SyntaxKind.AsyncKeyword);
|
|
138
|
+
if (!isAsync) {
|
|
139
|
+
const { line, col } = getLineAndCol(member);
|
|
140
|
+
addDiagnostic('error', 'retiam/async-run-required', line, col,
|
|
141
|
+
'The run() method of a Scene must be async.',
|
|
142
|
+
'Change signature to: public async run(): Promise<void>'
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 2. Check for missing await on this.play() or this.wait()
|
|
151
|
+
if (ts.isCallExpression(node)) {
|
|
152
|
+
const exprText = node.expression.getText(sourceFile);
|
|
153
|
+
const isPlayCall = exprText === 'this.play' || exprText === 'scene.play' || exprText.endsWith('.play');
|
|
154
|
+
const isWaitCall = exprText === 'this.wait' || exprText === 'scene.wait' || exprText.endsWith('.wait');
|
|
155
|
+
|
|
156
|
+
if (isPlayCall || isWaitCall) {
|
|
157
|
+
// Check if parent is an AwaitExpression
|
|
158
|
+
let parent = node.parent;
|
|
159
|
+
// Handle parentheses or expressions
|
|
160
|
+
while (parent && ts.isParenthesizedExpression(parent)) {
|
|
161
|
+
parent = parent.parent;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!parent || !ts.isAwaitExpression(parent)) {
|
|
165
|
+
// If it's part of a Promise.all or array, it might be fine, but standalone call without await causes sync race
|
|
166
|
+
const isInPromiseAll = parent && ts.isArrayLiteralExpression(parent) && parent.parent &&
|
|
167
|
+
ts.isCallExpression(parent.parent) && parent.parent.expression.getText(sourceFile).includes('Promise.all');
|
|
168
|
+
|
|
169
|
+
if (!isInPromiseAll) {
|
|
170
|
+
const { line, col } = getLineAndCol(node);
|
|
171
|
+
const callName = isPlayCall ? 'play()' : 'wait()';
|
|
172
|
+
addDiagnostic(
|
|
173
|
+
'error',
|
|
174
|
+
'retiam/missing-await',
|
|
175
|
+
line,
|
|
176
|
+
col,
|
|
177
|
+
`Calling ${callName} without 'await'. Animation steps will execute concurrently out of sequence.`,
|
|
178
|
+
`Add 'await' before ${node.getText(sourceFile)}`,
|
|
179
|
+
node.getText(sourceFile)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check camera setup
|
|
186
|
+
if (exprText.includes('camera.position') || exprText.includes('this.camera')) {
|
|
187
|
+
hasCameraSetup = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Track entities added to scene or containers
|
|
191
|
+
if (exprText === 'this.add' || exprText === 'scene.add' || exprText.endsWith('.add') || exprText.endsWith('.append')) {
|
|
192
|
+
for (const arg of node.arguments) {
|
|
193
|
+
referencedIdentifiers.add(arg.getText(sourceFile));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 3. Track variable assignments with `new EntityClass(...)`
|
|
199
|
+
if (ts.isVariableDeclaration(node) && node.initializer && ts.isNewExpression(node.initializer)) {
|
|
200
|
+
const className = node.initializer.expression.getText(sourceFile);
|
|
201
|
+
if (ENTITY_CLASSES.has(className) && ts.isIdentifier(node.name)) {
|
|
202
|
+
const varName = node.name.text;
|
|
203
|
+
const { line, col } = getLineAndCol(node);
|
|
204
|
+
instantiatedEntities.set(varName, { line, col, className });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 4. Check for Three.js or Python Manim hallucinations
|
|
209
|
+
if (ts.isIdentifier(node)) {
|
|
210
|
+
const text = node.text;
|
|
211
|
+
if (text === 'THREE') {
|
|
212
|
+
const { line, col } = getLineAndCol(node);
|
|
213
|
+
addDiagnostic(
|
|
214
|
+
'error',
|
|
215
|
+
'retiam/no-threejs',
|
|
216
|
+
line,
|
|
217
|
+
col,
|
|
218
|
+
'Detected Three.js reference. Retiam uses native WebGPU entities (e.g. Model, Sphere, Box3D).',
|
|
219
|
+
'Use Retiam 3D primitives: import { Model, Sphere, Box3D } from "retiam"'
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 5. Check for extreme coordinates in Vector arrays: e.g. [150, 200, 0]
|
|
225
|
+
if (ts.isArrayLiteralExpression(node) && node.elements.length >= 2 && node.elements.length <= 4) {
|
|
226
|
+
const parentExpr = node.parent ? node.parent.getText(sourceFile) : '';
|
|
227
|
+
if (parentExpr.includes('position') || parentExpr.includes('shift') || parentExpr.includes('set')) {
|
|
228
|
+
for (const elem of node.elements) {
|
|
229
|
+
if (ts.isNumericLiteral(elem)) {
|
|
230
|
+
const val = parseFloat(elem.text);
|
|
231
|
+
if (Math.abs(val) > 40) {
|
|
232
|
+
const { line, col } = getLineAndCol(elem);
|
|
233
|
+
addDiagnostic(
|
|
234
|
+
'warning',
|
|
235
|
+
'retiam/extreme-coordinate',
|
|
236
|
+
line,
|
|
237
|
+
col,
|
|
238
|
+
`Coordinate value ${val} is very large. Standard Retiam camera view is typically between -10 and 10 world units.`,
|
|
239
|
+
'Verify camera distance or use auto-layout/nextTo for positioning.'
|
|
240
|
+
);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Continue recursive AST traversal
|
|
249
|
+
ts.forEachChild(node, checkNode);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Traverse the AST
|
|
253
|
+
checkNode(sourceFile);
|
|
254
|
+
|
|
255
|
+
// Check if instantiated entities were ever added to the scene graph
|
|
256
|
+
for (const [varName, info] of instantiatedEntities.entries()) {
|
|
257
|
+
// If not referenced in an .add() call or as child
|
|
258
|
+
if (!referencedIdentifiers.has(varName)) {
|
|
259
|
+
// Also check if used in animations like `new Create(circle)`
|
|
260
|
+
const regex = new RegExp(`\\b${varName}\\b`, 'g');
|
|
261
|
+
const matches = (sourceCode.match(regex) || []).length;
|
|
262
|
+
// If it only appeared once (the declaration) or twice without being in this.add
|
|
263
|
+
if (matches <= 2) {
|
|
264
|
+
addDiagnostic(
|
|
265
|
+
'warning',
|
|
266
|
+
'retiam/dangling-entity',
|
|
267
|
+
info.line,
|
|
268
|
+
info.col,
|
|
269
|
+
`Entity '${varName}' (${info.className}) is created but might not be added to the scene.`,
|
|
270
|
+
`Ensure you call 'this.add(${varName})' or add it to a container (VBox/HBox).`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check Scene class presence
|
|
277
|
+
if (!hasSceneClass) {
|
|
278
|
+
addDiagnostic(
|
|
279
|
+
'error',
|
|
280
|
+
'retiam/missing-scene-class',
|
|
281
|
+
1,
|
|
282
|
+
1,
|
|
283
|
+
'No class extending "Scene" found in file.',
|
|
284
|
+
'Export a Scene class: export default class MyScene extends Scene { ... }'
|
|
285
|
+
);
|
|
286
|
+
} else if (!hasRunMethod) {
|
|
287
|
+
addDiagnostic(
|
|
288
|
+
'error',
|
|
289
|
+
'retiam/missing-run-method',
|
|
290
|
+
1,
|
|
291
|
+
1,
|
|
292
|
+
'The Scene class does not implement a run() method.',
|
|
293
|
+
'Implement: public async run(): Promise<void> { ... }'
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!hasCameraSetup && hasSceneClass) {
|
|
298
|
+
addDiagnostic(
|
|
299
|
+
'warning',
|
|
300
|
+
'retiam/no-camera-setup',
|
|
301
|
+
1,
|
|
302
|
+
1,
|
|
303
|
+
'No camera position configuration detected in the scene.',
|
|
304
|
+
'Initialize camera: this.camera.position.set([0, 0, 14]);'
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Compute results
|
|
309
|
+
const errors = diagnostics.filter(d => d.type === 'error');
|
|
310
|
+
const warnings = diagnostics.filter(d => d.type === 'warning');
|
|
311
|
+
const isValid = errors.length === 0;
|
|
312
|
+
|
|
313
|
+
if (jsonOutput) {
|
|
314
|
+
const result = {
|
|
315
|
+
file: targetFile,
|
|
316
|
+
valid: isValid,
|
|
317
|
+
summary: {
|
|
318
|
+
errors: errors.length,
|
|
319
|
+
warnings: warnings.length,
|
|
320
|
+
total: diagnostics.length
|
|
321
|
+
},
|
|
322
|
+
diagnostics
|
|
323
|
+
};
|
|
324
|
+
console.log(JSON.stringify(result, null, 2));
|
|
325
|
+
} else {
|
|
326
|
+
console.log(`\n🔍 Retiam Scene Linter: ${targetFile}`);
|
|
327
|
+
console.log('='.repeat(60));
|
|
328
|
+
|
|
329
|
+
if (diagnostics.length === 0) {
|
|
330
|
+
console.log('✅ All checks passed! Scene is syntactically and structurally sound.\n');
|
|
331
|
+
} else {
|
|
332
|
+
for (const diag of diagnostics) {
|
|
333
|
+
const icon = diag.type === 'error' ? '❌ [ERROR]' : '⚠️ [WARN]';
|
|
334
|
+
console.log(`\n${icon} Line ${diag.line}:${diag.col} (${diag.rule})`);
|
|
335
|
+
console.log(` Message: ${diag.message}`);
|
|
336
|
+
if (diag.codeSnippet) {
|
|
337
|
+
console.log(` Snippet: ${diag.codeSnippet}`);
|
|
338
|
+
}
|
|
339
|
+
if (diag.suggestion) {
|
|
340
|
+
console.log(` 💡 Fix: ${diag.suggestion}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log('\n' + '-'.repeat(60));
|
|
345
|
+
console.log(`Found ${errors.length} error(s), ${warnings.length} warning(s).`);
|
|
346
|
+
if (!isValid) {
|
|
347
|
+
console.log('❌ Scene has critical errors that may prevent proper animation playback.\n');
|
|
348
|
+
} else {
|
|
349
|
+
console.log('✨ No critical errors found. Please review the warnings above.\n');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
process.exit(isValid ? 0 : 1);
|