@kernlang/review 3.1.5 → 3.1.7

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 (156) hide show
  1. package/LICENSE +17 -0
  2. package/dist/cache.d.ts +1 -1
  3. package/dist/cache.js +5 -3
  4. package/dist/cache.js.map +1 -1
  5. package/dist/call-graph.d.ts +63 -0
  6. package/dist/call-graph.js +380 -0
  7. package/dist/call-graph.js.map +1 -0
  8. package/dist/concept-rules/boundary-mutation.d.ts +1 -1
  9. package/dist/concept-rules/boundary-mutation.js.map +1 -1
  10. package/dist/concept-rules/ignored-error.d.ts +1 -1
  11. package/dist/concept-rules/ignored-error.js.map +1 -1
  12. package/dist/concept-rules/illegal-dependency.d.ts +1 -1
  13. package/dist/concept-rules/illegal-dependency.js.map +1 -1
  14. package/dist/concept-rules/index.js +1 -6
  15. package/dist/concept-rules/index.js.map +1 -1
  16. package/dist/concept-rules/unguarded-effect.d.ts +1 -1
  17. package/dist/concept-rules/unguarded-effect.js.map +1 -1
  18. package/dist/concept-rules/unrecovered-effect.d.ts +1 -1
  19. package/dist/concept-rules/unrecovered-effect.js +2 -1
  20. package/dist/concept-rules/unrecovered-effect.js.map +1 -1
  21. package/dist/confidence.js +12 -8
  22. package/dist/confidence.js.map +1 -1
  23. package/dist/differ.js +3 -7
  24. package/dist/differ.js.map +1 -1
  25. package/dist/external-tools.js +5 -6
  26. package/dist/external-tools.js.map +1 -1
  27. package/dist/file-context.d.ts +21 -0
  28. package/dist/file-context.js +234 -0
  29. package/dist/file-context.js.map +1 -0
  30. package/dist/file-role.js +14 -7
  31. package/dist/file-role.js.map +1 -1
  32. package/dist/graph.d.ts +1 -1
  33. package/dist/graph.js +24 -16
  34. package/dist/graph.js.map +1 -1
  35. package/dist/index.d.ts +44 -35
  36. package/dist/index.js +221 -68
  37. package/dist/index.js.map +1 -1
  38. package/dist/inferrer.d.ts +8 -2
  39. package/dist/inferrer.js +80 -47
  40. package/dist/inferrer.js.map +1 -1
  41. package/dist/kern-lint.d.ts +3 -4
  42. package/dist/kern-lint.js +7 -5
  43. package/dist/kern-lint.js.map +1 -1
  44. package/dist/llm-bridge.d.ts +23 -7
  45. package/dist/llm-bridge.js +267 -31
  46. package/dist/llm-bridge.js.map +1 -1
  47. package/dist/llm-review.d.ts +16 -2
  48. package/dist/llm-review.js +240 -35
  49. package/dist/llm-review.js.map +1 -1
  50. package/dist/mappers/ts-concepts.d.ts +1 -1
  51. package/dist/mappers/ts-concepts.js +303 -32
  52. package/dist/mappers/ts-concepts.js.map +1 -1
  53. package/dist/norm-miner.d.ts +31 -0
  54. package/dist/norm-miner.js +119 -0
  55. package/dist/norm-miner.js.map +1 -0
  56. package/dist/obligations.d.ts +63 -0
  57. package/dist/obligations.js +158 -0
  58. package/dist/obligations.js.map +1 -0
  59. package/dist/quality-rules.d.ts +3 -3
  60. package/dist/quality-rules.js +4 -2
  61. package/dist/quality-rules.js.map +1 -1
  62. package/dist/reporter.d.ts +7 -2
  63. package/dist/reporter.js +82 -51
  64. package/dist/reporter.js.map +1 -1
  65. package/dist/rule-eval.d.ts +1 -2
  66. package/dist/rule-eval.js +5 -9
  67. package/dist/rule-eval.js.map +1 -1
  68. package/dist/rule-loader.js +16 -14
  69. package/dist/rule-loader.js.map +1 -1
  70. package/dist/rules/base.js +153 -69
  71. package/dist/rules/base.js.map +1 -1
  72. package/dist/rules/cli.d.ts +7 -0
  73. package/dist/rules/cli.js +99 -0
  74. package/dist/rules/cli.js.map +1 -0
  75. package/dist/rules/confidence.d.ts +1 -1
  76. package/dist/rules/confidence.js +5 -5
  77. package/dist/rules/confidence.js.map +1 -1
  78. package/dist/rules/dead-code.d.ts +10 -0
  79. package/dist/rules/dead-code.js +75 -0
  80. package/dist/rules/dead-code.js.map +1 -0
  81. package/dist/rules/dead-logic.js +35 -31
  82. package/dist/rules/dead-logic.js.map +1 -1
  83. package/dist/rules/express.d.ts +2 -1
  84. package/dist/rules/express.js +380 -126
  85. package/dist/rules/express.js.map +1 -1
  86. package/dist/rules/fastapi.d.ts +10 -0
  87. package/dist/rules/fastapi.js +183 -0
  88. package/dist/rules/fastapi.js.map +1 -0
  89. package/dist/rules/ground-layer.js +3 -3
  90. package/dist/rules/ground-layer.js.map +1 -1
  91. package/dist/rules/index.d.ts +5 -1
  92. package/dist/rules/index.js +602 -84
  93. package/dist/rules/index.js.map +1 -1
  94. package/dist/rules/ink.d.ts +8 -0
  95. package/dist/rules/ink.js +88 -0
  96. package/dist/rules/ink.js.map +1 -0
  97. package/dist/rules/kern-source.js +202 -63
  98. package/dist/rules/kern-source.js.map +1 -1
  99. package/dist/rules/nextjs.js +88 -33
  100. package/dist/rules/nextjs.js.map +1 -1
  101. package/dist/rules/null-safety.js +52 -26
  102. package/dist/rules/null-safety.js.map +1 -1
  103. package/dist/rules/nuxt.js +24 -29
  104. package/dist/rules/nuxt.js.map +1 -1
  105. package/dist/rules/react.js +355 -69
  106. package/dist/rules/react.js.map +1 -1
  107. package/dist/rules/security-v2.js +71 -57
  108. package/dist/rules/security-v2.js.map +1 -1
  109. package/dist/rules/security-v3.js.map +1 -1
  110. package/dist/rules/security-v4.js +54 -27
  111. package/dist/rules/security-v4.js.map +1 -1
  112. package/dist/rules/security.js +35 -5
  113. package/dist/rules/security.js.map +1 -1
  114. package/dist/rules/terminal.d.ts +8 -0
  115. package/dist/rules/terminal.js +139 -0
  116. package/dist/rules/terminal.js.map +1 -0
  117. package/dist/rules/vue.js +162 -107
  118. package/dist/rules/vue.js.map +1 -1
  119. package/dist/semantic-diff.d.ts +52 -0
  120. package/dist/semantic-diff.js +342 -0
  121. package/dist/semantic-diff.js.map +1 -0
  122. package/dist/spec-checker.js +11 -10
  123. package/dist/spec-checker.js.map +1 -1
  124. package/dist/suppression/apply-suppression.d.ts +2 -3
  125. package/dist/suppression/apply-suppression.js +3 -3
  126. package/dist/suppression/apply-suppression.js.map +1 -1
  127. package/dist/suppression/index.d.ts +2 -2
  128. package/dist/suppression/index.js +1 -1
  129. package/dist/suppression/index.js.map +1 -1
  130. package/dist/suppression/parse-directives.d.ts +1 -1
  131. package/dist/suppression/parse-directives.js +9 -4
  132. package/dist/suppression/parse-directives.js.map +1 -1
  133. package/dist/taint-ast.d.ts +20 -0
  134. package/dist/taint-ast.js +427 -0
  135. package/dist/taint-ast.js.map +1 -0
  136. package/dist/taint-crossfile.d.ts +28 -0
  137. package/dist/taint-crossfile.js +174 -0
  138. package/dist/taint-crossfile.js.map +1 -0
  139. package/dist/taint-findings.d.ts +17 -0
  140. package/dist/taint-findings.js +131 -0
  141. package/dist/taint-findings.js.map +1 -0
  142. package/dist/taint-regex.d.ts +61 -0
  143. package/dist/taint-regex.js +379 -0
  144. package/dist/taint-regex.js.map +1 -0
  145. package/dist/taint-types.d.ts +128 -0
  146. package/dist/taint-types.js +174 -0
  147. package/dist/taint-types.js.map +1 -0
  148. package/dist/taint.d.ts +13 -107
  149. package/dist/taint.js +16 -1067
  150. package/dist/taint.js.map +1 -1
  151. package/dist/template-detector.d.ts +2 -2
  152. package/dist/template-detector.js +11 -16
  153. package/dist/template-detector.js.map +1 -1
  154. package/dist/types.d.ts +35 -0
  155. package/dist/types.js.map +1 -1
  156. package/package.json +3 -3
@@ -3,142 +3,656 @@
3
3
  *
4
4
  * Layers:
5
5
  * [base] Always active — universal TS/KERN rules
6
- * [react] Active when target = nextjs | tailwind | web | native
6
+ * [react] Active when target = nextjs | tailwind | web | native | ink
7
+ * [ink] Active when target = ink (on top of react)
7
8
  * [vue] Active when target = vue | nuxt
8
9
  * [express] Active when target = express
10
+ * [cli] Active when target = cli
11
+ * [terminal] Active when target = terminal
12
+ * [fastapi] Active when target = fastapi (Python concept layer)
9
13
  * [nextjs] Active when target = nextjs (on top of react)
10
14
  * [nuxt] Active when target = nuxt (on top of vue)
11
15
  */
12
16
  import { baseRules } from './base.js';
17
+ import { cliRules } from './cli.js';
18
+ import { deadLogicRules } from './dead-logic.js';
19
+ import { expressRules } from './express.js';
20
+ import { fastapiRules } from './fastapi.js';
21
+ import { inkRules } from './ink.js';
22
+ import { nextjsRules } from './nextjs.js';
23
+ import { nullSafetyRules } from './null-safety.js';
24
+ import { nuxtRules } from './nuxt.js';
25
+ import { reactRules } from './react.js';
13
26
  import { securityRules } from './security.js';
14
27
  import { securityV2Rules } from './security-v2.js';
15
28
  import { securityV3Rules } from './security-v3.js';
16
29
  import { securityV4Rules } from './security-v4.js';
17
- import { deadLogicRules } from './dead-logic.js';
18
- import { reactRules } from './react.js';
30
+ import { terminalRules } from './terminal.js';
19
31
  import { vueRules } from './vue.js';
20
- import { nextjsRules } from './nextjs.js';
21
- import { nuxtRules } from './nuxt.js';
22
- import { expressRules } from './express.js';
23
- import { nullSafetyRules } from './null-safety.js';
24
- const REACT_TARGETS = new Set(['nextjs', 'tailwind', 'web', 'native']);
32
+ const REACT_TARGETS = new Set(['nextjs', 'tailwind', 'web', 'native', 'ink']);
25
33
  const VUE_TARGETS = new Set(['vue', 'nuxt']);
34
+ /** Backend targets — never load frontend-specific rules */
35
+ const BACKEND_TARGETS = new Set(['express', 'fastapi', 'mcp', 'cli', 'terminal']);
26
36
  /**
27
37
  * Get all active review rules for a given target.
28
38
  * Base + security + dead-logic + null-safety are always active; framework rules activate by target.
29
39
  */
30
40
  export function getActiveRules(target) {
31
- const rules = [...baseRules, ...securityRules, ...securityV2Rules, ...securityV3Rules, ...securityV4Rules, ...deadLogicRules, ...nullSafetyRules];
32
- if (target && REACT_TARGETS.has(target)) {
41
+ const rules = [
42
+ ...baseRules,
43
+ ...securityRules,
44
+ ...securityV2Rules,
45
+ ...securityV3Rules,
46
+ ...securityV4Rules,
47
+ ...deadLogicRules,
48
+ ...nullSafetyRules,
49
+ ];
50
+ // Backend targets never load frontend-specific rules
51
+ const isBackend = target ? BACKEND_TARGETS.has(target) : false;
52
+ if (!isBackend && target && REACT_TARGETS.has(target)) {
33
53
  rules.push(...reactRules);
34
54
  }
35
- if (target && VUE_TARGETS.has(target)) {
55
+ if (!isBackend && target && VUE_TARGETS.has(target)) {
36
56
  rules.push(...vueRules);
37
57
  }
38
- if (target === 'nextjs') {
58
+ if (!isBackend && target === 'nextjs') {
39
59
  rules.push(...nextjsRules);
40
60
  }
41
- if (target === 'nuxt') {
61
+ if (!isBackend && target === 'nuxt') {
42
62
  rules.push(...nuxtRules);
43
63
  }
44
64
  if (target === 'express') {
45
65
  rules.push(...expressRules);
46
66
  }
67
+ if (target === 'cli') {
68
+ rules.push(...cliRules);
69
+ }
70
+ if (target === 'terminal') {
71
+ rules.push(...terminalRules);
72
+ }
73
+ if (target === 'ink') {
74
+ rules.push(...inkRules);
75
+ }
76
+ if (target === 'fastapi') {
77
+ rules.push(...fastapiRules);
78
+ }
47
79
  return rules;
48
80
  }
49
81
  const REGISTRY = [
50
82
  // Base (always active)
51
- { id: 'floating-promise', layer: 'base', severity: 'error', description: 'Unresolved async operation — missing await/void/return' },
52
- { id: 'state-mutation', layer: 'base', severity: 'error', description: 'Illegal state mutation outside designated setter' },
83
+ {
84
+ id: 'floating-promise',
85
+ layer: 'base',
86
+ severity: 'error',
87
+ description: 'Unresolved async operation — missing await/void/return',
88
+ },
89
+ {
90
+ id: 'state-mutation',
91
+ layer: 'base',
92
+ severity: 'error',
93
+ description: 'Illegal state mutation outside designated setter',
94
+ },
53
95
  { id: 'empty-catch', layer: 'base', severity: 'warning', description: 'Catch block swallows exception silently' },
54
- { id: 'machine-gap', layer: 'base', severity: 'warning', description: 'Unreachable state or missing transition in state machine' },
55
- { id: 'config-default-mismatch', layer: 'base', severity: 'warning', description: 'Config schema default does not match type' },
56
- { id: 'event-map-mismatch', layer: 'base', severity: 'warning', description: 'Event handler type mismatch with event map' },
57
- { id: 'non-exhaustive-switch', layer: 'base', severity: 'warning', description: 'Switch/map missing cases for known variants' },
58
- { id: 'cognitive-complexity', layer: 'base', severity: 'warning', description: 'Function exceeds cognitive complexity threshold' },
59
- { id: 'template-available', layer: 'base', severity: 'info', description: 'Pattern matches a registered KERN template' },
60
- { id: 'handler-extraction', layer: 'base', severity: 'info', description: 'Handler-like pattern could be extracted to KERN' },
96
+ {
97
+ id: 'machine-gap',
98
+ layer: 'base',
99
+ severity: 'warning',
100
+ description: 'Unreachable state or missing transition in state machine',
101
+ },
102
+ {
103
+ id: 'config-default-mismatch',
104
+ layer: 'base',
105
+ severity: 'warning',
106
+ description: 'Config schema default does not match type',
107
+ },
108
+ {
109
+ id: 'event-map-mismatch',
110
+ layer: 'base',
111
+ severity: 'warning',
112
+ description: 'Event handler type mismatch with event map',
113
+ },
114
+ {
115
+ id: 'non-exhaustive-switch',
116
+ layer: 'base',
117
+ severity: 'warning',
118
+ description: 'Switch/map missing cases for known variants',
119
+ },
120
+ {
121
+ id: 'cognitive-complexity',
122
+ layer: 'base',
123
+ severity: 'warning',
124
+ description: 'Function exceeds cognitive complexity threshold',
125
+ },
126
+ {
127
+ id: 'template-available',
128
+ layer: 'base',
129
+ severity: 'info',
130
+ description: 'Pattern matches a registered KERN template',
131
+ },
132
+ {
133
+ id: 'handler-extraction',
134
+ layer: 'base',
135
+ severity: 'info',
136
+ description: 'Handler-like pattern could be extracted to KERN',
137
+ },
61
138
  { id: 'memory-leak', layer: 'base', severity: 'error', description: 'Event listener added without cleanup' },
62
139
  { id: 'unhandled-async', layer: 'base', severity: 'warning', description: 'Async function without error handling' },
63
- { id: 'sync-in-async', layer: 'base', severity: 'warning', description: 'Synchronous blocking call inside async function' },
64
- { id: 'bare-rethrow', layer: 'base', severity: 'warning', description: 'Catch rethrows error without adding context' },
140
+ {
141
+ id: 'sync-in-async',
142
+ layer: 'base',
143
+ severity: 'warning',
144
+ description: 'Synchronous blocking call inside async function',
145
+ },
146
+ {
147
+ id: 'bare-rethrow',
148
+ layer: 'base',
149
+ severity: 'warning',
150
+ description: 'Catch rethrows error without adding context',
151
+ },
65
152
  // Security
66
- { id: 'xss-unsafe-html', layer: 'security', severity: 'error', description: 'innerHTML/dangerouslySetInnerHTML with untrusted data' },
67
- { id: 'hardcoded-secret', layer: 'security', severity: 'error', description: 'API key, password, or secret in source code' },
68
- { id: 'command-injection', layer: 'security', severity: 'error', description: 'exec/spawn with user-controlled input' },
153
+ {
154
+ id: 'xss-unsafe-html',
155
+ layer: 'security',
156
+ severity: 'error',
157
+ description: 'innerHTML/dangerouslySetInnerHTML with untrusted data',
158
+ },
159
+ {
160
+ id: 'hardcoded-secret',
161
+ layer: 'security',
162
+ severity: 'error',
163
+ description: 'API key, password, or secret in source code',
164
+ },
165
+ {
166
+ id: 'command-injection',
167
+ layer: 'security',
168
+ severity: 'error',
169
+ description: 'exec/spawn with user-controlled input',
170
+ },
69
171
  { id: 'no-eval', layer: 'security', severity: 'error', description: 'eval() or Function() constructor usage' },
70
- { id: 'insecure-random', layer: 'security', severity: 'warning', description: 'Math.random() used for security-sensitive operations' },
172
+ {
173
+ id: 'insecure-random',
174
+ layer: 'security',
175
+ severity: 'warning',
176
+ description: 'Math.random() used for security-sensitive operations',
177
+ },
71
178
  { id: 'cors-wildcard', layer: 'security', severity: 'warning', description: 'CORS wildcard (*) origin allowed' },
72
- { id: 'helmet-missing', layer: 'security', severity: 'warning', description: 'Express app without helmet security headers' },
73
- { id: 'open-redirect', layer: 'security', severity: 'error', description: 'Unvalidated redirect target from user input' },
179
+ {
180
+ id: 'helmet-missing',
181
+ layer: 'security',
182
+ severity: 'warning',
183
+ description: 'Express app without helmet security headers',
184
+ },
185
+ {
186
+ id: 'open-redirect',
187
+ layer: 'security',
188
+ severity: 'error',
189
+ description: 'Unvalidated redirect target from user input',
190
+ },
74
191
  // Security v2
75
- { id: 'jwt-weak-verification', layer: 'security-v2', severity: 'warning', description: 'JWT verified without algorithm restriction' },
76
- { id: 'cookie-hardening', layer: 'security-v2', severity: 'error', description: 'Cookie missing secure/httpOnly/sameSite flags' },
77
- { id: 'csrf-detection', layer: 'security-v2', severity: 'error', description: 'State-changing endpoint without CSRF protection' },
78
- { id: 'csp-strength', layer: 'security-v2', severity: 'warning', description: 'Weak Content-Security-Policy headers' },
79
- { id: 'path-traversal', layer: 'security-v2', severity: 'error', description: 'File path from user input without sanitization' },
80
- { id: 'weak-password-hashing', layer: 'security-v2', severity: 'error', description: 'MD5/SHA1 for password hashing instead of bcrypt/argon2' },
192
+ {
193
+ id: 'jwt-weak-verification',
194
+ layer: 'security-v2',
195
+ severity: 'warning',
196
+ description: 'JWT verified without algorithm restriction',
197
+ },
198
+ {
199
+ id: 'cookie-hardening',
200
+ layer: 'security-v2',
201
+ severity: 'error',
202
+ description: 'Cookie missing secure/httpOnly/sameSite flags',
203
+ },
204
+ {
205
+ id: 'csrf-detection',
206
+ layer: 'security-v2',
207
+ severity: 'error',
208
+ description: 'State-changing endpoint without CSRF protection',
209
+ },
210
+ {
211
+ id: 'csp-strength',
212
+ layer: 'security-v2',
213
+ severity: 'warning',
214
+ description: 'Weak Content-Security-Policy headers',
215
+ },
216
+ {
217
+ id: 'path-traversal',
218
+ layer: 'security-v2',
219
+ severity: 'error',
220
+ description: 'File path from user input without sanitization',
221
+ },
222
+ {
223
+ id: 'weak-password-hashing',
224
+ layer: 'security-v2',
225
+ severity: 'error',
226
+ description: 'MD5/SHA1 for password hashing instead of bcrypt/argon2',
227
+ },
81
228
  // Security v3 — OWASP gap closure
82
- { id: 'regex-dos', layer: 'security-v3', severity: 'warning', description: 'Regex vulnerable to catastrophic backtracking (ReDoS)' },
83
- { id: 'missing-input-validation', layer: 'security-v3', severity: 'warning', description: 'User input used without validation' },
84
- { id: 'prototype-pollution', layer: 'security-v3', severity: 'error', description: 'Object.prototype mutation via user-controlled keys' },
85
- { id: 'information-exposure', layer: 'security-v3', severity: 'error', description: 'Stack traces or internal details in error responses' },
86
- { id: 'prompt-injection', layer: 'security-v3', severity: 'warning', description: 'User input concatenated into LLM prompts' },
229
+ {
230
+ id: 'regex-dos',
231
+ layer: 'security-v3',
232
+ severity: 'warning',
233
+ description: 'Regex vulnerable to catastrophic backtracking (ReDoS)',
234
+ },
235
+ {
236
+ id: 'missing-input-validation',
237
+ layer: 'security-v3',
238
+ severity: 'warning',
239
+ description: 'User input used without validation',
240
+ },
241
+ {
242
+ id: 'prototype-pollution',
243
+ layer: 'security-v3',
244
+ severity: 'error',
245
+ description: 'Object.prototype mutation via user-controlled keys',
246
+ },
247
+ {
248
+ id: 'information-exposure',
249
+ layer: 'security-v3',
250
+ severity: 'error',
251
+ description: 'Stack traces or internal details in error responses',
252
+ },
253
+ {
254
+ id: 'prompt-injection',
255
+ layer: 'security-v3',
256
+ severity: 'warning',
257
+ description: 'User input concatenated into LLM prompts',
258
+ },
87
259
  // Security v4 — LLM attack surface
88
- { id: 'indirect-prompt-injection', layer: 'security-v4', severity: 'warning', description: 'LLM prompt includes data from external/DB sources' },
89
- { id: 'llm-output-execution', layer: 'security-v4', severity: 'error', description: 'LLM-generated code executed without sandboxing' },
90
- { id: 'system-prompt-leakage', layer: 'security-v4', severity: 'warning', description: 'System prompt exposed in error paths or responses' },
91
- { id: 'rag-poisoning', layer: 'security-v4', severity: 'warning', description: 'RAG documents injected without provenance check' },
92
- { id: 'tool-calling-manipulation', layer: 'security-v4', severity: 'error', description: 'Tool/function call parameters from untrusted LLM output' },
93
- { id: 'encoding-bypass', layer: 'security-v4', severity: 'warning', description: 'Base64/unicode encoding used to bypass prompt filters' },
94
- { id: 'delimiter-injection', layer: 'security-v4', severity: 'warning', description: 'Prompt delimiter breakout via user input' },
95
- { id: 'unsanitized-history', layer: 'security-v4', severity: 'warning', description: 'Chat history concatenated without sanitization' },
96
- { id: 'json-output-manipulation', layer: 'security-v4', severity: 'warning', description: 'LLM JSON output used without schema validation' },
97
- { id: 'missing-output-validation', layer: 'security-v4', severity: 'warning', description: 'LLM output consumed without validation' },
260
+ {
261
+ id: 'indirect-prompt-injection',
262
+ layer: 'security-v4',
263
+ severity: 'warning',
264
+ description: 'LLM prompt includes data from external/DB sources',
265
+ },
266
+ {
267
+ id: 'llm-output-execution',
268
+ layer: 'security-v4',
269
+ severity: 'error',
270
+ description: 'LLM-generated code executed without sandboxing',
271
+ },
272
+ {
273
+ id: 'system-prompt-leakage',
274
+ layer: 'security-v4',
275
+ severity: 'warning',
276
+ description: 'System prompt exposed in error paths or responses',
277
+ },
278
+ {
279
+ id: 'rag-poisoning',
280
+ layer: 'security-v4',
281
+ severity: 'warning',
282
+ description: 'RAG documents injected without provenance check',
283
+ },
284
+ {
285
+ id: 'tool-calling-manipulation',
286
+ layer: 'security-v4',
287
+ severity: 'error',
288
+ description: 'Tool/function call parameters from untrusted LLM output',
289
+ },
290
+ {
291
+ id: 'encoding-bypass',
292
+ layer: 'security-v4',
293
+ severity: 'warning',
294
+ description: 'Base64/unicode encoding used to bypass prompt filters',
295
+ },
296
+ {
297
+ id: 'delimiter-injection',
298
+ layer: 'security-v4',
299
+ severity: 'warning',
300
+ description: 'Prompt delimiter breakout via user input',
301
+ },
302
+ {
303
+ id: 'unsanitized-history',
304
+ layer: 'security-v4',
305
+ severity: 'warning',
306
+ description: 'Chat history concatenated without sanitization',
307
+ },
308
+ {
309
+ id: 'json-output-manipulation',
310
+ layer: 'security-v4',
311
+ severity: 'warning',
312
+ description: 'LLM JSON output used without schema validation',
313
+ },
314
+ {
315
+ id: 'missing-output-validation',
316
+ layer: 'security-v4',
317
+ severity: 'warning',
318
+ description: 'LLM output consumed without validation',
319
+ },
98
320
  // Dead logic
99
- { id: 'identical-conditions', layer: 'dead-logic', severity: 'error', description: 'Duplicate conditions in if/else chain' },
100
- { id: 'identical-expressions', layer: 'dead-logic', severity: 'error', description: 'Same expression on both sides of operator' },
101
- { id: 'all-identical-branches', layer: 'dead-logic', severity: 'error', description: 'All branches produce identical code' },
102
- { id: 'constant-condition', layer: 'dead-logic', severity: 'warning', description: 'Condition is always true or always false' },
103
- { id: 'one-iteration-loop', layer: 'dead-logic', severity: 'warning', description: 'Loop body always exits on first iteration' },
104
- { id: 'unused-collection', layer: 'dead-logic', severity: 'warning', description: 'Collection created but never read' },
105
- { id: 'empty-collection-access', layer: 'dead-logic', severity: 'warning', description: 'Accessing elements of provably empty collection' },
106
- { id: 'redundant-jump', layer: 'dead-logic', severity: 'info', description: 'Unreachable code after return/break/continue' },
321
+ {
322
+ id: 'identical-conditions',
323
+ layer: 'dead-logic',
324
+ severity: 'error',
325
+ description: 'Duplicate conditions in if/else chain',
326
+ },
327
+ {
328
+ id: 'identical-expressions',
329
+ layer: 'dead-logic',
330
+ severity: 'error',
331
+ description: 'Same expression on both sides of operator',
332
+ },
333
+ {
334
+ id: 'all-identical-branches',
335
+ layer: 'dead-logic',
336
+ severity: 'error',
337
+ description: 'All branches produce identical code',
338
+ },
339
+ {
340
+ id: 'constant-condition',
341
+ layer: 'dead-logic',
342
+ severity: 'warning',
343
+ description: 'Condition is always true or always false',
344
+ },
345
+ {
346
+ id: 'one-iteration-loop',
347
+ layer: 'dead-logic',
348
+ severity: 'warning',
349
+ description: 'Loop body always exits on first iteration',
350
+ },
351
+ {
352
+ id: 'unused-collection',
353
+ layer: 'dead-logic',
354
+ severity: 'warning',
355
+ description: 'Collection created but never read',
356
+ },
357
+ {
358
+ id: 'empty-collection-access',
359
+ layer: 'dead-logic',
360
+ severity: 'warning',
361
+ description: 'Accessing elements of provably empty collection',
362
+ },
363
+ {
364
+ id: 'redundant-jump',
365
+ layer: 'dead-logic',
366
+ severity: 'info',
367
+ description: 'Unreachable code after return/break/continue',
368
+ },
107
369
  // Null safety
108
- { id: 'unchecked-find', layer: 'null-safety', severity: 'warning', description: 'array.find() result used without null check' },
109
- { id: 'optional-chain-bang', layer: 'null-safety', severity: 'warning', description: 'Optional chain (?) immediately negated by non-null assertion (!)' },
110
- { id: 'unchecked-cast', layer: 'null-safety', severity: 'warning', description: 'Unsafe type assertion without runtime guard' },
111
- // React (target: nextjs, tailwind, web, native)
370
+ {
371
+ id: 'unchecked-find',
372
+ layer: 'null-safety',
373
+ severity: 'warning',
374
+ description: 'array.find() result used without null check',
375
+ },
376
+ {
377
+ id: 'optional-chain-bang',
378
+ layer: 'null-safety',
379
+ severity: 'warning',
380
+ description: 'Optional chain (?) immediately negated by non-null assertion (!)',
381
+ },
382
+ {
383
+ id: 'unchecked-cast',
384
+ layer: 'null-safety',
385
+ severity: 'warning',
386
+ description: 'Unsafe type assertion without runtime guard',
387
+ },
388
+ // React (target: nextjs, tailwind, web, native, ink)
112
389
  { id: 'async-effect', layer: 'react', severity: 'error', description: 'Async function passed directly to useEffect' },
113
- { id: 'render-side-effect', layer: 'react', severity: 'error', description: 'Side effect (fetch, mutation) during render' },
114
- { id: 'unstable-key', layer: 'react', severity: 'warning', description: 'Non-stable key prop (index, random, Date.now)' },
390
+ {
391
+ id: 'render-side-effect',
392
+ layer: 'react',
393
+ severity: 'error',
394
+ description: 'Side effect (fetch, mutation) during render',
395
+ },
396
+ {
397
+ id: 'unstable-key',
398
+ layer: 'react',
399
+ severity: 'warning',
400
+ description: 'Non-stable key prop (index, random, Date.now)',
401
+ },
115
402
  { id: 'stale-closure', layer: 'react', severity: 'warning', description: 'Stale variable captured in hook closure' },
116
- { id: 'state-explosion', layer: 'react', severity: 'warning', description: 'Excessive useState calls — consider useReducer' },
403
+ {
404
+ id: 'state-explosion',
405
+ layer: 'react',
406
+ severity: 'warning',
407
+ description: 'Excessive useState calls — consider useReducer',
408
+ },
117
409
  { id: 'hook-order', layer: 'react', severity: 'error', description: 'React hook called inside condition or loop' },
118
- { id: 'effect-self-update-loop', layer: 'react', severity: 'error', description: 'useEffect updates its own dependency — infinite loop' },
410
+ {
411
+ id: 'effect-self-update-loop',
412
+ layer: 'react',
413
+ severity: 'error',
414
+ description: 'useEffect updates its own dependency — infinite loop',
415
+ },
416
+ // CLI (target: cli)
417
+ {
418
+ id: 'cli-missing-shebang',
419
+ layer: 'cli',
420
+ severity: 'warning',
421
+ description: 'Commander CLI entrypoint missing #!/usr/bin/env node',
422
+ },
423
+ {
424
+ id: 'cli-missing-parse',
425
+ layer: 'cli',
426
+ severity: 'error',
427
+ description: 'Command instance created without parse()/parseAsync()',
428
+ },
429
+ {
430
+ id: 'cli-async-parse-sync',
431
+ layer: 'cli',
432
+ severity: 'error',
433
+ description: 'Async Commander action paired with parse() instead of parseAsync()',
434
+ },
435
+ {
436
+ id: 'cli-process-exit-in-action',
437
+ layer: 'cli',
438
+ severity: 'warning',
439
+ description: 'Commander action handler calls process.exit() directly',
440
+ },
119
441
  // Vue (target: vue, nuxt)
120
- { id: 'missing-ref-value', layer: 'vue', severity: 'warning', description: 'ref() used without .value in script setup' },
121
- { id: 'missing-onUnmounted', layer: 'vue', severity: 'error', description: 'watch/addEventListener without onUnmounted cleanup' },
122
- { id: 'setup-side-effect', layer: 'vue', severity: 'warning', description: 'Top-level await in setup without onMounted' },
123
- { id: 'reactive-destructure', layer: 'vue', severity: 'warning', description: 'Destructuring reactive() loses reactivity' },
442
+ {
443
+ id: 'missing-ref-value',
444
+ layer: 'vue',
445
+ severity: 'warning',
446
+ description: 'ref() used without .value in script setup',
447
+ },
448
+ {
449
+ id: 'missing-onUnmounted',
450
+ layer: 'vue',
451
+ severity: 'error',
452
+ description: 'watch/addEventListener without onUnmounted cleanup',
453
+ },
454
+ {
455
+ id: 'setup-side-effect',
456
+ layer: 'vue',
457
+ severity: 'warning',
458
+ description: 'Top-level await in setup without onMounted',
459
+ },
460
+ {
461
+ id: 'reactive-destructure',
462
+ layer: 'vue',
463
+ severity: 'warning',
464
+ description: 'Destructuring reactive() loses reactivity',
465
+ },
466
+ // Terminal (target: terminal)
467
+ {
468
+ id: 'terminal-missing-tty-guard',
469
+ layer: 'terminal',
470
+ severity: 'warning',
471
+ description: 'Interactive terminal code runs without TTY guard',
472
+ },
473
+ {
474
+ id: 'terminal-raw-mode-no-restore',
475
+ layer: 'terminal',
476
+ severity: 'error',
477
+ description: 'stdin raw mode enabled without restore on exit',
478
+ },
479
+ {
480
+ id: 'terminal-readline-no-close',
481
+ layer: 'terminal',
482
+ severity: 'warning',
483
+ description: 'Readline interface never closed — process can hang',
484
+ },
485
+ {
486
+ id: 'terminal-alt-screen-no-restore',
487
+ layer: 'terminal',
488
+ severity: 'warning',
489
+ description: 'Alternate screen entered without restore on exit',
490
+ },
491
+ {
492
+ id: 'terminal-missing-signal-handler',
493
+ layer: 'terminal',
494
+ severity: 'warning',
495
+ description: 'No SIGINT/SIGTERM handler for cleanup',
496
+ },
497
+ {
498
+ id: 'terminal-cursor-not-restored',
499
+ layer: 'terminal',
500
+ severity: 'warning',
501
+ description: 'Cursor hidden without restore on exit',
502
+ },
503
+ {
504
+ id: 'terminal-unthrottled-render',
505
+ layer: 'terminal',
506
+ severity: 'warning',
507
+ description: 'Render loop with excessive refresh rate',
508
+ },
509
+ // Ink (target: ink, on top of React)
510
+ {
511
+ id: 'ink-console-output',
512
+ layer: 'ink',
513
+ severity: 'warning',
514
+ description: 'console.* output corrupts Ink terminal rendering',
515
+ },
516
+ {
517
+ id: 'ink-direct-stdout',
518
+ layer: 'ink',
519
+ severity: 'error',
520
+ description: 'Direct stdout/stderr writes bypass Ink renderer',
521
+ },
522
+ {
523
+ id: 'ink-process-exit',
524
+ layer: 'ink',
525
+ severity: 'warning',
526
+ description: 'process.exit() used instead of useApp().exit()',
527
+ },
528
+ {
529
+ id: 'ink-stdin-bypass',
530
+ layer: 'ink',
531
+ severity: 'warning',
532
+ description: 'Raw stdin/readline listeners bypass Ink useInput()',
533
+ },
534
+ {
535
+ id: 'ink-uncleared-interval',
536
+ layer: 'ink',
537
+ severity: 'warning',
538
+ description: 'setInterval without cleanup in Ink component',
539
+ },
540
+ {
541
+ id: 'ink-missing-error-boundary',
542
+ layer: 'ink',
543
+ severity: 'warning',
544
+ description: 'Ink render() without error handling',
545
+ },
124
546
  // Next.js (target: nextjs)
125
547
  { id: 'server-hook', layer: 'nextjs', severity: 'error', description: 'React hook used in Server Component' },
126
- { id: 'hydration-mismatch', layer: 'nextjs', severity: 'warning', description: 'Nondeterministic expression causes SSR/client mismatch' },
127
- { id: 'missing-use-client', layer: 'nextjs', severity: 'warning', description: 'Event handler in Server Component — needs use client' },
548
+ {
549
+ id: 'hydration-mismatch',
550
+ layer: 'nextjs',
551
+ severity: 'warning',
552
+ description: 'Nondeterministic expression causes SSR/client mismatch',
553
+ },
554
+ {
555
+ id: 'missing-use-client',
556
+ layer: 'nextjs',
557
+ severity: 'warning',
558
+ description: 'Event handler in Server Component — needs use client',
559
+ },
128
560
  // Nuxt (target: nuxt)
129
- { id: 'missing-ssr-guard', layer: 'nuxt', severity: 'error', description: 'Browser global accessed without SSR guard' },
130
- { id: 'nuxt-direct-fetch', layer: 'nuxt', severity: 'warning', description: 'Raw fetch() instead of $fetch/useFetch in Nuxt component' },
131
- { id: 'server-route-leak', layer: 'nuxt', severity: 'error', description: 'Server API route may expose sensitive fields' },
561
+ {
562
+ id: 'missing-ssr-guard',
563
+ layer: 'nuxt',
564
+ severity: 'error',
565
+ description: 'Browser global accessed without SSR guard',
566
+ },
567
+ {
568
+ id: 'nuxt-direct-fetch',
569
+ layer: 'nuxt',
570
+ severity: 'warning',
571
+ description: 'Raw fetch() instead of $fetch/useFetch in Nuxt component',
572
+ },
573
+ {
574
+ id: 'server-route-leak',
575
+ layer: 'nuxt',
576
+ severity: 'error',
577
+ description: 'Server API route may expose sensitive fields',
578
+ },
132
579
  // Express (target: express)
133
- { id: 'unvalidated-input', layer: 'express', severity: 'error', description: 'req.body/params/query used without validation' },
134
- { id: 'missing-error-middleware', layer: 'express', severity: 'warning', description: 'Express app without error-handling middleware' },
580
+ {
581
+ id: 'unvalidated-input',
582
+ layer: 'express',
583
+ severity: 'error',
584
+ description: 'req.body/params/query used without validation',
585
+ },
586
+ {
587
+ id: 'missing-error-middleware',
588
+ layer: 'express',
589
+ severity: 'warning',
590
+ description: 'Express app without error-handling middleware',
591
+ },
135
592
  { id: 'sync-in-handler', layer: 'express', severity: 'warning', description: 'Blocking I/O in request handler' },
136
- { id: 'double-response', layer: 'express', severity: 'error', description: 'Response sent twice without early return' },
593
+ {
594
+ id: 'double-response',
595
+ layer: 'express',
596
+ severity: 'error',
597
+ description: 'Response sent twice without early return',
598
+ },
599
+ {
600
+ id: 'express-missing-next',
601
+ layer: 'express',
602
+ severity: 'error',
603
+ description: 'Middleware accepts next but never calls it — request hangs',
604
+ },
605
+ // FastAPI (target: fastapi, concept-based Python pipeline)
606
+ {
607
+ id: 'fastapi-missing-response-model',
608
+ layer: 'fastapi',
609
+ severity: 'warning',
610
+ description: 'Endpoint without response_model — undocumented response',
611
+ },
612
+ {
613
+ id: 'fastapi-blocking-sync-route',
614
+ layer: 'fastapi',
615
+ severity: 'warning',
616
+ description: 'Blocking call in async route stalls event loop',
617
+ },
618
+ {
619
+ id: 'fastapi-shared-state',
620
+ layer: 'fastapi',
621
+ severity: 'error',
622
+ description: 'Route mutates global/module state — race condition',
623
+ },
624
+ {
625
+ id: 'fastapi-broad-except',
626
+ layer: 'fastapi',
627
+ severity: 'warning',
628
+ description: 'Broad except without re-raising HTTPException',
629
+ },
630
+ {
631
+ id: 'fastapi-broad-cors',
632
+ layer: 'fastapi',
633
+ severity: 'warning',
634
+ description: 'CORSMiddleware with allow_origins=["*"] — overly permissive',
635
+ },
137
636
  // Concept rules (always active, language-agnostic)
138
- { id: 'boundary-mutation', layer: 'concept', severity: 'warning', description: 'Global/shared state mutation across boundaries' },
637
+ {
638
+ id: 'boundary-mutation',
639
+ layer: 'concept',
640
+ severity: 'warning',
641
+ description: 'Global/shared state mutation across boundaries',
642
+ },
139
643
  { id: 'ignored-error', layer: 'concept', severity: 'warning', description: 'Caught exception silently ignored' },
140
- { id: 'unguarded-effect', layer: 'concept', severity: 'warning', description: 'Network/DB effect without auth/validation guard' },
141
- { id: 'unrecovered-effect', layer: 'concept', severity: 'warning', description: 'Network/DB effect without error recovery' },
644
+ {
645
+ id: 'unguarded-effect',
646
+ layer: 'concept',
647
+ severity: 'warning',
648
+ description: 'Network/DB effect without auth/validation guard',
649
+ },
650
+ {
651
+ id: 'unrecovered-effect',
652
+ layer: 'concept',
653
+ severity: 'warning',
654
+ description: 'Network/DB effect without error recovery',
655
+ },
142
656
  ];
143
657
  /** Layer → target mapping for filtering */
144
658
  const LAYER_TARGET_MAP = {
@@ -150,11 +664,15 @@ const LAYER_TARGET_MAP = {
150
664
  'dead-logic': null,
151
665
  'null-safety': null,
152
666
  concept: null,
153
- react: ['nextjs', 'tailwind', 'web', 'native'],
667
+ react: ['nextjs', 'tailwind', 'web', 'native', 'ink'],
668
+ cli: ['cli'],
154
669
  vue: ['vue', 'nuxt'],
670
+ ink: ['ink'],
671
+ terminal: ['terminal'],
155
672
  nextjs: ['nextjs'],
156
673
  nuxt: ['nuxt'],
157
674
  express: ['express'],
675
+ fastapi: ['fastapi'],
158
676
  };
159
677
  /**
160
678
  * Get the rule registry, optionally filtered by target.
@@ -163,7 +681,7 @@ const LAYER_TARGET_MAP = {
163
681
  export function getRuleRegistry(target) {
164
682
  if (!target)
165
683
  return [...REGISTRY];
166
- return REGISTRY.filter(r => {
684
+ return REGISTRY.filter((r) => {
167
685
  const targets = LAYER_TARGET_MAP[r.layer];
168
686
  return targets === null || targets.includes(target);
169
687
  });