@kernlang/review 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/concept-rules/boundary-mutation.d.ts +13 -0
  2. package/dist/concept-rules/boundary-mutation.js +40 -0
  3. package/dist/concept-rules/boundary-mutation.js.map +1 -0
  4. package/dist/concept-rules/ignored-error.d.ts +13 -0
  5. package/dist/concept-rules/ignored-error.js +40 -0
  6. package/dist/concept-rules/ignored-error.js.map +1 -0
  7. package/dist/concept-rules/illegal-dependency.d.ts +13 -0
  8. package/dist/concept-rules/illegal-dependency.js +49 -0
  9. package/dist/concept-rules/illegal-dependency.js.map +1 -0
  10. package/dist/concept-rules/index.d.ts +15 -0
  11. package/dist/concept-rules/index.js +27 -0
  12. package/dist/concept-rules/index.js.map +1 -0
  13. package/dist/concept-rules/unguarded-effect.d.ts +13 -0
  14. package/dist/concept-rules/unguarded-effect.js +58 -0
  15. package/dist/concept-rules/unguarded-effect.js.map +1 -0
  16. package/dist/concept-rules/unrecovered-effect.d.ts +13 -0
  17. package/dist/concept-rules/unrecovered-effect.js +61 -0
  18. package/dist/concept-rules/unrecovered-effect.js.map +1 -0
  19. package/dist/confidence.d.ts +92 -0
  20. package/dist/confidence.js +263 -0
  21. package/dist/confidence.js.map +1 -0
  22. package/dist/differ.js +4 -2
  23. package/dist/differ.js.map +1 -1
  24. package/dist/external-tools.js +7 -3
  25. package/dist/external-tools.js.map +1 -1
  26. package/dist/file-role.d.ts +10 -0
  27. package/dist/file-role.js +80 -0
  28. package/dist/file-role.js.map +1 -0
  29. package/dist/graph.d.ts +11 -0
  30. package/dist/graph.js +152 -0
  31. package/dist/graph.js.map +1 -0
  32. package/dist/index.d.ts +46 -3
  33. package/dist/index.js +313 -27
  34. package/dist/index.js.map +1 -1
  35. package/dist/inferrer.js +123 -25
  36. package/dist/inferrer.js.map +1 -1
  37. package/dist/kern-lint.d.ts +18 -0
  38. package/dist/kern-lint.js +24 -0
  39. package/dist/kern-lint.js.map +1 -0
  40. package/dist/llm-bridge.d.ts +42 -0
  41. package/dist/llm-bridge.js +176 -0
  42. package/dist/llm-bridge.js.map +1 -0
  43. package/dist/llm-review.d.ts +8 -1
  44. package/dist/llm-review.js +20 -7
  45. package/dist/llm-review.js.map +1 -1
  46. package/dist/mappers/ts-concepts.d.ts +9 -0
  47. package/dist/mappers/ts-concepts.js +512 -0
  48. package/dist/mappers/ts-concepts.js.map +1 -0
  49. package/dist/quality-rules.d.ts +3 -3
  50. package/dist/quality-rules.js +3 -11
  51. package/dist/quality-rules.js.map +1 -1
  52. package/dist/reporter.d.ts +19 -3
  53. package/dist/reporter.js +232 -20
  54. package/dist/reporter.js.map +1 -1
  55. package/dist/rules/base.js +164 -15
  56. package/dist/rules/base.js.map +1 -1
  57. package/dist/rules/confidence.d.ts +37 -0
  58. package/dist/rules/confidence.js +159 -0
  59. package/dist/rules/confidence.js.map +1 -0
  60. package/dist/rules/dead-logic.d.ts +13 -0
  61. package/dist/rules/dead-logic.js +386 -0
  62. package/dist/rules/dead-logic.js.map +1 -0
  63. package/dist/rules/express.js +69 -2
  64. package/dist/rules/express.js.map +1 -1
  65. package/dist/rules/ground-layer.d.ts +23 -0
  66. package/dist/rules/ground-layer.js +132 -0
  67. package/dist/rules/ground-layer.js.map +1 -0
  68. package/dist/rules/index.d.ts +1 -1
  69. package/dist/rules/index.js +8 -2
  70. package/dist/rules/index.js.map +1 -1
  71. package/dist/rules/kern-source.d.ts +16 -0
  72. package/dist/rules/kern-source.js +726 -0
  73. package/dist/rules/kern-source.js.map +1 -0
  74. package/dist/rules/nextjs.js +38 -10
  75. package/dist/rules/nextjs.js.map +1 -1
  76. package/dist/rules/null-safety.d.ts +12 -0
  77. package/dist/rules/null-safety.js +121 -0
  78. package/dist/rules/null-safety.js.map +1 -0
  79. package/dist/rules/react.js +64 -1
  80. package/dist/rules/react.js.map +1 -1
  81. package/dist/rules/security-v2.d.ts +12 -0
  82. package/dist/rules/security-v2.js +415 -0
  83. package/dist/rules/security-v2.js.map +1 -0
  84. package/dist/rules/security-v3.d.ts +12 -0
  85. package/dist/rules/security-v3.js +397 -0
  86. package/dist/rules/security-v3.js.map +1 -0
  87. package/dist/rules/security-v4.d.ts +22 -0
  88. package/dist/rules/security-v4.js +688 -0
  89. package/dist/rules/security-v4.js.map +1 -0
  90. package/dist/rules/security.d.ts +12 -0
  91. package/dist/rules/security.js +286 -0
  92. package/dist/rules/security.js.map +1 -0
  93. package/dist/rules/utils.d.ts +7 -0
  94. package/dist/rules/utils.js +21 -0
  95. package/dist/rules/utils.js.map +1 -0
  96. package/dist/rules/vue.js +1 -1
  97. package/dist/rules/vue.js.map +1 -1
  98. package/dist/spec-checker.d.ts +83 -0
  99. package/dist/spec-checker.js +405 -0
  100. package/dist/spec-checker.js.map +1 -0
  101. package/dist/suppression/apply-suppression.d.ts +17 -0
  102. package/dist/suppression/apply-suppression.js +94 -0
  103. package/dist/suppression/apply-suppression.js.map +1 -0
  104. package/dist/suppression/index.d.ts +6 -0
  105. package/dist/suppression/index.js +6 -0
  106. package/dist/suppression/index.js.map +1 -0
  107. package/dist/suppression/parse-directives.d.ts +25 -0
  108. package/dist/suppression/parse-directives.js +161 -0
  109. package/dist/suppression/parse-directives.js.map +1 -0
  110. package/dist/suppression/types.d.ts +32 -0
  111. package/dist/suppression/types.js +5 -0
  112. package/dist/suppression/types.js.map +1 -0
  113. package/dist/taint.d.ts +115 -0
  114. package/dist/taint.js +1052 -0
  115. package/dist/taint.js.map +1 -0
  116. package/dist/types.d.ts +71 -0
  117. package/dist/types.js.map +1 -1
  118. package/package.json +6 -3
@@ -0,0 +1,726 @@
1
+ /**
2
+ * .kern source review rules that operate on flattened IRNode[] plus file path.
3
+ *
4
+ * These are distinct from ground-layer lint rules because they need file-aware
5
+ * spans and scope-sensitive analysis over handler/expr bodies.
6
+ */
7
+ import { countTokens } from '@kernlang/core';
8
+ import { SyntaxKind } from 'ts-morph';
9
+ import { createInMemoryProject } from '../inferrer.js';
10
+ import { createFingerprint } from '../types.js';
11
+ const SCOPE_NODE_TYPES = new Set([
12
+ 'screen', 'hook', 'provider', 'fn', 'callback', 'memo', 'effect',
13
+ 'route', 'server', 'service', 'method', 'singleton', 'constructor',
14
+ 'middleware', 'cli', 'command', 'test', 'describe', 'it',
15
+ // Template node types + CLI
16
+ 'arrow-fn', 'swr-hook', 'zustand-store', 'zustand-selector', 'module',
17
+ 'cli',
18
+ ]);
19
+ const DIRECT_BINDING_NODE_TYPES = new Set([
20
+ 'state', 'const', 'fn', 'derive', 'import', 'ref', 'context',
21
+ 'callback', 'memo', 'effect', 'middleware', 'hook', 'method',
22
+ 'arg', 'flag',
23
+ // Template node types that declare a name binding
24
+ 'arrow-fn', 'swr-hook', 'zustand-store', 'zustand-selector',
25
+ ]);
26
+ const TYPE_POSITION_KINDS = new Set([
27
+ SyntaxKind.TypeReference,
28
+ SyntaxKind.ExpressionWithTypeArguments,
29
+ SyntaxKind.TypeAliasDeclaration,
30
+ SyntaxKind.InterfaceDeclaration,
31
+ SyntaxKind.TypeParameter,
32
+ SyntaxKind.TypeLiteral,
33
+ SyntaxKind.UnionType,
34
+ SyntaxKind.IntersectionType,
35
+ SyntaxKind.LiteralType,
36
+ SyntaxKind.ArrayType,
37
+ SyntaxKind.TupleType,
38
+ SyntaxKind.TypeOperator,
39
+ SyntaxKind.ParenthesizedType,
40
+ SyntaxKind.FunctionType,
41
+ SyntaxKind.ConstructorType,
42
+ SyntaxKind.ConditionalType,
43
+ SyntaxKind.IndexedAccessType,
44
+ SyntaxKind.MappedType,
45
+ SyntaxKind.ImportType,
46
+ SyntaxKind.TypePredicate,
47
+ SyntaxKind.ThisType,
48
+ SyntaxKind.QualifiedName,
49
+ SyntaxKind.HeritageClause,
50
+ ]);
51
+ const SAFE_STRING_MEMBERS = new Set([
52
+ 'at', 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'endsWith',
53
+ 'includes', 'indexOf', 'lastIndexOf', 'length', 'localeCompare',
54
+ 'match', 'matchAll', 'normalize', 'padEnd', 'padStart', 'repeat',
55
+ 'replace', 'replaceAll', 'search', 'slice', 'split', 'startsWith',
56
+ 'substring', 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase',
57
+ 'toString', 'toUpperCase', 'trim', 'trimEnd', 'trimStart', 'valueOf',
58
+ ]);
59
+ const AMBIENT_NAMES = new Set([
60
+ // JS globals
61
+ 'Array', 'AbortController', 'Boolean', 'Buffer', 'Date', 'Error', 'Headers',
62
+ 'JSON', 'Map', 'Math', 'Number', 'Object', 'Promise', 'RegExp', 'Request',
63
+ 'Response', 'Set', 'String', 'URL', 'URLSearchParams', 'console', 'crypto',
64
+ 'clearInterval', 'clearTimeout', 'document', 'fetch', 'globalThis', 'location',
65
+ 'navigator', 'process', 'setInterval', 'setTimeout', 'window',
66
+ 'undefined', 'NaN', 'Infinity', 'Symbol', 'WeakMap', 'WeakSet', 'Proxy',
67
+ 'Reflect', 'queueMicrotask', 'structuredClone', 'atob', 'btoa',
68
+ // HTTP handler context (injected by framework)
69
+ 'req', 'res', 'ctx', 'next', 'params', 'query', 'body', 'headers',
70
+ 'event', 'env', 'emit', 'send', 'status',
71
+ // State/dispatch (React/KERN runtime)
72
+ 'state', 'dispatch', 'get', 'set', 'props', 'self', 'this',
73
+ // Common external service references (injected via DI or app context)
74
+ 'db', 'redis', 'cache', 'store', 'config', 'logger', 'app',
75
+ // CLI context (common in KERN CLI examples)
76
+ 'opts', 'args', 'options', 'argv',
77
+ // SWR/hook context (data comes from framework)
78
+ 'data', 'error', 'isLoading', 'mutate', 'isValidating',
79
+ ]);
80
+ const EXTERNAL_SIGNAL_RE = /\b(fetch|axios|db|redis|stripe|openai|supabase|client|api|provider|registry|http|https)\b|await\s+[A-Za-z_$]/;
81
+ const UNCERTAIN_SIGNAL_RE = /\b(guess|heuristic|fallback|bestEffort|approx|uncertain|unknown|maybe)\b|Math\.random|Date\.now/;
82
+ function props(node) {
83
+ return node.props || {};
84
+ }
85
+ function loc(node) {
86
+ return { line: node.loc?.line || 1, col: node.loc?.col || 1 };
87
+ }
88
+ function finding(ruleId, severity, category, message, filePath, node, extra) {
89
+ const { line, col } = loc(node);
90
+ return {
91
+ source: 'kern',
92
+ ruleId,
93
+ severity,
94
+ category,
95
+ message,
96
+ primarySpan: { file: filePath, startLine: line, startCol: col, endLine: line, endCol: col },
97
+ fingerprint: createFingerprint(ruleId, line, col),
98
+ ...extra,
99
+ };
100
+ }
101
+ function capitalize(text) {
102
+ return text.charAt(0).toUpperCase() + text.slice(1);
103
+ }
104
+ function buildParentMap(nodes) {
105
+ const parentMap = new Map();
106
+ for (const node of nodes) {
107
+ if (!parentMap.has(node))
108
+ parentMap.set(node, undefined);
109
+ }
110
+ for (const node of nodes) {
111
+ for (const child of node.children || []) {
112
+ if (parentMap.has(child))
113
+ parentMap.set(child, node);
114
+ }
115
+ }
116
+ return parentMap;
117
+ }
118
+ function isScopeNode(node) {
119
+ return SCOPE_NODE_TYPES.has(node.type);
120
+ }
121
+ function splitTopLevel(text, separator) {
122
+ const parts = [];
123
+ let current = '';
124
+ let depth = 0;
125
+ let inQuote = false;
126
+ for (let i = 0; i < text.length; i++) {
127
+ const ch = text[i];
128
+ if (ch === '"' || ch === '\'') {
129
+ inQuote = !inQuote;
130
+ current += ch;
131
+ continue;
132
+ }
133
+ if (!inQuote && (ch === '<' || ch === '(' || ch === '{' || ch === '[')) {
134
+ depth++;
135
+ }
136
+ else if (!inQuote && depth > 0 && (ch === '>' || ch === ')' || ch === '}' || ch === ']')) {
137
+ depth--;
138
+ }
139
+ if (!inQuote && depth === 0 && ch === separator) {
140
+ if (current.trim())
141
+ parts.push(current.trim());
142
+ current = '';
143
+ continue;
144
+ }
145
+ current += ch;
146
+ }
147
+ if (current.trim())
148
+ parts.push(current.trim());
149
+ return parts;
150
+ }
151
+ function findTopLevelChar(text, wanted) {
152
+ let depth = 0;
153
+ let inQuote = false;
154
+ for (let i = 0; i < text.length; i++) {
155
+ const ch = text[i];
156
+ if (ch === '"' || ch === '\'') {
157
+ inQuote = !inQuote;
158
+ continue;
159
+ }
160
+ if (!inQuote && (ch === '<' || ch === '(' || ch === '{' || ch === '['))
161
+ depth++;
162
+ else if (!inQuote && depth > 0 && (ch === '>' || ch === ')' || ch === '}' || ch === ']'))
163
+ depth--;
164
+ else if (!inQuote && depth === 0 && ch === wanted)
165
+ return i;
166
+ }
167
+ return -1;
168
+ }
169
+ function parseParamBindings(raw) {
170
+ if (typeof raw !== 'string' || raw.trim() === '')
171
+ return [];
172
+ const bindings = [];
173
+ for (const part of splitTopLevel(raw, ',')) {
174
+ const colonIdx = findTopLevelChar(part, ':');
175
+ const eqIdx = findTopLevelChar(part, '=');
176
+ const rawName = colonIdx >= 0 ? part.slice(0, colonIdx).trim() : (eqIdx >= 0 ? part.slice(0, eqIdx).trim() : part.trim());
177
+ if (!/^[A-Za-z_$][\w$]*$/.test(rawName))
178
+ continue;
179
+ let typeName;
180
+ if (colonIdx >= 0) {
181
+ const rawType = (eqIdx >= 0 ? part.slice(colonIdx + 1, eqIdx) : part.slice(colonIdx + 1)).trim();
182
+ const directAlias = getDirectTypeAlias(rawType);
183
+ if (directAlias)
184
+ typeName = directAlias;
185
+ }
186
+ bindings.push({ name: rawName, typeName });
187
+ }
188
+ return bindings;
189
+ }
190
+ function getDirectTypeAlias(typeText) {
191
+ if (!typeText)
192
+ return undefined;
193
+ const compact = typeText.replace(/\s+/g, '');
194
+ if (!compact)
195
+ return undefined;
196
+ if (compact.includes('<') || compact.includes('[') || compact.includes('{') || compact.includes('&'))
197
+ return undefined;
198
+ const parts = compact.split('|').filter(part => part !== 'null' && part !== 'undefined');
199
+ if (parts.length !== 1)
200
+ return undefined;
201
+ return /^[A-Za-z_$][\w$]*$/.test(parts[0]) ? parts[0] : undefined;
202
+ }
203
+ function getUnionLiteralKind(variants) {
204
+ const trimmed = variants.map(variant => variant.trim()).filter(Boolean);
205
+ if (trimmed.length === 0)
206
+ return 'mixed';
207
+ if (trimmed.every(variant => variant === 'true' || variant === 'false'))
208
+ return 'boolean';
209
+ if (trimmed.every(variant => /^-?\d+(?:\.\d+)?$/.test(variant)))
210
+ return 'number';
211
+ if (trimmed.every(variant => /^".*"$/.test(variant) || /^'.*'$/.test(variant) || /^[A-Za-z_][A-Za-z0-9_-]*$/.test(variant))) {
212
+ return 'string';
213
+ }
214
+ return 'mixed';
215
+ }
216
+ function buildUnionAliasMap(nodes) {
217
+ const unions = new Map();
218
+ for (const node of nodes) {
219
+ if (node.type !== 'type')
220
+ continue;
221
+ const p = props(node);
222
+ if (typeof p.name !== 'string' || typeof p.values !== 'string')
223
+ continue;
224
+ const variants = p.values.split('|').map(part => part.trim()).filter(Boolean);
225
+ if (variants.length < 2)
226
+ continue;
227
+ unions.set(p.name, {
228
+ node,
229
+ variants,
230
+ literalKind: getUnionLiteralKind(variants),
231
+ });
232
+ }
233
+ return unions;
234
+ }
235
+ function addBinding(target, name, info) {
236
+ if (!name || target.has(name))
237
+ return;
238
+ target.set(name, info);
239
+ }
240
+ function addBindingsFromScopeNode(scopeNode, target) {
241
+ const p = props(scopeNode);
242
+ for (const binding of parseParamBindings(p.params)) {
243
+ addBinding(target, binding.name, { kind: 'param', node: scopeNode, typeName: binding.typeName });
244
+ }
245
+ if (DIRECT_BINDING_NODE_TYPES.has(scopeNode.type) && typeof p.name === 'string') {
246
+ addBinding(target, p.name, {
247
+ kind: scopeNode.type,
248
+ node: scopeNode,
249
+ typeName: typeof p.type === 'string' ? getDirectTypeAlias(p.type) : undefined,
250
+ });
251
+ if (scopeNode.type === 'state') {
252
+ addBinding(target, `set${capitalize(p.name)}`, { kind: 'state-setter', node: scopeNode });
253
+ }
254
+ }
255
+ for (const child of scopeNode.children || []) {
256
+ const cp = props(child);
257
+ if (child.type === 'params' && Array.isArray(cp.items)) {
258
+ for (const item of cp.items) {
259
+ if (typeof item.name !== 'string')
260
+ continue;
261
+ addBinding(target, item.name, {
262
+ kind: 'param',
263
+ node: child,
264
+ typeName: typeof item.type === 'string' ? getDirectTypeAlias(item.type) : undefined,
265
+ });
266
+ }
267
+ continue;
268
+ }
269
+ if (!DIRECT_BINDING_NODE_TYPES.has(child.type))
270
+ continue;
271
+ // Import nodes use 'names' (comma-separated) instead of 'name'
272
+ if (child.type === 'import' && typeof cp.names === 'string') {
273
+ for (const importedName of cp.names.split(',').map((s) => s.trim()).filter(Boolean)) {
274
+ addBinding(target, importedName, { kind: 'import', node: child });
275
+ }
276
+ continue;
277
+ }
278
+ if (typeof cp.name !== 'string')
279
+ continue;
280
+ addBinding(target, cp.name, {
281
+ kind: child.type,
282
+ node: child,
283
+ typeName: typeof cp.type === 'string' ? getDirectTypeAlias(cp.type) : undefined,
284
+ });
285
+ if (child.type === 'state') {
286
+ addBinding(target, `set${capitalize(cp.name)}`, { kind: 'state-setter', node: child });
287
+ }
288
+ }
289
+ }
290
+ function collectVisibleBindings(node, parentMap) {
291
+ const bindings = new Map();
292
+ let current = parentMap.get(node);
293
+ while (current) {
294
+ if (isScopeNode(current) || parentMap.get(current) === undefined) {
295
+ addBindingsFromScopeNode(current, bindings);
296
+ }
297
+ current = parentMap.get(current);
298
+ }
299
+ return bindings;
300
+ }
301
+ function createSnippetAnalysis(project, code, key, mode) {
302
+ const filePath = `__kern_${key.replace(/[^A-Za-z0-9_]/g, '_')}_${Math.random().toString(36).slice(2)}.tsx`;
303
+ const wrapped = mode === 'expr'
304
+ ? `async function __kern__() { return (${code}); }\n`
305
+ : `async function __kern__() {\n${code}\n}\n`;
306
+ const sourceFile = project.createSourceFile(filePath, wrapped, { overwrite: true });
307
+ const localBindings = collectLocalBindings(sourceFile);
308
+ const referenceNames = new Set();
309
+ const propertyAccesses = [];
310
+ const elementAccesses = [];
311
+ const objectDestructures = [];
312
+ for (const identifier of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
313
+ const name = identifier.getText();
314
+ if (!/^[A-Za-z_$][\w$]*$/.test(name))
315
+ continue;
316
+ if (AMBIENT_NAMES.has(name))
317
+ continue;
318
+ if (isDeclarationIdentifier(identifier))
319
+ continue;
320
+ if (isTypeOnlyIdentifier(identifier))
321
+ continue;
322
+ if (isNonReferencePropertyName(identifier))
323
+ continue;
324
+ referenceNames.add(name);
325
+ }
326
+ for (const access of sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
327
+ const expr = access.getExpression();
328
+ if (expr.getKind() !== SyntaxKind.Identifier)
329
+ continue;
330
+ const baseName = expr.getText();
331
+ if (!/^[A-Za-z_$][\w$]*$/.test(baseName))
332
+ continue;
333
+ propertyAccesses.push({ baseName, propertyName: access.getName() });
334
+ }
335
+ for (const access of sourceFile.getDescendantsOfKind(SyntaxKind.ElementAccessExpression)) {
336
+ const expr = access.getExpression();
337
+ if (expr.getKind() !== SyntaxKind.Identifier)
338
+ continue;
339
+ const baseName = expr.getText();
340
+ if (!/^[A-Za-z_$][\w$]*$/.test(baseName))
341
+ continue;
342
+ const arg = access.getArgumentExpression();
343
+ let propertyName;
344
+ if (arg && arg.getKind() === SyntaxKind.StringLiteral) {
345
+ propertyName = arg.getText().slice(1, -1);
346
+ }
347
+ elementAccesses.push({ baseName, propertyName });
348
+ }
349
+ for (const decl of sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
350
+ const nameNode = decl.getNameNode();
351
+ if (nameNode.getKind() !== SyntaxKind.ObjectBindingPattern)
352
+ continue;
353
+ const init = decl.getInitializer();
354
+ if (!init || init.getKind() !== SyntaxKind.Identifier)
355
+ continue;
356
+ objectDestructures.push({ sourceName: init.getText() });
357
+ }
358
+ sourceFile.forget();
359
+ return { localBindings, referenceNames, propertyAccesses, elementAccesses, objectDestructures };
360
+ }
361
+ function collectLocalBindings(sourceFile) {
362
+ const bindings = new Set();
363
+ for (const decl of sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
364
+ addBindingNamesFromNode(decl.getNameNode(), bindings);
365
+ }
366
+ for (const param of sourceFile.getDescendantsOfKind(SyntaxKind.Parameter)) {
367
+ addBindingNamesFromNode(param.getNameNode(), bindings);
368
+ }
369
+ for (const fn of sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration)) {
370
+ const name = fn.getName();
371
+ if (name)
372
+ bindings.add(name);
373
+ }
374
+ for (const cls of sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration)) {
375
+ const name = cls.getName();
376
+ if (name)
377
+ bindings.add(name);
378
+ }
379
+ for (const catchClause of sourceFile.getDescendantsOfKind(SyntaxKind.CatchClause)) {
380
+ const variable = catchClause.getVariableDeclaration();
381
+ if (variable)
382
+ addBindingNamesFromNode(variable.getNameNode(), bindings);
383
+ }
384
+ return bindings;
385
+ }
386
+ function addBindingNamesFromNode(node, bindings) {
387
+ if (node.getKind() === SyntaxKind.Identifier) {
388
+ bindings.add(node.getText());
389
+ return;
390
+ }
391
+ if (node.getKind() === SyntaxKind.ObjectBindingPattern || node.getKind() === SyntaxKind.ArrayBindingPattern) {
392
+ for (const descendant of node.getDescendantsOfKind(SyntaxKind.BindingElement)) {
393
+ addBindingNamesFromNode(descendant.getNameNode(), bindings);
394
+ }
395
+ }
396
+ }
397
+ function isDeclarationIdentifier(identifier) {
398
+ const parent = identifier.getParent();
399
+ if (!parent)
400
+ return false;
401
+ if (parent.getKind() === SyntaxKind.BindingElement && parent.getNameNode() === identifier) {
402
+ return true;
403
+ }
404
+ if (parent.getKind() === SyntaxKind.Parameter && parent.getNameNode() === identifier) {
405
+ return true;
406
+ }
407
+ if (parent.getKind() === SyntaxKind.VariableDeclaration && parent.getNameNode() === identifier) {
408
+ return true;
409
+ }
410
+ const parentAny = parent;
411
+ return parentAny.getNameNode?.() === identifier;
412
+ }
413
+ function isNonReferencePropertyName(identifier) {
414
+ const parent = identifier.getParent();
415
+ if (!parent)
416
+ return false;
417
+ switch (parent.getKind()) {
418
+ case SyntaxKind.PropertyAccessExpression:
419
+ return parent.getNameNode() === identifier;
420
+ case SyntaxKind.PropertyAssignment:
421
+ return parent.getNameNode() === identifier;
422
+ case SyntaxKind.PropertyDeclaration:
423
+ case SyntaxKind.PropertySignature:
424
+ case SyntaxKind.MethodDeclaration:
425
+ case SyntaxKind.GetAccessor:
426
+ case SyntaxKind.SetAccessor:
427
+ return parent.getNameNode() === identifier;
428
+ case SyntaxKind.BindingElement:
429
+ return parent.getPropertyNameNode() === identifier;
430
+ default:
431
+ return false;
432
+ }
433
+ }
434
+ function isTypeOnlyIdentifier(identifier) {
435
+ let current = identifier.getParent();
436
+ while (current) {
437
+ if (TYPE_POSITION_KINDS.has(current.getKind()))
438
+ return true;
439
+ if (current.getKind() === SyntaxKind.ExpressionStatement ||
440
+ current.getKind() === SyntaxKind.ReturnStatement ||
441
+ current.getKind() === SyntaxKind.VariableDeclaration ||
442
+ current.getKind() === SyntaxKind.CallExpression ||
443
+ current.getKind() === SyntaxKind.BinaryExpression ||
444
+ current.getKind() === SyntaxKind.IfStatement) {
445
+ return false;
446
+ }
447
+ current = current.getParent();
448
+ }
449
+ return false;
450
+ }
451
+ function getCodeProp(node, key = 'code') {
452
+ const value = props(node)[key];
453
+ return typeof value === 'string' && value.trim() !== '' ? value : undefined;
454
+ }
455
+ function getExprCode(node, key = 'expr') {
456
+ const value = props(node)[key];
457
+ if (!value || typeof value !== 'object')
458
+ return undefined;
459
+ const expr = value;
460
+ return typeof expr.code === 'string' && expr.code.trim() !== '' ? expr.code : undefined;
461
+ }
462
+ function walkSubtree(node, visit) {
463
+ visit(node);
464
+ for (const child of node.children || []) {
465
+ walkSubtree(child, visit);
466
+ }
467
+ }
468
+ function stringifyPropValue(value) {
469
+ if (value === null || value === undefined)
470
+ return '';
471
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
472
+ return String(value);
473
+ if (Array.isArray(value))
474
+ return value.map(stringifyPropValue).join(' ');
475
+ if (typeof value === 'object') {
476
+ const entries = Object.entries(value);
477
+ return entries.map(([key, nested]) => `${key} ${stringifyPropValue(nested)}`).join(' ');
478
+ }
479
+ return String(value);
480
+ }
481
+ function countStructuralTokens(node) {
482
+ const parts = [node.type];
483
+ for (const [key, value] of Object.entries(props(node))) {
484
+ if (key === 'code')
485
+ continue;
486
+ parts.push(key);
487
+ parts.push(stringifyPropValue(value));
488
+ }
489
+ return countTokens(parts.join(' '));
490
+ }
491
+ function describeNode(node, parentMap) {
492
+ const p = props(node);
493
+ if (typeof p.name === 'string' && p.name)
494
+ return p.name;
495
+ const parent = parentMap.get(node);
496
+ const parentName = parent ? props(parent).name : undefined;
497
+ if (typeof parentName === 'string' && parentName)
498
+ return `${parentName}.${node.type}`;
499
+ return `${node.type}@L${loc(node).line}`;
500
+ }
501
+ export const undefinedReference = (nodes, filePath) => {
502
+ const findings = [];
503
+ const parentMap = buildParentMap(nodes);
504
+ const project = createInMemoryProject();
505
+ for (const node of nodes) {
506
+ if (node.type !== 'handler')
507
+ continue;
508
+ const code = getCodeProp(node);
509
+ if (!code)
510
+ continue;
511
+ const visibleBindings = collectVisibleBindings(node, parentMap);
512
+ const analysis = createSnippetAnalysis(project, code, `undef_${loc(node).line}`, 'block');
513
+ const unresolved = [...analysis.referenceNames]
514
+ .filter(name => !analysis.localBindings.has(name))
515
+ .filter(name => !visibleBindings.has(name))
516
+ .filter(name => !AMBIENT_NAMES.has(name))
517
+ .sort();
518
+ if (unresolved.length === 0)
519
+ continue;
520
+ findings.push(finding('undefined-reference', 'error', 'bug', `Handler references name(s) that are not declared in visible KERN scope: ${unresolved.join(', ')}`, filePath, node, {
521
+ suggestion: 'Declare the value as state/const/fn/derive, add it as a param, or qualify it through an existing scoped object.',
522
+ }));
523
+ }
524
+ return findings;
525
+ };
526
+ export const typeModelMismatch = (nodes, filePath) => {
527
+ const findings = [];
528
+ const parentMap = buildParentMap(nodes);
529
+ const unionAliases = buildUnionAliasMap(nodes);
530
+ if (unionAliases.size === 0)
531
+ return findings;
532
+ const project = createInMemoryProject();
533
+ for (const node of nodes) {
534
+ if (node.type !== 'handler')
535
+ continue;
536
+ const code = getCodeProp(node);
537
+ if (!code)
538
+ continue;
539
+ const visibleBindings = collectVisibleBindings(node, parentMap);
540
+ const unionBindings = new Map();
541
+ for (const [name, binding] of visibleBindings) {
542
+ if (!binding.typeName)
543
+ continue;
544
+ const union = unionAliases.get(binding.typeName);
545
+ if (!union)
546
+ continue;
547
+ unionBindings.set(name, { alias: binding.typeName, union, sourceNode: binding.node });
548
+ }
549
+ if (unionBindings.size === 0)
550
+ continue;
551
+ const analysis = createSnippetAnalysis(project, code, `type_mismatch_${loc(node).line}`, 'block');
552
+ const mismatches = new Map();
553
+ for (const access of analysis.propertyAccesses) {
554
+ const binding = unionBindings.get(access.baseName);
555
+ if (!binding)
556
+ continue;
557
+ if (binding.union.literalKind === 'string' && SAFE_STRING_MEMBERS.has(access.propertyName))
558
+ continue;
559
+ const key = `${access.baseName}:${binding.alias}`;
560
+ if (!mismatches.has(key))
561
+ mismatches.set(key, new Set());
562
+ mismatches.get(key).add(access.propertyName);
563
+ }
564
+ for (const access of analysis.elementAccesses) {
565
+ const binding = unionBindings.get(access.baseName);
566
+ if (!binding)
567
+ continue;
568
+ const key = `${access.baseName}:${binding.alias}`;
569
+ if (!mismatches.has(key))
570
+ mismatches.set(key, new Set());
571
+ mismatches.get(key).add(access.propertyName || '[computed]');
572
+ }
573
+ for (const destructure of analysis.objectDestructures) {
574
+ const binding = unionBindings.get(destructure.sourceName);
575
+ if (!binding)
576
+ continue;
577
+ const key = `${destructure.sourceName}:${binding.alias}`;
578
+ if (!mismatches.has(key))
579
+ mismatches.set(key, new Set());
580
+ mismatches.get(key).add('{...}');
581
+ }
582
+ for (const [key, members] of mismatches) {
583
+ const [bindingName, alias] = key.split(':');
584
+ const union = unionAliases.get(alias);
585
+ const relatedInfo = union != null
586
+ ? {
587
+ relatedSpans: [{
588
+ file: filePath,
589
+ startLine: loc(union.node).line,
590
+ startCol: loc(union.node).col,
591
+ endLine: loc(union.node).line,
592
+ endCol: loc(union.node).col,
593
+ }],
594
+ suggestion: `Use '${bindingName}' as a literal value, or change '${alias}' to an interface/union with object variants if field access is intended.`,
595
+ }
596
+ : undefined;
597
+ findings.push(finding('type-model-mismatch', 'warning', 'type', `Literal-union type '${alias}' is used like an object in handler code: ${bindingName}.${[...members].join(', ')}`, filePath, node, relatedInfo));
598
+ }
599
+ }
600
+ return findings;
601
+ };
602
+ export const unusedState = (nodes, filePath) => {
603
+ const findings = [];
604
+ const parentMap = buildParentMap(nodes);
605
+ const project = createInMemoryProject();
606
+ for (const node of nodes) {
607
+ if (node.type !== 'state')
608
+ continue;
609
+ const p = props(node);
610
+ if (typeof p.name !== 'string' || !p.name)
611
+ continue;
612
+ const scopeRoot = parentMap.get(node);
613
+ if (!scopeRoot)
614
+ continue;
615
+ // Skip states inside machine nodes — they're used by transitions (from=/to= props)
616
+ if (scopeRoot.type === 'machine')
617
+ continue;
618
+ const stateName = p.name;
619
+ const setterName = `set${capitalize(stateName)}`;
620
+ let used = false;
621
+ walkSubtree(scopeRoot, current => {
622
+ if (used || current === node)
623
+ return;
624
+ // Check bind= attribute (e.g., input bind=query)
625
+ const cp = props(current);
626
+ if (typeof cp.bind === 'string' && cp.bind === stateName) {
627
+ used = true;
628
+ return;
629
+ }
630
+ // Check value/initial/expr props for direct state references (e.g., value={{ query }})
631
+ for (const val of Object.values(cp)) {
632
+ if (typeof val === 'string' && new RegExp(`\\b${stateName}\\b`).test(val)) {
633
+ used = true;
634
+ return;
635
+ }
636
+ }
637
+ const blockCode = current.type === 'handler' || current.type === 'logic' || current.type === 'body'
638
+ ? getCodeProp(current)
639
+ : undefined;
640
+ if (blockCode) {
641
+ const analysis = createSnippetAnalysis(project, blockCode, `state_${stateName}_${loc(current).line}`, 'block');
642
+ const readsState = analysis.referenceNames.has(stateName) && !analysis.localBindings.has(stateName);
643
+ const usesSetter = analysis.referenceNames.has(setterName) && !analysis.localBindings.has(setterName);
644
+ if (readsState || usesSetter)
645
+ used = true;
646
+ return;
647
+ }
648
+ const exprCode = current.type === 'guard' || current.type === 'derive'
649
+ ? getExprCode(current)
650
+ : undefined;
651
+ if (!exprCode)
652
+ return;
653
+ const analysis = createSnippetAnalysis(project, exprCode, `state_expr_${stateName}_${loc(current).line}`, 'expr');
654
+ const readsState = analysis.referenceNames.has(stateName) && !analysis.localBindings.has(stateName);
655
+ if (readsState)
656
+ used = true;
657
+ });
658
+ if (used)
659
+ continue;
660
+ findings.push(finding('unused-state', 'warning', 'structure', `State '${p.name}' is declared but never referenced in handlers, derives, guards, or logic within its scope`, filePath, node, {
661
+ suggestion: `Remove '${p.name}' or wire it into the surrounding KERN logic.`,
662
+ }));
663
+ }
664
+ return findings;
665
+ };
666
+ export const handlerHeavy = (nodes, filePath) => {
667
+ const handlers = nodes.filter(node => node.type === 'handler' && getCodeProp(node));
668
+ if (handlers.length === 0)
669
+ return [];
670
+ let handlerTokens = 0;
671
+ let structureTokens = 0;
672
+ for (const node of nodes) {
673
+ structureTokens += countStructuralTokens(node);
674
+ if (node.type === 'handler') {
675
+ const code = getCodeProp(node);
676
+ if (code)
677
+ handlerTokens += countTokens(code);
678
+ }
679
+ }
680
+ const totalTokens = handlerTokens + structureTokens;
681
+ if (totalTokens === 0)
682
+ return [];
683
+ const ratio = handlerTokens / totalTokens;
684
+ if (ratio <= 0.6)
685
+ return [];
686
+ const anchor = handlers[0];
687
+ return [finding('handler-heavy', 'warning', 'structure', `Embedded handler code accounts for ${(ratio * 100).toFixed(0)}% of file tokens (${handlerTokens}/${totalTokens}); this file is mostly inline JS with a thin KERN wrapper`, filePath, anchor, {
688
+ suggestion: 'Promote repeated handler logic into native KERN nodes such as derive, guard, collect, respond, or named fn blocks.',
689
+ })];
690
+ };
691
+ export const missingConfidence = (nodes, filePath) => {
692
+ if (nodes.some(node => props(node).confidence !== undefined))
693
+ return [];
694
+ if (nodes.length === 0)
695
+ return [];
696
+ const parentMap = buildParentMap(nodes);
697
+ const candidates = new Set();
698
+ for (const node of nodes) {
699
+ const code = getCodeProp(node) || getExprCode(node);
700
+ if (!code)
701
+ continue;
702
+ if (!EXTERNAL_SIGNAL_RE.test(code) && !UNCERTAIN_SIGNAL_RE.test(code))
703
+ continue;
704
+ candidates.add(describeNode(node.type === 'handler' ? (parentMap.get(node) || node) : node, parentMap));
705
+ if (candidates.size >= 3)
706
+ break;
707
+ }
708
+ const anchor = nodes[0];
709
+ const suffix = candidates.size > 0
710
+ ? ` Candidate nodes: ${[...candidates].join(', ')}.`
711
+ : '';
712
+ return [finding('missing-confidence', 'info', 'pattern', `No confidence annotations found in this .kern file. Add confidence props to external-service calls or uncertain logic.${suffix}`, filePath, anchor, {
713
+ suggestion: 'Annotate critical nodes with confidence=0.x, confidence=from:name, or confidence=min:a,b to make trust levels explicit.',
714
+ })];
715
+ };
716
+ export function lintKernSourceIR(nodes, filePath, rules = KERN_SOURCE_RULES) {
717
+ return rules.flatMap(rule => rule(nodes, filePath));
718
+ }
719
+ export const KERN_SOURCE_RULES = [
720
+ undefinedReference,
721
+ typeModelMismatch,
722
+ unusedState,
723
+ handlerHeavy,
724
+ missingConfidence,
725
+ ];
726
+ //# sourceMappingURL=kern-source.js.map