@rettangoli/check 0.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.
Files changed (42) hide show
  1. package/README.md +295 -0
  2. package/ROADMAP.md +175 -0
  3. package/package.json +46 -0
  4. package/src/cli/bin.js +325 -0
  5. package/src/cli/check.js +232 -0
  6. package/src/cli/index.js +1 -0
  7. package/src/core/analyze.js +227 -0
  8. package/src/core/discovery.js +83 -0
  9. package/src/core/exportedFunctions.js +235 -0
  10. package/src/core/model.js +898 -0
  11. package/src/core/parsers.js +2726 -0
  12. package/src/core/registry.js +779 -0
  13. package/src/core/schema.js +161 -0
  14. package/src/core/scopeGraph.js +1400 -0
  15. package/src/core/semantic.js +329 -0
  16. package/src/diagnostics/autofix.js +191 -0
  17. package/src/diagnostics/catalog.js +89 -0
  18. package/src/index.js +2 -0
  19. package/src/reporters/index.js +13 -0
  20. package/src/reporters/json.js +42 -0
  21. package/src/reporters/sarif.js +213 -0
  22. package/src/reporters/text.js +145 -0
  23. package/src/rules/compatibility.js +318 -0
  24. package/src/rules/constants.js +22 -0
  25. package/src/rules/crossFileSymbols.js +108 -0
  26. package/src/rules/expression.js +338 -0
  27. package/src/rules/feParity.js +65 -0
  28. package/src/rules/index.js +39 -0
  29. package/src/rules/jempl.js +80 -0
  30. package/src/rules/lifecycle.js +4 -0
  31. package/src/rules/listenerConfig.js +556 -0
  32. package/src/rules/listenerSymbols.js +49 -0
  33. package/src/rules/methods.js +117 -0
  34. package/src/rules/refs.js +20 -0
  35. package/src/rules/schema.js +118 -0
  36. package/src/rules/shared.js +20 -0
  37. package/src/rules/yahtmlAttrs.js +238 -0
  38. package/src/semantic/engine.js +778 -0
  39. package/src/semantic/index.js +9 -0
  40. package/src/types/lattice.js +281 -0
  41. package/src/utils/case.js +9 -0
  42. package/src/utils/fs.js +30 -0
@@ -0,0 +1,778 @@
1
+ import { buildComponentScopeGraph } from "../core/scopeGraph.js";
2
+
3
+ const SORT_STRING = (left, right) => String(left).localeCompare(String(right));
4
+
5
+ const sortByComponentAndName = (left, right) => (
6
+ String(left.componentKey).localeCompare(String(right.componentKey))
7
+ || String(left.name).localeCompare(String(right.name))
8
+ || String(left.kind || "").localeCompare(String(right.kind || ""))
9
+ );
10
+
11
+ const isObjectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
12
+
13
+ const toSortedSetArray = (value) => {
14
+ if (!(value instanceof Set)) {
15
+ return [];
16
+ }
17
+ return [...value].sort(SORT_STRING);
18
+ };
19
+
20
+ const toSortedObjectKeys = (value) => {
21
+ if (!isObjectRecord(value)) {
22
+ return [];
23
+ }
24
+ return Object.keys(value).sort(SORT_STRING);
25
+ };
26
+
27
+ const normalizeReferences = (references = []) => {
28
+ return [...(Array.isArray(references) ? references : [])]
29
+ .filter((reference) => reference && typeof reference === "object")
30
+ .sort((left, right) => (
31
+ (left.line || 0) - (right.line || 0)
32
+ || (left.column || 0) - (right.column || 0)
33
+ || String(left.expression || "").localeCompare(String(right.expression || ""))
34
+ || String(left.context || "").localeCompare(String(right.context || ""))
35
+ ));
36
+ };
37
+
38
+ const resolveSymbolFilePath = ({ model, kind }) => {
39
+ switch (kind) {
40
+ case "handler":
41
+ return model?.handlers?.filePath || model?.files?.handlers;
42
+ case "action":
43
+ return model?.store?.filePath || model?.files?.store;
44
+ case "method":
45
+ return model?.methods?.filePath || model?.files?.methods;
46
+ case "constant":
47
+ return model?.constants?.filePath || model?.files?.constants;
48
+ case "prop":
49
+ return model?.schema?.filePath || model?.files?.schema;
50
+ case "ref":
51
+ return model?.view?.filePath || model?.files?.view;
52
+ default:
53
+ return model?.view?.filePath || model?.files?.view;
54
+ }
55
+ };
56
+
57
+ const pushSymbolRow = ({ rows, rowBySymbolKey, componentKey, name, kind, filePath }) => {
58
+ if (!name) {
59
+ return;
60
+ }
61
+ const key = `${componentKey}::${name}`;
62
+ if (!rowBySymbolKey.has(key)) {
63
+ rowBySymbolKey.set(key, {
64
+ id: `${componentKey}::symbol::${name}`,
65
+ componentKey,
66
+ name,
67
+ kind,
68
+ kinds: new Set([kind]),
69
+ filePath,
70
+ });
71
+ return;
72
+ }
73
+
74
+ const existing = rowBySymbolKey.get(key);
75
+ existing.kinds.add(kind);
76
+ if (!existing.filePath && filePath) {
77
+ existing.filePath = filePath;
78
+ }
79
+ };
80
+
81
+ export const buildGlobalSymbolTable = ({ models = [] } = {}) => {
82
+ const rowBySymbolKey = new Map();
83
+
84
+ [...models]
85
+ .filter((model) => model && typeof model.componentKey === "string")
86
+ .sort((left, right) => left.componentKey.localeCompare(right.componentKey))
87
+ .forEach((model) => {
88
+ const componentKey = model.componentKey;
89
+
90
+ toSortedSetArray(model?.semanticGraph?.globalSymbols).forEach((name) => {
91
+ pushSymbolRow({
92
+ rows: rowBySymbolKey,
93
+ rowBySymbolKey,
94
+ componentKey,
95
+ name,
96
+ kind: "expression-root",
97
+ filePath: model?.view?.filePath || model?.files?.view,
98
+ });
99
+ });
100
+
101
+ toSortedSetArray(model?.handlers?.exports).forEach((name) => {
102
+ pushSymbolRow({
103
+ rows: rowBySymbolKey,
104
+ rowBySymbolKey,
105
+ componentKey,
106
+ name,
107
+ kind: "handler",
108
+ filePath: resolveSymbolFilePath({ model, kind: "handler" }),
109
+ });
110
+ });
111
+
112
+ toSortedSetArray(model?.store?.exports).forEach((name) => {
113
+ pushSymbolRow({
114
+ rows: rowBySymbolKey,
115
+ rowBySymbolKey,
116
+ componentKey,
117
+ name,
118
+ kind: "action",
119
+ filePath: resolveSymbolFilePath({ model, kind: "action" }),
120
+ });
121
+ });
122
+
123
+ toSortedSetArray(model?.methods?.exports).forEach((name) => {
124
+ pushSymbolRow({
125
+ rows: rowBySymbolKey,
126
+ rowBySymbolKey,
127
+ componentKey,
128
+ name,
129
+ kind: "method",
130
+ filePath: resolveSymbolFilePath({ model, kind: "method" }),
131
+ });
132
+ });
133
+
134
+ toSortedObjectKeys(model?.constants?.yaml).forEach((name) => {
135
+ pushSymbolRow({
136
+ rows: rowBySymbolKey,
137
+ rowBySymbolKey,
138
+ componentKey,
139
+ name,
140
+ kind: "constant",
141
+ filePath: resolveSymbolFilePath({ model, kind: "constant" }),
142
+ });
143
+ });
144
+
145
+ toSortedObjectKeys(model?.view?.yaml?.refs).forEach((name) => {
146
+ pushSymbolRow({
147
+ rows: rowBySymbolKey,
148
+ rowBySymbolKey,
149
+ componentKey,
150
+ name,
151
+ kind: "ref",
152
+ filePath: resolveSymbolFilePath({ model, kind: "ref" }),
153
+ });
154
+ });
155
+
156
+ const normalizedProps = model?.schema?.normalized?.props?.names;
157
+ if (Array.isArray(normalizedProps)) {
158
+ [...normalizedProps].sort(SORT_STRING).forEach((name) => {
159
+ pushSymbolRow({
160
+ rows: rowBySymbolKey,
161
+ rowBySymbolKey,
162
+ componentKey,
163
+ name,
164
+ kind: "prop",
165
+ filePath: resolveSymbolFilePath({ model, kind: "prop" }),
166
+ });
167
+ });
168
+ }
169
+ });
170
+
171
+ const rows = [...rowBySymbolKey.values()]
172
+ .map((row) => ({
173
+ ...row,
174
+ kinds: [...row.kinds].sort(SORT_STRING),
175
+ kind: [...row.kinds].sort(SORT_STRING)[0],
176
+ }))
177
+ .sort(sortByComponentAndName);
178
+
179
+ const byComponent = new Map();
180
+ rows.forEach((row) => {
181
+ if (!byComponent.has(row.componentKey)) {
182
+ byComponent.set(row.componentKey, new Map());
183
+ }
184
+ byComponent.get(row.componentKey).set(row.name, row);
185
+ });
186
+
187
+ return {
188
+ rows,
189
+ byComponent,
190
+ };
191
+ };
192
+
193
+ const levenshteinDistance = (left = "", right = "") => {
194
+ const a = String(left);
195
+ const b = String(right);
196
+ if (a === b) return 0;
197
+ if (a.length === 0) return b.length;
198
+ if (b.length === 0) return a.length;
199
+
200
+ const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
201
+ for (let i = 0; i <= a.length; i += 1) matrix[i][0] = i;
202
+ for (let j = 0; j <= b.length; j += 1) matrix[0][j] = j;
203
+
204
+ for (let i = 1; i <= a.length; i += 1) {
205
+ for (let j = 1; j <= b.length; j += 1) {
206
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
207
+ matrix[i][j] = Math.min(
208
+ matrix[i - 1][j] + 1,
209
+ matrix[i][j - 1] + 1,
210
+ matrix[i - 1][j - 1] + cost,
211
+ );
212
+ }
213
+ }
214
+
215
+ return matrix[a.length][b.length];
216
+ };
217
+
218
+ const rankCandidates = ({ symbolName, candidates = [] }) => {
219
+ return [...candidates]
220
+ .map((candidate) => {
221
+ const name = String(candidate?.name || "");
222
+ const exactPrefix = name.startsWith(symbolName) || symbolName.startsWith(name);
223
+ return {
224
+ ...candidate,
225
+ rankScore: exactPrefix ? 0 : 1,
226
+ distance: levenshteinDistance(symbolName, name),
227
+ };
228
+ })
229
+ .sort((left, right) => (
230
+ left.rankScore - right.rankScore
231
+ || left.distance - right.distance
232
+ || String(left.name).localeCompare(String(right.name))
233
+ ));
234
+ };
235
+
236
+ export const buildComponentScopeGraphs = ({ models = [] } = {}) => {
237
+ const rows = [...models]
238
+ .filter((model) => model && typeof model.componentKey === "string")
239
+ .sort((left, right) => left.componentKey.localeCompare(right.componentKey))
240
+ .map((model) => {
241
+ const graph = model?.semanticGraph || buildComponentScopeGraph(model);
242
+ return {
243
+ componentKey: model.componentKey,
244
+ globalSymbols: toSortedSetArray(graph?.globalSymbols),
245
+ references: normalizeReferences(graph?.references),
246
+ };
247
+ });
248
+
249
+ return {
250
+ rows,
251
+ byComponent: new Map(rows.map((row) => [row.componentKey, row])),
252
+ };
253
+ };
254
+
255
+ const symbolKindsPreferredOrder = [
256
+ "handler",
257
+ "action",
258
+ "method",
259
+ "prop",
260
+ "constant",
261
+ "ref",
262
+ "expression-root",
263
+ ];
264
+
265
+ const normalizeLocalSymbols = (value) => {
266
+ if (value instanceof Set) {
267
+ return [...value].sort(SORT_STRING);
268
+ }
269
+ if (Array.isArray(value)) {
270
+ return [...new Set(value.map((entry) => String(entry)))].sort(SORT_STRING);
271
+ }
272
+ return [];
273
+ };
274
+
275
+ const toResolutionKind = ({ root, localSymbols, globalSymbolMap }) => {
276
+ if (localSymbols.includes(root)) {
277
+ return {
278
+ kind: "local",
279
+ symbolId: `local::${root}`,
280
+ symbolKinds: ["local"],
281
+ };
282
+ }
283
+
284
+ const globalSymbol = globalSymbolMap.get(root);
285
+ if (globalSymbol) {
286
+ return {
287
+ kind: "global",
288
+ symbolId: globalSymbol.id,
289
+ symbolKinds: [...(globalSymbol.kinds || [])],
290
+ symbolFilePath: globalSymbol.filePath,
291
+ };
292
+ }
293
+
294
+ return {
295
+ kind: "unresolved",
296
+ symbolId: null,
297
+ symbolKinds: [],
298
+ };
299
+ };
300
+
301
+ const buildUnresolvedSymbolDiagnostic = ({
302
+ model,
303
+ reference,
304
+ root,
305
+ rankedCandidates = [],
306
+ }) => {
307
+ const related = rankedCandidates.slice(0, 3).map((candidate) => ({
308
+ message: `Candidate symbol '${candidate.name}' (${candidate.kind}).`,
309
+ filePath: candidate.filePath || model?.view?.filePath || model?.files?.view || "unknown",
310
+ }));
311
+
312
+ return {
313
+ code: "RTGL-CHECK-SEM-001",
314
+ severity: "error",
315
+ filePath: model?.view?.filePath || model?.files?.view || "unknown",
316
+ line: Number.isInteger(reference?.line) ? reference.line : undefined,
317
+ column: Number.isInteger(reference?.column) ? reference.column : undefined,
318
+ endLine: Number.isInteger(reference?.endLine) ? reference.endLine : undefined,
319
+ endColumn: Number.isInteger(reference?.endColumn) ? reference.endColumn : undefined,
320
+ message: `${model.componentKey}: unresolved symbol '${root}' in expression '${reference?.expression || ""}'.`,
321
+ related,
322
+ };
323
+ };
324
+
325
+ const buildAmbiguityDiagnostic = ({ model, symbolName, symbolRows = [] }) => {
326
+ const rankedKinds = [...symbolRows]
327
+ .flatMap((row) => Array.isArray(row.kinds) ? row.kinds : [row.kind])
328
+ .filter(Boolean)
329
+ .sort((left, right) => (
330
+ symbolKindsPreferredOrder.indexOf(left) - symbolKindsPreferredOrder.indexOf(right)
331
+ || String(left).localeCompare(String(right))
332
+ ));
333
+
334
+ const preferredKind = rankedKinds[0] || "unknown";
335
+ const related = symbolRows.slice(0, 5).map((row) => ({
336
+ message: `Candidate '${row.name}' kind '${row.kind}'.`,
337
+ filePath: row.filePath || model?.view?.filePath || model?.files?.view || "unknown",
338
+ }));
339
+
340
+ return {
341
+ code: "RTGL-CHECK-SEM-002",
342
+ severity: "warn",
343
+ filePath: model?.view?.filePath || model?.files?.view || "unknown",
344
+ message: `${model.componentKey}: ambiguous symbol '${symbolName}' resolves to multiple kinds [${rankedKinds.join(", ")}]. Preferred '${preferredKind}'.`,
345
+ related,
346
+ };
347
+ };
348
+
349
+ export const resolveReferenceSymbols = ({ models = [], globalSymbolTable, scopeGraphs }) => {
350
+ const diagnostics = [];
351
+ const resolvedReferences = [];
352
+ const edgeRows = [];
353
+ const localSymbolRows = [];
354
+
355
+ const modelByComponent = new Map(
356
+ [...models]
357
+ .filter((model) => model && typeof model.componentKey === "string")
358
+ .map((model) => [model.componentKey, model]),
359
+ );
360
+
361
+ scopeGraphs.rows.forEach((scopeRow) => {
362
+ const model = modelByComponent.get(scopeRow.componentKey);
363
+ const globalSymbols = globalSymbolTable.byComponent.get(scopeRow.componentKey) || new Map();
364
+
365
+ scopeRow.references.forEach((reference, referenceIndex) => {
366
+ const localSymbols = normalizeLocalSymbols(reference?.localSymbols);
367
+ const roots = Array.isArray(reference?.roots)
368
+ ? [...new Set(reference.roots.map((entry) => String(entry || "")).filter(Boolean))].sort(SORT_STRING)
369
+ : [];
370
+
371
+ localSymbols.forEach((localSymbolName) => {
372
+ localSymbolRows.push({
373
+ id: `${scopeRow.componentKey}::local::${referenceIndex}::${localSymbolName}`,
374
+ componentKey: scopeRow.componentKey,
375
+ name: localSymbolName,
376
+ kind: "local",
377
+ });
378
+ });
379
+
380
+ roots.forEach((root) => {
381
+ const resolution = toResolutionKind({
382
+ root,
383
+ localSymbols,
384
+ globalSymbolMap: globalSymbols,
385
+ });
386
+
387
+ const baseResolution = {
388
+ id: `${scopeRow.componentKey}::ref-res::${referenceIndex}::${root}`,
389
+ componentKey: scopeRow.componentKey,
390
+ expression: reference?.expression || "",
391
+ context: reference?.context || "unknown",
392
+ source: reference?.source || "unknown",
393
+ root,
394
+ resolutionKind: resolution.kind,
395
+ symbolId: resolution.symbolId,
396
+ symbolKinds: resolution.symbolKinds,
397
+ line: Number.isInteger(reference?.line) ? reference.line : undefined,
398
+ column: Number.isInteger(reference?.column) ? reference.column : undefined,
399
+ endLine: Number.isInteger(reference?.endLine) ? reference.endLine : undefined,
400
+ endColumn: Number.isInteger(reference?.endColumn) ? reference.endColumn : undefined,
401
+ };
402
+
403
+ resolvedReferences.push(baseResolution);
404
+
405
+ if (resolution.kind === "global" && resolution.symbolId) {
406
+ edgeRows.push({
407
+ id: `${scopeRow.componentKey}::edge::${referenceIndex}::${root}`,
408
+ componentKey: scopeRow.componentKey,
409
+ kind: "ref-root",
410
+ from: `${scopeRow.componentKey}::ref::${referenceIndex}`,
411
+ to: resolution.symbolId,
412
+ });
413
+ }
414
+
415
+ if (resolution.kind === "unresolved") {
416
+ const candidates = rankCandidates({
417
+ symbolName: root,
418
+ candidates: [...globalSymbols.values()],
419
+ });
420
+ diagnostics.push(buildUnresolvedSymbolDiagnostic({
421
+ model,
422
+ reference,
423
+ root,
424
+ rankedCandidates: candidates,
425
+ }));
426
+ }
427
+ });
428
+ });
429
+
430
+ const symbolsByName = new Map();
431
+ [...globalSymbols.values()].forEach((symbolRow) => {
432
+ if (!symbolsByName.has(symbolRow.name)) {
433
+ symbolsByName.set(symbolRow.name, []);
434
+ }
435
+ symbolsByName.get(symbolRow.name).push(symbolRow);
436
+ });
437
+
438
+ symbolsByName.forEach((rowsByName, symbolName) => {
439
+ const mergedKinds = [...new Set(rowsByName.flatMap((row) => row.kinds || [row.kind]))];
440
+ if (mergedKinds.length <= 1) {
441
+ return;
442
+ }
443
+ diagnostics.push(buildAmbiguityDiagnostic({
444
+ model,
445
+ symbolName,
446
+ symbolRows: rowsByName,
447
+ }));
448
+ });
449
+ });
450
+
451
+ return {
452
+ resolvedReferences: resolvedReferences.sort((left, right) => (
453
+ String(left.componentKey).localeCompare(String(right.componentKey))
454
+ || (left.line || 0) - (right.line || 0)
455
+ || String(left.root).localeCompare(String(right.root))
456
+ )),
457
+ localSymbols: localSymbolRows.sort(sortByComponentAndName),
458
+ edges: edgeRows.sort((left, right) => String(left.id).localeCompare(String(right.id))),
459
+ diagnostics: diagnostics.sort((left, right) => (
460
+ String(left.code).localeCompare(String(right.code))
461
+ || String(left.filePath).localeCompare(String(right.filePath))
462
+ || (left.line || 0) - (right.line || 0)
463
+ || String(left.message).localeCompare(String(right.message))
464
+ )),
465
+ };
466
+ };
467
+
468
+ export const resolveFeSymbols = ({ models = [] } = {}) => {
469
+ const diagnostics = [];
470
+ const rows = [];
471
+
472
+ [...models]
473
+ .filter((model) => model && typeof model.componentKey === "string")
474
+ .sort((left, right) => left.componentKey.localeCompare(right.componentKey))
475
+ .forEach((model) => {
476
+ const refs = isObjectRecord(model?.view?.yaml?.refs) ? model.view.yaml.refs : {};
477
+ const listenerRows = Array.isArray(model?.view?.refListeners) ? model.view.refListeners : [];
478
+
479
+ listenerRows.forEach((listener, index) => {
480
+ const eventConfig = listener?.eventConfig;
481
+ const handlerName = String(eventConfig?.handler || "").trim();
482
+ const actionName = String(eventConfig?.action || "").trim();
483
+
484
+ if (handlerName) {
485
+ const hasHandler = model?.handlers?.exports instanceof Set
486
+ ? model.handlers.exports.has(handlerName)
487
+ : false;
488
+ rows.push({
489
+ id: `${model.componentKey}::fe::handler::${index}`,
490
+ componentKey: model.componentKey,
491
+ kind: "handler",
492
+ name: handlerName,
493
+ resolved: hasHandler,
494
+ });
495
+ if (!hasHandler) {
496
+ diagnostics.push({
497
+ code: "RTGL-CHECK-SEM-004",
498
+ severity: "error",
499
+ filePath: model?.view?.filePath || model?.files?.view || "unknown",
500
+ line: Number.isInteger(listener?.line) ? listener.line : undefined,
501
+ message: `${model.componentKey}: unresolved FE handler symbol '${handlerName}'.`,
502
+ related: [{
503
+ message: "Expected handler export in .handlers.js.",
504
+ filePath: model?.handlers?.filePath || model?.files?.handlers || "unknown",
505
+ }],
506
+ });
507
+ }
508
+ }
509
+
510
+ if (actionName) {
511
+ const hasAction = model?.store?.exports instanceof Set
512
+ ? model.store.exports.has(actionName)
513
+ : false;
514
+ rows.push({
515
+ id: `${model.componentKey}::fe::action::${index}`,
516
+ componentKey: model.componentKey,
517
+ kind: "action",
518
+ name: actionName,
519
+ resolved: hasAction,
520
+ });
521
+ if (!hasAction) {
522
+ diagnostics.push({
523
+ code: "RTGL-CHECK-SEM-004",
524
+ severity: "error",
525
+ filePath: model?.view?.filePath || model?.files?.view || "unknown",
526
+ line: Number.isInteger(listener?.line) ? listener.line : undefined,
527
+ message: `${model.componentKey}: unresolved FE action symbol '${actionName}'.`,
528
+ related: [{
529
+ message: "Expected action export in .store.js.",
530
+ filePath: model?.store?.filePath || model?.files?.store || "unknown",
531
+ }],
532
+ });
533
+ }
534
+ }
535
+ });
536
+
537
+ const declaredMethods = toSortedObjectKeys(model?.schema?.yaml?.methods?.properties);
538
+ declaredMethods.forEach((methodName) => {
539
+ const hasMethod = model?.methods?.exports instanceof Set
540
+ ? model.methods.exports.has(methodName)
541
+ : false;
542
+ rows.push({
543
+ id: `${model.componentKey}::fe::method::${methodName}`,
544
+ componentKey: model.componentKey,
545
+ kind: "method",
546
+ name: methodName,
547
+ resolved: hasMethod,
548
+ });
549
+ if (!hasMethod) {
550
+ diagnostics.push({
551
+ code: "RTGL-CHECK-SEM-004",
552
+ severity: "error",
553
+ filePath: model?.schema?.filePath || model?.files?.schema || "unknown",
554
+ message: `${model.componentKey}: unresolved FE method symbol '${methodName}'.`,
555
+ related: [{
556
+ message: "Expected method export in .methods.js.",
557
+ filePath: model?.methods?.filePath || model?.files?.methods || "unknown",
558
+ }],
559
+ });
560
+ }
561
+ });
562
+
563
+ toSortedObjectKeys(refs).forEach((refName) => {
564
+ rows.push({
565
+ id: `${model.componentKey}::fe::ref::${refName}`,
566
+ componentKey: model.componentKey,
567
+ kind: "ref",
568
+ name: refName,
569
+ resolved: true,
570
+ });
571
+ });
572
+ });
573
+
574
+ return {
575
+ rows: rows.sort(sortByComponentAndName),
576
+ diagnostics: diagnostics.sort((left, right) => (
577
+ String(left.code).localeCompare(String(right.code))
578
+ || String(left.filePath).localeCompare(String(right.filePath))
579
+ || (left.line || 0) - (right.line || 0)
580
+ || String(left.message).localeCompare(String(right.message))
581
+ )),
582
+ };
583
+ };
584
+
585
+ export const resolveCrossComponentReferences = ({ models = [], registry = new Map() } = {}) => {
586
+ const rows = [];
587
+ const diagnostics = [];
588
+
589
+ [...models]
590
+ .filter((model) => model && typeof model.componentKey === "string")
591
+ .sort((left, right) => left.componentKey.localeCompare(right.componentKey))
592
+ .forEach((model) => {
593
+ const nodes = Array.isArray(model?.view?.templateAst?.nodes) ? model.view.templateAst.nodes : [];
594
+ nodes.forEach((node, index) => {
595
+ const tagName = String(node?.tagName || "").trim();
596
+ if (!tagName || !tagName.startsWith("rtgl-")) {
597
+ return;
598
+ }
599
+
600
+ const resolved = registry instanceof Map ? registry.has(tagName) : false;
601
+ rows.push({
602
+ id: `${model.componentKey}::xref::${index}`,
603
+ componentKey: model.componentKey,
604
+ tagName,
605
+ resolved,
606
+ line: Number.isInteger(node?.range?.line) ? node.range.line : undefined,
607
+ filePath: model?.view?.filePath || model?.files?.view || "unknown",
608
+ });
609
+
610
+ if (!resolved) {
611
+ diagnostics.push({
612
+ code: "RTGL-CHECK-SEM-005",
613
+ severity: "error",
614
+ filePath: model?.view?.filePath || model?.files?.view || "unknown",
615
+ line: Number.isInteger(node?.range?.line) ? node.range.line : undefined,
616
+ message: `${model.componentKey}: unresolved cross-component tag '${tagName}'.`,
617
+ });
618
+ }
619
+ });
620
+ });
621
+
622
+ return {
623
+ rows: rows.sort((left, right) => (
624
+ String(left.componentKey).localeCompare(String(right.componentKey))
625
+ || String(left.tagName).localeCompare(String(right.tagName))
626
+ || (left.line || 0) - (right.line || 0)
627
+ )),
628
+ diagnostics,
629
+ };
630
+ };
631
+
632
+ export const runSemanticInvariants = ({ symbols = [], refs = [], edges = [] } = {}) => {
633
+ const issues = [];
634
+ const seenIds = new Set();
635
+
636
+ const pushIssue = (code, message, path) => {
637
+ issues.push({ code, message, path });
638
+ };
639
+
640
+ const symbolIds = new Set();
641
+ const refIds = new Set();
642
+
643
+ symbols.forEach((symbol, index) => {
644
+ const id = symbol?.id;
645
+ const path = `symbols.${index}`;
646
+ if (!id) {
647
+ pushIssue("RTGL-CHECK-SEM-INV-001", "Symbol id is required.", path);
648
+ return;
649
+ }
650
+ if (seenIds.has(id)) {
651
+ pushIssue("RTGL-CHECK-SEM-INV-002", `Duplicate symbol id '${id}'.`, path);
652
+ return;
653
+ }
654
+ seenIds.add(id);
655
+ symbolIds.add(id);
656
+ });
657
+
658
+ refs.forEach((ref, index) => {
659
+ const id = ref?.id;
660
+ const path = `refs.${index}`;
661
+ if (!id) {
662
+ pushIssue("RTGL-CHECK-SEM-INV-003", "Reference id is required.", path);
663
+ return;
664
+ }
665
+ if (seenIds.has(id)) {
666
+ pushIssue("RTGL-CHECK-SEM-INV-004", `Duplicate ref id '${id}'.`, path);
667
+ return;
668
+ }
669
+ seenIds.add(id);
670
+ refIds.add(id);
671
+ });
672
+
673
+ const validIds = new Set([...symbolIds, ...refIds]);
674
+ edges.forEach((edge, index) => {
675
+ const path = `edges.${index}`;
676
+ const from = edge?.from;
677
+ const to = edge?.to;
678
+ if (typeof from === "string" && from.length > 0 && !validIds.has(from)) {
679
+ pushIssue("RTGL-CHECK-SEM-INV-005", `Dangling edge 'from' id '${from}'.`, path);
680
+ }
681
+ if (typeof to === "string" && to.length > 0 && !validIds.has(to)) {
682
+ pushIssue("RTGL-CHECK-SEM-INV-006", `Dangling edge 'to' id '${to}'.`, path);
683
+ }
684
+ });
685
+
686
+ return {
687
+ ok: issues.length === 0,
688
+ issues: issues.sort((left, right) => (
689
+ String(left.code).localeCompare(String(right.code))
690
+ || String(left.path).localeCompare(String(right.path))
691
+ || String(left.message).localeCompare(String(right.message))
692
+ )),
693
+ };
694
+ };
695
+
696
+ export const runSemanticEngine = ({ models = [], registry = new Map() } = {}) => {
697
+ const globalSymbolTable = buildGlobalSymbolTable({ models });
698
+ const scopeGraphs = buildComponentScopeGraphs({ models });
699
+ const referenceResolution = resolveReferenceSymbols({
700
+ models,
701
+ globalSymbolTable,
702
+ scopeGraphs,
703
+ });
704
+ const feResolution = resolveFeSymbols({ models });
705
+ const crossComponentResolution = resolveCrossComponentReferences({ models, registry });
706
+
707
+ const semanticSymbols = [
708
+ ...globalSymbolTable.rows,
709
+ ...referenceResolution.localSymbols,
710
+ ].sort((left, right) => (
711
+ String(left.id).localeCompare(String(right.id))
712
+ || String(left.componentKey).localeCompare(String(right.componentKey))
713
+ ));
714
+
715
+ const semanticRefs = referenceResolution.resolvedReferences.map((reference) => ({
716
+ id: reference.id,
717
+ componentKey: reference.componentKey,
718
+ expression: reference.expression,
719
+ context: reference.context,
720
+ source: reference.source,
721
+ line: reference.line,
722
+ column: reference.column,
723
+ endLine: reference.endLine,
724
+ endColumn: reference.endColumn,
725
+ root: reference.root,
726
+ resolutionKind: reference.resolutionKind,
727
+ symbolId: reference.symbolId,
728
+ }));
729
+
730
+ const graphRefs = scopeGraphs.rows.flatMap((row) => (
731
+ row.references.map((reference, index) => ({
732
+ id: `${row.componentKey}::ref::${index}`,
733
+ componentKey: row.componentKey,
734
+ expression: reference?.expression || "",
735
+ line: reference?.line,
736
+ column: reference?.column,
737
+ }))
738
+ ));
739
+
740
+ const semanticEdges = referenceResolution.edges;
741
+ const invariants = runSemanticInvariants({
742
+ symbols: semanticSymbols,
743
+ refs: graphRefs,
744
+ edges: semanticEdges,
745
+ });
746
+
747
+ const diagnostics = [
748
+ ...referenceResolution.diagnostics,
749
+ ...feResolution.diagnostics,
750
+ ...crossComponentResolution.diagnostics,
751
+ ...invariants.issues.map((issue) => ({
752
+ code: issue.code,
753
+ severity: "error",
754
+ filePath: "unknown",
755
+ message: issue.message,
756
+ })),
757
+ ].sort((left, right) => (
758
+ String(left.code).localeCompare(String(right.code))
759
+ || String(left.filePath || "").localeCompare(String(right.filePath || ""))
760
+ || (left.line || 0) - (right.line || 0)
761
+ || String(left.message || "").localeCompare(String(right.message || ""))
762
+ ));
763
+
764
+ return {
765
+ globalSymbolTable,
766
+ scopeGraphs,
767
+ referenceResolution,
768
+ feResolution,
769
+ crossComponentResolution,
770
+ semanticGraph: {
771
+ symbols: semanticSymbols,
772
+ refs: graphRefs,
773
+ edges: semanticEdges,
774
+ },
775
+ invariants,
776
+ diagnostics,
777
+ };
778
+ };