@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,329 @@
1
+ import { toCamelCase } from "../utils/case.js";
2
+ import { parseSync } from "oxc-parser";
3
+ import { JEMPL_NODE, parseJemplForCompiler } from "./parsers.js";
4
+
5
+ const BUILTIN_ALLOWED_ROOTS = new Set([
6
+ "_event",
7
+ "_action",
8
+ "event",
9
+ "state",
10
+ "props",
11
+ "refs",
12
+ "constants",
13
+ "window",
14
+ "document",
15
+ "Math",
16
+ "Number",
17
+ "String",
18
+ "Boolean",
19
+ "Object",
20
+ "Array",
21
+ "Date",
22
+ "JSON",
23
+ "console",
24
+ ]);
25
+
26
+ const RESERVED_WORDS = new Set([
27
+ "if",
28
+ "else",
29
+ "for",
30
+ "while",
31
+ "switch",
32
+ "case",
33
+ "break",
34
+ "continue",
35
+ "return",
36
+ "throw",
37
+ "try",
38
+ "catch",
39
+ "finally",
40
+ "new",
41
+ "typeof",
42
+ "instanceof",
43
+ "in",
44
+ "void",
45
+ "delete",
46
+ "await",
47
+ "async",
48
+ "true",
49
+ "false",
50
+ "null",
51
+ "undefined",
52
+ ]);
53
+
54
+ const stripStringLiterals = (source = "") => {
55
+ return source.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/gu, " ");
56
+ };
57
+
58
+ const extractExpressionRootIdentifiersRegexFallback = (expression = "") => {
59
+ const source = stripStringLiterals(String(expression));
60
+ const roots = new Set();
61
+ const regex = /(?:^|[^.\w$])([A-Za-z_$][A-Za-z0-9_$]*)/g;
62
+ let match = regex.exec(source);
63
+
64
+ while (match) {
65
+ const candidate = match[1];
66
+ if (!candidate || RESERVED_WORDS.has(candidate)) {
67
+ match = regex.exec(source);
68
+ continue;
69
+ }
70
+ roots.add(candidate);
71
+ match = regex.exec(source);
72
+ }
73
+
74
+ return [...roots];
75
+ };
76
+
77
+ const toPathRootIdentifier = (pathExpression = "") => {
78
+ const match = String(pathExpression || "").trim().match(/^([A-Za-z_$][A-Za-z0-9_$]*)/u);
79
+ if (!match) {
80
+ return null;
81
+ }
82
+ return match[1];
83
+ };
84
+
85
+ const collectExpressionRootIdentifiersFromAst = (expressionAst) => {
86
+ const roots = new Set();
87
+ const visit = (node) => {
88
+ if (Array.isArray(node)) {
89
+ node.forEach((item) => visit(item));
90
+ return;
91
+ }
92
+ if (!node || typeof node !== "object") {
93
+ return;
94
+ }
95
+
96
+ if (node.type === JEMPL_NODE.PATH && typeof node.path === "string") {
97
+ const root = toPathRootIdentifier(node.path);
98
+ if (root && !RESERVED_WORDS.has(root)) {
99
+ roots.add(root);
100
+ }
101
+ }
102
+
103
+ Object.values(node).forEach((value) => visit(value));
104
+ };
105
+
106
+ visit(expressionAst);
107
+ return [...roots];
108
+ };
109
+
110
+ const collectObjectExpressionKeys = (node) => {
111
+ if (!node || node.type !== "ObjectExpression" || !Array.isArray(node.properties)) {
112
+ return [];
113
+ }
114
+
115
+ const keys = [];
116
+ node.properties.forEach((property) => {
117
+ if (!property || property.type !== "Property") {
118
+ return;
119
+ }
120
+ if (property.computed) {
121
+ return;
122
+ }
123
+ const keyNode = property.key;
124
+ if (keyNode?.type === "Identifier" && keyNode.name) {
125
+ keys.push(keyNode.name);
126
+ return;
127
+ }
128
+ if (
129
+ keyNode?.type === "StringLiteral"
130
+ || keyNode?.type === "Literal"
131
+ || keyNode?.type === "NumericLiteral"
132
+ ) {
133
+ const value = keyNode.value;
134
+ if (typeof value === "string" && value) {
135
+ keys.push(value);
136
+ }
137
+ }
138
+ });
139
+ return keys;
140
+ };
141
+
142
+ const collectReturnObjectKeysFromFunctionNode = (functionNode) => {
143
+ if (!functionNode || typeof functionNode !== "object") {
144
+ return [];
145
+ }
146
+
147
+ if (functionNode.body?.type === "ObjectExpression") {
148
+ return collectObjectExpressionKeys(functionNode.body);
149
+ }
150
+
151
+ if (functionNode.body?.type !== "BlockStatement" || !Array.isArray(functionNode.body.body)) {
152
+ return [];
153
+ }
154
+
155
+ for (let index = 0; index < functionNode.body.body.length; index += 1) {
156
+ const statement = functionNode.body.body[index];
157
+ if (!statement || statement.type !== "ReturnStatement") {
158
+ continue;
159
+ }
160
+ return collectObjectExpressionKeys(statement.argument);
161
+ }
162
+
163
+ return [];
164
+ };
165
+
166
+ export const collectSelectViewDataRoots = (sourceText = "", filePath = "unknown.js") => {
167
+ const roots = new Set();
168
+ if (!sourceText) {
169
+ return roots;
170
+ }
171
+
172
+ let parsed = null;
173
+ try {
174
+ parsed = parseSync(filePath, sourceText, { sourceType: "unambiguous" });
175
+ } catch {
176
+ return roots;
177
+ }
178
+ if (!parsed?.program?.body || !Array.isArray(parsed.program.body)) {
179
+ return roots;
180
+ }
181
+ if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
182
+ return roots;
183
+ }
184
+
185
+ parsed.program.body.forEach((statement) => {
186
+ if (!statement || statement.type !== "ExportNamedDeclaration" || !statement.declaration) {
187
+ return;
188
+ }
189
+
190
+ const declaration = statement.declaration;
191
+ if (declaration.type === "FunctionDeclaration" && declaration.id?.name === "selectViewData") {
192
+ collectReturnObjectKeysFromFunctionNode(declaration).forEach((key) => roots.add(key));
193
+ return;
194
+ }
195
+
196
+ if (declaration.type === "VariableDeclaration" && Array.isArray(declaration.declarations)) {
197
+ declaration.declarations.forEach((declarator) => {
198
+ if (declarator?.id?.name !== "selectViewData") {
199
+ return;
200
+ }
201
+ const init = declarator.init;
202
+ if (!init || (init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression")) {
203
+ return;
204
+ }
205
+ collectReturnObjectKeysFromFunctionNode(init).forEach((key) => roots.add(key));
206
+ });
207
+ }
208
+ });
209
+
210
+ return roots;
211
+ };
212
+
213
+ const isNumericLiteral = (value = "") => {
214
+ return /^[-+]?(?:\d+\.?\d*|\d*\.?\d+)$/u.test(value.trim());
215
+ };
216
+
217
+ export const extractExpressionRootIdentifiers = (expression = "") => {
218
+ const normalizedExpression = String(expression || "").trim();
219
+ if (!normalizedExpression) {
220
+ return [];
221
+ }
222
+
223
+ const parsedExpression = parseJemplForCompiler({ source: normalizedExpression });
224
+ if (parsedExpression?.ast && !parsedExpression.parseError) {
225
+ const astRoots = collectExpressionRootIdentifiersFromAst(parsedExpression.ast);
226
+ if (astRoots.length > 0) {
227
+ return astRoots;
228
+ }
229
+ }
230
+
231
+ return extractExpressionRootIdentifiersRegexFallback(normalizedExpression);
232
+ };
233
+
234
+ export const collectKnownExpressionRoots = (model) => {
235
+ const known = new Set(BUILTIN_ALLOWED_ROOTS);
236
+ const normalizedPropNames = model?.schema?.normalized?.props?.names;
237
+ if (Array.isArray(normalizedPropNames) && normalizedPropNames.length > 0) {
238
+ normalizedPropNames.forEach((propName) => {
239
+ if (!propName) {
240
+ return;
241
+ }
242
+ known.add(propName);
243
+ known.add(toCamelCase(propName));
244
+ });
245
+ } else {
246
+ const schemaProperties = model?.schema?.yaml?.propsSchema?.properties;
247
+ if (schemaProperties && typeof schemaProperties === "object" && !Array.isArray(schemaProperties)) {
248
+ Object.keys(schemaProperties).forEach((propName) => {
249
+ if (!propName) {
250
+ return;
251
+ }
252
+ known.add(propName);
253
+ known.add(toCamelCase(propName));
254
+ });
255
+ }
256
+ }
257
+
258
+ const constants = model?.constants?.yaml;
259
+ if (constants && typeof constants === "object" && !Array.isArray(constants)) {
260
+ Object.keys(constants).forEach((constantName) => {
261
+ if (constantName) {
262
+ known.add(constantName);
263
+ }
264
+ });
265
+ }
266
+
267
+ [model?.handlers?.exports, model?.methods?.exports, model?.store?.exports].forEach((exportSet) => {
268
+ if (exportSet instanceof Set) {
269
+ exportSet.forEach((name) => {
270
+ if (name) {
271
+ known.add(name);
272
+ }
273
+ });
274
+ }
275
+ });
276
+
277
+ collectSelectViewDataRoots(
278
+ model?.store?.sourceText || "",
279
+ model?.store?.filePath || "unknown.store.js",
280
+ ).forEach((key) => known.add(key));
281
+
282
+ return known;
283
+ };
284
+
285
+ export const collectTemplateExpressionReferences = (model) => {
286
+ const references = [];
287
+ const nodes = Array.isArray(model?.view?.templateAst?.nodes) ? model.view.templateAst.nodes : [];
288
+
289
+ nodes.forEach((node) => {
290
+ const attributes = Array.isArray(node?.attributes) ? node.attributes : [];
291
+ attributes.forEach((attribute) => {
292
+ const sourceType = String(attribute?.sourceType || "");
293
+ if (sourceType === "legacy-prop" || sourceType === "event") {
294
+ return;
295
+ }
296
+
297
+ const expressions = Array.isArray(attribute?.expressions) ? attribute.expressions : [];
298
+ expressions.forEach((expression) => {
299
+ const roots = extractExpressionRootIdentifiers(expression);
300
+ references.push({
301
+ expression,
302
+ roots,
303
+ line: attribute?.range?.line || node?.range?.line,
304
+ column: attribute?.range?.column,
305
+ endLine: attribute?.range?.endLine || node?.range?.endLine,
306
+ endColumn: attribute?.range?.endColumn,
307
+ });
308
+ });
309
+ });
310
+ });
311
+
312
+ return references;
313
+ };
314
+
315
+ export const isUnknownExpressionRoot = ({
316
+ root = "",
317
+ knownRoots = new Set(),
318
+ }) => {
319
+ if (!root) {
320
+ return false;
321
+ }
322
+ if (knownRoots.has(root)) {
323
+ return false;
324
+ }
325
+ if (isNumericLiteral(root)) {
326
+ return false;
327
+ }
328
+ return true;
329
+ };
@@ -0,0 +1,191 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+
3
+ const toLineOffset = ({ lines = [], line = 1, column = 1 }) => {
4
+ const lineIndex = Math.max(1, line) - 1;
5
+ const prefix = lines.slice(0, lineIndex).join("\n");
6
+ const prefixLength = prefix.length + (lineIndex > 0 ? 1 : 0);
7
+ return prefixLength + Math.max(1, column) - 1;
8
+ };
9
+
10
+ const normalizeFix = (fix = {}, diagnostic = {}) => {
11
+ if (!fix || typeof fix !== "object") {
12
+ return null;
13
+ }
14
+ const filePath = typeof fix.filePath === "string" && fix.filePath
15
+ ? fix.filePath
16
+ : diagnostic.filePath;
17
+ if (!filePath || filePath === "unknown") {
18
+ return null;
19
+ }
20
+
21
+ return {
22
+ kind: String(fix.kind || ""),
23
+ filePath,
24
+ line: Number.isInteger(fix.line) ? fix.line : undefined,
25
+ column: Number.isInteger(fix.column) ? fix.column : undefined,
26
+ endLine: Number.isInteger(fix.endLine) ? fix.endLine : undefined,
27
+ endColumn: Number.isInteger(fix.endColumn) ? fix.endColumn : undefined,
28
+ pattern: typeof fix.pattern === "string" ? fix.pattern : undefined,
29
+ replacement: typeof fix.replacement === "string" ? fix.replacement : "",
30
+ flags: typeof fix.flags === "string" ? fix.flags : "g",
31
+ description: typeof fix.description === "string" ? fix.description : "autofix",
32
+ confidence: Number.isFinite(fix.confidence) ? Number(fix.confidence) : 0,
33
+ safe: fix.safe !== false,
34
+ };
35
+ };
36
+
37
+ const buildUnifiedPatch = ({ filePath, before, after }) => {
38
+ const beforeLines = String(before).split("\n");
39
+ const afterLines = String(after).split("\n");
40
+
41
+ let startIndex = 0;
42
+ while (
43
+ startIndex < beforeLines.length
44
+ && startIndex < afterLines.length
45
+ && beforeLines[startIndex] === afterLines[startIndex]
46
+ ) {
47
+ startIndex += 1;
48
+ }
49
+
50
+ let endBeforeIndex = beforeLines.length - 1;
51
+ let endAfterIndex = afterLines.length - 1;
52
+ while (
53
+ endBeforeIndex >= startIndex
54
+ && endAfterIndex >= startIndex
55
+ && beforeLines[endBeforeIndex] === afterLines[endAfterIndex]
56
+ ) {
57
+ endBeforeIndex -= 1;
58
+ endAfterIndex -= 1;
59
+ }
60
+
61
+ const removed = beforeLines.slice(startIndex, endBeforeIndex + 1);
62
+ const added = afterLines.slice(startIndex, endAfterIndex + 1);
63
+ const startLine = startIndex + 1;
64
+ const fromCount = Math.max(0, removed.length);
65
+ const toCount = Math.max(0, added.length);
66
+
67
+ const lines = [
68
+ `--- ${filePath}`,
69
+ `+++ ${filePath}`,
70
+ `@@ -${startLine},${fromCount} +${startLine},${toCount} @@`,
71
+ ...removed.map((line) => `-${line}`),
72
+ ...added.map((line) => `+${line}`),
73
+ ];
74
+
75
+ return lines.join("\n");
76
+ };
77
+
78
+ export const applyDiagnosticFixes = ({
79
+ diagnostics = [],
80
+ dryRun = true,
81
+ minConfidence = 0.9,
82
+ includePatchText = false,
83
+ } = {}) => {
84
+ const files = new Map();
85
+ const patches = [];
86
+ let candidateCount = 0;
87
+ let appliedCount = 0;
88
+ let skippedCount = 0;
89
+
90
+ const getFileState = (filePath) => {
91
+ if (!files.has(filePath)) {
92
+ const content = readFileSync(filePath, "utf8");
93
+ files.set(filePath, {
94
+ original: content,
95
+ content,
96
+ });
97
+ }
98
+ return files.get(filePath);
99
+ };
100
+
101
+ diagnostics.forEach((diagnostic) => {
102
+ const fix = normalizeFix(diagnostic.fix, diagnostic);
103
+ if (!fix) {
104
+ return;
105
+ }
106
+
107
+ candidateCount += 1;
108
+ if (!fix.safe || fix.confidence < minConfidence) {
109
+ skippedCount += 1;
110
+ return;
111
+ }
112
+
113
+ const state = getFileState(fix.filePath);
114
+ const before = state.content;
115
+ let after = before;
116
+
117
+ if (fix.kind === "line-regex-replace") {
118
+ const lines = before.split("\n");
119
+ const lineIndex = (fix.line || 1) - 1;
120
+ if (lineIndex < 0 || lineIndex >= lines.length || !fix.pattern) {
121
+ skippedCount += 1;
122
+ return;
123
+ }
124
+
125
+ const regex = new RegExp(fix.pattern, fix.flags || "g");
126
+ const replacedLine = lines[lineIndex].replace(regex, fix.replacement);
127
+ if (replacedLine === lines[lineIndex]) {
128
+ skippedCount += 1;
129
+ return;
130
+ }
131
+ lines[lineIndex] = replacedLine;
132
+ after = lines.join("\n");
133
+ } else if (fix.kind === "replace-range") {
134
+ if (!Number.isInteger(fix.line) || !Number.isInteger(fix.column) || !Number.isInteger(fix.endColumn)) {
135
+ skippedCount += 1;
136
+ return;
137
+ }
138
+
139
+ const lines = before.split("\n");
140
+ const start = toLineOffset({ lines, line: fix.line, column: fix.column });
141
+ const end = toLineOffset({ lines, line: fix.endLine || fix.line, column: fix.endColumn });
142
+ if (end <= start || start < 0 || end > before.length) {
143
+ skippedCount += 1;
144
+ return;
145
+ }
146
+ after = `${before.slice(0, start)}${fix.replacement}${before.slice(end)}`;
147
+ } else {
148
+ skippedCount += 1;
149
+ return;
150
+ }
151
+
152
+ if (after === before) {
153
+ skippedCount += 1;
154
+ return;
155
+ }
156
+
157
+ state.content = after;
158
+ appliedCount += 1;
159
+ const patch = {
160
+ code: diagnostic.code,
161
+ filePath: fix.filePath,
162
+ line: fix.line,
163
+ description: fix.description,
164
+ confidence: fix.confidence,
165
+ };
166
+ if (includePatchText) {
167
+ patch.patch = buildUnifiedPatch({
168
+ filePath: fix.filePath,
169
+ before,
170
+ after,
171
+ });
172
+ }
173
+ patches.push(patch);
174
+ });
175
+
176
+ if (!dryRun) {
177
+ files.forEach((state, filePath) => {
178
+ if (state.content !== state.original) {
179
+ writeFileSync(filePath, state.content, "utf8");
180
+ }
181
+ });
182
+ }
183
+
184
+ return {
185
+ candidateCount,
186
+ appliedCount,
187
+ skippedCount,
188
+ dryRun,
189
+ patches,
190
+ };
191
+ };
@@ -0,0 +1,89 @@
1
+ const CODE_NAMESPACE_REGEX = /^RTGL-(CHECK|CONTRACT|CLI|IR)-[A-Z0-9-]+-\d{3}$/;
2
+
3
+ const DEFAULT_FAMILY_CONFIG = [
4
+ { prefix: "RTGL-CHECK-YAHTML-", family: "template", severity: "error", title: "YAHTML template diagnostic" },
5
+ { prefix: "RTGL-CHECK-JEMPL-", family: "template", severity: "error", title: "Jempl template diagnostic" },
6
+ { prefix: "RTGL-CHECK-EXPR-", family: "expression", severity: "error", title: "Expression diagnostic" },
7
+ { prefix: "RTGL-CHECK-SCHEMA-", family: "schema", severity: "error", title: "Schema diagnostic" },
8
+ { prefix: "RTGL-CHECK-SYMBOL-", family: "symbols", severity: "error", title: "Symbol resolution diagnostic" },
9
+ { prefix: "RTGL-CHECK-LISTENER-", family: "listeners", severity: "error", title: "Listener contract diagnostic" },
10
+ { prefix: "RTGL-CHECK-REF-", family: "refs", severity: "error", title: "Ref contract diagnostic" },
11
+ { prefix: "RTGL-CHECK-HANDLER-", family: "handlers", severity: "error", title: "Handler contract diagnostic" },
12
+ { prefix: "RTGL-CHECK-LIFECYCLE-", family: "lifecycle", severity: "error", title: "Lifecycle contract diagnostic" },
13
+ { prefix: "RTGL-CHECK-COMPAT-", family: "compatibility", severity: "error", title: "Compatibility diagnostic" },
14
+ { prefix: "RTGL-CHECK-METHOD-", family: "methods", severity: "error", title: "Method contract diagnostic" },
15
+ { prefix: "RTGL-CHECK-CONTRACT-", family: "contracts", severity: "error", title: "Contract diagnostic" },
16
+ { prefix: "RTGL-CONTRACT-", family: "contracts", severity: "error", title: "Legacy contract diagnostic" },
17
+ { prefix: "RTGL-CHECK-SEM-", family: "semantic", severity: "warn", title: "Semantic engine diagnostic" },
18
+ { prefix: "RTGL-CHECK-SEM-INV-", family: "semantic-invariants", severity: "error", title: "Semantic invariant diagnostic" },
19
+ { prefix: "RTGL-CHECK-CONSTANTS-", family: "constants", severity: "error", title: "Constants diagnostic" },
20
+ { prefix: "RTGL-CHECK-COMPONENT-", family: "component-identity", severity: "error", title: "Component identity diagnostic" },
21
+ { prefix: "RTGL-CHECK-READ-", family: "io", severity: "error", title: "Read/input diagnostic" },
22
+ { prefix: "RTGL-CHECK-PARSE-", family: "parse", severity: "error", title: "Parse diagnostic" },
23
+ { prefix: "RTGL-IR-VAL-", family: "ir-validation", severity: "error", title: "IR validation diagnostic" },
24
+ { prefix: "RTGL-IR-INV-", family: "ir-invariants", severity: "error", title: "IR invariant diagnostic" },
25
+ { prefix: "RTGL-CLI-", family: "cli", severity: "error", title: "CLI runtime diagnostic" },
26
+ ];
27
+
28
+ const CATALOG_VERSION = 1;
29
+
30
+ const CODE_OVERRIDES = {
31
+ "RTGL-CONTRACT-003": {
32
+ title: "Legacy dot-prop binding",
33
+ description: "Legacy '.prop=' template bindings are unsupported and should be converted to ':prop='.",
34
+ tags: ["autofix-safe", "template"],
35
+ },
36
+ "RTGL-CHECK-YAHTML-002": {
37
+ title: "Legacy YAHTML prop binding",
38
+ description: "Legacy '.prop' YAHTML binding is unsupported and should be converted to ':prop'.",
39
+ tags: ["autofix-safe", "template"],
40
+ },
41
+ "RTGL-CLI-001": {
42
+ title: "CLI runtime failure",
43
+ description: "CLI encountered invalid input or runtime execution failure.",
44
+ tags: ["cli"],
45
+ },
46
+ };
47
+
48
+ const findFamilyConfig = (code = "") => {
49
+ return DEFAULT_FAMILY_CONFIG.find((entry) => code.startsWith(entry.prefix)) || {
50
+ family: "general",
51
+ severity: "error",
52
+ title: "General diagnostic",
53
+ };
54
+ };
55
+
56
+ const codeToDocsAnchor = (code = "") => {
57
+ return `docs/diagnostics-reference.md#${code.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-")}`;
58
+ };
59
+
60
+ export const isValidDiagnosticCodeNamespace = (code = "") => {
61
+ return CODE_NAMESPACE_REGEX.test(String(code));
62
+ };
63
+
64
+ export const getDiagnosticCatalogEntry = (code = "") => {
65
+ const normalizedCode = String(code || "RTGL-CHECK-UNKNOWN");
66
+ const familyConfig = findFamilyConfig(normalizedCode);
67
+ const override = CODE_OVERRIDES[normalizedCode] || {};
68
+
69
+ return {
70
+ version: CATALOG_VERSION,
71
+ code: normalizedCode,
72
+ family: override.family || familyConfig.family,
73
+ title: override.title || familyConfig.title,
74
+ description: override.description || `${familyConfig.title}.`,
75
+ defaultSeverity: override.defaultSeverity || familyConfig.severity,
76
+ namespaceValid: isValidDiagnosticCodeNamespace(normalizedCode),
77
+ docsPath: override.docsPath || codeToDocsAnchor(normalizedCode),
78
+ tags: Array.isArray(override.tags) ? [...override.tags] : [familyConfig.family],
79
+ };
80
+ };
81
+
82
+ export const buildDiagnosticCatalog = (codes = []) => {
83
+ const unique = [...new Set((Array.isArray(codes) ? codes : []).map((code) => String(code || "")).filter(Boolean))]
84
+ .sort((left, right) => left.localeCompare(right));
85
+
86
+ return unique.map((code) => getDiagnosticCatalogEntry(code));
87
+ };
88
+
89
+ export const DIAGNOSTIC_CATALOG_VERSION = CATALOG_VERSION;
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { analyzeProject } from "./core/analyze.js";
2
+ export { runSemanticEngine } from "./semantic/engine.js";
@@ -0,0 +1,13 @@
1
+ import { formatJsonReport } from "./json.js";
2
+ import { formatSarifReport } from "./sarif.js";
3
+ import { formatTextReport } from "./text.js";
4
+
5
+ export const formatReport = ({ format = "text", result, warnAsError = false }) => {
6
+ if (format === "json") {
7
+ return formatJsonReport({ result, warnAsError });
8
+ }
9
+ if (format === "sarif") {
10
+ return formatSarifReport({ result });
11
+ }
12
+ return formatTextReport({ result, warnAsError });
13
+ };
@@ -0,0 +1,42 @@
1
+ import { buildDiagnosticCatalog, DIAGNOSTIC_CATALOG_VERSION } from "../diagnostics/catalog.js";
2
+
3
+ const compareDiagnostics = (left = {}, right = {}) => {
4
+ const leftLine = Number.isInteger(left.line) ? left.line : 0;
5
+ const rightLine = Number.isInteger(right.line) ? right.line : 0;
6
+ const leftColumn = Number.isInteger(left.column) ? left.column : 0;
7
+ const rightColumn = Number.isInteger(right.column) ? right.column : 0;
8
+
9
+ return (
10
+ String(left.code || "").localeCompare(String(right.code || ""))
11
+ || String(left.severity || "").localeCompare(String(right.severity || ""))
12
+ || String(left.filePath || "").localeCompare(String(right.filePath || ""))
13
+ || leftLine - rightLine
14
+ || leftColumn - rightColumn
15
+ || String(left.message || "").localeCompare(String(right.message || ""))
16
+ );
17
+ };
18
+
19
+ export const formatJsonReport = ({ result, warnAsError = false }) => {
20
+ const effectiveErrors = result.summary.bySeverity.error
21
+ + (warnAsError ? result.summary.bySeverity.warn : 0);
22
+ const diagnostics = Array.isArray(result.diagnostics)
23
+ ? [...result.diagnostics].sort(compareDiagnostics)
24
+ : [];
25
+ const catalog = buildDiagnosticCatalog(diagnostics.map((diagnostic) => diagnostic.code));
26
+
27
+ return JSON.stringify({
28
+ $schema: "docs/diagnostics-json-schema-v1.json",
29
+ schemaVersion: 1,
30
+ contractVersion: 1,
31
+ reportFormat: "json",
32
+ diagnosticCatalogVersion: DIAGNOSTIC_CATALOG_VERSION,
33
+ ok: effectiveErrors === 0,
34
+ componentCount: result.componentCount,
35
+ registryTagCount: result.registryTagCount,
36
+ summary: result.summary,
37
+ warnAsError,
38
+ diagnosticCatalog: catalog,
39
+ autofix: result.autofix || undefined,
40
+ diagnostics,
41
+ }, null, 2);
42
+ };