@kernlang/review 3.3.8 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache.js +1 -1
- package/dist/call-graph.d.ts +10 -0
- package/dist/call-graph.js +138 -9
- package/dist/call-graph.js.map +1 -1
- package/dist/concept-rules/auth-drift.js +2 -0
- package/dist/concept-rules/auth-drift.js.map +1 -1
- package/dist/concept-rules/auth-propagation-drift.d.ts +10 -0
- package/dist/concept-rules/auth-propagation-drift.js +85 -0
- package/dist/concept-rules/auth-propagation-drift.js.map +1 -0
- package/dist/concept-rules/body-shape-drift.d.ts +32 -0
- package/dist/concept-rules/body-shape-drift.js +98 -0
- package/dist/concept-rules/body-shape-drift.js.map +1 -0
- package/dist/concept-rules/contract-drift.js +3 -1
- package/dist/concept-rules/contract-drift.js.map +1 -1
- package/dist/concept-rules/contract-method-drift.js +2 -0
- package/dist/concept-rules/contract-method-drift.js.map +1 -1
- package/dist/concept-rules/cross-stack-utils.d.ts +24 -0
- package/dist/concept-rules/cross-stack-utils.js +123 -29
- package/dist/concept-rules/cross-stack-utils.js.map +1 -1
- package/dist/concept-rules/index.d.ts +4 -2
- package/dist/concept-rules/index.js +22 -3
- package/dist/concept-rules/index.js.map +1 -1
- package/dist/concept-rules/mutation-without-idempotency.d.ts +10 -0
- package/dist/concept-rules/mutation-without-idempotency.js +47 -0
- package/dist/concept-rules/mutation-without-idempotency.js.map +1 -0
- package/dist/concept-rules/request-validation-drift.d.ts +11 -0
- package/dist/concept-rules/request-validation-drift.js +99 -0
- package/dist/concept-rules/request-validation-drift.js.map +1 -0
- package/dist/concept-rules/root-cause.d.ts +4 -0
- package/dist/concept-rules/root-cause.js +31 -0
- package/dist/concept-rules/root-cause.js.map +1 -0
- package/dist/concept-rules/unbounded-collection-query.d.ts +10 -0
- package/dist/concept-rules/unbounded-collection-query.js +58 -0
- package/dist/concept-rules/unbounded-collection-query.js.map +1 -0
- package/dist/concept-rules/unhandled-api-error-shape.d.ts +10 -0
- package/dist/concept-rules/unhandled-api-error-shape.js +59 -0
- package/dist/concept-rules/unhandled-api-error-shape.js.map +1 -0
- package/dist/default-export.d.ts +41 -0
- package/dist/default-export.js +76 -0
- package/dist/default-export.js.map +1 -0
- package/dist/eval.d.ts +67 -0
- package/dist/eval.js +177 -0
- package/dist/eval.js.map +1 -0
- package/dist/external-tools.js +52 -3
- package/dist/external-tools.js.map +1 -1
- package/dist/file-context.js +32 -13
- package/dist/file-context.js.map +1 -1
- package/dist/file-role.d.ts +6 -0
- package/dist/file-role.js +27 -0
- package/dist/file-role.js.map +1 -1
- package/dist/framework-seeds.d.ts +46 -0
- package/dist/framework-seeds.js +245 -0
- package/dist/framework-seeds.js.map +1 -0
- package/dist/git-env.d.ts +1 -0
- package/dist/git-env.js +25 -0
- package/dist/git-env.js.map +1 -0
- package/dist/graph.js +246 -21
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.js +314 -96
- package/dist/index.js.map +1 -1
- package/dist/mappers/ts-concepts.js +730 -1
- package/dist/mappers/ts-concepts.js.map +1 -1
- package/dist/path-canonical.d.ts +34 -0
- package/dist/path-canonical.js +85 -0
- package/dist/path-canonical.js.map +1 -0
- package/dist/policy.d.ts +22 -0
- package/dist/policy.js +47 -0
- package/dist/policy.js.map +1 -0
- package/dist/project-context.d.ts +135 -0
- package/dist/project-context.js +563 -0
- package/dist/project-context.js.map +1 -0
- package/dist/public-api.d.ts +21 -0
- package/dist/public-api.js +17 -2
- package/dist/public-api.js.map +1 -1
- package/dist/python-fallback.d.ts +2 -0
- package/dist/python-fallback.js +506 -0
- package/dist/python-fallback.js.map +1 -0
- package/dist/reporter.js +106 -1
- package/dist/reporter.js.map +1 -1
- package/dist/rule-quality.d.ts +58 -0
- package/dist/rule-quality.js +357 -0
- package/dist/rule-quality.js.map +1 -0
- package/dist/rules/base.js +21 -3
- package/dist/rules/base.js.map +1 -1
- package/dist/rules/dead-code.d.ts +2 -2
- package/dist/rules/dead-code.js +88 -4
- package/dist/rules/dead-code.js.map +1 -1
- package/dist/rules/index.d.ts +22 -0
- package/dist/rules/index.js +72 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/kern-source.d.ts +4 -0
- package/dist/rules/kern-source.js +184 -0
- package/dist/rules/kern-source.js.map +1 -1
- package/dist/rules/react.js +52 -3
- package/dist/rules/react.js.map +1 -1
- package/dist/rules/suggest-kern-primitive.js +0 -1
- package/dist/rules/suggest-kern-primitive.js.map +1 -1
- package/dist/semantic-diff.js +2 -0
- package/dist/semantic-diff.js.map +1 -1
- package/dist/suppression/apply-suppression.js +2 -0
- package/dist/suppression/apply-suppression.js.map +1 -1
- package/dist/suppression/parse-directives.d.ts +13 -5
- package/dist/suppression/parse-directives.js +62 -8
- package/dist/suppression/parse-directives.js.map +1 -1
- package/dist/suppression/types.d.ts +9 -0
- package/dist/suppression/types.js +6 -1
- package/dist/suppression/types.js.map +1 -1
- package/dist/taint-crossfile.js +15 -8
- package/dist/taint-crossfile.js.map +1 -1
- package/dist/telemetry.d.ts +126 -0
- package/dist/telemetry.js +303 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/types.d.ts +172 -2
- package/dist/types.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -11,32 +11,46 @@
|
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
12
|
import { countTokens, parseWithDiagnostics, serializeIR } from '@kernlang/core';
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
14
|
-
import { dirname, join, relative } from 'path';
|
|
14
|
+
import { dirname, join, relative, sep } from 'path';
|
|
15
15
|
import { Project } from 'ts-morph';
|
|
16
16
|
// This module compiles to ESM (`type: "module"`), so the runtime has no `require`.
|
|
17
17
|
// `@kernlang/review-python` is an optional peer — we load it on demand with a
|
|
18
18
|
// createRequire shim. Without this shim the dynamic load throws
|
|
19
19
|
// `ReferenceError: require is not defined`, the catch swallows the error, and
|
|
20
|
-
//
|
|
21
|
-
//
|
|
20
|
+
// Python files use a precise optional mapper when present, then degrade to the
|
|
21
|
+
// built-in fallback extractor so fullstack wedge rules still run in npm installs
|
|
22
|
+
// that do not ship @kernlang/review-python.
|
|
22
23
|
const moduleRequire = createRequire(import.meta.url);
|
|
24
|
+
function optionalPackageSpecifier(scope, name) {
|
|
25
|
+
return `${scope}/${name}`;
|
|
26
|
+
}
|
|
27
|
+
function optionalLocalSpecifier(...parts) {
|
|
28
|
+
return parts.join('/');
|
|
29
|
+
}
|
|
30
|
+
function requireOptionalModule(specifier) {
|
|
31
|
+
return moduleRequire(specifier);
|
|
32
|
+
}
|
|
23
33
|
import { buildCallGraph } from './call-graph.js';
|
|
24
34
|
import { runConceptRules } from './concept-rules/index.js';
|
|
25
35
|
import { structuralDiff } from './differ.js';
|
|
26
36
|
import { runTSCDiagnostics } from './external-tools.js';
|
|
27
37
|
import { buildFileContextMap } from './file-context.js';
|
|
28
|
-
import { classifyFileRole } from './file-role.js';
|
|
38
|
+
import { classifyFileRole, classifyFileRoleByPath } from './file-role.js';
|
|
29
39
|
import { resolveImportGraph } from './graph.js';
|
|
30
40
|
import { createInMemoryProject, findTsConfig, inferFromSourceFile } from './inferrer.js';
|
|
31
41
|
import { flattenIR, lintKernIR } from './kern-lint.js';
|
|
32
42
|
import { extractTsConcepts } from './mappers/ts-concepts.js';
|
|
33
43
|
import { mineNorms } from './norm-miner.js';
|
|
34
44
|
import { synthesizeObligations } from './obligations.js';
|
|
45
|
+
import { canonicalize } from './path-canonical.js';
|
|
46
|
+
import { findProjectRoot, getProjectContext, isPathIgnored, isReviewable, } from './project-context.js';
|
|
35
47
|
import { buildPublicApiMap, expandPublicApiThroughReExports } from './public-api.js';
|
|
48
|
+
import { extractPythonConceptsFallback } from './python-fallback.js';
|
|
36
49
|
import { runQualityRules } from './quality-rules.js';
|
|
37
50
|
import { assignDefaultConfidence, calculateStats, sortAndDedup, sortFindings } from './reporter.js';
|
|
38
51
|
import { debugDetail, ReviewHealthBuilder } from './review-health.js';
|
|
39
52
|
import { loadBuiltinNativeRules, loadNativeRules } from './rule-loader.js';
|
|
53
|
+
import { applyOverlapCalibration, applyRoleAwareConfidence, applyRuleQualityCalibration } from './rule-quality.js';
|
|
40
54
|
import { lintConfidenceGraph, lintMultiFileConfidenceGraph } from './rules/confidence.js';
|
|
41
55
|
import { crossFileAsyncRule, deadExportRule } from './rules/dead-code.js';
|
|
42
56
|
import { runFastapiConceptRules } from './rules/fastapi.js';
|
|
@@ -57,14 +71,84 @@ import { buildConfidenceGraph, computeConfidenceSummary, serializeGraph } from '
|
|
|
57
71
|
import { applySuppression } from './suppression/index.js';
|
|
58
72
|
import { analyzeTaint, analyzeTaintCrossFile, crossFileTaintToFindings, taintToFindings } from './taint.js';
|
|
59
73
|
import { createFingerprint } from './types.js';
|
|
74
|
+
const TYPESCRIPT_CONCEPT_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);
|
|
75
|
+
let cachedPythonExtractor;
|
|
76
|
+
function loadPythonConceptExtractor() {
|
|
77
|
+
if (cachedPythonExtractor)
|
|
78
|
+
return cachedPythonExtractor;
|
|
79
|
+
const failures = [];
|
|
80
|
+
const candidates = [
|
|
81
|
+
optionalPackageSpecifier('@kernlang', 'review-python'),
|
|
82
|
+
optionalLocalSpecifier('..', '..', 'review-python', 'dist', 'index.js'),
|
|
83
|
+
];
|
|
84
|
+
for (const candidate of candidates) {
|
|
85
|
+
try {
|
|
86
|
+
const mod = requireOptionalModule(candidate);
|
|
87
|
+
if (typeof mod.extractPythonConcepts === 'function') {
|
|
88
|
+
cachedPythonExtractor = { extractor: mod.extractPythonConcepts, usingFallback: false };
|
|
89
|
+
return cachedPythonExtractor;
|
|
90
|
+
}
|
|
91
|
+
failures.push(`${candidate}: extractPythonConcepts export missing`);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
failures.push(`${candidate}: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
cachedPythonExtractor = {
|
|
98
|
+
extractor: extractPythonConceptsFallback,
|
|
99
|
+
usingFallback: true,
|
|
100
|
+
failureDetail: failures.join('\n'),
|
|
101
|
+
};
|
|
102
|
+
return cachedPythonExtractor;
|
|
103
|
+
}
|
|
104
|
+
function extensionOf(filePath) {
|
|
105
|
+
const dot = filePath.lastIndexOf('.');
|
|
106
|
+
return dot === -1 ? '' : filePath.slice(dot);
|
|
107
|
+
}
|
|
108
|
+
function isTypeScriptConceptFile(filePath) {
|
|
109
|
+
return TYPESCRIPT_CONCEPT_EXTENSIONS.has(extensionOf(filePath));
|
|
110
|
+
}
|
|
111
|
+
function isConceptMap(value) {
|
|
112
|
+
if (!value || typeof value !== 'object')
|
|
113
|
+
return false;
|
|
114
|
+
const candidate = value;
|
|
115
|
+
return typeof candidate.filePath === 'string' && Array.isArray(candidate.nodes) && Array.isArray(candidate.edges);
|
|
116
|
+
}
|
|
117
|
+
function normalizeExternalConcepts(externalConcepts) {
|
|
118
|
+
const normalized = new Map();
|
|
119
|
+
if (!externalConcepts)
|
|
120
|
+
return normalized;
|
|
121
|
+
for (const [key, value] of externalConcepts) {
|
|
122
|
+
if (!isConceptMap(value))
|
|
123
|
+
continue;
|
|
124
|
+
normalized.set(key, value);
|
|
125
|
+
}
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
function extractConceptsFromSource(source, filePath, health, tsProject) {
|
|
129
|
+
if (isTypeScriptConceptFile(filePath)) {
|
|
130
|
+
const project = tsProject ?? createInMemoryProject();
|
|
131
|
+
const sf = project.createSourceFile(filePath, source, { overwrite: true });
|
|
132
|
+
return extractTsConcepts(sf, filePath);
|
|
133
|
+
}
|
|
134
|
+
if (filePath.endsWith('.py')) {
|
|
135
|
+
const pythonExtractor = loadPythonConceptExtractor();
|
|
136
|
+
if (pythonExtractor.usingFallback) {
|
|
137
|
+
health?.noteKind('concept-extraction', 'fallback', 'Using built-in Python fallback extractor — install/fix @kernlang/review-python for tree-sitter precision', debugDetail(pythonExtractor.failureDetail));
|
|
138
|
+
}
|
|
139
|
+
return pythonExtractor.extractor(source, filePath);
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
60
143
|
export { buildCallGraph } from './call-graph.js';
|
|
61
144
|
export { runConceptRules } from './concept-rules/index.js';
|
|
62
145
|
// Confidence layer
|
|
63
146
|
export { buildConfidenceGraph, buildMultiFileConfidenceGraph, computeConfidenceSummary, parseConfidence, propagateConfidence, resolveBaseConfidence, serializeGraph, } from './confidence.js';
|
|
64
147
|
export { structuralDiff } from './differ.js';
|
|
148
|
+
export { evaluateReviewReports, formatReviewEvalSummary, normalizeReviewEvalManifest, summarizeReviewEvalResults, } from './eval.js';
|
|
65
149
|
export { linkToNodes, runESLint, runTSCDiagnostics, runTSCDiagnosticsFromPaths } from './external-tools.js';
|
|
66
150
|
export { buildFileContextMap, clearFileContextCache } from './file-context.js';
|
|
67
|
-
export { classifyFileRole } from './file-role.js';
|
|
151
|
+
export { classifyFileRole, classifyFileRoleByPath } from './file-role.js';
|
|
68
152
|
export { resolveImportGraph } from './graph.js';
|
|
69
153
|
export { findTsConfig, inferFromFile, inferFromSource } from './inferrer.js';
|
|
70
154
|
// KERN-IR lint pipeline (ground layer)
|
|
@@ -76,10 +160,12 @@ export { extractTsConcepts } from './mappers/ts-concepts.js';
|
|
|
76
160
|
// Norm mining + obligations
|
|
77
161
|
export { mineNorms } from './norm-miner.js';
|
|
78
162
|
export { obligationsFromNorms, obligationsFromStructure, synthesizeObligations } from './obligations.js';
|
|
163
|
+
export { applyReviewPolicyDefaults, getReviewPolicyProfile, inferReviewPolicy, } from './policy.js';
|
|
79
164
|
export { buildPublicApiMap, EMPTY_PUBLIC_API, expandPublicApiThroughReExports, isPublicApi, resolvePackageEntryFiles, resolveSpecifierToSrc, } from './public-api.js';
|
|
80
165
|
export { runQualityRules } from './quality-rules.js';
|
|
81
166
|
export { assignDefaultConfidence, calculateStats, checkEnforcement, dedup, formatEnforcement, formatReport, formatReportJSON, formatSARIF, formatSARIFWithMetadata, formatSARIFWithSuppressions, formatSummary, sortAndDedup, sortFindings, } from './reporter.js';
|
|
82
167
|
export { debugDetail, ReviewHealthBuilder } from './review-health.js';
|
|
168
|
+
export { applyRuleQualityCalibration, applyRuleSupersession, getRuleQualityProfile, isRulePromotedForCi, validateRuleQualityRegistry, } from './rule-quality.js';
|
|
83
169
|
export { CONFIDENCE_RULES, lintConfidenceGraph, lintMultiFileConfidenceGraph } from './rules/confidence.js';
|
|
84
170
|
export { actionMissingIdempotent, assumeLowTrust, branchNonExhaustive, collectUnbounded, expectRangeInverted, GROUND_LAYER_RULES, guardWithoutElse, reasonWithoutBasis, } from './rules/ground-layer.js';
|
|
85
171
|
export { getRuleRegistry } from './rules/index.js';
|
|
@@ -92,6 +178,7 @@ export { computeSemanticDiff, computeSemanticDiffFromSource, formatSemanticDiff,
|
|
|
92
178
|
export { applySuppression, configDirectives, isConceptRule, parseDirectives } from './suppression/index.js';
|
|
93
179
|
// Taint tracking (Phase 2 + cross-file)
|
|
94
180
|
export { analyzeTaint, analyzeTaintCrossFile, buildExportMap, buildImportMap, crossFileTaintToFindings, isSanitizerSufficient, taintToFindings, } from './taint.js';
|
|
181
|
+
export { buildReviewTelemetry, formatReviewTelemetrySummary, parseReviewTelemetryJsonl, readReviewTelemetrySnapshots, summarizeReviewTelemetry, writeReviewTelemetrySnapshot, } from './telemetry.js';
|
|
95
182
|
export { detectTemplates } from './template-detector.js';
|
|
96
183
|
export { createFingerprint } from './types.js';
|
|
97
184
|
// Cache (Phase 0)
|
|
@@ -251,32 +338,16 @@ export function extractConceptsForGraph(filePaths) {
|
|
|
251
338
|
// is not cheap; on a whole-repo cache-prime (hundreds of files) the
|
|
252
339
|
// per-file alloc adds real latency we don't want on a push webhook.
|
|
253
340
|
let tsProject = null;
|
|
254
|
-
let extractPythonConcepts;
|
|
255
341
|
for (const filePath of filePaths) {
|
|
342
|
+
if (!isReviewableFile(filePath) || !existsSync(filePath))
|
|
343
|
+
continue;
|
|
256
344
|
try {
|
|
257
345
|
const source = readFileSync(filePath, 'utf-8');
|
|
258
|
-
if (filePath
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
tsProject = createInMemoryProject();
|
|
264
|
-
const sf = tsProject.createSourceFile(filePath, source);
|
|
265
|
-
out.set(filePath, extractTsConcepts(sf, filePath));
|
|
266
|
-
}
|
|
267
|
-
else if (filePath.endsWith('.py')) {
|
|
268
|
-
if (extractPythonConcepts === undefined) {
|
|
269
|
-
try {
|
|
270
|
-
extractPythonConcepts = moduleRequire('@kernlang/review-python').extractPythonConcepts;
|
|
271
|
-
}
|
|
272
|
-
catch {
|
|
273
|
-
extractPythonConcepts = null;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (extractPythonConcepts) {
|
|
277
|
-
out.set(filePath, extractPythonConcepts(source, filePath));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
346
|
+
if (isTypeScriptConceptFile(filePath) && !tsProject)
|
|
347
|
+
tsProject = createInMemoryProject();
|
|
348
|
+
const concepts = extractConceptsFromSource(source, filePath, undefined, tsProject ?? undefined);
|
|
349
|
+
if (concepts)
|
|
350
|
+
out.set(filePath, concepts);
|
|
280
351
|
}
|
|
281
352
|
catch {
|
|
282
353
|
// Best-effort — caller sees which files made it into the map.
|
|
@@ -460,7 +531,7 @@ function reviewSourceInternal(source, filePath, config, project, sourceFile) {
|
|
|
460
531
|
// Phase 6: Concept extraction + concept rules (universal, cross-language)
|
|
461
532
|
const emptyConcepts = { filePath, language: 'typescript', nodes: [], edges: [], extractorVersion: '0' };
|
|
462
533
|
const concepts = safePhase('concepts', () => extractTsConcepts(sourceFile, filePath), emptyConcepts);
|
|
463
|
-
allFindings.push(...safePhase('concept-rules', () => runConceptRules(concepts, filePath), []));
|
|
534
|
+
allFindings.push(...safePhase('concept-rules', () => runConceptRules(concepts, filePath, undefined, undefined, config), []));
|
|
464
535
|
// Phase 7: KERN-IR lint (ground layer + confidence rules on inferred nodes)
|
|
465
536
|
const irNodes = inferred.map((r) => r.node);
|
|
466
537
|
const groundFindings = safePhase('ground-lint', () => lintKernIR(irNodes, GROUND_LAYER_RULES), []);
|
|
@@ -510,8 +581,18 @@ function reviewSourceInternal(source, filePath, config, project, sourceFile) {
|
|
|
510
581
|
}
|
|
511
582
|
// Merge, dedup, sort — single shared utility
|
|
512
583
|
const dedupedFindings = sortAndDedup(allFindings);
|
|
513
|
-
// Assign calibrated confidence scores to all findings
|
|
584
|
+
// Assign calibrated confidence scores to all findings.
|
|
585
|
+
// Order matters: assign-defaults → role-aware → overlap → rule-quality. The
|
|
586
|
+
// rule-quality pass flips the per-finding `calibrated` flag last so that
|
|
587
|
+
// graph-mode rerun on a union of findings does not compound multipliers.
|
|
514
588
|
assignDefaultConfidence(dedupedFindings);
|
|
589
|
+
applyRoleAwareConfidence(dedupedFindings, fileRole, config);
|
|
590
|
+
const projectRoot = findProjectRoot(dirname(filePath));
|
|
591
|
+
if (projectRoot) {
|
|
592
|
+
const projectCtx = getProjectContext(projectRoot);
|
|
593
|
+
applyOverlapCalibration(dedupedFindings, projectCtx.external, config);
|
|
594
|
+
}
|
|
595
|
+
applyRuleQualityCalibration(dedupedFindings, config);
|
|
515
596
|
// Apply suppression (inline comments + config disabledRules)
|
|
516
597
|
const suppression = applySuppression(dedupedFindings, source, filePath, config, config?.strict ?? false);
|
|
517
598
|
const findings = sortAndDedup(suppression.findings);
|
|
@@ -653,6 +734,8 @@ export function reviewKernSource(source, filePath = 'input.kern', _config) {
|
|
|
653
734
|
});
|
|
654
735
|
const dedupedFindings = sortAndDedup(allFindings);
|
|
655
736
|
assignDefaultConfidence(dedupedFindings);
|
|
737
|
+
applyRoleAwareConfidence(dedupedFindings, classifyFileRoleByPath(filePath), _config);
|
|
738
|
+
applyRuleQualityCalibration(dedupedFindings, _config);
|
|
656
739
|
const suppression = applySuppression(dedupedFindings, source, filePath, _config, _config?.strict ?? false);
|
|
657
740
|
const findings = sortAndDedup(suppression.findings);
|
|
658
741
|
const kernTokens = countTokens(source);
|
|
@@ -683,11 +766,14 @@ export function reviewPythonSource(source, filePath = 'input.py', config) {
|
|
|
683
766
|
const totalLines = source.split('\n').length;
|
|
684
767
|
// Python: concept extraction + concept rules only
|
|
685
768
|
let conceptFindings = [];
|
|
769
|
+
const health = new ReviewHealthBuilder();
|
|
686
770
|
try {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
771
|
+
const pythonExtractor = loadPythonConceptExtractor();
|
|
772
|
+
if (pythonExtractor.usingFallback) {
|
|
773
|
+
health.noteKind('concept-extraction', 'fallback', 'Using built-in Python fallback extractor — install/fix @kernlang/review-python for tree-sitter precision', debugDetail(pythonExtractor.failureDetail));
|
|
774
|
+
}
|
|
775
|
+
const concepts = pythonExtractor.extractor(source, filePath);
|
|
776
|
+
conceptFindings = runConceptRules(concepts, filePath, undefined, undefined, config);
|
|
691
777
|
if (config?.target === 'fastapi') {
|
|
692
778
|
conceptFindings.push(...runFastapiConceptRules(concepts, filePath, source));
|
|
693
779
|
}
|
|
@@ -704,30 +790,33 @@ export function reviewPythonSource(source, filePath = 'input.py', config) {
|
|
|
704
790
|
}
|
|
705
791
|
catch (err) {
|
|
706
792
|
if (process.env.KERN_DEBUG)
|
|
707
|
-
console.error(`python
|
|
708
|
-
// @kernlang/review-python not installed — skip concept extraction
|
|
793
|
+
console.error(`python concept extraction failed: ${err.message}`);
|
|
709
794
|
conceptFindings = [
|
|
710
795
|
{
|
|
711
796
|
source: 'kern',
|
|
712
|
-
ruleId: '
|
|
797
|
+
ruleId: 'python-concept-extraction-failed',
|
|
713
798
|
severity: 'info',
|
|
714
799
|
category: 'structure',
|
|
715
|
-
message: '
|
|
800
|
+
message: 'Python concept extraction failed — Python findings may be incomplete',
|
|
716
801
|
primarySpan: { file: filePath, startLine: 1, startCol: 1, endLine: 1, endCol: 1 },
|
|
717
|
-
fingerprint: '
|
|
802
|
+
fingerprint: 'python-concept-extraction-failed-0',
|
|
718
803
|
},
|
|
719
804
|
];
|
|
720
805
|
}
|
|
721
806
|
const dedupedFindings = sortAndDedup(conceptFindings);
|
|
722
807
|
assignDefaultConfidence(dedupedFindings);
|
|
808
|
+
applyRoleAwareConfidence(dedupedFindings, classifyFileRoleByPath(filePath), config);
|
|
809
|
+
applyRuleQualityCalibration(dedupedFindings, config);
|
|
723
810
|
const suppression = applySuppression(dedupedFindings, source, filePath, config, config?.strict ?? false);
|
|
724
811
|
const findings = sortAndDedup(suppression.findings);
|
|
812
|
+
const reviewHealth = health.build();
|
|
725
813
|
return {
|
|
726
814
|
filePath,
|
|
727
815
|
inferred: [],
|
|
728
816
|
templateMatches: [],
|
|
729
817
|
findings,
|
|
730
818
|
...(suppression.suppressed.length > 0 ? { suppressedFindings: sortAndDedup(suppression.suppressed) } : {}),
|
|
819
|
+
...(reviewHealth ? { health: reviewHealth } : {}),
|
|
731
820
|
stats: {
|
|
732
821
|
totalLines,
|
|
733
822
|
coveredLines: 0,
|
|
@@ -741,10 +830,15 @@ export function reviewPythonSource(source, filePath = 'input.py', config) {
|
|
|
741
830
|
}
|
|
742
831
|
/**
|
|
743
832
|
* Review all .ts/.tsx/.py files in a directory.
|
|
833
|
+
*
|
|
834
|
+
* Honors the project's .gitignore by skipping untracked-and-ignored files —
|
|
835
|
+
* tracked artifacts are always reviewed even when their directory matches
|
|
836
|
+
* .gitignore, so published `dist/*.gen.ts` and similar do not slip through.
|
|
744
837
|
*/
|
|
745
838
|
export function reviewDirectory(dirPath, recursive = false, config) {
|
|
746
839
|
const reports = [];
|
|
747
|
-
const
|
|
840
|
+
const ctx = getProjectContext(dirPath);
|
|
841
|
+
const files = collectReviewableFiles(dirPath, recursive, ctx);
|
|
748
842
|
for (const file of files) {
|
|
749
843
|
try {
|
|
750
844
|
reports.push(reviewFile(file, config));
|
|
@@ -760,7 +854,7 @@ export function reviewDirectory(dirPath, recursive = false, config) {
|
|
|
760
854
|
* Entry files get normal findings, upstream dependencies get origin='upstream'.
|
|
761
855
|
*/
|
|
762
856
|
export function reviewGraph(entryFiles, config, graphOptions) {
|
|
763
|
-
const graph = resolveImportGraph(entryFiles, graphOptions);
|
|
857
|
+
const graph = graphOptions?.precomputedGraph ?? resolveImportGraph(entryFiles, graphOptions);
|
|
764
858
|
const entrySet = new Set(graph.entryFiles);
|
|
765
859
|
const reports = [];
|
|
766
860
|
// Graph-wide subsystem status — one entry per (subsystem, kind) across the whole run.
|
|
@@ -860,35 +954,13 @@ export function reviewGraph(entryFiles, config, graphOptions) {
|
|
|
860
954
|
// Cross-file concept analysis — re-run concept rules with full graph context
|
|
861
955
|
// This fixes false positives where guards are in middleware files and effects in handlers
|
|
862
956
|
const allConcepts = new Map();
|
|
863
|
-
// Cache the optional Python mapper: require() it once per graph run instead
|
|
864
|
-
// of per-file, and remember if it's absent so we don't pay the throw cost.
|
|
865
|
-
let extractPythonConcepts;
|
|
866
957
|
for (const report of reports) {
|
|
867
958
|
const filePath = report.filePath;
|
|
868
959
|
try {
|
|
869
960
|
const source = readFileSync(filePath, 'utf-8');
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
allConcepts.set(filePath, extractTsConcepts(sf, filePath));
|
|
874
|
-
}
|
|
875
|
-
else if (filePath.endsWith('.py')) {
|
|
876
|
-
// Python concept seeding is what powers the fullstack wedge rules
|
|
877
|
-
// (contract-drift, untyped-api-response, tainted-across-wire) in graph
|
|
878
|
-
// mode. Without this branch, `allConcepts` only contained TS entries
|
|
879
|
-
// and the rules silently found nothing on cross-stack repos.
|
|
880
|
-
if (extractPythonConcepts === undefined) {
|
|
881
|
-
try {
|
|
882
|
-
extractPythonConcepts = moduleRequire('@kernlang/review-python').extractPythonConcepts;
|
|
883
|
-
}
|
|
884
|
-
catch {
|
|
885
|
-
extractPythonConcepts = null;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
if (extractPythonConcepts) {
|
|
889
|
-
allConcepts.set(filePath, extractPythonConcepts(source, filePath));
|
|
890
|
-
}
|
|
891
|
-
}
|
|
961
|
+
const concepts = extractConceptsFromSource(source, filePath, graphHealth);
|
|
962
|
+
if (concepts)
|
|
963
|
+
allConcepts.set(filePath, concepts);
|
|
892
964
|
}
|
|
893
965
|
catch (err) {
|
|
894
966
|
// Per-file failure — record once at graph level (builder dedupes), then move on.
|
|
@@ -918,7 +990,7 @@ export function reviewGraph(entryFiles, config, graphOptions) {
|
|
|
918
990
|
if (config.externalConcepts.size > MAX_EXTERNAL_CONCEPTS) {
|
|
919
991
|
throw new Error(`reviewGraph: externalConcepts size ${config.externalConcepts.size} exceeds cap ${MAX_EXTERNAL_CONCEPTS}`);
|
|
920
992
|
}
|
|
921
|
-
for (const [path, cm] of config.externalConcepts) {
|
|
993
|
+
for (const [path, cm] of normalizeExternalConcepts(config.externalConcepts)) {
|
|
922
994
|
if (!allConcepts.has(path))
|
|
923
995
|
allConcepts.set(path, cm);
|
|
924
996
|
}
|
|
@@ -927,10 +999,15 @@ export function reviewGraph(entryFiles, config, graphOptions) {
|
|
|
927
999
|
// Concept rule IDs to replace (remove per-file findings, add cross-file ones)
|
|
928
1000
|
const CONCEPT_RULE_IDS = new Set([
|
|
929
1001
|
'boundary-mutation',
|
|
1002
|
+
'auth-propagation-drift',
|
|
930
1003
|
'ignored-error',
|
|
931
1004
|
'missing-response-model',
|
|
1005
|
+
'mutation-without-idempotency',
|
|
1006
|
+
'request-validation-drift',
|
|
932
1007
|
'sync-handler-does-io',
|
|
1008
|
+
'unbounded-collection-query',
|
|
933
1009
|
'unguarded-effect',
|
|
1010
|
+
'unhandled-api-error-shape',
|
|
934
1011
|
'unrecovered-effect',
|
|
935
1012
|
]);
|
|
936
1013
|
for (const report of reports) {
|
|
@@ -940,7 +1017,7 @@ export function reviewGraph(entryFiles, config, graphOptions) {
|
|
|
940
1017
|
// Remove per-file concept findings (they were run without cross-file context)
|
|
941
1018
|
report.findings = report.findings.filter((f) => !CONCEPT_RULE_IDS.has(f.ruleId));
|
|
942
1019
|
// Re-run concept rules with cross-file context
|
|
943
|
-
const crossFileConceptFindings = runConceptRules(concepts, report.filePath, allConcepts, graphImports);
|
|
1020
|
+
const crossFileConceptFindings = runConceptRules(concepts, report.filePath, allConcepts, graphImports, graphConfig);
|
|
944
1021
|
report.findings.push(...crossFileConceptFindings);
|
|
945
1022
|
}
|
|
946
1023
|
}
|
|
@@ -982,7 +1059,12 @@ export function reviewGraph(entryFiles, config, graphOptions) {
|
|
|
982
1059
|
});
|
|
983
1060
|
for (const gf of graph.files) {
|
|
984
1061
|
try {
|
|
985
|
-
cgProject
|
|
1062
|
+
// Seed cgProject with CANONICAL paths so its internal SourceFile
|
|
1063
|
+
// store collapses pnpm/symlink duplicates (red-team #9). This pairs
|
|
1064
|
+
// with graph.ts feeding canonical paths to its own ts-morph project,
|
|
1065
|
+
// so all subsequent ts-morph lookups (here AND in buildCallGraph)
|
|
1066
|
+
// run through the same canonical key space.
|
|
1067
|
+
cgProject.addSourceFileAtPath(gf.canonicalPath);
|
|
986
1068
|
}
|
|
987
1069
|
catch {
|
|
988
1070
|
/* skip unresolvable */
|
|
@@ -993,13 +1075,99 @@ export function reviewGraph(entryFiles, config, graphOptions) {
|
|
|
993
1075
|
// Build the public-API map once per run — package.json walk is the heavy bit.
|
|
994
1076
|
// Then propagate through re-export chains so curated barrels (Agon-style:
|
|
995
1077
|
// `export { foo } from './worker.js'`) carry public-API status upstream.
|
|
996
|
-
|
|
997
|
-
|
|
1078
|
+
// Pass canonical paths so the resulting `entryFiles` set keys match the
|
|
1079
|
+
// canonical paths that cgProject (and therefore callGraph.functions[i]
|
|
1080
|
+
// .filePath) uses. Otherwise red-team #9 sneaks back in: the call graph
|
|
1081
|
+
// sees `/private/var/...` while the public-API map contains `/var/...`,
|
|
1082
|
+
// and isPublicApi() misses every entry.
|
|
1083
|
+
const basePublicApi = buildPublicApiMap(graph.files.map((gf) => gf.canonicalPath), config?.publicApi);
|
|
1084
|
+
// Step 8: seeded files (Next.js conventions, package.json entries) may
|
|
1085
|
+
// legitimately live outside the BFS-walked graph — for example a lazy
|
|
1086
|
+
// route file beyond `maxDepth`, or a barrel re-exporting a helper that
|
|
1087
|
+
// no eager import in the graph reaches. Without this on-demand load,
|
|
1088
|
+
// expandPublicApiThroughReExports would silently drop those entries
|
|
1089
|
+
// (sourceFileFor returns undefined → loop skips). Loading on demand
|
|
1090
|
+
// costs only the seed entries we actually need, no extra graph work.
|
|
1091
|
+
const sourceFileFor = (path) => {
|
|
1092
|
+
if (!cgProject)
|
|
1093
|
+
return undefined;
|
|
1094
|
+
// Lookup must use canonical too — callers from buildPublicApiMap or
|
|
1095
|
+
// expandPublicApiThroughReExports may hand us a path captured in its
|
|
1096
|
+
// symlink form. Without canonicalisation this would miss a file that
|
|
1097
|
+
// cgProject has stored under its realpath.
|
|
1098
|
+
const canonical = canonicalize(path);
|
|
1099
|
+
const known = cgProject.getSourceFile(canonical);
|
|
1100
|
+
if (known)
|
|
1101
|
+
return known;
|
|
1102
|
+
try {
|
|
1103
|
+
return cgProject.addSourceFileAtPath(canonical);
|
|
1104
|
+
}
|
|
1105
|
+
catch {
|
|
1106
|
+
return undefined;
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
const publicApi = expandPublicApiThroughReExports(basePublicApi, sourceFileFor);
|
|
1110
|
+
// Step 10: blockers are passed alongside publicApi so dead-export's
|
|
1111
|
+
// step 9b cap+trail logic can consume them. Producer 1 (unresolved
|
|
1112
|
+
// named re-export with relative specifier) is wired in graph.ts and
|
|
1113
|
+
// rides on `graph.blockers`; Producer 2 (non-literal dynamic import)
|
|
1114
|
+
// is telemetry-only and surfaces via the unmappedDynamicImports counter
|
|
1115
|
+
// below. Empty list is a valid "no blockers detected" state — the rule
|
|
1116
|
+
// short-circuits without any change to existing semantics.
|
|
1117
|
+
const reachabilityBlockers = graph.blockers ?? [];
|
|
1118
|
+
// canonicalToDisplay maps canonical paths back to caller-facing forms
|
|
1119
|
+
// so findings emitted by the rules carry user-friendly paths even
|
|
1120
|
+
// though the rules themselves match against canonical fn.filePath.
|
|
1121
|
+
const canonicalToDisplay = new Map();
|
|
1122
|
+
for (const gf of graph.files) {
|
|
1123
|
+
canonicalToDisplay.set(gf.canonicalPath, gf.path);
|
|
1124
|
+
}
|
|
998
1125
|
for (const report of reports) {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1126
|
+
// The rules iterate callGraph.functions whose filePath is canonical
|
|
1127
|
+
// (cgProject seeded canonical → ts-morph getFilePath returns canonical).
|
|
1128
|
+
// report.filePath is the user-facing display form for diagnostics —
|
|
1129
|
+
// canonicalise at this boundary so the rule's `fn.filePath !== filePath`
|
|
1130
|
+
// filter matches.
|
|
1131
|
+
const ruleFilePath = canonicalize(report.filePath);
|
|
1132
|
+
const deadExportFindings = deadExportRule(callGraph, ruleFilePath, publicApi, reachabilityBlockers);
|
|
1133
|
+
const asyncFindings = crossFileAsyncRule(callGraph, ruleFilePath);
|
|
1134
|
+
// Map canonical paths in finding spans back to display so the user
|
|
1135
|
+
// sees `/var/...` not `/private/var/...` (and pnpm symlink paths
|
|
1136
|
+
// are expressed in their workspace-relative form).
|
|
1137
|
+
for (const f of [...deadExportFindings, ...asyncFindings]) {
|
|
1138
|
+
const display = canonicalToDisplay.get(f.primarySpan.file);
|
|
1139
|
+
if (display)
|
|
1140
|
+
f.primarySpan.file = display;
|
|
1141
|
+
if (f.relatedSpans) {
|
|
1142
|
+
for (const rs of f.relatedSpans) {
|
|
1143
|
+
const rsDisplay = canonicalToDisplay.get(rs.file);
|
|
1144
|
+
if (rsDisplay)
|
|
1145
|
+
rs.file = rsDisplay;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
report.findings.push(...deadExportFindings, ...asyncFindings);
|
|
1150
|
+
}
|
|
1151
|
+
// Surface graph-traversal telemetry. Two independent failure modes feed
|
|
1152
|
+
// a SINGLE health note here because ReviewHealthBuilder dedupes by
|
|
1153
|
+
// `(subsystem, kind)` — emitting two `'call-graph' / 'fallback'` notes
|
|
1154
|
+
// would silently drop the second one (Codex review caught this).
|
|
1155
|
+
// • Producer 2: non-literal `import(expr)` — dead-export findings on
|
|
1156
|
+
// those files may include FPs the cap mechanism cannot reach (no
|
|
1157
|
+
// target file/exportName to attach a blocker to).
|
|
1158
|
+
// • Malformed static imports — previously a silent catch swallowed
|
|
1159
|
+
// these; now counted and surfaced. KERN_DEBUG logs each.
|
|
1160
|
+
const unmappedDyn = graph.unmappedDynamicImports ?? 0;
|
|
1161
|
+
const malformed = graph.malformedImports ?? 0;
|
|
1162
|
+
if (unmappedDyn > 0 || malformed > 0) {
|
|
1163
|
+
const parts = [];
|
|
1164
|
+
if (unmappedDyn > 0) {
|
|
1165
|
+
parts.push(`${unmappedDyn} non-literal dynamic import(s) could not be traced — dead-export findings in those files may include false positives that cannot be capped`);
|
|
1166
|
+
}
|
|
1167
|
+
if (malformed > 0) {
|
|
1168
|
+
parts.push(`${malformed} static import declaration(s) could not be read by ts-morph — set KERN_DEBUG to surface the underlying errors`);
|
|
1169
|
+
}
|
|
1170
|
+
graphHealth.noteKind('call-graph', 'fallback', `${parts.join('; ')}.`);
|
|
1003
1171
|
}
|
|
1004
1172
|
}
|
|
1005
1173
|
catch (err) {
|
|
@@ -1014,6 +1182,8 @@ export function reviewGraph(entryFiles, config, graphOptions) {
|
|
|
1014
1182
|
try {
|
|
1015
1183
|
const source = readFileSync(report.filePath, 'utf-8');
|
|
1016
1184
|
const unsuppressedCandidates = [...report.findings, ...(report.suppressedFindings ?? [])];
|
|
1185
|
+
assignDefaultConfidence(unsuppressedCandidates);
|
|
1186
|
+
applyRuleQualityCalibration(unsuppressedCandidates, config);
|
|
1017
1187
|
const suppression = applySuppression(sortAndDedup(unsuppressedCandidates), source, report.filePath, config, config?.strict ?? false);
|
|
1018
1188
|
report.findings = sortAndDedup(suppression.findings);
|
|
1019
1189
|
report.suppressedFindings = suppression.suppressed.length > 0 ? sortAndDedup(suppression.suppressed) : undefined;
|
|
@@ -1022,6 +1192,35 @@ export function reviewGraph(entryFiles, config, graphOptions) {
|
|
|
1022
1192
|
report.findings = sortAndDedup(report.findings);
|
|
1023
1193
|
}
|
|
1024
1194
|
}
|
|
1195
|
+
// Universal canonical → display remap. Multiple graph-aware rules
|
|
1196
|
+
// (use-client-drilled-too-high, missing-use-client, server-hook,
|
|
1197
|
+
// next-client-api-in-server, …) emit findings whose `relatedSpans`
|
|
1198
|
+
// reference cross-file paths sourced from cgProject SourceFiles —
|
|
1199
|
+
// those paths are canonical (red-team #9 fix). Walk every finding
|
|
1200
|
+
// once and translate canonical paths back to the caller-facing
|
|
1201
|
+
// display via the graph's display index. Cheap: O(findings * spans)
|
|
1202
|
+
// with a pre-built Map; symmetric across all rules without each one
|
|
1203
|
+
// needing to know about the canonicalisation invariant.
|
|
1204
|
+
const canonicalToDisplayFinal = new Map();
|
|
1205
|
+
for (const gf of graph.files) {
|
|
1206
|
+
canonicalToDisplayFinal.set(gf.canonicalPath, gf.path);
|
|
1207
|
+
}
|
|
1208
|
+
if (canonicalToDisplayFinal.size > 0) {
|
|
1209
|
+
for (const report of reports) {
|
|
1210
|
+
for (const f of report.findings) {
|
|
1211
|
+
const displayPrimary = canonicalToDisplayFinal.get(f.primarySpan.file);
|
|
1212
|
+
if (displayPrimary)
|
|
1213
|
+
f.primarySpan.file = displayPrimary;
|
|
1214
|
+
if (f.relatedSpans) {
|
|
1215
|
+
for (const rs of f.relatedSpans) {
|
|
1216
|
+
const display = canonicalToDisplayFinal.get(rs.file);
|
|
1217
|
+
if (display)
|
|
1218
|
+
rs.file = display;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1025
1224
|
// Merge graph-level health into every report. Each report may already carry per-file health
|
|
1026
1225
|
// (e.g. fs-project fallback); fold those entries into the graph builder so every report sees
|
|
1027
1226
|
// the complete, deduped picture before we emit.
|
|
@@ -1035,35 +1234,54 @@ export function reviewGraph(entryFiles, config, graphOptions) {
|
|
|
1035
1234
|
}
|
|
1036
1235
|
return reports;
|
|
1037
1236
|
}
|
|
1038
|
-
function collectReviewableFiles(dirPath, recursive) {
|
|
1237
|
+
function collectReviewableFiles(dirPath, recursive, ctx) {
|
|
1039
1238
|
const files = [];
|
|
1239
|
+
// Hardcoded skips for directories that are never useful to descend into,
|
|
1240
|
+
// even if a tracked file lives inside them. ProjectContext.isReviewable
|
|
1241
|
+
// handles the gitignore-vs-tracked logic for files we DO descend into.
|
|
1242
|
+
const HARD_SKIP_DIRS = new Set(['node_modules', '__pycache__', '.venv', 'venv']);
|
|
1040
1243
|
for (const entry of readdirSync(dirPath)) {
|
|
1041
1244
|
const full = join(dirPath, entry);
|
|
1042
1245
|
const stat = statSync(full);
|
|
1043
|
-
if (stat.isDirectory()
|
|
1044
|
-
recursive
|
|
1045
|
-
|
|
1046
|
-
entry
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1246
|
+
if (stat.isDirectory()) {
|
|
1247
|
+
if (!recursive)
|
|
1248
|
+
continue;
|
|
1249
|
+
if (entry.startsWith('.') || HARD_SKIP_DIRS.has(entry))
|
|
1250
|
+
continue;
|
|
1251
|
+
// Honor .gitignore at the directory level only if NO tracked file lives
|
|
1252
|
+
// inside it (cheap check via path prefix on the tracked set).
|
|
1253
|
+
if (ctx && isPathIgnored(full, ctx) && !directoryHasTrackedDescendant(full, ctx))
|
|
1254
|
+
continue;
|
|
1255
|
+
files.push(...collectReviewableFiles(full, true, ctx));
|
|
1256
|
+
continue;
|
|
1052
1257
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1258
|
+
if (!stat.isFile())
|
|
1259
|
+
continue;
|
|
1260
|
+
const isReviewableExt = ((entry.endsWith('.ts') || entry.endsWith('.tsx')) &&
|
|
1055
1261
|
!entry.endsWith('.d.ts') &&
|
|
1056
1262
|
!entry.endsWith('.test.ts') &&
|
|
1057
|
-
!entry.endsWith('.test.tsx'))
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1263
|
+
!entry.endsWith('.test.tsx')) ||
|
|
1264
|
+
(entry.endsWith('.py') && !entry.startsWith('test_') && !entry.endsWith('_test.py')) ||
|
|
1265
|
+
entry.endsWith('.kern');
|
|
1266
|
+
if (!isReviewableExt)
|
|
1267
|
+
continue;
|
|
1268
|
+
// Skip-list: gitignored AND not tracked. Tracked-but-gitignored files
|
|
1269
|
+
// (e.g. checked-in dist/*.gen.ts) remain reviewable — Phase 1 red-team #4.
|
|
1270
|
+
if (ctx && !isReviewable(full, ctx))
|
|
1271
|
+
continue;
|
|
1272
|
+
files.push(full);
|
|
1066
1273
|
}
|
|
1067
1274
|
return files;
|
|
1068
1275
|
}
|
|
1276
|
+
function directoryHasTrackedDescendant(dirAbsPath, ctx) {
|
|
1277
|
+
const rel = relative(ctx.root, dirAbsPath).split(sep).join('/');
|
|
1278
|
+
if (!rel || rel.startsWith('..'))
|
|
1279
|
+
return false;
|
|
1280
|
+
const prefix = rel.endsWith('/') ? rel : `${rel}/`;
|
|
1281
|
+
for (const tracked of ctx.gitTrackedFiles) {
|
|
1282
|
+
if (tracked.startsWith(prefix))
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1069
1287
|
//# sourceMappingURL=index.js.map
|