@kernlang/review 3.1.6 → 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 (150) hide show
  1. package/dist/cache.d.ts +1 -1
  2. package/dist/cache.js +5 -3
  3. package/dist/cache.js.map +1 -1
  4. package/dist/call-graph.d.ts +63 -0
  5. package/dist/call-graph.js +380 -0
  6. package/dist/call-graph.js.map +1 -0
  7. package/dist/concept-rules/boundary-mutation.d.ts +1 -1
  8. package/dist/concept-rules/boundary-mutation.js.map +1 -1
  9. package/dist/concept-rules/ignored-error.d.ts +1 -1
  10. package/dist/concept-rules/ignored-error.js.map +1 -1
  11. package/dist/concept-rules/illegal-dependency.d.ts +1 -1
  12. package/dist/concept-rules/illegal-dependency.js.map +1 -1
  13. package/dist/concept-rules/index.js +1 -6
  14. package/dist/concept-rules/index.js.map +1 -1
  15. package/dist/concept-rules/unguarded-effect.d.ts +1 -1
  16. package/dist/concept-rules/unguarded-effect.js.map +1 -1
  17. package/dist/concept-rules/unrecovered-effect.d.ts +1 -1
  18. package/dist/concept-rules/unrecovered-effect.js +2 -1
  19. package/dist/concept-rules/unrecovered-effect.js.map +1 -1
  20. package/dist/confidence.js +12 -8
  21. package/dist/confidence.js.map +1 -1
  22. package/dist/differ.js +3 -7
  23. package/dist/differ.js.map +1 -1
  24. package/dist/external-tools.js +5 -6
  25. package/dist/external-tools.js.map +1 -1
  26. package/dist/file-context.d.ts +21 -0
  27. package/dist/file-context.js +234 -0
  28. package/dist/file-context.js.map +1 -0
  29. package/dist/file-role.js +14 -7
  30. package/dist/file-role.js.map +1 -1
  31. package/dist/graph.d.ts +1 -1
  32. package/dist/graph.js +24 -16
  33. package/dist/graph.js.map +1 -1
  34. package/dist/index.d.ts +44 -35
  35. package/dist/index.js +210 -121
  36. package/dist/index.js.map +1 -1
  37. package/dist/inferrer.d.ts +8 -2
  38. package/dist/inferrer.js +80 -47
  39. package/dist/inferrer.js.map +1 -1
  40. package/dist/kern-lint.d.ts +3 -4
  41. package/dist/kern-lint.js +7 -5
  42. package/dist/kern-lint.js.map +1 -1
  43. package/dist/llm-bridge.d.ts +23 -7
  44. package/dist/llm-bridge.js +267 -31
  45. package/dist/llm-bridge.js.map +1 -1
  46. package/dist/llm-review.d.ts +16 -2
  47. package/dist/llm-review.js +240 -35
  48. package/dist/llm-review.js.map +1 -1
  49. package/dist/mappers/ts-concepts.d.ts +1 -1
  50. package/dist/mappers/ts-concepts.js +303 -32
  51. package/dist/mappers/ts-concepts.js.map +1 -1
  52. package/dist/norm-miner.d.ts +31 -0
  53. package/dist/norm-miner.js +119 -0
  54. package/dist/norm-miner.js.map +1 -0
  55. package/dist/obligations.d.ts +63 -0
  56. package/dist/obligations.js +158 -0
  57. package/dist/obligations.js.map +1 -0
  58. package/dist/quality-rules.d.ts +3 -3
  59. package/dist/quality-rules.js +4 -2
  60. package/dist/quality-rules.js.map +1 -1
  61. package/dist/reporter.d.ts +7 -2
  62. package/dist/reporter.js +82 -51
  63. package/dist/reporter.js.map +1 -1
  64. package/dist/rule-eval.d.ts +1 -2
  65. package/dist/rule-eval.js +5 -9
  66. package/dist/rule-eval.js.map +1 -1
  67. package/dist/rule-loader.js +16 -14
  68. package/dist/rule-loader.js.map +1 -1
  69. package/dist/rules/base.js +153 -69
  70. package/dist/rules/base.js.map +1 -1
  71. package/dist/rules/cli.js +23 -19
  72. package/dist/rules/cli.js.map +1 -1
  73. package/dist/rules/confidence.d.ts +1 -1
  74. package/dist/rules/confidence.js +5 -5
  75. package/dist/rules/confidence.js.map +1 -1
  76. package/dist/rules/dead-code.d.ts +10 -0
  77. package/dist/rules/dead-code.js +75 -0
  78. package/dist/rules/dead-code.js.map +1 -0
  79. package/dist/rules/dead-logic.js +35 -31
  80. package/dist/rules/dead-logic.js.map +1 -1
  81. package/dist/rules/express.d.ts +2 -1
  82. package/dist/rules/express.js +380 -126
  83. package/dist/rules/express.js.map +1 -1
  84. package/dist/rules/fastapi.js +53 -19
  85. package/dist/rules/fastapi.js.map +1 -1
  86. package/dist/rules/ground-layer.js +3 -3
  87. package/dist/rules/ground-layer.js.map +1 -1
  88. package/dist/rules/index.js +574 -105
  89. package/dist/rules/index.js.map +1 -1
  90. package/dist/rules/ink.js +9 -8
  91. package/dist/rules/ink.js.map +1 -1
  92. package/dist/rules/kern-source.js +202 -63
  93. package/dist/rules/kern-source.js.map +1 -1
  94. package/dist/rules/nextjs.js +88 -33
  95. package/dist/rules/nextjs.js.map +1 -1
  96. package/dist/rules/null-safety.js +52 -26
  97. package/dist/rules/null-safety.js.map +1 -1
  98. package/dist/rules/nuxt.js +24 -29
  99. package/dist/rules/nuxt.js.map +1 -1
  100. package/dist/rules/react.js +355 -69
  101. package/dist/rules/react.js.map +1 -1
  102. package/dist/rules/security-v2.js +71 -57
  103. package/dist/rules/security-v2.js.map +1 -1
  104. package/dist/rules/security-v3.js.map +1 -1
  105. package/dist/rules/security-v4.js +54 -27
  106. package/dist/rules/security-v4.js.map +1 -1
  107. package/dist/rules/security.js +35 -5
  108. package/dist/rules/security.js.map +1 -1
  109. package/dist/rules/terminal.js +17 -5
  110. package/dist/rules/terminal.js.map +1 -1
  111. package/dist/rules/vue.js +162 -107
  112. package/dist/rules/vue.js.map +1 -1
  113. package/dist/semantic-diff.d.ts +52 -0
  114. package/dist/semantic-diff.js +342 -0
  115. package/dist/semantic-diff.js.map +1 -0
  116. package/dist/spec-checker.js +11 -10
  117. package/dist/spec-checker.js.map +1 -1
  118. package/dist/suppression/apply-suppression.d.ts +2 -3
  119. package/dist/suppression/apply-suppression.js +3 -3
  120. package/dist/suppression/apply-suppression.js.map +1 -1
  121. package/dist/suppression/index.d.ts +2 -2
  122. package/dist/suppression/index.js +1 -1
  123. package/dist/suppression/index.js.map +1 -1
  124. package/dist/suppression/parse-directives.d.ts +1 -1
  125. package/dist/suppression/parse-directives.js +9 -4
  126. package/dist/suppression/parse-directives.js.map +1 -1
  127. package/dist/taint-ast.d.ts +20 -0
  128. package/dist/taint-ast.js +427 -0
  129. package/dist/taint-ast.js.map +1 -0
  130. package/dist/taint-crossfile.d.ts +28 -0
  131. package/dist/taint-crossfile.js +174 -0
  132. package/dist/taint-crossfile.js.map +1 -0
  133. package/dist/taint-findings.d.ts +17 -0
  134. package/dist/taint-findings.js +131 -0
  135. package/dist/taint-findings.js.map +1 -0
  136. package/dist/taint-regex.d.ts +61 -0
  137. package/dist/taint-regex.js +379 -0
  138. package/dist/taint-regex.js.map +1 -0
  139. package/dist/taint-types.d.ts +128 -0
  140. package/dist/taint-types.js +174 -0
  141. package/dist/taint-types.js.map +1 -0
  142. package/dist/taint.d.ts +13 -107
  143. package/dist/taint.js +16 -1067
  144. package/dist/taint.js.map +1 -1
  145. package/dist/template-detector.d.ts +2 -2
  146. package/dist/template-detector.js +11 -16
  147. package/dist/template-detector.js.map +1 -1
  148. package/dist/types.d.ts +35 -0
  149. package/dist/types.js.map +1 -1
  150. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -8,25 +8,30 @@
8
8
  *
9
9
  * v2: Unified ReviewFinding pipeline. All findings merged into single array.
10
10
  */
11
- import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
12
- import { relative, join } from 'path';
13
- import { parseWithDiagnostics, countTokens, serializeIR } from '@kernlang/core';
14
- import { inferFromSourceFile, createInMemoryProject } from './inferrer.js';
15
- import { resolveImportGraph } from './graph.js';
16
- import { detectTemplates } from './template-detector.js';
11
+ import { countTokens, parseWithDiagnostics, serializeIR } from '@kernlang/core';
12
+ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
13
+ import { join, relative } from 'path';
14
+ import { buildCallGraph } from './call-graph.js';
15
+ import { runConceptRules } from './concept-rules/index.js';
17
16
  import { structuralDiff } from './differ.js';
18
- import { runQualityRules } from './quality-rules.js';
19
- import { calculateStats, sortAndDedup, sortFindings } from './reporter.js';
20
- import { classifyFileRole } from './file-role.js';
21
17
  import { runTSCDiagnostics } from './external-tools.js';
18
+ import { buildFileContextMap } from './file-context.js';
19
+ import { classifyFileRole } from './file-role.js';
20
+ import { resolveImportGraph } from './graph.js';
21
+ import { createInMemoryProject, inferFromSourceFile } from './inferrer.js';
22
+ import { flattenIR, lintKernIR } from './kern-lint.js';
22
23
  import { extractTsConcepts } from './mappers/ts-concepts.js';
23
- import { runConceptRules } from './concept-rules/index.js';
24
- import { lintConfidenceGraph } from './rules/confidence.js';
25
- import { lintKernIR, flattenIR } from './kern-lint.js';
24
+ import { mineNorms } from './norm-miner.js';
25
+ import { synthesizeObligations } from './obligations.js';
26
+ import { runQualityRules } from './quality-rules.js';
27
+ import { assignDefaultConfidence, calculateStats, sortAndDedup, sortFindings } from './reporter.js';
26
28
  import { loadBuiltinNativeRules, loadNativeRules } from './rule-loader.js';
27
- import { lintKernSourceIR, KERN_SOURCE_RULES } from './rules/kern-source.js';
28
- import { GROUND_LAYER_RULES } from './rules/ground-layer.js';
29
+ import { lintConfidenceGraph } from './rules/confidence.js';
30
+ import { crossFileAsyncRule, deadExportRule } from './rules/dead-code.js';
29
31
  import { runFastapiConceptRules } from './rules/fastapi.js';
32
+ import { GROUND_LAYER_RULES } from './rules/ground-layer.js';
33
+ import { KERN_SOURCE_RULES, lintKernSourceIR } from './rules/kern-source.js';
34
+ import { detectTemplates } from './template-detector.js';
30
35
  // Load native .kern rules once at module init
31
36
  // Guard: import.meta.url is undefined when bundled as CJS (e.g. esbuild for VS Code worker)
32
37
  let NATIVE_RULES = [];
@@ -36,46 +41,77 @@ try {
36
41
  catch {
37
42
  // CJS bundle — native .kern rules not available, regex rules still work
38
43
  }
39
- import { buildConfidenceGraph, serializeGraph, computeConfidenceSummary } from './confidence.js';
40
- import { analyzeTaint, taintToFindings, analyzeTaintCrossFile, crossFileTaintToFindings } from './taint.js';
44
+ import { buildConfidenceGraph, computeConfidenceSummary, serializeGraph } from './confidence.js';
41
45
  import { applySuppression } from './suppression/index.js';
46
+ import { analyzeTaint, analyzeTaintCrossFile, crossFileTaintToFindings, taintToFindings } from './taint.js';
42
47
  import { createFingerprint } from './types.js';
43
- export { resolveImportGraph } from './graph.js';
44
- export { createFingerprint } from './types.js';
45
- export { inferFromSource, inferFromFile } from './inferrer.js';
46
- export { classifyFileRole } from './file-role.js';
47
- export { detectTemplates } from './template-detector.js';
48
+ export { buildCallGraph } from './call-graph.js';
49
+ export { runConceptRules } from './concept-rules/index.js';
50
+ // Confidence layer
51
+ export { buildConfidenceGraph, buildMultiFileConfidenceGraph, computeConfidenceSummary, parseConfidence, propagateConfidence, resolveBaseConfidence, serializeGraph, } from './confidence.js';
48
52
  export { structuralDiff } from './differ.js';
53
+ export { linkToNodes, runESLint, runTSCDiagnostics, runTSCDiagnosticsFromPaths } from './external-tools.js';
54
+ export { buildFileContextMap, clearFileContextCache } from './file-context.js';
55
+ export { classifyFileRole } from './file-role.js';
56
+ export { resolveImportGraph } from './graph.js';
57
+ export { inferFromFile, inferFromSource } from './inferrer.js';
58
+ // KERN-IR lint pipeline (ground layer)
59
+ export { flattenIR, lintKernIR } from './kern-lint.js';
60
+ // LLM bridge (Phase 3)
61
+ export { buildReviewInstructions, isLLMAvailable, runLLMReview } from './llm-bridge.js';
62
+ export { buildLLMPrompt, exportKernIR, parseLLMResponse } from './llm-review.js';
63
+ export { extractTsConcepts } from './mappers/ts-concepts.js';
64
+ // Norm mining + obligations
65
+ export { mineNorms } from './norm-miner.js';
66
+ export { obligationsFromNorms, obligationsFromStructure, synthesizeObligations } from './obligations.js';
49
67
  export { runQualityRules } from './quality-rules.js';
68
+ export { assignDefaultConfidence, calculateStats, checkEnforcement, dedup, formatEnforcement, formatReport, formatReportJSON, formatSARIF, formatSARIFWithSuppressions, formatSummary, sortAndDedup, sortFindings, } from './reporter.js';
69
+ export { CONFIDENCE_RULES, lintConfidenceGraph, lintMultiFileConfidenceGraph } from './rules/confidence.js';
70
+ export { actionMissingIdempotent, assumeLowTrust, branchNonExhaustive, collectUnbounded, expectRangeInverted, GROUND_LAYER_RULES, guardWithoutElse, reasonWithoutBasis, } from './rules/ground-layer.js';
50
71
  export { getRuleRegistry } from './rules/index.js';
51
- export { calculateStats, formatReport, formatReportJSON, formatSARIF, formatSARIFWithSuppressions, formatSummary, checkEnforcement, formatEnforcement, dedup, sortAndDedup, sortFindings } from './reporter.js';
52
- export { exportKernIR, buildLLMPrompt, parseLLMResponse } from './llm-review.js';
53
- export { runESLint, runTSCDiagnostics, runTSCDiagnosticsFromPaths, linkToNodes } from './external-tools.js';
54
- export { extractTsConcepts } from './mappers/ts-concepts.js';
55
- export { runConceptRules } from './concept-rules/index.js';
56
- // Suppression
57
- export { applySuppression, parseDirectives, configDirectives, isConceptRule } from './suppression/index.js';
58
- // KERN-IR lint pipeline (ground layer)
59
- export { lintKernIR, flattenIR } from './kern-lint.js';
60
- export { GROUND_LAYER_RULES } from './rules/ground-layer.js';
61
- export { lintKernSourceIR, KERN_SOURCE_RULES, undefinedReference, typeModelMismatch, unusedState, handlerHeavy, missingConfidence } from './rules/kern-source.js';
62
- export { guardWithoutElse, actionMissingIdempotent, branchNonExhaustive, collectUnbounded, reasonWithoutBasis, assumeLowTrust, expectRangeInverted, } from './rules/ground-layer.js';
63
- // Confidence layer
64
- export { parseConfidence, buildConfidenceGraph, buildMultiFileConfidenceGraph, propagateConfidence, resolveBaseConfidence, serializeGraph, computeConfidenceSummary, } from './confidence.js';
65
- export { lintConfidenceGraph, lintMultiFileConfidenceGraph, CONFIDENCE_RULES } from './rules/confidence.js';
72
+ export { handlerHeavy, KERN_SOURCE_RULES, lintKernSourceIR, missingConfidence, typeModelMismatch, undefinedReference, unusedState, } from './rules/kern-source.js';
66
73
  // ReDoS detection (reusable by rule compilers)
67
74
  export { isReDoSVulnerable } from './rules/security-v3.js';
75
+ // Semantic diff
76
+ export { computeSemanticDiff, computeSemanticDiffFromSource, formatSemanticDiff, getOldFileContent, semanticChangesToFindings, } from './semantic-diff.js';
77
+ // Suppression
78
+ export { applySuppression, configDirectives, isConceptRule, parseDirectives } from './suppression/index.js';
68
79
  // Taint tracking (Phase 2 + cross-file)
69
- export { analyzeTaint, taintToFindings, analyzeTaintCrossFile, crossFileTaintToFindings, buildExportMap, buildImportMap, isSanitizerSufficient } from './taint.js';
70
- // LLM bridge (Phase 3)
71
- export { runLLMReview, isLLMAvailable } from './llm-bridge.js';
80
+ export { analyzeTaint, analyzeTaintCrossFile, buildExportMap, buildImportMap, crossFileTaintToFindings, isSanitizerSufficient, taintToFindings, } from './taint.js';
81
+ export { detectTemplates } from './template-detector.js';
82
+ export { createFingerprint } from './types.js';
72
83
  // Cache (Phase 0)
73
- import { computeCacheKey, reviewCache, clearReviewCache } from './cache.js';
74
- export { clearReviewCache };
84
+ import { clearReviewCache, computeCacheKey, reviewCache } from './cache.js';
75
85
  // Spec checker — .kern contract vs .ts implementation
76
- export { checkSpec, checkSpecFiles, extractSpecContracts, extractImplRoutes, matchRoutes, verifyRouteContract, specViolationsToFindings } from './spec-checker.js';
86
+ export { checkSpec, checkSpecFiles, extractImplRoutes, extractSpecContracts, matchRoutes, specViolationsToFindings, verifyRouteContract, } from './spec-checker.js';
87
+ export { clearReviewCache };
88
+ /** Shared filesystem-backed Project for type-aware analysis (reused across reviewFile calls) */
89
+ let _fsProject;
90
+ function getOrCreateFsProject() {
91
+ if (!_fsProject) {
92
+ const { Project } = require('ts-morph');
93
+ _fsProject = new Project({
94
+ compilerOptions: {
95
+ strict: true,
96
+ target: 99 /* Latest */,
97
+ module: 99 /* ESNext */,
98
+ moduleResolution: 100 /* Bundler */,
99
+ skipLibCheck: true,
100
+ noEmit: true,
101
+ },
102
+ useInMemoryFileSystem: false,
103
+ skipAddingFilesFromTsConfig: true,
104
+ });
105
+ }
106
+ return _fsProject;
107
+ }
108
+ /** Reset the shared project (for tests / watch mode) */
109
+ export function resetFsProject() {
110
+ _fsProject = undefined;
111
+ }
77
112
  /**
78
113
  * Review a single file. Auto-detects language from extension.
114
+ * Uses a filesystem-backed ts-morph Project for type-aware analysis.
79
115
  * Supports: .ts, .tsx, .py, .kern
80
116
  */
81
117
  export function reviewFile(filePath, config) {
@@ -95,7 +131,8 @@ export function reviewFile(filePath, config) {
95
131
  report = reviewPythonSource(source, filePath, config);
96
132
  }
97
133
  else {
98
- report = reviewSource(source, filePath, config);
134
+ // Use filesystem-backed project for real files (enables TypeChecker)
135
+ report = reviewSourceWithProject(source, filePath, config);
99
136
  }
100
137
  if (key) {
101
138
  reviewCache.set(key, report);
@@ -103,13 +140,41 @@ export function reviewFile(filePath, config) {
103
140
  return report;
104
141
  }
105
142
  /**
106
- * Review TypeScript source code (string).
143
+ * Review TypeScript source with a filesystem-backed project.
144
+ * The fs project enables .getReturnType() to resolve types from node_modules.
145
+ */
146
+ function reviewSourceWithProject(source, filePath, config) {
147
+ try {
148
+ const fsProject = getOrCreateFsProject();
149
+ // Add or update the file in the project
150
+ let sf = fsProject.getSourceFile(filePath);
151
+ if (sf) {
152
+ sf.replaceWithText(source);
153
+ }
154
+ else {
155
+ sf = fsProject.addSourceFileAtPath(filePath);
156
+ }
157
+ return reviewSourceInternal(source, filePath, config, fsProject, sf);
158
+ }
159
+ catch {
160
+ // Fallback to in-memory project if fs project fails
161
+ return reviewSource(source, filePath, config);
162
+ }
163
+ }
164
+ /**
165
+ * Review TypeScript source code (string). Uses in-memory project (no type resolution).
166
+ * For file-from-disk review with type resolution, use reviewFile() instead.
107
167
  */
108
168
  export function reviewSource(source, filePath = 'input.ts', config) {
109
- const totalLines = source.split('\n').length;
110
- // ── Shared context: single AST parse, shared across all phases ──
111
169
  const project = createInMemoryProject();
112
170
  const sourceFile = project.createSourceFile(filePath, source);
171
+ return reviewSourceInternal(source, filePath, config, project, sourceFile);
172
+ }
173
+ /**
174
+ * Internal review implementation — shared between reviewSource (in-memory) and reviewFile (filesystem).
175
+ */
176
+ function reviewSourceInternal(source, filePath, config, project, sourceFile) {
177
+ const totalLines = source.split('\n').length;
113
178
  const fileRole = classifyFileRole(sourceFile, filePath);
114
179
  // Helper: run a phase safely, collect findings even if a phase throws
115
180
  const allFindings = [];
@@ -119,7 +184,10 @@ export function reviewSource(source, filePath = 'input.ts', config) {
119
184
  }
120
185
  catch (err) {
121
186
  allFindings.push({
122
- source: 'kern', ruleId: 'internal-error', severity: 'info', category: 'structure',
187
+ source: 'kern',
188
+ ruleId: 'internal-error',
189
+ severity: 'info',
190
+ category: 'structure',
123
191
  message: `Review phase '${name}' failed: ${err.message}`,
124
192
  primarySpan: { file: filePath, startLine: 1, startCol: 1, endLine: 1, endCol: 1 },
125
193
  fingerprint: createFingerprint('internal-error', 1, name.charCodeAt(0)),
@@ -139,13 +207,13 @@ export function reviewSource(source, filePath = 'input.ts', config) {
139
207
  // Phase 4: Structural diff → unified findings
140
208
  allFindings.push(...safePhase('diff', () => structuralDiff(source, inferred, filePath), []));
141
209
  // Phase 5: Quality rules → unified findings (receives fileRole)
142
- allFindings.push(...safePhase('quality', () => runQualityRules(sourceFile, inferred, templateMatches, config, fileRole), []));
210
+ allFindings.push(...safePhase('quality', () => runQualityRules(sourceFile, inferred, templateMatches, config, fileRole, project), []));
143
211
  // Phase 6: Concept extraction + concept rules (universal, cross-language)
144
212
  const emptyConcepts = { filePath, language: 'typescript', nodes: [], edges: [], extractorVersion: '0' };
145
213
  const concepts = safePhase('concepts', () => extractTsConcepts(sourceFile, filePath), emptyConcepts);
146
214
  allFindings.push(...safePhase('concept-rules', () => runConceptRules(concepts, filePath), []));
147
215
  // Phase 7: KERN-IR lint (ground layer + confidence rules on inferred nodes)
148
- const irNodes = inferred.map(r => r.node);
216
+ const irNodes = inferred.map((r) => r.node);
149
217
  const groundFindings = safePhase('ground-lint', () => lintKernIR(irNodes, GROUND_LAYER_RULES), []);
150
218
  for (const f of groundFindings) {
151
219
  if (!f.primarySpan.file)
@@ -161,7 +229,7 @@ export function reviewSource(source, filePath = 'input.ts', config) {
161
229
  // Phase 7b: Native .kern rules (built-in + custom)
162
230
  const rulesToRun = [...NATIVE_RULES];
163
231
  if (config?.rulesDirs && config.rulesDirs.length > 0) {
164
- const builtinIds = new Set(NATIVE_RULES.map(r => r.ruleId).filter(Boolean));
232
+ const builtinIds = new Set(NATIVE_RULES.map((r) => r.ruleId).filter(Boolean));
165
233
  const customRules = loadNativeRules(config.rulesDirs, builtinIds);
166
234
  rulesToRun.push(...customRules);
167
235
  }
@@ -178,7 +246,7 @@ export function reviewSource(source, filePath = 'input.ts', config) {
178
246
  // Build confidence graph if any nodes have confidence props
179
247
  let confidenceGraph;
180
248
  let confidenceSummary;
181
- const hasConfidence = irNodes.some(n => n.props?.confidence !== undefined);
249
+ const hasConfidence = irNodes.some((n) => n.props?.confidence !== undefined);
182
250
  if (hasConfidence) {
183
251
  const graph = buildConfidenceGraph(irNodes);
184
252
  confidenceGraph = serializeGraph(graph);
@@ -186,6 +254,8 @@ export function reviewSource(source, filePath = 'input.ts', config) {
186
254
  }
187
255
  // Merge, dedup, sort — single shared utility
188
256
  const dedupedFindings = sortAndDedup(allFindings);
257
+ // Assign calibrated confidence scores to all findings
258
+ assignDefaultConfidence(dedupedFindings);
189
259
  // Apply suppression (inline comments + config disabledRules)
190
260
  const suppression = applySuppression(dedupedFindings, source, filePath, config, config?.strict ?? false);
191
261
  const findings = sortAndDedup(suppression.findings);
@@ -215,7 +285,10 @@ export function reviewKernSource(source, filePath = 'input.kern', _config) {
215
285
  }
216
286
  catch (err) {
217
287
  allFindings.push({
218
- source: 'kern', ruleId: 'internal-error', severity: 'info', category: 'structure',
288
+ source: 'kern',
289
+ ruleId: 'internal-error',
290
+ severity: 'info',
291
+ category: 'structure',
219
292
  message: `Review phase '${name}' failed: ${err.message}`,
220
293
  primarySpan: { file: filePath, startLine: 1, startCol: 1, endLine: 1, endCol: 1 },
221
294
  fingerprint: createFingerprint('internal-error', 1, name.charCodeAt(0)),
@@ -224,9 +297,12 @@ export function reviewKernSource(source, filePath = 'input.kern', _config) {
224
297
  }
225
298
  }
226
299
  // Parse .kern → IR tree + structured diagnostics
227
- const { root, diagnostics: parseDiags } = safePhase('parse', () => parseWithDiagnostics(source), { root: { type: 'document' }, diagnostics: [] });
300
+ const { root, diagnostics: parseDiags } = safePhase('parse', () => parseWithDiagnostics(source), {
301
+ root: { type: 'document' },
302
+ diagnostics: [],
303
+ });
228
304
  // Map parse diagnostics → ReviewFindings (severity capped at 'warning' unless --strict-parse is enabled)
229
- const hasParseErrors = parseDiags.some(d => d.severity === 'error');
305
+ const hasParseErrors = parseDiags.some((d) => d.severity === 'error');
230
306
  for (const d of parseDiags) {
231
307
  allFindings.push({
232
308
  source: 'kern',
@@ -240,7 +316,7 @@ export function reviewKernSource(source, filePath = 'input.kern', _config) {
240
316
  });
241
317
  }
242
318
  // Flatten IR tree for rule consumption
243
- const flatNodes = flattenIR(root).filter(n => n.type !== 'document');
319
+ const flatNodes = flattenIR(root).filter((n) => n.type !== 'document');
244
320
  // Skip structural lint when parse has errors — partial tree causes cascading false positives
245
321
  if (!hasParseErrors) {
246
322
  // Ground-layer rules on IR nodes
@@ -263,7 +339,7 @@ export function reviewKernSource(source, filePath = 'input.kern', _config) {
263
339
  // Native .kern rules (built-in + custom)
264
340
  const rulesToRunKern = [...NATIVE_RULES];
265
341
  if (_config?.rulesDirs && _config.rulesDirs.length > 0) {
266
- const builtinIds = new Set(NATIVE_RULES.map(r => r.ruleId).filter(Boolean));
342
+ const builtinIds = new Set(NATIVE_RULES.map((r) => r.ruleId).filter(Boolean));
267
343
  const customRules = loadNativeRules(_config.rulesDirs, builtinIds);
268
344
  rulesToRunKern.push(...customRules);
269
345
  }
@@ -279,7 +355,7 @@ export function reviewKernSource(source, filePath = 'input.kern', _config) {
279
355
  // Confidence graph
280
356
  let confidenceGraph;
281
357
  let confidenceSummary;
282
- if (flatNodes.some(n => n.props?.confidence !== undefined)) {
358
+ if (flatNodes.some((n) => n.props?.confidence !== undefined)) {
283
359
  const graph = buildConfidenceGraph(flatNodes);
284
360
  confidenceGraph = serializeGraph(graph);
285
361
  confidenceSummary = computeConfidenceSummary(graph);
@@ -297,10 +373,15 @@ export function reviewKernSource(source, filePath = 'input.kern', _config) {
297
373
  promptAlias: `N${i + 1}`,
298
374
  startLine: line,
299
375
  endLine,
300
- sourceSpans: [{
301
- file: filePath, startLine: line, startCol: node.loc?.col ?? 1,
302
- endLine, endCol: node.loc?.endCol ?? 1,
303
- }],
376
+ sourceSpans: [
377
+ {
378
+ file: filePath,
379
+ startLine: line,
380
+ startCol: node.loc?.col ?? 1,
381
+ endLine,
382
+ endCol: node.loc?.endCol ?? 1,
383
+ },
384
+ ],
304
385
  summary: `${node.type}${name !== node.type ? ` ${name}` : ''}`,
305
386
  confidence: 'high',
306
387
  confidencePct: 100,
@@ -309,6 +390,7 @@ export function reviewKernSource(source, filePath = 'input.kern', _config) {
309
390
  };
310
391
  });
311
392
  const dedupedFindings = sortAndDedup(allFindings);
393
+ assignDefaultConfidence(dedupedFindings);
312
394
  const suppression = applySuppression(dedupedFindings, source, filePath, _config, _config?.strict ?? false);
313
395
  const findings = sortAndDedup(suppression.findings);
314
396
  const kernTokens = countTokens(source);
@@ -349,7 +431,7 @@ export function reviewPythonSource(source, filePath = 'input.py', config) {
349
431
  // Native .kern rules with concept matching (built-in + custom)
350
432
  const rulesToRunPy = [...NATIVE_RULES];
351
433
  if (config?.rulesDirs && config.rulesDirs.length > 0) {
352
- const builtinIds = new Set(NATIVE_RULES.map(r => r.ruleId).filter(Boolean));
434
+ const builtinIds = new Set(NATIVE_RULES.map((r) => r.ruleId).filter(Boolean));
353
435
  const customRules = loadNativeRules(config.rulesDirs, builtinIds);
354
436
  rulesToRunPy.push(...customRules);
355
437
  }
@@ -359,7 +441,8 @@ export function reviewPythonSource(source, filePath = 'input.py', config) {
359
441
  }
360
442
  catch (_err) {
361
443
  // @kernlang/review-python not installed — skip concept extraction
362
- conceptFindings = [{
444
+ conceptFindings = [
445
+ {
363
446
  source: 'kern',
364
447
  ruleId: 'missing-python-support',
365
448
  severity: 'info',
@@ -367,9 +450,11 @@ export function reviewPythonSource(source, filePath = 'input.py', config) {
367
450
  message: 'Install @kernlang/review-python for Python concept analysis',
368
451
  primarySpan: { file: filePath, startLine: 1, startCol: 1, endLine: 1, endCol: 1 },
369
452
  fingerprint: 'missing-python-0',
370
- }];
453
+ },
454
+ ];
371
455
  }
372
456
  const dedupedFindings = sortAndDedup(conceptFindings);
457
+ assignDefaultConfidence(dedupedFindings);
373
458
  const suppression = applySuppression(dedupedFindings, source, filePath, config, config?.strict ?? false);
374
459
  const findings = sortAndDedup(suppression.findings);
375
460
  return {
@@ -412,11 +497,14 @@ export function reviewGraph(entryFiles, config, graphOptions) {
412
497
  const graph = resolveImportGraph(entryFiles, graphOptions);
413
498
  const entrySet = new Set(graph.entryFiles);
414
499
  const reports = [];
500
+ // Build file context map — every file gets import chain awareness
501
+ const fileContextMap = buildFileContextMap(graph);
502
+ const graphConfig = { ...config, fileContextMap };
415
503
  for (const gf of graph.files) {
416
504
  if (!existsSync(gf.path))
417
505
  continue;
418
506
  try {
419
- const report = reviewFile(gf.path, config);
507
+ const report = reviewFile(gf.path, graphConfig);
420
508
  const isEntry = entrySet.has(gf.path);
421
509
  // Tag every finding with provenance
422
510
  for (const f of report.findings) {
@@ -452,11 +540,20 @@ export function reviewGraph(entryFiles, config, graphOptions) {
452
540
  const crossFileFindings = crossFileTaintToFindings(crossFileResults);
453
541
  // Add cross-file findings to the caller's report, then re-run suppression
454
542
  for (const f of crossFileFindings) {
455
- const callerReport = reports.find(r => r.filePath === f.primarySpan.file);
543
+ const callerReport = reports.find((r) => r.filePath === f.primarySpan.file);
456
544
  if (callerReport) {
457
545
  callerReport.findings.push(f);
458
546
  }
459
547
  }
548
+ // Attach raw cross-file taint results for structured output
549
+ for (const result of crossFileResults) {
550
+ const callerReport = reports.find((r) => r.filePath === result.callerFile);
551
+ if (callerReport) {
552
+ if (!callerReport.crossFileTaint)
553
+ callerReport.crossFileTaint = [];
554
+ callerReport.crossFileTaint.push(result);
555
+ }
556
+ }
460
557
  }
461
558
  // Cross-file concept analysis — re-run concept rules with full graph context
462
559
  // This fixes false positives where guards are in middleware files and effects in handlers
@@ -483,71 +580,53 @@ export function reviewGraph(entryFiles, config, graphOptions) {
483
580
  if (!concepts)
484
581
  continue;
485
582
  // Remove per-file concept findings (they were run without cross-file context)
486
- report.findings = report.findings.filter(f => !CONCEPT_RULE_IDS.has(f.ruleId));
583
+ report.findings = report.findings.filter((f) => !CONCEPT_RULE_IDS.has(f.ruleId));
487
584
  // Re-run concept rules with cross-file context
488
585
  const crossFileConceptFindings = runConceptRules(concepts, report.filePath, allConcepts, graphImports);
489
586
  report.findings.push(...crossFileConceptFindings);
490
587
  }
491
588
  }
492
- // ── Post-filter: suppress server-hook / missing-use-client false positives ──
493
- // In graph mode, if ALL importers of a file are within a client boundary
494
- // ('use client' recursively), hooks and event handlers are safe on the client.
495
- if (config?.target === 'nextjs') {
496
- const NEXTJS_CLIENT_RULES = new Set(['server-hook', 'missing-use-client']);
497
- const hasNextjsFindings = reports.some(r => r.findings.some(f => NEXTJS_CLIENT_RULES.has(f.ruleId)));
498
- if (hasNextjsFindings) {
499
- const importedByMap = new Map();
589
+ // Note: server-hook / missing-use-client client boundary suppression is now handled
590
+ // by FileContext in the rules themselves (ctx.fileContext.isClientBoundary).
591
+ // ── Norm mining + proof obligations ──
592
+ if (allConcepts.size > 0) {
593
+ const normViolations = mineNorms(allConcepts);
594
+ for (const report of reports) {
595
+ const obligations = synthesizeObligations(allConcepts, fileContextMap, report.filePath, normViolations);
596
+ // Attach obligations to the report (as metadata, not findings — they're for the LLM reviewer)
597
+ report.obligations = obligations;
598
+ }
599
+ }
600
+ // ── Call graph analysis: dead exports + cross-file async ──
601
+ try {
602
+ // Use provided project, or build one with all graph files loaded
603
+ let cgProject = graphOptions?.project;
604
+ if (!cgProject) {
605
+ const { Project } = require('ts-morph');
606
+ cgProject = new Project({
607
+ compilerOptions: { strict: true, target: 99, module: 99, moduleResolution: 100, skipLibCheck: true },
608
+ useInMemoryFileSystem: false,
609
+ skipAddingFilesFromTsConfig: true,
610
+ });
500
611
  for (const gf of graph.files) {
501
- importedByMap.set(gf.path, gf.importedBy);
502
- }
503
- const useClientCache = new Map();
504
- function hasUseClient(fp) {
505
- let r = useClientCache.get(fp);
506
- if (r !== undefined)
507
- return r;
508
612
  try {
509
- const src = readFileSync(fp, 'utf-8');
510
- r = /^['"]use client['"];?\s*$/m.test(src.substring(0, 200));
613
+ cgProject.addSourceFileAtPath(gf.path);
511
614
  }
512
615
  catch {
513
- r = false;
514
- }
515
- useClientCache.set(fp, r);
516
- return r;
517
- }
518
- const boundaryCache = new Map();
519
- function isWithinClientBoundary(fp, visiting) {
520
- if (hasUseClient(fp))
521
- return true;
522
- const c = boundaryCache.get(fp);
523
- if (c !== undefined)
524
- return c;
525
- // Entry files without 'use client' are server components by definition
526
- if (entrySet.has(fp)) {
527
- boundaryCache.set(fp, false);
528
- return false;
529
- }
530
- if (visiting.has(fp))
531
- return true; // cycle — optimistic
532
- const importers = importedByMap.get(fp);
533
- if (!importers || importers.length === 0) {
534
- boundaryCache.set(fp, false);
535
- return false;
536
- }
537
- visiting.add(fp);
538
- const ok = importers.every(i => isWithinClientBoundary(i, visiting));
539
- visiting.delete(fp);
540
- boundaryCache.set(fp, ok);
541
- return ok;
542
- }
543
- for (const report of reports) {
544
- if (!report.findings.some(f => NEXTJS_CLIENT_RULES.has(f.ruleId)))
545
- continue;
546
- if (isWithinClientBoundary(report.filePath, new Set())) {
547
- report.findings = report.findings.filter(f => !NEXTJS_CLIENT_RULES.has(f.ruleId));
616
+ /* skip unresolvable */
548
617
  }
549
618
  }
550
619
  }
620
+ const callGraph = buildCallGraph(graph, cgProject);
621
+ for (const report of reports) {
622
+ const deadExportFindings = deadExportRule(callGraph, report.filePath);
623
+ report.findings.push(...deadExportFindings);
624
+ const asyncFindings = crossFileAsyncRule(callGraph, report.filePath);
625
+ report.findings.push(...asyncFindings);
626
+ }
627
+ }
628
+ catch {
629
+ // Call graph build failure should not crash the review pipeline
551
630
  }
552
631
  // Re-run suppression + dedup on all reports (cross-file findings were injected after initial suppression)
553
632
  for (const report of reports) {
@@ -567,10 +646,20 @@ function collectReviewableFiles(dirPath, recursive) {
567
646
  for (const entry of readdirSync(dirPath)) {
568
647
  const full = join(dirPath, entry);
569
648
  const stat = statSync(full);
570
- if (stat.isDirectory() && recursive && !entry.startsWith('.') && entry !== 'node_modules' && entry !== 'dist' && entry !== '__pycache__' && entry !== '.venv' && entry !== 'venv') {
649
+ if (stat.isDirectory() &&
650
+ recursive &&
651
+ !entry.startsWith('.') &&
652
+ entry !== 'node_modules' &&
653
+ entry !== 'dist' &&
654
+ entry !== '__pycache__' &&
655
+ entry !== '.venv' &&
656
+ entry !== 'venv') {
571
657
  files.push(...collectReviewableFiles(full, true));
572
658
  }
573
- else if ((entry.endsWith('.ts') || entry.endsWith('.tsx')) && !entry.endsWith('.d.ts') && !entry.endsWith('.test.ts') && !entry.endsWith('.test.tsx')) {
659
+ else if ((entry.endsWith('.ts') || entry.endsWith('.tsx')) &&
660
+ !entry.endsWith('.d.ts') &&
661
+ !entry.endsWith('.test.ts') &&
662
+ !entry.endsWith('.test.tsx')) {
574
663
  files.push(full);
575
664
  }
576
665
  else if (entry.endsWith('.py') && !entry.startsWith('test_') && !entry.endsWith('_test.py')) {