@kernlang/review 3.3.4 → 3.3.6
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/auth-drift.d.ts +29 -0
- package/dist/concept-rules/auth-drift.js +127 -0
- package/dist/concept-rules/auth-drift.js.map +1 -0
- package/dist/concept-rules/contract-drift.d.ts +21 -0
- package/dist/concept-rules/contract-drift.js +65 -0
- package/dist/concept-rules/contract-drift.js.map +1 -0
- package/dist/concept-rules/contract-method-drift.d.ts +22 -0
- package/dist/concept-rules/contract-method-drift.js +105 -0
- package/dist/concept-rules/contract-method-drift.js.map +1 -0
- package/dist/concept-rules/cross-stack-utils.d.ts +96 -0
- package/dist/concept-rules/cross-stack-utils.js +259 -0
- package/dist/concept-rules/cross-stack-utils.js.map +1 -0
- package/dist/concept-rules/duplicate-route.d.ts +20 -0
- package/dist/concept-rules/duplicate-route.js +112 -0
- package/dist/concept-rules/duplicate-route.js.map +1 -0
- package/dist/concept-rules/index.js +26 -1
- package/dist/concept-rules/index.js.map +1 -1
- package/dist/concept-rules/missing-response-model.d.ts +10 -0
- package/dist/concept-rules/missing-response-model.js +38 -0
- package/dist/concept-rules/missing-response-model.js.map +1 -0
- package/dist/concept-rules/orphan-route.d.ts +20 -0
- package/dist/concept-rules/orphan-route.js +96 -0
- package/dist/concept-rules/orphan-route.js.map +1 -0
- package/dist/concept-rules/sync-handler-does-io.d.ts +9 -0
- package/dist/concept-rules/sync-handler-does-io.js +56 -0
- package/dist/concept-rules/sync-handler-does-io.js.map +1 -0
- package/dist/concept-rules/tainted-across-wire.d.ts +33 -0
- package/dist/concept-rules/tainted-across-wire.js +95 -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 +73 -0
- package/dist/concept-rules/untyped-api-response.js.map +1 -0
- package/dist/concept-rules/untyped-both-ends-response.d.ts +10 -0
- package/dist/concept-rules/untyped-both-ends-response.js +55 -0
- package/dist/concept-rules/untyped-both-ends-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 +115 -9
- package/dist/index.js.map +1 -1
- package/dist/llm-bridge.d.ts +38 -1
- package/dist/llm-bridge.js +172 -12
- 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 +650 -11
- package/dist/mappers/ts-concepts.js.map +1 -1
- package/dist/rules/index.js +17 -1
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/kern-source.js +37 -5
- package/dist/rules/kern-source.js.map +1 -1
- package/dist/rules/set-setter-collision.d.ts +21 -0
- package/dist/rules/set-setter-collision.js +74 -0
- package/dist/rules/set-setter-collision.js.map +1 -0
- package/dist/rules/suggest-kern-primitive.d.ts +30 -0
- package/dist/rules/suggest-kern-primitive.js +543 -0
- package/dist/rules/suggest-kern-primitive.js.map +1 -0
- 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,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: auth-drift
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend network call targets a server
|
|
5
|
+
* route file that declares an auth guard (FastAPI `Depends(get_current_user)`,
|
|
6
|
+
* Flask `@login_required`, early-return `if (!req.user)` Express patterns),
|
|
7
|
+
* but the client call sends no Authorization header.
|
|
8
|
+
*
|
|
9
|
+
* Real-bug classes:
|
|
10
|
+
* - Protected endpoint called from a public page or before login finished.
|
|
11
|
+
* - Frontend forgot to add the Bearer token after a refactor that moved
|
|
12
|
+
* auth out of a wrapper.
|
|
13
|
+
* - Endpoint was auth-gated late in the PR but the client still fires
|
|
14
|
+
* unauthenticated.
|
|
15
|
+
*
|
|
16
|
+
* v1 scope:
|
|
17
|
+
* - Only fires on raw `fetch(...)`. Wrapped clients typically inject auth
|
|
18
|
+
* inside the wrapper; the TS mapper already reports
|
|
19
|
+
* `hasAuthHeader: undefined` for them.
|
|
20
|
+
* - Auth guard is "present" when any guard concept of subtype 'auth' lives
|
|
21
|
+
* in the same file as a matching server route. File granularity is
|
|
22
|
+
* coarse on purpose — FastAPI's idiomatic `APIRouter(dependencies=[...])`
|
|
23
|
+
* puts the guard on the router, not each handler.
|
|
24
|
+
*
|
|
25
|
+
* Confidence: `CROSS_STACK_EXACT_CONFIDENCE` (0.9). Graph mode only.
|
|
26
|
+
*/
|
|
27
|
+
import type { ReviewFinding } from '../types.js';
|
|
28
|
+
import type { ConceptRuleContext } from './index.js';
|
|
29
|
+
export declare function authDrift(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: auth-drift
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend network call targets a server
|
|
5
|
+
* route file that declares an auth guard (FastAPI `Depends(get_current_user)`,
|
|
6
|
+
* Flask `@login_required`, early-return `if (!req.user)` Express patterns),
|
|
7
|
+
* but the client call sends no Authorization header.
|
|
8
|
+
*
|
|
9
|
+
* Real-bug classes:
|
|
10
|
+
* - Protected endpoint called from a public page or before login finished.
|
|
11
|
+
* - Frontend forgot to add the Bearer token after a refactor that moved
|
|
12
|
+
* auth out of a wrapper.
|
|
13
|
+
* - Endpoint was auth-gated late in the PR but the client still fires
|
|
14
|
+
* unauthenticated.
|
|
15
|
+
*
|
|
16
|
+
* v1 scope:
|
|
17
|
+
* - Only fires on raw `fetch(...)`. Wrapped clients typically inject auth
|
|
18
|
+
* inside the wrapper; the TS mapper already reports
|
|
19
|
+
* `hasAuthHeader: undefined` for them.
|
|
20
|
+
* - Auth guard is "present" when any guard concept of subtype 'auth' lives
|
|
21
|
+
* in the same file as a matching server route. File granularity is
|
|
22
|
+
* coarse on purpose — FastAPI's idiomatic `APIRouter(dependencies=[...])`
|
|
23
|
+
* puts the guard on the router, not each handler.
|
|
24
|
+
*
|
|
25
|
+
* Confidence: `CROSS_STACK_EXACT_CONFIDENCE` (0.9). Graph mode only.
|
|
26
|
+
*/
|
|
27
|
+
import { createFingerprint } from '../types.js';
|
|
28
|
+
import { API_PATH_RE, CROSS_STACK_EXACT_CONFIDENCE, collectRoutesAcrossGraph, findMatchingRoute, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
29
|
+
export function authDrift(ctx) {
|
|
30
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
31
|
+
return [];
|
|
32
|
+
const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
33
|
+
if (serverRoutes.length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
// Map each file to the set of containerIds that have an auth guard AND
|
|
36
|
+
// record which files have BOTH guarded and unguarded routes ("mixed").
|
|
37
|
+
// Codex review flagged that file-level presence was too coarse: a file
|
|
38
|
+
// with `/api/me` (guarded) + `/api/public` (not) would false-positive on
|
|
39
|
+
// the public endpoint. The fix: only fire when we can prove the SPECIFIC
|
|
40
|
+
// route is guarded (matching containerId), OR when every route in the
|
|
41
|
+
// file shares the file's guard scope. Mixed files stay silent.
|
|
42
|
+
const authGuardContainers = collectAuthGuardContainers(ctx.allConcepts);
|
|
43
|
+
if (authGuardContainers.size === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const findings = [];
|
|
46
|
+
for (const [, conceptMap] of ctx.allConcepts) {
|
|
47
|
+
for (const node of conceptMap.nodes) {
|
|
48
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
49
|
+
continue;
|
|
50
|
+
if (node.primarySpan.file !== ctx.filePath)
|
|
51
|
+
continue;
|
|
52
|
+
// Must be explicitly `false`. `undefined` = mapper couldn't tell; stay silent.
|
|
53
|
+
if (node.payload.hasAuthHeader !== false)
|
|
54
|
+
continue;
|
|
55
|
+
const target = node.payload.target;
|
|
56
|
+
if (typeof target !== 'string')
|
|
57
|
+
continue;
|
|
58
|
+
const normalized = normalizeClientUrl(target);
|
|
59
|
+
if (!normalized || !API_PATH_RE.test(normalized))
|
|
60
|
+
continue;
|
|
61
|
+
const route = findMatchingRoute(normalized, serverRoutes);
|
|
62
|
+
if (!route?.node)
|
|
63
|
+
continue;
|
|
64
|
+
const serverFile = route.node.primarySpan.file;
|
|
65
|
+
const fileGuards = authGuardContainers.get(serverFile);
|
|
66
|
+
if (!fileGuards)
|
|
67
|
+
continue;
|
|
68
|
+
// Proof that this specific route is guarded:
|
|
69
|
+
// (a) the route's containerId matches an auth-guard containerId
|
|
70
|
+
// (FastAPI pattern: `@router.get` + `Depends(...)` share the
|
|
71
|
+
// function body), OR
|
|
72
|
+
// (b) the file contains exactly one route (Express pattern: guard
|
|
73
|
+
// is inside the handler callback, not the app.get call, so
|
|
74
|
+
// containers differ but there's no ambiguity about which route
|
|
75
|
+
// the guard protects).
|
|
76
|
+
// Mixed multi-route files with no container match fall through to
|
|
77
|
+
// silent — Codex review called out this false-positive class.
|
|
78
|
+
const routeContainer = route.node.containerId;
|
|
79
|
+
const routeIsGuardedByContainer = routeContainer !== undefined && fileGuards.routeContainers.has(routeContainer);
|
|
80
|
+
const routeIsGuardedBySingleRouteFile = fileGuards.totalRoutesInFile === 1;
|
|
81
|
+
if (!routeIsGuardedByContainer && !routeIsGuardedBySingleRouteFile)
|
|
82
|
+
continue;
|
|
83
|
+
findings.push({
|
|
84
|
+
source: 'kern',
|
|
85
|
+
ruleId: 'auth-drift',
|
|
86
|
+
severity: 'warning',
|
|
87
|
+
category: 'bug',
|
|
88
|
+
message: `Frontend calls \`${target}\` without an Authorization header, but the server route requires authentication (guard declared in ${shortPath(serverFile)}). Add the Authorization header or change the server guard.`,
|
|
89
|
+
primarySpan: node.primarySpan,
|
|
90
|
+
fingerprint: createFingerprint('auth-drift', node.primarySpan.startLine, node.primarySpan.startCol),
|
|
91
|
+
confidence: node.confidence * CROSS_STACK_EXACT_CONFIDENCE,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return findings;
|
|
96
|
+
}
|
|
97
|
+
function collectAuthGuardContainers(allConcepts) {
|
|
98
|
+
const result = new Map();
|
|
99
|
+
for (const [filePath, map] of allConcepts) {
|
|
100
|
+
const routeContainers = new Set();
|
|
101
|
+
let routeCount = 0;
|
|
102
|
+
for (const node of map.nodes) {
|
|
103
|
+
if (node.kind === 'entrypoint' && node.payload.kind === 'entrypoint' && node.payload.subtype === 'route') {
|
|
104
|
+
routeCount++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (node.kind !== 'guard' || node.payload.kind !== 'guard')
|
|
108
|
+
continue;
|
|
109
|
+
if (node.payload.subtype !== 'auth')
|
|
110
|
+
continue;
|
|
111
|
+
// The guard's containerId is the scope that enforces auth. Routes
|
|
112
|
+
// sharing that containerId (same function body) are considered
|
|
113
|
+
// guarded.
|
|
114
|
+
if (node.containerId !== undefined)
|
|
115
|
+
routeContainers.add(node.containerId);
|
|
116
|
+
}
|
|
117
|
+
if (routeContainers.size > 0) {
|
|
118
|
+
result.set(filePath, { routeContainers, totalRoutesInFile: routeCount });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function shortPath(filePath) {
|
|
124
|
+
const parts = filePath.split('/');
|
|
125
|
+
return parts.slice(-2).join('/');
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=auth-drift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-drift.js","sourceRoot":"","sources":["../../src/concept-rules/auth-drift.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,65 @@
|
|
|
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, collectRoutesAcrossGraph, 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 = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
26
|
+
const clientCalls = [];
|
|
27
|
+
for (const [, conceptMap] of ctx.allConcepts) {
|
|
28
|
+
for (const node of conceptMap.nodes) {
|
|
29
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
30
|
+
continue;
|
|
31
|
+
const target = node.payload.target;
|
|
32
|
+
if (typeof target !== 'string')
|
|
33
|
+
continue;
|
|
34
|
+
const normalized = normalizeClientUrl(target);
|
|
35
|
+
if (!normalized || !API_PATH_RE.test(normalized))
|
|
36
|
+
continue;
|
|
37
|
+
clientCalls.push({ target, normalizedPath: normalized, node });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Rule gate: need at least one route AND one client call, otherwise the
|
|
41
|
+
// project isn't a full-stack app and we'd fire on every external API hit.
|
|
42
|
+
if (serverRoutes.length === 0 || clientCalls.length === 0)
|
|
43
|
+
return [];
|
|
44
|
+
const findings = [];
|
|
45
|
+
for (const call of clientCalls) {
|
|
46
|
+
// Only report on calls that happen in files from the reviewed project —
|
|
47
|
+
// avoids firing on third-party SDK targets.
|
|
48
|
+
if (call.node.primarySpan.file !== ctx.filePath)
|
|
49
|
+
continue;
|
|
50
|
+
if (hasMatchingRoute(call.normalizedPath, serverRoutes))
|
|
51
|
+
continue;
|
|
52
|
+
findings.push({
|
|
53
|
+
source: 'kern',
|
|
54
|
+
ruleId: 'contract-drift',
|
|
55
|
+
severity: 'warning',
|
|
56
|
+
category: 'bug',
|
|
57
|
+
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.`,
|
|
58
|
+
primarySpan: call.node.primarySpan,
|
|
59
|
+
fingerprint: createFingerprint('contract-drift', call.node.primarySpan.startLine, call.node.primarySpan.startCol),
|
|
60
|
+
confidence: call.node.confidence * CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return findings;
|
|
64
|
+
}
|
|
65
|
+
//# 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,wBAAwB,EACxB,gBAAgB,EAChB,kBAAkB,GACnB,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,GAAG,wBAAwB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC/D,MAAM,WAAW,GAAiB,EAAE,CAAC;IAErC,KAAK,MAAM,CAAC,EAAE,UAAU,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;QAC7C,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,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: contract-method-drift
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend network call targets an API path
|
|
5
|
+
* the server DOES define, but only for different HTTP methods. Sibling to
|
|
6
|
+
* `contract-drift`:
|
|
7
|
+
* - contract-drift : "no server route exists at this path"
|
|
8
|
+
* - contract-method-drift : "server routes exist at this path, just not
|
|
9
|
+
* for your verb"
|
|
10
|
+
*
|
|
11
|
+
* Keeping them separate matters: the messages and fixes differ (rename the
|
|
12
|
+
* client URL vs. change the verb), the fingerprints can't collide on the
|
|
13
|
+
* same line, and the existing contract-drift `continue` gate stays intact.
|
|
14
|
+
*
|
|
15
|
+
* Confidence multiplier: `CROSS_STACK_EXACT_CONFIDENCE` (0.9) — once the path
|
|
16
|
+
* matches, a method mismatch is unambiguous.
|
|
17
|
+
*
|
|
18
|
+
* Requires graph mode; silent in single-file review.
|
|
19
|
+
*/
|
|
20
|
+
import type { ReviewFinding } from '../types.js';
|
|
21
|
+
import type { ConceptRuleContext } from './index.js';
|
|
22
|
+
export declare function contractMethodDrift(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: contract-method-drift
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — fires when a frontend network call targets an API path
|
|
5
|
+
* the server DOES define, but only for different HTTP methods. Sibling to
|
|
6
|
+
* `contract-drift`:
|
|
7
|
+
* - contract-drift : "no server route exists at this path"
|
|
8
|
+
* - contract-method-drift : "server routes exist at this path, just not
|
|
9
|
+
* for your verb"
|
|
10
|
+
*
|
|
11
|
+
* Keeping them separate matters: the messages and fixes differ (rename the
|
|
12
|
+
* client URL vs. change the verb), the fingerprints can't collide on the
|
|
13
|
+
* same line, and the existing contract-drift `continue` gate stays intact.
|
|
14
|
+
*
|
|
15
|
+
* Confidence multiplier: `CROSS_STACK_EXACT_CONFIDENCE` (0.9) — once the path
|
|
16
|
+
* matches, a method mismatch is unambiguous.
|
|
17
|
+
*
|
|
18
|
+
* Requires graph mode; silent in single-file review.
|
|
19
|
+
*/
|
|
20
|
+
import { createFingerprint } from '../types.js';
|
|
21
|
+
import { API_PATH_RE, CROSS_STACK_EXACT_CONFIDENCE, collectRoutesAcrossGraph, findRoutesAtPath, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
22
|
+
// Verbs the TS/Python mappers emit for handlers that intentionally accept any
|
|
23
|
+
// method (Express `app.all()` emits `ALL`; `app.use()` emits `undefined`).
|
|
24
|
+
const WILDCARD_METHODS = new Set(['ALL', 'ANY']);
|
|
25
|
+
export function contractMethodDrift(ctx) {
|
|
26
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
27
|
+
return [];
|
|
28
|
+
const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
29
|
+
if (serverRoutes.length === 0)
|
|
30
|
+
return [];
|
|
31
|
+
const clientCalls = [];
|
|
32
|
+
for (const [, conceptMap] of ctx.allConcepts) {
|
|
33
|
+
for (const node of conceptMap.nodes) {
|
|
34
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
35
|
+
continue;
|
|
36
|
+
const target = node.payload.target;
|
|
37
|
+
const method = node.payload.method;
|
|
38
|
+
if (typeof target !== 'string' || typeof method !== 'string')
|
|
39
|
+
continue;
|
|
40
|
+
const normalized = normalizeClientUrl(target);
|
|
41
|
+
if (!normalized || !API_PATH_RE.test(normalized))
|
|
42
|
+
continue;
|
|
43
|
+
clientCalls.push({ target, normalizedPath: normalized, method, node });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (clientCalls.length === 0)
|
|
47
|
+
return [];
|
|
48
|
+
const findings = [];
|
|
49
|
+
for (const call of clientCalls) {
|
|
50
|
+
if (call.node.primarySpan.file !== ctx.filePath)
|
|
51
|
+
continue;
|
|
52
|
+
const routesAtPath = findRoutesAtPath(call.normalizedPath, serverRoutes);
|
|
53
|
+
if (routesAtPath.length === 0)
|
|
54
|
+
continue;
|
|
55
|
+
const hasMethodMatch = routesAtPath.some((r) => methodMatches(r.method, call.method));
|
|
56
|
+
if (hasMethodMatch)
|
|
57
|
+
continue;
|
|
58
|
+
const serverMethods = collectKnownMethods(routesAtPath);
|
|
59
|
+
if (serverMethods.length === 0)
|
|
60
|
+
continue;
|
|
61
|
+
const methodList = serverMethods.join(', ');
|
|
62
|
+
const hint = serverMethods.length === 1 ? ` Did you mean \`${serverMethods[0]} ${call.target}\`?` : '';
|
|
63
|
+
findings.push({
|
|
64
|
+
source: 'kern',
|
|
65
|
+
ruleId: 'contract-method-drift',
|
|
66
|
+
severity: 'warning',
|
|
67
|
+
category: 'bug',
|
|
68
|
+
message: `Frontend calls \`${call.method} ${call.target}\` but the server only defines [${methodList}] for this path.${hint}`,
|
|
69
|
+
primarySpan: call.node.primarySpan,
|
|
70
|
+
fingerprint: createFingerprint('contract-method-drift', call.node.primarySpan.startLine, call.node.primarySpan.startCol),
|
|
71
|
+
confidence: call.node.confidence * CROSS_STACK_EXACT_CONFIDENCE,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return findings;
|
|
75
|
+
}
|
|
76
|
+
function methodMatches(routeMethod, clientMethod) {
|
|
77
|
+
if (!routeMethod)
|
|
78
|
+
return true;
|
|
79
|
+
const r = routeMethod.toUpperCase();
|
|
80
|
+
if (WILDCARD_METHODS.has(r))
|
|
81
|
+
return true;
|
|
82
|
+
const c = clientMethod.toUpperCase();
|
|
83
|
+
if (r === c)
|
|
84
|
+
return true;
|
|
85
|
+
// Express and Starlette/FastAPI both auto-respond to HEAD on GET routes
|
|
86
|
+
// (returning headers, no body). Firing method-drift on `HEAD /api/x`
|
|
87
|
+
// against `app.get('/api/x')` is a false positive — the server DOES
|
|
88
|
+
// satisfy the request. Codex review called this out.
|
|
89
|
+
if (c === 'HEAD' && r === 'GET')
|
|
90
|
+
return true;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
function collectKnownMethods(routes) {
|
|
94
|
+
const set = new Set();
|
|
95
|
+
for (const r of routes) {
|
|
96
|
+
if (!r.method)
|
|
97
|
+
continue;
|
|
98
|
+
const m = r.method.toUpperCase();
|
|
99
|
+
if (WILDCARD_METHODS.has(m))
|
|
100
|
+
continue;
|
|
101
|
+
set.add(m);
|
|
102
|
+
}
|
|
103
|
+
return Array.from(set).sort();
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=contract-method-drift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-method-drift.js","sourceRoot":"","sources":["../../src/concept-rules/contract-method-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,4BAA4B,EAC5B,wBAAwB,EACxB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,wBAAwB,CAAC;AAUhC,8EAA8E;AAC9E,2EAA2E;AAC3E,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAEjD,MAAM,UAAU,mBAAmB,CAAC,GAAuB;IACzD,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAE9D,MAAM,YAAY,GAAG,wBAAwB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC/D,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEzC,MAAM,WAAW,GAAiB,EAAE,CAAC;IACrC,KAAK,MAAM,CAAC,EAAE,UAAU,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;QAC7C,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,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;YACnC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK,QAAQ;gBAAE,SAAS;YACvE,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,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IACD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAExC,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,KAAK,GAAG,CAAC,QAAQ;YAAE,SAAS;QAC1D,MAAM,YAAY,GAAG,gBAAgB,CAAC,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QACzE,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACxC,MAAM,cAAc,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QACtF,IAAI,cAAc;YAAE,SAAS;QAE7B,MAAM,aAAa,GAAG,mBAAmB,CAAC,YAAY,CAAC,CAAC;QACxD,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,mBAAmB,aAAa,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACvG,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,uBAAuB;YAC/B,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,oBAAoB,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,mCAAmC,UAAU,mBAAmB,IAAI,EAAE;YAC7H,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW;YAClC,WAAW,EAAE,iBAAiB,CAC5B,uBAAuB,EACvB,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAC/B,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAC/B;YACD,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,4BAA4B;SAChE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,aAAa,CAAC,WAA+B,EAAE,YAAoB;IAC1E,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,CAAC,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;IACpC,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,CAAC,GAAG,YAAY,CAAC,WAAW,EAAE,CAAC;IACrC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzB,wEAAwE;IACxE,qEAAqE;IACrE,oEAAoE;IACpE,qDAAqD;IACrD,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAC7C,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,mBAAmB,CAAC,MAAiD;IAC5E,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,CAAC,CAAC,MAAM;YAAE,SAAS;QACxB,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QACjC,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,SAAS;QACtC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACb,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
/**
|
|
21
|
+
* Multiplier for rules where the correlation is unambiguous: the path matches
|
|
22
|
+
* exactly AND a second dimension (HTTP method, auth header, …) disagrees.
|
|
23
|
+
* `contract-method-drift`, `duplicate-route`, and `auth-drift` use this —
|
|
24
|
+
* once the path matches, a verb mismatch, duplicate declaration, or missing
|
|
25
|
+
* Authorization header is a real bug, not a heuristic.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CROSS_STACK_EXACT_CONFIDENCE = 0.9;
|
|
28
|
+
/** Client URLs we consider "internal" to the reviewed project. */
|
|
29
|
+
export declare const API_PATH_RE: RegExp;
|
|
30
|
+
export interface ServerRoute {
|
|
31
|
+
path: string;
|
|
32
|
+
method: string | undefined;
|
|
33
|
+
/** Present when the caller needs to cite the server route in a finding. */
|
|
34
|
+
node?: ConceptNode;
|
|
35
|
+
}
|
|
36
|
+
export declare function hasFastApiEvidence(map: ConceptMap): boolean;
|
|
37
|
+
export declare function isFastApiRouteMissingResponseModel(node: ConceptNode, map?: ConceptMap): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Pull every server-side route out of a concept map. Callers typically fold
|
|
40
|
+
* this across `ctx.allConcepts` to collect routes for the whole project.
|
|
41
|
+
*
|
|
42
|
+
* Per-file use (legacy signature): just emits the decorator path as-is.
|
|
43
|
+
*
|
|
44
|
+
* Cross-project use (preferred): call `collectRoutesAcrossGraph` instead,
|
|
45
|
+
* which joins route-mount concepts (FastAPI `app.include_router(prefix=…)`)
|
|
46
|
+
* with the per-file route decorators so `@router.get("/current")` mounted
|
|
47
|
+
* under `prefix="/api/nutrition-goals"` surfaces as `/api/nutrition-goals/current`.
|
|
48
|
+
* Without that join the wedge rules silently find nothing on every FastAPI
|
|
49
|
+
* app that follows the standard APIRouter pattern.
|
|
50
|
+
*/
|
|
51
|
+
export declare function collectRoutes(map: ConceptMap, routes: ServerRoute[]): void;
|
|
52
|
+
/**
|
|
53
|
+
* Graph-wide route collection with FastAPI router-prefix expansion.
|
|
54
|
+
*
|
|
55
|
+
* Walks every concept map twice:
|
|
56
|
+
* 1. Collect `route-mount` concepts (FastAPI `app.include_router(<router>,
|
|
57
|
+
* prefix=…)` calls). Each mount carries `prefix`, `routerName`, and —
|
|
58
|
+
* when the router was imported from another module — `sourceModule`
|
|
59
|
+
* like `app.api.nutrition_goals`.
|
|
60
|
+
* 2. For each per-file `route` concept, look up a matching mount by
|
|
61
|
+
* `sourceModule` ↔ file path suffix (Python `app.api.nutrition_goals`
|
|
62
|
+
* resolves to any file path ending in `app/api/nutrition_goals.py`),
|
|
63
|
+
* falling back to a project-wide `routerName` match when the mount
|
|
64
|
+
* is in the same file as the routes.
|
|
65
|
+
*
|
|
66
|
+
* Per-file routes with no mount are still emitted with their declared path
|
|
67
|
+
* — Flask / Express routes and FastAPI apps that decorate directly on
|
|
68
|
+
* `@app.get(...)` already carry the full path.
|
|
69
|
+
*/
|
|
70
|
+
export declare function collectRoutesAcrossGraph(allConcepts: ReadonlyMap<string, ConceptMap>): ServerRoute[];
|
|
71
|
+
/**
|
|
72
|
+
* Strip scheme/host, query string, and fragment from a client URL so it can
|
|
73
|
+
* match against a server route template. Returns undefined when the input
|
|
74
|
+
* isn't a recognisable path (e.g. a bare variable reference or an
|
|
75
|
+
* unresolved template expression).
|
|
76
|
+
*/
|
|
77
|
+
export declare function normalizeClientUrl(raw: string): string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Match a client-side concrete path against server-side route templates.
|
|
80
|
+
* Returns the first matching route (so callers can cite it in findings) or
|
|
81
|
+
* `undefined`. Server templates may contain params — Express/Koa `:id`,
|
|
82
|
+
* FastAPI `{id}` — which match any single segment. Trailing slashes are
|
|
83
|
+
* normalised on both sides. Case-sensitive (matches Express/FastAPI default
|
|
84
|
+
* behaviour).
|
|
85
|
+
*/
|
|
86
|
+
export declare function findMatchingRoute(clientPath: string, routes: readonly ServerRoute[]): ServerRoute | undefined;
|
|
87
|
+
/** Boolean-returning thin wrapper preserved for callers that just need a yes/no. */
|
|
88
|
+
export declare function hasMatchingRoute(clientPath: string, routes: readonly ServerRoute[]): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Return every server route whose path template matches the client path,
|
|
91
|
+
* regardless of HTTP method. Used by `contract-method-drift` and
|
|
92
|
+
* `orphan-route` to distinguish "no server exists here" (contract-drift
|
|
93
|
+
* territory) from "server exists but only responds to a different verb /
|
|
94
|
+
* no one calls it" (method-drift / orphan-route territory).
|
|
95
|
+
*/
|
|
96
|
+
export declare function findRoutesAtPath(clientPath: string, routes: readonly ServerRoute[]): ServerRoute[];
|