@kernlang/review 3.3.4 → 3.3.5
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/concept-rules/contract-drift.d.ts +21 -0
- package/dist/concept-rules/contract-drift.js +66 -0
- package/dist/concept-rules/contract-drift.js.map +1 -0
- package/dist/concept-rules/cross-stack-utils.d.ts +50 -0
- package/dist/concept-rules/cross-stack-utils.js +98 -0
- package/dist/concept-rules/cross-stack-utils.js.map +1 -0
- package/dist/concept-rules/index.js +12 -1
- package/dist/concept-rules/index.js.map +1 -1
- package/dist/concept-rules/tainted-across-wire.d.ts +33 -0
- package/dist/concept-rules/tainted-across-wire.js +98 -0
- package/dist/concept-rules/tainted-across-wire.js.map +1 -0
- package/dist/concept-rules/untyped-api-response.d.ts +30 -0
- package/dist/concept-rules/untyped-api-response.js +71 -0
- package/dist/concept-rules/untyped-api-response.js.map +1 -0
- package/dist/external-tools.d.ts +17 -4
- package/dist/external-tools.js +12 -1
- package/dist/external-tools.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +75 -6
- package/dist/index.js.map +1 -1
- package/dist/llm-bridge.d.ts +26 -1
- package/dist/llm-bridge.js +42 -6
- package/dist/llm-bridge.js.map +1 -1
- package/dist/llm-review.js +29 -11
- package/dist/llm-review.js.map +1 -1
- package/dist/mappers/ts-concepts.js +247 -1
- package/dist/mappers/ts-concepts.js.map +1 -1
- package/dist/rules/index.js +1 -1
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/kern-source.js +35 -5
- package/dist/rules/kern-source.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/cache.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync
|
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { dirname, join, resolve } from 'path';
|
|
5
5
|
// Version stamp for cache invalidation — changes when rules/analyzers change
|
|
6
|
-
const REVIEW_CACHE_VERSION = '3.2.3-review-cache-
|
|
6
|
+
const REVIEW_CACHE_VERSION = '3.2.3-review-cache-3';
|
|
7
7
|
const IMPORT_SPECIFIER_RE = /(?:import|export)\s+(?:[^'"`]*?\s+from\s+)?['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
8
8
|
const EXTENSION_FALLBACK = {
|
|
9
9
|
'.js': ['.ts', '.tsx', '.mts', '.cts'],
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: contract-drift
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend (TS) network call targets an API
|
|
5
|
+
* path that has no matching server-side route in the reviewed project.
|
|
6
|
+
*
|
|
7
|
+
* This is the moat rule for TS ↔ Python projects: Pydantic schemas drift,
|
|
8
|
+
* endpoints get renamed, frontend hits `/api/users/:id` but the FastAPI
|
|
9
|
+
* handler was moved to `/api/v2/users/:id`. ESLint and Bandit can't see
|
|
10
|
+
* this because they each only see one side of the wire.
|
|
11
|
+
*
|
|
12
|
+
* v1 scope: URL-path drift only (is there a server for this client?). Body
|
|
13
|
+
* shape / Pydantic field correlation is a follow-up once the mappers emit
|
|
14
|
+
* body concepts — see the TODO comments in ts-concepts.ts / review-python.
|
|
15
|
+
*
|
|
16
|
+
* Requires graph mode: `ctx.allConcepts` must be populated. Single-file
|
|
17
|
+
* review silently returns no findings (can't correlate from one file).
|
|
18
|
+
*/
|
|
19
|
+
import type { ReviewFinding } from '../types.js';
|
|
20
|
+
import type { ConceptRuleContext } from './index.js';
|
|
21
|
+
export declare function contractDrift(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: contract-drift
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend (TS) network call targets an API
|
|
5
|
+
* path that has no matching server-side route in the reviewed project.
|
|
6
|
+
*
|
|
7
|
+
* This is the moat rule for TS ↔ Python projects: Pydantic schemas drift,
|
|
8
|
+
* endpoints get renamed, frontend hits `/api/users/:id` but the FastAPI
|
|
9
|
+
* handler was moved to `/api/v2/users/:id`. ESLint and Bandit can't see
|
|
10
|
+
* this because they each only see one side of the wire.
|
|
11
|
+
*
|
|
12
|
+
* v1 scope: URL-path drift only (is there a server for this client?). Body
|
|
13
|
+
* shape / Pydantic field correlation is a follow-up once the mappers emit
|
|
14
|
+
* body concepts — see the TODO comments in ts-concepts.ts / review-python.
|
|
15
|
+
*
|
|
16
|
+
* Requires graph mode: `ctx.allConcepts` must be populated. Single-file
|
|
17
|
+
* review silently returns no findings (can't correlate from one file).
|
|
18
|
+
*/
|
|
19
|
+
import { createFingerprint } from '../types.js';
|
|
20
|
+
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutes, hasMatchingRoute, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
21
|
+
export function contractDrift(ctx) {
|
|
22
|
+
// Graph mode only — URL correlation is useless within a single file.
|
|
23
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
24
|
+
return [];
|
|
25
|
+
const serverRoutes = [];
|
|
26
|
+
const clientCalls = [];
|
|
27
|
+
for (const [, conceptMap] of ctx.allConcepts) {
|
|
28
|
+
collectRoutes(conceptMap, serverRoutes);
|
|
29
|
+
for (const node of conceptMap.nodes) {
|
|
30
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
31
|
+
continue;
|
|
32
|
+
const target = node.payload.target;
|
|
33
|
+
if (typeof target !== 'string')
|
|
34
|
+
continue;
|
|
35
|
+
const normalized = normalizeClientUrl(target);
|
|
36
|
+
if (!normalized || !API_PATH_RE.test(normalized))
|
|
37
|
+
continue;
|
|
38
|
+
clientCalls.push({ target, normalizedPath: normalized, node });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Rule gate: need at least one route AND one client call, otherwise the
|
|
42
|
+
// project isn't a full-stack app and we'd fire on every external API hit.
|
|
43
|
+
if (serverRoutes.length === 0 || clientCalls.length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const findings = [];
|
|
46
|
+
for (const call of clientCalls) {
|
|
47
|
+
// Only report on calls that happen in files from the reviewed project —
|
|
48
|
+
// avoids firing on third-party SDK targets.
|
|
49
|
+
if (call.node.primarySpan.file !== ctx.filePath)
|
|
50
|
+
continue;
|
|
51
|
+
if (hasMatchingRoute(call.normalizedPath, serverRoutes))
|
|
52
|
+
continue;
|
|
53
|
+
findings.push({
|
|
54
|
+
source: 'kern',
|
|
55
|
+
ruleId: 'contract-drift',
|
|
56
|
+
severity: 'warning',
|
|
57
|
+
category: 'bug',
|
|
58
|
+
message: `Frontend calls \`${call.target}\` but no server-side route matches this path in the reviewed project. Either the endpoint was renamed/removed on the backend or the frontend is targeting the wrong URL.`,
|
|
59
|
+
primarySpan: call.node.primarySpan,
|
|
60
|
+
fingerprint: createFingerprint('contract-drift', call.node.primarySpan.startLine, call.node.primarySpan.startCol),
|
|
61
|
+
confidence: call.node.confidence * CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return findings;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=contract-drift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-drift.js","sourceRoot":"","sources":["../../src/concept-rules/contract-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,gCAAgC,EAChC,aAAa,EACb,gBAAgB,EAChB,kBAAkB,GAEnB,MAAM,wBAAwB,CAAC;AAShC,MAAM,UAAU,aAAa,CAAC,GAAuB;IACnD,qEAAqE;IACrE,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAE9D,MAAM,YAAY,GAAkB,EAAE,CAAC;IACvC,MAAM,WAAW,GAAiB,EAAE,CAAC;IAErC,KAAK,MAAM,CAAC,EAAE,UAAU,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;QAC7C,aAAa,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QACxC,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS;gBAAE,SAAS;YAC7G,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;YACnC,IAAI,OAAO,MAAM,KAAK,QAAQ;gBAAE,SAAS;YACzC,MAAM,UAAU,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;YAC9C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC;gBAAE,SAAS;YAC3D,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IAED,wEAAwE;IACxE,0EAA0E;IAC1E,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAErE,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,wEAAwE;QACxE,4CAA4C;QAC5C,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,KAAK,GAAG,CAAC,QAAQ;YAAE,SAAS;QAC1D,IAAI,gBAAgB,CAAC,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC;YAAE,SAAS;QAElE,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,gBAAgB;YACxB,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,oBAAoB,IAAI,CAAC,MAAM,2KAA2K;YACnN,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW;YAClC,WAAW,EAAE,iBAAiB,CAAC,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;YACjH,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,gCAAgC;SACpE,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for cross-stack concept rules.
|
|
3
|
+
*
|
|
4
|
+
* Every rule that correlates a frontend network call against a server-side
|
|
5
|
+
* route — contract-drift, untyped-api-response, and the upcoming
|
|
6
|
+
* tainted-across-wire — uses the same URL normalisation + route matching
|
|
7
|
+
* pipeline. Centralising it here means a bug fix (or new matching case like
|
|
8
|
+
* Next.js catch-all `[...slug]`) applies to every rule in one place.
|
|
9
|
+
*/
|
|
10
|
+
import type { ConceptMap, ConceptNode } from '@kernlang/core';
|
|
11
|
+
/**
|
|
12
|
+
* Multiplier applied to a node's base confidence when firing a cross-stack
|
|
13
|
+
* finding. Each current rule matches only on URL-path shape — no HTTP-method
|
|
14
|
+
* correlation, no body-type correlation — so we intentionally cap confidence
|
|
15
|
+
* below 1.0 to reflect the heuristic nature. Upgrade per-rule once the
|
|
16
|
+
* matching is richer (e.g. once the Python mapper surfaces response_model=,
|
|
17
|
+
* untyped-api-response can bump its own multiplier).
|
|
18
|
+
*/
|
|
19
|
+
export declare const CROSS_STACK_HEURISTIC_CONFIDENCE = 0.7;
|
|
20
|
+
/** Client URLs we consider "internal" to the reviewed project. */
|
|
21
|
+
export declare const API_PATH_RE: RegExp;
|
|
22
|
+
export interface ServerRoute {
|
|
23
|
+
path: string;
|
|
24
|
+
method: string | undefined;
|
|
25
|
+
/** Present when the caller needs to cite the server route in a finding. */
|
|
26
|
+
node?: ConceptNode;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pull every server-side route out of a concept map. Callers typically fold
|
|
30
|
+
* this across `ctx.allConcepts` to collect routes for the whole project.
|
|
31
|
+
*/
|
|
32
|
+
export declare function collectRoutes(map: ConceptMap, routes: ServerRoute[]): void;
|
|
33
|
+
/**
|
|
34
|
+
* Strip scheme/host, query string, and fragment from a client URL so it can
|
|
35
|
+
* match against a server route template. Returns undefined when the input
|
|
36
|
+
* isn't a recognisable path (e.g. a bare variable reference or an
|
|
37
|
+
* unresolved template expression).
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeClientUrl(raw: string): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Match a client-side concrete path against server-side route templates.
|
|
42
|
+
* Returns the first matching route (so callers can cite it in findings) or
|
|
43
|
+
* `undefined`. Server templates may contain params — Express/Koa `:id`,
|
|
44
|
+
* FastAPI `{id}` — which match any single segment. Trailing slashes are
|
|
45
|
+
* normalised on both sides. Case-sensitive (matches Express/FastAPI default
|
|
46
|
+
* behaviour).
|
|
47
|
+
*/
|
|
48
|
+
export declare function findMatchingRoute(clientPath: string, routes: readonly ServerRoute[]): ServerRoute | undefined;
|
|
49
|
+
/** Boolean-returning thin wrapper preserved for callers that just need a yes/no. */
|
|
50
|
+
export declare function hasMatchingRoute(clientPath: string, routes: readonly ServerRoute[]): boolean;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for cross-stack concept rules.
|
|
3
|
+
*
|
|
4
|
+
* Every rule that correlates a frontend network call against a server-side
|
|
5
|
+
* route — contract-drift, untyped-api-response, and the upcoming
|
|
6
|
+
* tainted-across-wire — uses the same URL normalisation + route matching
|
|
7
|
+
* pipeline. Centralising it here means a bug fix (or new matching case like
|
|
8
|
+
* Next.js catch-all `[...slug]`) applies to every rule in one place.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Multiplier applied to a node's base confidence when firing a cross-stack
|
|
12
|
+
* finding. Each current rule matches only on URL-path shape — no HTTP-method
|
|
13
|
+
* correlation, no body-type correlation — so we intentionally cap confidence
|
|
14
|
+
* below 1.0 to reflect the heuristic nature. Upgrade per-rule once the
|
|
15
|
+
* matching is richer (e.g. once the Python mapper surfaces response_model=,
|
|
16
|
+
* untyped-api-response can bump its own multiplier).
|
|
17
|
+
*/
|
|
18
|
+
export const CROSS_STACK_HEURISTIC_CONFIDENCE = 0.7;
|
|
19
|
+
/** Client URLs we consider "internal" to the reviewed project. */
|
|
20
|
+
export const API_PATH_RE = /^\/api\//;
|
|
21
|
+
/**
|
|
22
|
+
* Pull every server-side route out of a concept map. Callers typically fold
|
|
23
|
+
* this across `ctx.allConcepts` to collect routes for the whole project.
|
|
24
|
+
*/
|
|
25
|
+
export function collectRoutes(map, routes) {
|
|
26
|
+
for (const node of map.nodes) {
|
|
27
|
+
if (node.kind !== 'entrypoint' || node.payload.kind !== 'entrypoint' || node.payload.subtype !== 'route')
|
|
28
|
+
continue;
|
|
29
|
+
const path = node.payload.name;
|
|
30
|
+
if (typeof path !== 'string' || !path.startsWith('/'))
|
|
31
|
+
continue;
|
|
32
|
+
routes.push({ path, method: node.payload.httpMethod, node });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Strip scheme/host, query string, and fragment from a client URL so it can
|
|
37
|
+
* match against a server route template. Returns undefined when the input
|
|
38
|
+
* isn't a recognisable path (e.g. a bare variable reference or an
|
|
39
|
+
* unresolved template expression).
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeClientUrl(raw) {
|
|
42
|
+
let url = raw.trim();
|
|
43
|
+
if (url.startsWith('`') && !url.startsWith('`/'))
|
|
44
|
+
return undefined;
|
|
45
|
+
url = url.replace(/^`|`$/g, '');
|
|
46
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
47
|
+
const pathStart = url.indexOf('/', url.indexOf('://') + 3);
|
|
48
|
+
url = pathStart === -1 ? '/' : url.slice(pathStart);
|
|
49
|
+
}
|
|
50
|
+
const q = url.indexOf('?');
|
|
51
|
+
if (q !== -1)
|
|
52
|
+
url = url.slice(0, q);
|
|
53
|
+
const h = url.indexOf('#');
|
|
54
|
+
if (h !== -1)
|
|
55
|
+
url = url.slice(0, h);
|
|
56
|
+
return url || undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Match a client-side concrete path against server-side route templates.
|
|
60
|
+
* Returns the first matching route (so callers can cite it in findings) or
|
|
61
|
+
* `undefined`. Server templates may contain params — Express/Koa `:id`,
|
|
62
|
+
* FastAPI `{id}` — which match any single segment. Trailing slashes are
|
|
63
|
+
* normalised on both sides. Case-sensitive (matches Express/FastAPI default
|
|
64
|
+
* behaviour).
|
|
65
|
+
*/
|
|
66
|
+
export function findMatchingRoute(clientPath, routes) {
|
|
67
|
+
const clientSegments = trimTrailing(clientPath).split('/');
|
|
68
|
+
for (const route of routes) {
|
|
69
|
+
const routeSegments = trimTrailing(route.path).split('/');
|
|
70
|
+
if (routeSegments.length !== clientSegments.length)
|
|
71
|
+
continue;
|
|
72
|
+
let matched = true;
|
|
73
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
74
|
+
const rs = routeSegments[i];
|
|
75
|
+
const cs = clientSegments[i];
|
|
76
|
+
if (isParamSegment(rs))
|
|
77
|
+
continue;
|
|
78
|
+
if (rs !== cs) {
|
|
79
|
+
matched = false;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (matched)
|
|
84
|
+
return route;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
/** Boolean-returning thin wrapper preserved for callers that just need a yes/no. */
|
|
89
|
+
export function hasMatchingRoute(clientPath, routes) {
|
|
90
|
+
return findMatchingRoute(clientPath, routes) !== undefined;
|
|
91
|
+
}
|
|
92
|
+
function trimTrailing(path) {
|
|
93
|
+
return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path;
|
|
94
|
+
}
|
|
95
|
+
function isParamSegment(seg) {
|
|
96
|
+
return seg.startsWith(':') || (seg.startsWith('{') && seg.endsWith('}'));
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=cross-stack-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-stack-utils.js","sourceRoot":"","sources":["../../src/concept-rules/cross-stack-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,10 +5,21 @@
|
|
|
5
5
|
* Language-agnostic by design.
|
|
6
6
|
*/
|
|
7
7
|
import { boundaryMutation } from './boundary-mutation.js';
|
|
8
|
+
import { contractDrift } from './contract-drift.js';
|
|
8
9
|
import { ignoredError } from './ignored-error.js';
|
|
10
|
+
import { taintedAcrossWire } from './tainted-across-wire.js';
|
|
9
11
|
import { unguardedEffect } from './unguarded-effect.js';
|
|
10
12
|
import { unrecoveredEffect } from './unrecovered-effect.js';
|
|
11
|
-
|
|
13
|
+
import { untypedApiResponse } from './untyped-api-response.js';
|
|
14
|
+
export const conceptRules = [
|
|
15
|
+
boundaryMutation,
|
|
16
|
+
contractDrift,
|
|
17
|
+
ignoredError,
|
|
18
|
+
taintedAcrossWire,
|
|
19
|
+
unguardedEffect,
|
|
20
|
+
unrecoveredEffect,
|
|
21
|
+
untypedApiResponse,
|
|
22
|
+
];
|
|
12
23
|
export function runConceptRules(concepts, filePath, allConcepts, graphImports) {
|
|
13
24
|
const ctx = { concepts, filePath, allConcepts, graphImports };
|
|
14
25
|
const findings = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/concept-rules/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/concept-rules/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAa/D,MAAM,CAAC,MAAM,YAAY,GAAkB;IACzC,gBAAgB;IAChB,aAAa;IACb,YAAY;IACZ,iBAAiB;IACjB,eAAe;IACf,iBAAiB;IACjB,kBAAkB;CACnB,CAAC;AAEF,MAAM,UAAU,eAAe,CAC7B,QAAoB,EACpB,QAAgB,EAChB,WAAqC,EACrC,YAAoC;IAEpC,MAAM,GAAG,GAAuB,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;IAClF,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: tainted-across-wire
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend (TS) network call sends a
|
|
5
|
+
* dynamic (user-controlled) body to a server-side route in the reviewed
|
|
6
|
+
* project AND that route's handler has no validation guard in scope.
|
|
7
|
+
*
|
|
8
|
+
* This is rule #3 of the fullstack wedge. Pure server-side taint analysis
|
|
9
|
+
* (already in @kernlang/review's taint-crossfile module) finds sinks
|
|
10
|
+
* reachable from `req.body`, but it can't see the *client-side call site*
|
|
11
|
+
* that's feeding the unvalidated input. And pure client-side analysis
|
|
12
|
+
* doesn't know whether the server validates — so it either fires on every
|
|
13
|
+
* dynamic POST (noise) or on none (misses the moat).
|
|
14
|
+
*
|
|
15
|
+
* By correlating both sides via the concept graph we can fire precisely:
|
|
16
|
+
* "here's the fetch that sends user input to an endpoint whose handler
|
|
17
|
+
* doesn't parse it with zod/yup/joi/pydantic". The finding lands on the
|
|
18
|
+
* client-side call so the fix is visible where the developer is working.
|
|
19
|
+
*
|
|
20
|
+
* Preconditions to fire:
|
|
21
|
+
* 1. Graph mode (`ctx.allConcepts` populated).
|
|
22
|
+
* 2. Client concept has `bodyKind === 'dynamic'` — we know real data is
|
|
23
|
+
* crossing the wire, not a ping/HEAD/etc.
|
|
24
|
+
* 3. Client target path matches a server route in the graph.
|
|
25
|
+
* 4. That server route's container has NO `guard` concept with
|
|
26
|
+
* `subtype === 'validation'` (schema.parse / zod.parse / …).
|
|
27
|
+
*
|
|
28
|
+
* Silent on `bodyKind === undefined` (mapper couldn't classify) and on
|
|
29
|
+
* missing server matches (contract-drift owns that class).
|
|
30
|
+
*/
|
|
31
|
+
import type { ReviewFinding } from '../types.js';
|
|
32
|
+
import type { ConceptRuleContext } from './index.js';
|
|
33
|
+
export declare function taintedAcrossWire(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: tainted-across-wire
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend (TS) network call sends a
|
|
5
|
+
* dynamic (user-controlled) body to a server-side route in the reviewed
|
|
6
|
+
* project AND that route's handler has no validation guard in scope.
|
|
7
|
+
*
|
|
8
|
+
* This is rule #3 of the fullstack wedge. Pure server-side taint analysis
|
|
9
|
+
* (already in @kernlang/review's taint-crossfile module) finds sinks
|
|
10
|
+
* reachable from `req.body`, but it can't see the *client-side call site*
|
|
11
|
+
* that's feeding the unvalidated input. And pure client-side analysis
|
|
12
|
+
* doesn't know whether the server validates — so it either fires on every
|
|
13
|
+
* dynamic POST (noise) or on none (misses the moat).
|
|
14
|
+
*
|
|
15
|
+
* By correlating both sides via the concept graph we can fire precisely:
|
|
16
|
+
* "here's the fetch that sends user input to an endpoint whose handler
|
|
17
|
+
* doesn't parse it with zod/yup/joi/pydantic". The finding lands on the
|
|
18
|
+
* client-side call so the fix is visible where the developer is working.
|
|
19
|
+
*
|
|
20
|
+
* Preconditions to fire:
|
|
21
|
+
* 1. Graph mode (`ctx.allConcepts` populated).
|
|
22
|
+
* 2. Client concept has `bodyKind === 'dynamic'` — we know real data is
|
|
23
|
+
* crossing the wire, not a ping/HEAD/etc.
|
|
24
|
+
* 3. Client target path matches a server route in the graph.
|
|
25
|
+
* 4. That server route's container has NO `guard` concept with
|
|
26
|
+
* `subtype === 'validation'` (schema.parse / zod.parse / …).
|
|
27
|
+
*
|
|
28
|
+
* Silent on `bodyKind === undefined` (mapper couldn't classify) and on
|
|
29
|
+
* missing server matches (contract-drift owns that class).
|
|
30
|
+
*/
|
|
31
|
+
import { createFingerprint } from '../types.js';
|
|
32
|
+
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutes, findMatchingRoute, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
33
|
+
export function taintedAcrossWire(ctx) {
|
|
34
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
35
|
+
return [];
|
|
36
|
+
const serverRoutes = [];
|
|
37
|
+
for (const [, conceptMap] of ctx.allConcepts) {
|
|
38
|
+
collectRoutes(conceptMap, serverRoutes);
|
|
39
|
+
}
|
|
40
|
+
if (serverRoutes.length === 0)
|
|
41
|
+
return [];
|
|
42
|
+
// Build the set of files that contain at least one validation guard.
|
|
43
|
+
// Container-level matching is too strict: zod/yup parsers typically live
|
|
44
|
+
// inside the route callback body (arrow function container) while the
|
|
45
|
+
// route entrypoint itself is emitted at the call-expression level (module
|
|
46
|
+
// container). File-level matching is coarser but safer — it silences the
|
|
47
|
+
// common case where a validator exists in the same server file as the
|
|
48
|
+
// route, which is a strong signal the handler is guarded. False negatives
|
|
49
|
+
// (a validation-less route in a file that validates *other* routes) cost
|
|
50
|
+
// us less than false positives on the pitch.
|
|
51
|
+
const validatedFiles = collectValidatedFiles(ctx.allConcepts);
|
|
52
|
+
const findings = [];
|
|
53
|
+
const localConcepts = ctx.allConcepts.get(ctx.filePath) ?? ctx.concepts;
|
|
54
|
+
for (const node of localConcepts.nodes) {
|
|
55
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
56
|
+
continue;
|
|
57
|
+
if (node.payload.bodyKind !== 'dynamic')
|
|
58
|
+
continue;
|
|
59
|
+
const target = node.payload.target;
|
|
60
|
+
if (typeof target !== 'string')
|
|
61
|
+
continue;
|
|
62
|
+
const normalized = normalizeClientUrl(target);
|
|
63
|
+
if (!normalized || !API_PATH_RE.test(normalized))
|
|
64
|
+
continue;
|
|
65
|
+
const matchedRoute = findMatchingRoute(normalized, serverRoutes);
|
|
66
|
+
if (!matchedRoute)
|
|
67
|
+
continue; // contract-drift owns the "wrong URL" class.
|
|
68
|
+
const routeFile = matchedRoute.node?.primarySpan.file;
|
|
69
|
+
if (routeFile && validatedFiles.has(routeFile))
|
|
70
|
+
continue;
|
|
71
|
+
findings.push({
|
|
72
|
+
source: 'kern',
|
|
73
|
+
ruleId: 'tainted-across-wire',
|
|
74
|
+
severity: 'warning',
|
|
75
|
+
category: 'pattern',
|
|
76
|
+
message: `Dynamic body sent to \`${target}\` but the matching server route has no validation guard (schema.parse / zod / yup / pydantic). Add a validator on the server before trusting the payload, or move validation to the client if this endpoint is internal-only.`,
|
|
77
|
+
primarySpan: node.primarySpan,
|
|
78
|
+
fingerprint: createFingerprint('tainted-across-wire', node.primarySpan.startLine, node.primarySpan.startCol),
|
|
79
|
+
confidence: node.confidence * CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return findings;
|
|
83
|
+
}
|
|
84
|
+
function collectValidatedFiles(allConcepts) {
|
|
85
|
+
const set = new Set();
|
|
86
|
+
for (const [file, conceptMap] of allConcepts) {
|
|
87
|
+
for (const node of conceptMap.nodes) {
|
|
88
|
+
if (node.kind !== 'guard' || node.payload.kind !== 'guard')
|
|
89
|
+
continue;
|
|
90
|
+
if (node.payload.subtype !== 'validation')
|
|
91
|
+
continue;
|
|
92
|
+
set.add(file);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return set;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=tainted-across-wire.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tainted-across-wire.js","sourceRoot":"","sources":["../../src/concept-rules/tainted-across-wire.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: untyped-api-response
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend (TS) network call targets a
|
|
5
|
+
* server-side route in the reviewed project AND consumes the JSON body
|
|
6
|
+
* without a type annotation, `as T` cast, or `satisfies T` clause.
|
|
7
|
+
*
|
|
8
|
+
* This is rule #2 of the fullstack wedge (TS ↔ Python/Express). The server
|
|
9
|
+
* has a declared response shape (Pydantic `response_model=`, Express
|
|
10
|
+
* `Response<T>`, …) but the client is treating the payload as `any`, which
|
|
11
|
+
* means any breaking change in the response shape will silently rot the
|
|
12
|
+
* frontend at runtime. ESLint can catch "no-explicit-any" at the TS level
|
|
13
|
+
* but it can't tell you *which* fetch() is actually talking to a project
|
|
14
|
+
* endpoint — only the concept graph knows that.
|
|
15
|
+
*
|
|
16
|
+
* Preconditions to fire:
|
|
17
|
+
* 1. Graph mode (`ctx.allConcepts` populated).
|
|
18
|
+
* 2. Client concept has `responseAsserted === false` (mapper proved the
|
|
19
|
+
* .json() consumption is untyped).
|
|
20
|
+
* 3. The call's target path matches a server-side route in the graph —
|
|
21
|
+
* otherwise this is just a generic untyped-fetch case, which Biome
|
|
22
|
+
* already covers and we don't want to duplicate.
|
|
23
|
+
*
|
|
24
|
+
* Kept conservative: when the mapper returns `undefined` for
|
|
25
|
+
* responseAsserted (patterns it couldn't analyze) the rule stays silent.
|
|
26
|
+
* False positives here would poison the pitch.
|
|
27
|
+
*/
|
|
28
|
+
import type { ReviewFinding } from '../types.js';
|
|
29
|
+
import type { ConceptRuleContext } from './index.js';
|
|
30
|
+
export declare function untypedApiResponse(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: untyped-api-response
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend (TS) network call targets a
|
|
5
|
+
* server-side route in the reviewed project AND consumes the JSON body
|
|
6
|
+
* without a type annotation, `as T` cast, or `satisfies T` clause.
|
|
7
|
+
*
|
|
8
|
+
* This is rule #2 of the fullstack wedge (TS ↔ Python/Express). The server
|
|
9
|
+
* has a declared response shape (Pydantic `response_model=`, Express
|
|
10
|
+
* `Response<T>`, …) but the client is treating the payload as `any`, which
|
|
11
|
+
* means any breaking change in the response shape will silently rot the
|
|
12
|
+
* frontend at runtime. ESLint can catch "no-explicit-any" at the TS level
|
|
13
|
+
* but it can't tell you *which* fetch() is actually talking to a project
|
|
14
|
+
* endpoint — only the concept graph knows that.
|
|
15
|
+
*
|
|
16
|
+
* Preconditions to fire:
|
|
17
|
+
* 1. Graph mode (`ctx.allConcepts` populated).
|
|
18
|
+
* 2. Client concept has `responseAsserted === false` (mapper proved the
|
|
19
|
+
* .json() consumption is untyped).
|
|
20
|
+
* 3. The call's target path matches a server-side route in the graph —
|
|
21
|
+
* otherwise this is just a generic untyped-fetch case, which Biome
|
|
22
|
+
* already covers and we don't want to duplicate.
|
|
23
|
+
*
|
|
24
|
+
* Kept conservative: when the mapper returns `undefined` for
|
|
25
|
+
* responseAsserted (patterns it couldn't analyze) the rule stays silent.
|
|
26
|
+
* False positives here would poison the pitch.
|
|
27
|
+
*/
|
|
28
|
+
import { createFingerprint } from '../types.js';
|
|
29
|
+
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutes, hasMatchingRoute, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
30
|
+
export function untypedApiResponse(ctx) {
|
|
31
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
32
|
+
return [];
|
|
33
|
+
const serverRoutes = [];
|
|
34
|
+
for (const [, conceptMap] of ctx.allConcepts) {
|
|
35
|
+
collectRoutes(conceptMap, serverRoutes);
|
|
36
|
+
}
|
|
37
|
+
if (serverRoutes.length === 0)
|
|
38
|
+
return [];
|
|
39
|
+
const findings = [];
|
|
40
|
+
// Only scan this file's concepts so we don't duplicate findings per call.
|
|
41
|
+
const localConcepts = ctx.allConcepts.get(ctx.filePath) ?? ctx.concepts;
|
|
42
|
+
for (const node of localConcepts.nodes) {
|
|
43
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
44
|
+
continue;
|
|
45
|
+
if (node.payload.responseAsserted !== false)
|
|
46
|
+
continue; // undefined stays silent
|
|
47
|
+
const target = node.payload.target;
|
|
48
|
+
if (typeof target !== 'string')
|
|
49
|
+
continue;
|
|
50
|
+
const normalized = normalizeClientUrl(target);
|
|
51
|
+
if (!normalized || !API_PATH_RE.test(normalized))
|
|
52
|
+
continue;
|
|
53
|
+
if (!hasMatchingRoute(normalized, serverRoutes))
|
|
54
|
+
continue;
|
|
55
|
+
findings.push({
|
|
56
|
+
source: 'kern',
|
|
57
|
+
ruleId: 'untyped-api-response',
|
|
58
|
+
severity: 'warning',
|
|
59
|
+
category: 'bug',
|
|
60
|
+
message: `Response from \`${target}\` is consumed without a type annotation. The server route defines a response shape — assign the awaited value to a typed variable or use \`as T\` / \`satisfies T\` so response-shape drift is caught at compile time instead of breaking at runtime.`,
|
|
61
|
+
primarySpan: node.primarySpan,
|
|
62
|
+
fingerprint: createFingerprint('untyped-api-response', node.primarySpan.startLine, node.primarySpan.startCol),
|
|
63
|
+
// Same tier as contract-drift. Upgrade once the Python mapper surfaces
|
|
64
|
+
// `response_model=` and we can also cite the specific server type the
|
|
65
|
+
// frontend should be asserting against.
|
|
66
|
+
confidence: node.confidence * CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return findings;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=untyped-api-response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"untyped-api-response.js","sourceRoot":"","sources":["../../src/concept-rules/untyped-api-response.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,gCAAgC,EAChC,aAAa,EACb,gBAAgB,EAChB,kBAAkB,GAEnB,MAAM,wBAAwB,CAAC;AAGhC,MAAM,UAAU,kBAAkB,CAAC,GAAuB;IACxD,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAE9D,MAAM,YAAY,GAAkB,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,EAAE,UAAU,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;QAC7C,aAAa,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC1C,CAAC;IACD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEzC,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,0EAA0E;IAC1E,MAAM,aAAa,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC;IACxE,KAAK,MAAM,IAAI,IAAI,aAAa,CAAC,KAAK,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS;YAAE,SAAS;QAC7G,IAAI,IAAI,CAAC,OAAO,CAAC,gBAAgB,KAAK,KAAK;YAAE,SAAS,CAAC,yBAAyB;QAChF,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QACnC,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,SAAS;QACzC,MAAM,UAAU,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAC9C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC;YAAE,SAAS;QAC3D,IAAI,CAAC,gBAAgB,CAAC,UAAU,EAAE,YAAY,CAAC;YAAE,SAAS;QAE1D,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,sBAAsB;YAC9B,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,mBAAmB,MAAM,wPAAwP;YAC1R,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,WAAW,EAAE,iBAAiB,CAAC,sBAAsB,EAAE,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;YAC7G,uEAAuE;YACvE,sEAAsE;YACtE,wCAAwC;YACxC,UAAU,EAAE,IAAI,CAAC,UAAU,GAAG,gCAAgC;SAC/D,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/dist/external-tools.d.ts
CHANGED
|
@@ -22,10 +22,23 @@ import type { InferResult, ReviewFinding } from './types.js';
|
|
|
22
22
|
export declare function runESLint(filePaths: string[], cwd: string, health?: ReviewHealthBuilder): Promise<ReviewFinding[]>;
|
|
23
23
|
export interface RunTSCDiagnosticsOptions {
|
|
24
24
|
/**
|
|
25
|
-
* When true, suppress
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* When true, suppress TS diagnostics that fire as kern-review infrastructure noise when we inject
|
|
26
|
+
* ad-hoc files into a Project that carries a host tsconfig. Suppressed codes fall into two classes:
|
|
27
|
+
*
|
|
28
|
+
* Project membership (in-memory Project vs host rootDir):
|
|
29
|
+
* - TS6059 — "File is not listed within the file list of project"
|
|
30
|
+
* - TS6307 — "File is not under 'rootDir'"
|
|
31
|
+
*
|
|
32
|
+
* Environmental (in-memory Project doesn't mirror host compilerOptions — moduleResolution, jsx, lib):
|
|
33
|
+
* - TS2792 — "Cannot find module X. Did you mean to set 'moduleResolution' to 'nodenext'?"
|
|
34
|
+
* - TS17004 — "Cannot use JSX unless the '--jsx' flag is provided"
|
|
35
|
+
* - TS2580 / TS2591 — "Cannot find name 'process'/'require'/'module'" (@types/node missing)
|
|
36
|
+
*
|
|
37
|
+
* The dev already sees the environmental class in their IDE / local `tsc --noEmit` when real.
|
|
38
|
+
* Set this only for the standard review path. The --lint path must leave it false so real
|
|
39
|
+
* tsconfig misconfigurations still surface as errors.
|
|
40
|
+
*
|
|
41
|
+
* The name is kept for backward compatibility; scope broadened deliberately.
|
|
29
42
|
*/
|
|
30
43
|
downgradeProjectLoadingErrors?: boolean;
|
|
31
44
|
}
|
package/dist/external-tools.js
CHANGED
|
@@ -151,8 +151,19 @@ export function runTSCDiagnostics(project, options = {}, health) {
|
|
|
151
151
|
// them as info still pollutes every barrel/re-export report in composite monorepos.
|
|
152
152
|
// ts6059 — "File is not listed within the file list of project"
|
|
153
153
|
// ts6307 — "File is not under 'rootDir'"
|
|
154
|
+
// The following codes are environmental: they reflect ts-morph's in-memory Project not
|
|
155
|
+
// perfectly mirroring the host's compilerOptions (moduleResolution, jsx, lib). The dev
|
|
156
|
+
// already sees them in their IDE / local `tsc --noEmit` if real; the review's value-add
|
|
157
|
+
// is KERN-relevant findings, not duplicating compiler output. A sweep of the agon repo
|
|
158
|
+
// (451 files) emitted 1869 of these as errors — pure noise drowning real findings.
|
|
159
|
+
// ts2792 — "Cannot find module X. Did you mean to set 'moduleResolution' to 'nodenext'?"
|
|
160
|
+
// ts17004 — "Cannot use JSX unless the '--jsx' flag is provided"
|
|
161
|
+
// ts2580 / ts2591 — "Cannot find name 'process'/'require'/'module'. Install @types/node?"
|
|
162
|
+
// (TS emits 2580 when the name resolves via global lib shims, 2591 when it doesn't —
|
|
163
|
+
// both point at the same user-side remedy, both are environmental from review's POV.)
|
|
154
164
|
const isLoadingNoise = code === 6059 || code === 6307;
|
|
155
|
-
|
|
165
|
+
const isEnvironmentalNoise = code === 2792 || code === 17004 || code === 2580 || code === 2591;
|
|
166
|
+
if ((isLoadingNoise || isEnvironmentalNoise) && options.downgradeProjectLoadingErrors) {
|
|
156
167
|
continue;
|
|
157
168
|
}
|
|
158
169
|
findings.push({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"external-tools.js","sourceRoot":"","sources":["../src/external-tools.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"external-tools.js","sourceRoot":"","sources":["../src/external-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAA4B,MAAM,oBAAoB,CAAC;AAE3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,GAAY;IACpC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAClD,MAAM,IAAI,GAAI,GAA0B,CAAC,IAAI,CAAC;IAC9C,OAAO,IAAI,KAAK,kBAAkB,IAAI,IAAI,KAAK,sBAAsB,CAAC;AACxE,CAAC;AAED,4EAA4E;AAE5E;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,SAAmB,EACnB,GAAW,EACX,MAA4B;IAE5B,uFAAuF;IACvF,sFAAsF;IACtF,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IAClC,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,CAAC,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAQ,CAAC;QAC7D,MAAM,GAAG,YAAY,CAAC,MAAM,IAAI,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC;IAC/D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,gCAAgC,CAAC,CAAC;YACxE,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,uBAAuB,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/E,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU;YAAE,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;QACxF,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,4DAA4D,CAAC,CAAC;QACpG,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QAElD,MAAM,QAAQ,GAAoB,EAAE,CAAC;QAErC,KAAK,MAAM,MAAM,IAAI,OAAgB,EAAE,CAAC;YACtC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,QAAiB,EAAE,CAAC;gBAC3C,MAAM,QAAQ,GACZ,GAAG,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;gBAEzE,MAAM,WAAW,GAAe;oBAC9B,IAAI,EAAE,MAAM,CAAC,QAAQ;oBACrB,SAAS,EAAE,GAAG,CAAC,IAAI;oBACnB,QAAQ,EAAE,GAAG,CAAC,MAAM;oBACpB,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,IAAI;oBAChC,MAAM,EAAE,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,MAAM;iBACpC,CAAC;gBAEF,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,QAAQ;oBAChB,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,gBAAgB;oBACtC,QAAQ;oBACR,QAAQ,EAAE,oBAAoB,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC;oBAChD,OAAO,EAAE,GAAG,CAAC,OAAO;oBACpB,WAAW;oBACX,UAAU,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS;oBAChD,WAAW,EAAE,iBAAiB,CAAC,GAAG,CAAC,MAAM,IAAI,QAAQ,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC;iBAC7E,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,wFAAwF;QACxF,sFAAsF;QACtF,4CAA4C;QAC5C,MAAM,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,+BAA+B,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;QACvF,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU;YAAE,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;QACxF,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,oBAAoB,CAAC,MAAc;IAC1C,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,OAAO,CAAC;IAC/E,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAC1E,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9E,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,WAAW,CAAC;IAC/E,OAAO,SAAS,CAAC;AACnB,CAAC;AA2BD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAgB,EAChB,UAAoC,EAAE,EACtC,MAA4B;IAE5B,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;QAEpD,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;YAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;YACxC,IAAI,CAAC,UAAU;gBAAE,SAAS;YAE1B,MAAM,QAAQ,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;YAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAEhC,IAAI,SAAS,GAAG,CAAC,CAAC;YAClB,IAAI,QAAQ,GAAG,CAAC,CAAC;YACjB,IAAI,OAAO,GAAG,CAAC,CAAC;YAChB,IAAI,MAAM,GAAG,CAAC,CAAC;YAEf,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,MAAM,QAAQ,GAAG,UAAU,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC;gBACzD,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC;gBAC1B,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC;gBAE3B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBACzB,MAAM,MAAM,GAAG,UAAU,CAAC,qBAAqB,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC;oBAChE,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC;oBACtB,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;gBACzB,CAAC;qBAAM,CAAC;oBACN,OAAO,GAAG,SAAS,CAAC;oBACpB,MAAM,GAAG,QAAQ,CAAC;gBACpB,CAAC;YACH,CAAC;YAED,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YACpC,MAAM,QAAQ,GACZ,QAAQ,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;YAE3F,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;YAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;YACtC,MAAM,UAAU,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;YAEpF,sFAAsF;YACtF,6FAA6F;YAC7F,6FAA6F;YAC7F,oFAAoF;YACpF,kEAAkE;YAClE,2CAA2C;YAC3C,uFAAuF;YACvF,uFAAuF;YACvF,wFAAwF;YACxF,uFAAuF;YACvF,mFAAmF;YACnF,4FAA4F;YAC5F,mEAAmE;YACnE,4FAA4F;YAC5F,yFAAyF;YACzF,0FAA0F;YAC1F,MAAM,cAAc,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC;YACtD,MAAM,oBAAoB,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC;YAC/F,IAAI,CAAC,cAAc,IAAI,oBAAoB,CAAC,IAAI,OAAO,CAAC,6BAA6B,EAAE,CAAC;gBACtF,SAAS;YACX,CAAC;YAED,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,KAAK;gBACb,MAAM,EAAE,KAAK,IAAI,EAAE;gBACnB,QAAQ;gBACR,QAAQ,EAAE,MAAM;gBAChB,OAAO,EAAE,UAAU;gBACnB,WAAW,EAAE;oBACX,IAAI,EAAE,QAAQ;oBACd,SAAS;oBACT,QAAQ;oBACR,OAAO;oBACP,MAAM;iBACP;gBACD,WAAW,EAAE,iBAAiB,CAAC,KAAK,IAAI,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC;aACjE,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,0EAA0E;QAC1E,MAAM,EAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,gCAAgC,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;QACrF,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU;YAAE,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;IAC9F,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,yEAAyE;AAEzE;;;;;;;;GAQG;AACH,MAAM,UAAU,0BAA0B,CAAC,SAAmB,EAAE,MAA4B;IAC1F,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEtC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,OAAO,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;YAClC,CAAC;YAAC,OAAO,EAAE,EAAE,CAAC;gBACZ,KAAK,EAAE,CAAC,CAAC,iDAAiD;YAC5D,CAAC;QACH,CAAC;QACD,OAAO,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,oDAAoD,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;QACzG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU;YAAE,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;QAC9F,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,4EAA4E;AAE5E;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,QAAyB,EAAE,QAAuB;IAC5E,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS,CAAC,iBAAiB;QAElE,MAAM,IAAI,GAAG,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC;QACrC,MAAM,YAAY,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,IAAI,IAAI,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC;QAEpF,IAAI,YAAY,EAAE,CAAC;YACjB,CAAC,CAAC,OAAO,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|