retiam 1.0.0 → 1.0.2

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/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);