@kernlang/review 3.1.6 → 3.1.8
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.
- package/dist/cache.d.ts +1 -1
- package/dist/cache.js +5 -3
- package/dist/cache.js.map +1 -1
- package/dist/call-graph.d.ts +63 -0
- package/dist/call-graph.js +380 -0
- package/dist/call-graph.js.map +1 -0
- package/dist/concept-rules/boundary-mutation.d.ts +1 -1
- package/dist/concept-rules/boundary-mutation.js.map +1 -1
- package/dist/concept-rules/ignored-error.d.ts +1 -1
- package/dist/concept-rules/ignored-error.js.map +1 -1
- package/dist/concept-rules/illegal-dependency.d.ts +1 -1
- package/dist/concept-rules/illegal-dependency.js.map +1 -1
- package/dist/concept-rules/index.js +1 -6
- package/dist/concept-rules/index.js.map +1 -1
- package/dist/concept-rules/unguarded-effect.d.ts +1 -1
- package/dist/concept-rules/unguarded-effect.js.map +1 -1
- package/dist/concept-rules/unrecovered-effect.d.ts +1 -1
- package/dist/concept-rules/unrecovered-effect.js +2 -1
- package/dist/concept-rules/unrecovered-effect.js.map +1 -1
- package/dist/confidence.js +12 -8
- package/dist/confidence.js.map +1 -1
- package/dist/differ.js +3 -7
- package/dist/differ.js.map +1 -1
- package/dist/external-tools.js +5 -6
- package/dist/external-tools.js.map +1 -1
- package/dist/file-context.d.ts +21 -0
- package/dist/file-context.js +234 -0
- package/dist/file-context.js.map +1 -0
- package/dist/file-role.js +14 -7
- package/dist/file-role.js.map +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/graph.js +24 -16
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +44 -35
- package/dist/index.js +210 -121
- package/dist/index.js.map +1 -1
- package/dist/inferrer.d.ts +8 -2
- package/dist/inferrer.js +80 -47
- package/dist/inferrer.js.map +1 -1
- package/dist/kern-lint.d.ts +3 -4
- package/dist/kern-lint.js +7 -5
- package/dist/kern-lint.js.map +1 -1
- package/dist/llm-bridge.d.ts +23 -7
- package/dist/llm-bridge.js +267 -31
- package/dist/llm-bridge.js.map +1 -1
- package/dist/llm-review.d.ts +16 -2
- package/dist/llm-review.js +240 -35
- package/dist/llm-review.js.map +1 -1
- package/dist/mappers/ts-concepts.d.ts +1 -1
- package/dist/mappers/ts-concepts.js +303 -32
- package/dist/mappers/ts-concepts.js.map +1 -1
- package/dist/norm-miner.d.ts +31 -0
- package/dist/norm-miner.js +119 -0
- package/dist/norm-miner.js.map +1 -0
- package/dist/obligations.d.ts +63 -0
- package/dist/obligations.js +158 -0
- package/dist/obligations.js.map +1 -0
- package/dist/quality-rules.d.ts +3 -3
- package/dist/quality-rules.js +4 -2
- package/dist/quality-rules.js.map +1 -1
- package/dist/reporter.d.ts +7 -2
- package/dist/reporter.js +82 -51
- package/dist/reporter.js.map +1 -1
- package/dist/rule-eval.d.ts +1 -2
- package/dist/rule-eval.js +5 -9
- package/dist/rule-eval.js.map +1 -1
- package/dist/rule-loader.js +16 -14
- package/dist/rule-loader.js.map +1 -1
- package/dist/rules/base.js +153 -69
- package/dist/rules/base.js.map +1 -1
- package/dist/rules/cli.js +23 -19
- package/dist/rules/cli.js.map +1 -1
- package/dist/rules/confidence.d.ts +1 -1
- package/dist/rules/confidence.js +5 -5
- package/dist/rules/confidence.js.map +1 -1
- package/dist/rules/dead-code.d.ts +10 -0
- package/dist/rules/dead-code.js +75 -0
- package/dist/rules/dead-code.js.map +1 -0
- package/dist/rules/dead-logic.js +35 -31
- package/dist/rules/dead-logic.js.map +1 -1
- package/dist/rules/express.d.ts +2 -1
- package/dist/rules/express.js +380 -126
- package/dist/rules/express.js.map +1 -1
- package/dist/rules/fastapi.js +53 -19
- package/dist/rules/fastapi.js.map +1 -1
- package/dist/rules/ground-layer.js +3 -3
- package/dist/rules/ground-layer.js.map +1 -1
- package/dist/rules/index.js +574 -105
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/ink.js +9 -8
- package/dist/rules/ink.js.map +1 -1
- package/dist/rules/kern-source.js +202 -63
- package/dist/rules/kern-source.js.map +1 -1
- package/dist/rules/nextjs.js +88 -33
- package/dist/rules/nextjs.js.map +1 -1
- package/dist/rules/null-safety.js +52 -26
- package/dist/rules/null-safety.js.map +1 -1
- package/dist/rules/nuxt.js +24 -29
- package/dist/rules/nuxt.js.map +1 -1
- package/dist/rules/react.js +355 -69
- package/dist/rules/react.js.map +1 -1
- package/dist/rules/security-v2.js +71 -57
- package/dist/rules/security-v2.js.map +1 -1
- package/dist/rules/security-v3.js.map +1 -1
- package/dist/rules/security-v4.js +54 -27
- package/dist/rules/security-v4.js.map +1 -1
- package/dist/rules/security.js +35 -5
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/terminal.js +17 -5
- package/dist/rules/terminal.js.map +1 -1
- package/dist/rules/vue.js +162 -107
- package/dist/rules/vue.js.map +1 -1
- package/dist/semantic-diff.d.ts +52 -0
- package/dist/semantic-diff.js +342 -0
- package/dist/semantic-diff.js.map +1 -0
- package/dist/spec-checker.js +11 -10
- package/dist/spec-checker.js.map +1 -1
- package/dist/suppression/apply-suppression.d.ts +2 -3
- package/dist/suppression/apply-suppression.js +3 -3
- package/dist/suppression/apply-suppression.js.map +1 -1
- package/dist/suppression/index.d.ts +2 -2
- package/dist/suppression/index.js +1 -1
- package/dist/suppression/index.js.map +1 -1
- package/dist/suppression/parse-directives.d.ts +1 -1
- package/dist/suppression/parse-directives.js +9 -4
- package/dist/suppression/parse-directives.js.map +1 -1
- package/dist/taint-ast.d.ts +20 -0
- package/dist/taint-ast.js +427 -0
- package/dist/taint-ast.js.map +1 -0
- package/dist/taint-crossfile.d.ts +28 -0
- package/dist/taint-crossfile.js +174 -0
- package/dist/taint-crossfile.js.map +1 -0
- package/dist/taint-findings.d.ts +17 -0
- package/dist/taint-findings.js +131 -0
- package/dist/taint-findings.js.map +1 -0
- package/dist/taint-regex.d.ts +61 -0
- package/dist/taint-regex.js +379 -0
- package/dist/taint-regex.js.map +1 -0
- package/dist/taint-types.d.ts +128 -0
- package/dist/taint-types.js +174 -0
- package/dist/taint-types.js.map +1 -0
- package/dist/taint.d.ts +13 -107
- package/dist/taint.js +16 -1067
- package/dist/taint.js.map +1 -1
- package/dist/template-detector.d.ts +2 -2
- package/dist/template-detector.js +11 -16
- package/dist/template-detector.js.map +1 -1
- package/dist/types.d.ts +35 -0
- package/dist/types.js.map +1 -1
- 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 {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
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 {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
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 {
|
|
28
|
-
import {
|
|
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,
|
|
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 {
|
|
44
|
-
export {
|
|
45
|
-
|
|
46
|
-
export {
|
|
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 {
|
|
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,
|
|
70
|
-
|
|
71
|
-
export {
|
|
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
|
|
74
|
-
export { clearReviewCache };
|
|
84
|
+
import { clearReviewCache, computeCacheKey, reviewCache } from './cache.js';
|
|
75
85
|
// Spec checker — .kern contract vs .ts implementation
|
|
76
|
-
export { checkSpec, checkSpecFiles,
|
|
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
|
-
|
|
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
|
|
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',
|
|
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',
|
|
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), {
|
|
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
|
-
|
|
302
|
-
|
|
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,
|
|
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
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
if (
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
510
|
-
r = /^['"]use client['"];?\s*$/m.test(src.substring(0, 200));
|
|
613
|
+
cgProject.addSourceFileAtPath(gf.path);
|
|
511
614
|
}
|
|
512
615
|
catch {
|
|
513
|
-
|
|
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() &&
|
|
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')) &&
|
|
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')) {
|