@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
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
/**
|
|
20
|
+
* Multiplier for rules where the correlation is unambiguous: the path matches
|
|
21
|
+
* exactly AND a second dimension (HTTP method, auth header, …) disagrees.
|
|
22
|
+
* `contract-method-drift`, `duplicate-route`, and `auth-drift` use this —
|
|
23
|
+
* once the path matches, a verb mismatch, duplicate declaration, or missing
|
|
24
|
+
* Authorization header is a real bug, not a heuristic.
|
|
25
|
+
*/
|
|
26
|
+
export const CROSS_STACK_EXACT_CONFIDENCE = 0.9;
|
|
27
|
+
/** Client URLs we consider "internal" to the reviewed project. */
|
|
28
|
+
export const API_PATH_RE = /^\/api\//;
|
|
29
|
+
export function hasFastApiEvidence(map) {
|
|
30
|
+
if (map.language !== 'py')
|
|
31
|
+
return false;
|
|
32
|
+
return map.edges.some((edge) => {
|
|
33
|
+
if (edge.kind !== 'dependency' || edge.payload.kind !== 'dependency')
|
|
34
|
+
return false;
|
|
35
|
+
return edge.payload.specifier === 'fastapi' || edge.payload.specifier.startsWith('fastapi.');
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function isFastApiRouteMissingResponseModel(node, map) {
|
|
39
|
+
if (node.language !== 'py')
|
|
40
|
+
return false;
|
|
41
|
+
if (node.kind !== 'entrypoint' || node.payload.kind !== 'entrypoint')
|
|
42
|
+
return false;
|
|
43
|
+
if (node.payload.subtype !== 'route')
|
|
44
|
+
return false;
|
|
45
|
+
if (node.payload.responseModel)
|
|
46
|
+
return false;
|
|
47
|
+
return map ? hasFastApiEvidence(map) : false;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Pull every server-side route out of a concept map. Callers typically fold
|
|
51
|
+
* this across `ctx.allConcepts` to collect routes for the whole project.
|
|
52
|
+
*
|
|
53
|
+
* Per-file use (legacy signature): just emits the decorator path as-is.
|
|
54
|
+
*
|
|
55
|
+
* Cross-project use (preferred): call `collectRoutesAcrossGraph` instead,
|
|
56
|
+
* which joins route-mount concepts (FastAPI `app.include_router(prefix=…)`)
|
|
57
|
+
* with the per-file route decorators so `@router.get("/current")` mounted
|
|
58
|
+
* under `prefix="/api/nutrition-goals"` surfaces as `/api/nutrition-goals/current`.
|
|
59
|
+
* Without that join the wedge rules silently find nothing on every FastAPI
|
|
60
|
+
* app that follows the standard APIRouter pattern.
|
|
61
|
+
*/
|
|
62
|
+
export function collectRoutes(map, routes) {
|
|
63
|
+
for (const node of map.nodes) {
|
|
64
|
+
if (node.kind !== 'entrypoint' || node.payload.kind !== 'entrypoint' || node.payload.subtype !== 'route')
|
|
65
|
+
continue;
|
|
66
|
+
const path = node.payload.name;
|
|
67
|
+
if (typeof path !== 'string' || !path.startsWith('/'))
|
|
68
|
+
continue;
|
|
69
|
+
routes.push({ path, method: node.payload.httpMethod, node });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Graph-wide route collection with FastAPI router-prefix expansion.
|
|
74
|
+
*
|
|
75
|
+
* Walks every concept map twice:
|
|
76
|
+
* 1. Collect `route-mount` concepts (FastAPI `app.include_router(<router>,
|
|
77
|
+
* prefix=…)` calls). Each mount carries `prefix`, `routerName`, and —
|
|
78
|
+
* when the router was imported from another module — `sourceModule`
|
|
79
|
+
* like `app.api.nutrition_goals`.
|
|
80
|
+
* 2. For each per-file `route` concept, look up a matching mount by
|
|
81
|
+
* `sourceModule` ↔ file path suffix (Python `app.api.nutrition_goals`
|
|
82
|
+
* resolves to any file path ending in `app/api/nutrition_goals.py`),
|
|
83
|
+
* falling back to a project-wide `routerName` match when the mount
|
|
84
|
+
* is in the same file as the routes.
|
|
85
|
+
*
|
|
86
|
+
* Per-file routes with no mount are still emitted with their declared path
|
|
87
|
+
* — Flask / Express routes and FastAPI apps that decorate directly on
|
|
88
|
+
* `@app.get(...)` already carry the full path.
|
|
89
|
+
*/
|
|
90
|
+
export function collectRoutesAcrossGraph(allConcepts) {
|
|
91
|
+
const routes = [];
|
|
92
|
+
// Build the mount index first so each route can look up its prefix.
|
|
93
|
+
const mountsByModule = new Map();
|
|
94
|
+
const mountsByRouter = new Map();
|
|
95
|
+
for (const [mountFile, map] of allConcepts) {
|
|
96
|
+
for (const node of map.nodes) {
|
|
97
|
+
if (node.kind !== 'entrypoint' || node.payload.kind !== 'entrypoint')
|
|
98
|
+
continue;
|
|
99
|
+
if (node.payload.subtype !== 'route-mount')
|
|
100
|
+
continue;
|
|
101
|
+
const prefix = node.payload.name;
|
|
102
|
+
const routerName = node.payload.routerName;
|
|
103
|
+
const sourceModule = node.payload.sourceModule;
|
|
104
|
+
if (sourceModule) {
|
|
105
|
+
const list = mountsByModule.get(sourceModule) ?? [];
|
|
106
|
+
list.push(prefix);
|
|
107
|
+
mountsByModule.set(sourceModule, list);
|
|
108
|
+
}
|
|
109
|
+
if (routerName) {
|
|
110
|
+
const list = mountsByRouter.get(routerName) ?? [];
|
|
111
|
+
list.push({ prefix, mountFile });
|
|
112
|
+
mountsByRouter.set(routerName, list);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const [routeFile, map] of allConcepts) {
|
|
117
|
+
for (const node of map.nodes) {
|
|
118
|
+
if (node.kind !== 'entrypoint' || node.payload.kind !== 'entrypoint')
|
|
119
|
+
continue;
|
|
120
|
+
if (node.payload.subtype !== 'route')
|
|
121
|
+
continue;
|
|
122
|
+
const path = node.payload.name;
|
|
123
|
+
if (typeof path !== 'string' || !path.startsWith('/'))
|
|
124
|
+
continue;
|
|
125
|
+
const prefix = resolveMountPrefix(routeFile, node.payload.routerName, mountsByModule, mountsByRouter);
|
|
126
|
+
const fullPath = prefix ? joinPaths(prefix, path) : path;
|
|
127
|
+
routes.push({ path: fullPath, method: node.payload.httpMethod, node });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return routes;
|
|
131
|
+
}
|
|
132
|
+
function resolveMountPrefix(routeFile, routerName, mountsByModule, mountsByRouter) {
|
|
133
|
+
// Module-based match. TS mounts emit a `sourceModule` that already carries a
|
|
134
|
+
// code extension (e.g. `routes/review.ts`) — use it as a path suffix directly.
|
|
135
|
+
// Python mounts emit a dotted module name (`app.api.nutrition_goals`) — translate
|
|
136
|
+
// to `app/api/nutrition_goals.py` first. The leading-slash boundary check in
|
|
137
|
+
// both branches prevents `blog/api.py` from false-matching module `api`.
|
|
138
|
+
for (const [sourceModule, prefixes] of mountsByModule) {
|
|
139
|
+
if (prefixes.length === 0)
|
|
140
|
+
continue;
|
|
141
|
+
const relTail = /\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(sourceModule)
|
|
142
|
+
? sourceModule
|
|
143
|
+
: `${sourceModule.replace(/\./g, '/')}.py`;
|
|
144
|
+
if (routeFile === relTail || routeFile.endsWith(`/${relTail}`))
|
|
145
|
+
return prefixes[0];
|
|
146
|
+
}
|
|
147
|
+
// Same-file match: `router = APIRouter(); app.include_router(router, prefix=…)`.
|
|
148
|
+
// The mount has no `sourceModule` but shares the file with the routes.
|
|
149
|
+
if (routerName) {
|
|
150
|
+
const entries = mountsByRouter.get(routerName);
|
|
151
|
+
if (entries) {
|
|
152
|
+
const sameFile = entries.find((e) => e.mountFile === routeFile);
|
|
153
|
+
if (sameFile)
|
|
154
|
+
return sameFile.prefix;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
function joinPaths(prefix, path) {
|
|
160
|
+
const trimmedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
161
|
+
const trimmedPath = path.startsWith('/') ? path : `/${path}`;
|
|
162
|
+
if (trimmedPath === '/')
|
|
163
|
+
return trimmedPrefix || '/';
|
|
164
|
+
return `${trimmedPrefix}${trimmedPath}`;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Strip scheme/host, query string, and fragment from a client URL so it can
|
|
168
|
+
* match against a server route template. Returns undefined when the input
|
|
169
|
+
* isn't a recognisable path (e.g. a bare variable reference or an
|
|
170
|
+
* unresolved template expression).
|
|
171
|
+
*/
|
|
172
|
+
export function normalizeClientUrl(raw) {
|
|
173
|
+
let url = raw.trim();
|
|
174
|
+
if (url.startsWith('`') && !url.startsWith('`/'))
|
|
175
|
+
return undefined;
|
|
176
|
+
url = url.replace(/^`|`$/g, '');
|
|
177
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
178
|
+
const pathStart = url.indexOf('/', url.indexOf('://') + 3);
|
|
179
|
+
url = pathStart === -1 ? '/' : url.slice(pathStart);
|
|
180
|
+
}
|
|
181
|
+
const q = url.indexOf('?');
|
|
182
|
+
if (q !== -1)
|
|
183
|
+
url = url.slice(0, q);
|
|
184
|
+
const h = url.indexOf('#');
|
|
185
|
+
if (h !== -1)
|
|
186
|
+
url = url.slice(0, h);
|
|
187
|
+
return url || undefined;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Match a client-side concrete path against server-side route templates.
|
|
191
|
+
* Returns the first matching route (so callers can cite it in findings) or
|
|
192
|
+
* `undefined`. Server templates may contain params — Express/Koa `:id`,
|
|
193
|
+
* FastAPI `{id}` — which match any single segment. Trailing slashes are
|
|
194
|
+
* normalised on both sides. Case-sensitive (matches Express/FastAPI default
|
|
195
|
+
* behaviour).
|
|
196
|
+
*/
|
|
197
|
+
export function findMatchingRoute(clientPath, routes) {
|
|
198
|
+
const clientSegments = trimTrailing(clientPath).split('/');
|
|
199
|
+
for (const route of routes) {
|
|
200
|
+
const routeSegments = trimTrailing(route.path).split('/');
|
|
201
|
+
if (routeSegments.length !== clientSegments.length)
|
|
202
|
+
continue;
|
|
203
|
+
let matched = true;
|
|
204
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
205
|
+
const rs = routeSegments[i];
|
|
206
|
+
const cs = clientSegments[i];
|
|
207
|
+
if (isParamSegment(rs))
|
|
208
|
+
continue;
|
|
209
|
+
if (rs !== cs) {
|
|
210
|
+
matched = false;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (matched)
|
|
215
|
+
return route;
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
/** Boolean-returning thin wrapper preserved for callers that just need a yes/no. */
|
|
220
|
+
export function hasMatchingRoute(clientPath, routes) {
|
|
221
|
+
return findMatchingRoute(clientPath, routes) !== undefined;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Return every server route whose path template matches the client path,
|
|
225
|
+
* regardless of HTTP method. Used by `contract-method-drift` and
|
|
226
|
+
* `orphan-route` to distinguish "no server exists here" (contract-drift
|
|
227
|
+
* territory) from "server exists but only responds to a different verb /
|
|
228
|
+
* no one calls it" (method-drift / orphan-route territory).
|
|
229
|
+
*/
|
|
230
|
+
export function findRoutesAtPath(clientPath, routes) {
|
|
231
|
+
const clientSegments = trimTrailing(clientPath).split('/');
|
|
232
|
+
const matches = [];
|
|
233
|
+
for (const route of routes) {
|
|
234
|
+
const routeSegments = trimTrailing(route.path).split('/');
|
|
235
|
+
if (routeSegments.length !== clientSegments.length)
|
|
236
|
+
continue;
|
|
237
|
+
let matched = true;
|
|
238
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
239
|
+
const rs = routeSegments[i];
|
|
240
|
+
const cs = clientSegments[i];
|
|
241
|
+
if (isParamSegment(rs))
|
|
242
|
+
continue;
|
|
243
|
+
if (rs !== cs) {
|
|
244
|
+
matched = false;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (matched)
|
|
249
|
+
matches.push(route);
|
|
250
|
+
}
|
|
251
|
+
return matches;
|
|
252
|
+
}
|
|
253
|
+
function trimTrailing(path) {
|
|
254
|
+
return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path;
|
|
255
|
+
}
|
|
256
|
+
function isParamSegment(seg) {
|
|
257
|
+
return seg.startsWith(':') || (seg.startsWith('{') && seg.endsWith('}'));
|
|
258
|
+
}
|
|
259
|
+
//# 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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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: duplicate-route
|
|
3
|
+
*
|
|
4
|
+
* Server-side rule — fires when two or more route decorators declare the
|
|
5
|
+
* same `{path, method}` combination in the reviewed project. Real-bug
|
|
6
|
+
* classes:
|
|
7
|
+
* - Someone renamed a handler but forgot to delete the old one; both fire,
|
|
8
|
+
* order-dependent, one silently shadows the other.
|
|
9
|
+
* - A copy-paste left an `@router.get("/users")` pair intact.
|
|
10
|
+
* - A FastAPI router was mounted twice under the same prefix by accident.
|
|
11
|
+
*
|
|
12
|
+
* Fires on the SECOND and later occurrences (the first is the canonical
|
|
13
|
+
* declaration; duplicates are the bug). No-verb routes (`app.use`) key
|
|
14
|
+
* under `ANY` so two `use('/x')` calls still surface as duplicates.
|
|
15
|
+
*
|
|
16
|
+
* Path-only scope: does not cross-correlate client calls. Graph mode only.
|
|
17
|
+
*/
|
|
18
|
+
import type { ReviewFinding } from '../types.js';
|
|
19
|
+
import type { ConceptRuleContext } from './index.js';
|
|
20
|
+
export declare function duplicateRoute(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: duplicate-route
|
|
3
|
+
*
|
|
4
|
+
* Server-side rule — fires when two or more route decorators declare the
|
|
5
|
+
* same `{path, method}` combination in the reviewed project. Real-bug
|
|
6
|
+
* classes:
|
|
7
|
+
* - Someone renamed a handler but forgot to delete the old one; both fire,
|
|
8
|
+
* order-dependent, one silently shadows the other.
|
|
9
|
+
* - A copy-paste left an `@router.get("/users")` pair intact.
|
|
10
|
+
* - A FastAPI router was mounted twice under the same prefix by accident.
|
|
11
|
+
*
|
|
12
|
+
* Fires on the SECOND and later occurrences (the first is the canonical
|
|
13
|
+
* declaration; duplicates are the bug). No-verb routes (`app.use`) key
|
|
14
|
+
* under `ANY` so two `use('/x')` calls still surface as duplicates.
|
|
15
|
+
*
|
|
16
|
+
* Path-only scope: does not cross-correlate client calls. Graph mode only.
|
|
17
|
+
*/
|
|
18
|
+
import { createFingerprint } from '../types.js';
|
|
19
|
+
import { CROSS_STACK_EXACT_CONFIDENCE, collectRoutesAcrossGraph } from './cross-stack-utils.js';
|
|
20
|
+
export function duplicateRoute(ctx) {
|
|
21
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
22
|
+
return [];
|
|
23
|
+
const routes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
24
|
+
if (routes.length < 2)
|
|
25
|
+
return [];
|
|
26
|
+
// Group by `${METHOD} ${path}` for exact duplicates, BUT wildcard routes
|
|
27
|
+
// (`ALL`/`ANY`/undefined) shadow specific verbs on the same path. Codex
|
|
28
|
+
// review caught that the naïve keying missed `app.all('/x')` +
|
|
29
|
+
// `app.get('/x')` collisions. We do two passes:
|
|
30
|
+
// 1. Group routes at each path by wildcard-vs-specific classification.
|
|
31
|
+
// 2. Within a path, if there's a wildcard route AND any specific verb
|
|
32
|
+
// route, they collide (pick the later one as the "duplicate").
|
|
33
|
+
// 3. Also flag same-path-same-method duplicates as before.
|
|
34
|
+
const byKey = new Map();
|
|
35
|
+
const byPath = new Map();
|
|
36
|
+
for (const r of routes) {
|
|
37
|
+
const method = (r.method ?? 'ANY').toUpperCase();
|
|
38
|
+
const key = `${method} ${r.path}`;
|
|
39
|
+
const keyList = byKey.get(key) ?? [];
|
|
40
|
+
keyList.push(r);
|
|
41
|
+
byKey.set(key, keyList);
|
|
42
|
+
const pathList = byPath.get(r.path) ?? [];
|
|
43
|
+
pathList.push(r);
|
|
44
|
+
byPath.set(r.path, pathList);
|
|
45
|
+
}
|
|
46
|
+
// Wildcard-vs-specific collisions: when a path has a wildcard-accepting
|
|
47
|
+
// route and any specific-verb route, the handlers shadow each other.
|
|
48
|
+
const WILDCARD = new Set(['ALL', 'ANY']);
|
|
49
|
+
for (const [, pathRoutes] of byPath) {
|
|
50
|
+
if (pathRoutes.length < 2)
|
|
51
|
+
continue;
|
|
52
|
+
const wildcards = pathRoutes.filter((r) => {
|
|
53
|
+
const m = (r.method ?? 'ANY').toUpperCase();
|
|
54
|
+
return WILDCARD.has(m);
|
|
55
|
+
});
|
|
56
|
+
if (wildcards.length === 0)
|
|
57
|
+
continue;
|
|
58
|
+
// Synthesize a collision key so the existing emission loop fires.
|
|
59
|
+
// Keep the wildcard as canonical (first), flag every specific-verb route
|
|
60
|
+
// on this path as the duplicate.
|
|
61
|
+
const specifics = pathRoutes.filter((r) => {
|
|
62
|
+
const m = (r.method ?? 'ANY').toUpperCase();
|
|
63
|
+
return !WILDCARD.has(m);
|
|
64
|
+
});
|
|
65
|
+
if (specifics.length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
const collisionKey = `* ${wildcards[0].path}`;
|
|
68
|
+
if (!byKey.has(collisionKey))
|
|
69
|
+
byKey.set(collisionKey, [wildcards[0], ...specifics]);
|
|
70
|
+
}
|
|
71
|
+
const findings = [];
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (const [key, list] of byKey) {
|
|
74
|
+
if (list.length < 2)
|
|
75
|
+
continue;
|
|
76
|
+
const isWildcardCollision = key.startsWith('* ');
|
|
77
|
+
const duplicates = list.slice(1);
|
|
78
|
+
for (const dup of duplicates) {
|
|
79
|
+
if (!dup.node || dup.node.primarySpan.file !== ctx.filePath)
|
|
80
|
+
continue;
|
|
81
|
+
const fingerprint = createFingerprint('duplicate-route', dup.node.primarySpan.startLine, dup.node.primarySpan.startCol);
|
|
82
|
+
if (seen.has(fingerprint))
|
|
83
|
+
continue;
|
|
84
|
+
seen.add(fingerprint);
|
|
85
|
+
const first = list[0];
|
|
86
|
+
const firstFile = first.node?.primarySpan.file ?? 'another file';
|
|
87
|
+
const firstLine = first.node?.primarySpan.startLine;
|
|
88
|
+
const firstRef = firstLine != null ? `${shortPath(firstFile)}:${firstLine}` : shortPath(firstFile);
|
|
89
|
+
const firstMethod = (first.method ?? 'ANY').toUpperCase();
|
|
90
|
+
const dupMethod = (dup.method ?? 'ANY').toUpperCase();
|
|
91
|
+
const message = isWildcardCollision
|
|
92
|
+
? `Route \`${dupMethod} ${dup.path}\` is shadowed by wildcard route \`${firstMethod} ${dup.path}\` at ${firstRef}. The wildcard handler will match ${dupMethod} requests depending on registration order.`
|
|
93
|
+
: `Duplicate route declaration: \`${key}\` is already declared at ${firstRef}. One of the two handlers will silently shadow the other.`;
|
|
94
|
+
findings.push({
|
|
95
|
+
source: 'kern',
|
|
96
|
+
ruleId: 'duplicate-route',
|
|
97
|
+
severity: 'warning',
|
|
98
|
+
category: 'bug',
|
|
99
|
+
message,
|
|
100
|
+
primarySpan: dup.node.primarySpan,
|
|
101
|
+
fingerprint,
|
|
102
|
+
confidence: dup.node.confidence * CROSS_STACK_EXACT_CONFIDENCE,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return findings;
|
|
107
|
+
}
|
|
108
|
+
function shortPath(filePath) {
|
|
109
|
+
const parts = filePath.split('/');
|
|
110
|
+
return parts.slice(-2).join('/');
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=duplicate-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duplicate-route.js","sourceRoot":"","sources":["../../src/concept-rules/duplicate-route.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,11 +4,36 @@
|
|
|
4
4
|
* These rules work on any language that emits concepts.
|
|
5
5
|
* Language-agnostic by design.
|
|
6
6
|
*/
|
|
7
|
+
import { authDrift } from './auth-drift.js';
|
|
7
8
|
import { boundaryMutation } from './boundary-mutation.js';
|
|
9
|
+
import { contractDrift } from './contract-drift.js';
|
|
10
|
+
import { contractMethodDrift } from './contract-method-drift.js';
|
|
11
|
+
import { duplicateRoute } from './duplicate-route.js';
|
|
8
12
|
import { ignoredError } from './ignored-error.js';
|
|
13
|
+
import { missingResponseModel } from './missing-response-model.js';
|
|
14
|
+
import { orphanRoute } from './orphan-route.js';
|
|
15
|
+
import { syncHandlerDoesIo } from './sync-handler-does-io.js';
|
|
16
|
+
import { taintedAcrossWire } from './tainted-across-wire.js';
|
|
9
17
|
import { unguardedEffect } from './unguarded-effect.js';
|
|
10
18
|
import { unrecoveredEffect } from './unrecovered-effect.js';
|
|
11
|
-
|
|
19
|
+
import { untypedApiResponse } from './untyped-api-response.js';
|
|
20
|
+
import { untypedBothEndsResponse } from './untyped-both-ends-response.js';
|
|
21
|
+
export const conceptRules = [
|
|
22
|
+
authDrift,
|
|
23
|
+
boundaryMutation,
|
|
24
|
+
contractDrift,
|
|
25
|
+
contractMethodDrift,
|
|
26
|
+
duplicateRoute,
|
|
27
|
+
ignoredError,
|
|
28
|
+
missingResponseModel,
|
|
29
|
+
syncHandlerDoesIo,
|
|
30
|
+
orphanRoute,
|
|
31
|
+
taintedAcrossWire,
|
|
32
|
+
unguardedEffect,
|
|
33
|
+
unrecoveredEffect,
|
|
34
|
+
untypedApiResponse,
|
|
35
|
+
untypedBothEndsResponse,
|
|
36
|
+
];
|
|
12
37
|
export function runConceptRules(concepts, filePath, allConcepts, graphImports) {
|
|
13
38
|
const ctx = { concepts, filePath, allConcepts, graphImports };
|
|
14
39
|
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,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,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;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAa1E,MAAM,CAAC,MAAM,YAAY,GAAkB;IACzC,SAAS;IACT,gBAAgB;IAChB,aAAa;IACb,mBAAmB;IACnB,cAAc;IACd,YAAY;IACZ,oBAAoB;IACpB,iBAAiB;IACjB,WAAW;IACX,iBAAiB;IACjB,eAAe;IACf,iBAAiB;IACjB,kBAAkB;IAClB,uBAAuB;CACxB,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,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: missing-response-model
|
|
3
|
+
*
|
|
4
|
+
* Fires on Python route decorators that do not declare a FastAPI
|
|
5
|
+
* `response_model=...`. Kept Python-scoped because other route mappers do
|
|
6
|
+
* not currently surface an equivalent response-schema signal.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReviewFinding } from '../types.js';
|
|
9
|
+
import type { ConceptRuleContext } from './index.js';
|
|
10
|
+
export declare function missingResponseModel(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: missing-response-model
|
|
3
|
+
*
|
|
4
|
+
* Fires on Python route decorators that do not declare a FastAPI
|
|
5
|
+
* `response_model=...`. Kept Python-scoped because other route mappers do
|
|
6
|
+
* not currently surface an equivalent response-schema signal.
|
|
7
|
+
*/
|
|
8
|
+
import { createFingerprint } from '../types.js';
|
|
9
|
+
import { CROSS_STACK_HEURISTIC_CONFIDENCE, hasFastApiEvidence } from './cross-stack-utils.js';
|
|
10
|
+
export function missingResponseModel(ctx) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
if (!hasFastApiEvidence(ctx.concepts))
|
|
13
|
+
return findings;
|
|
14
|
+
for (const node of ctx.concepts.nodes) {
|
|
15
|
+
if (node.kind !== 'entrypoint')
|
|
16
|
+
continue;
|
|
17
|
+
if (node.payload.kind !== 'entrypoint')
|
|
18
|
+
continue;
|
|
19
|
+
if (node.payload.subtype !== 'route')
|
|
20
|
+
continue;
|
|
21
|
+
if (node.language !== 'py')
|
|
22
|
+
continue;
|
|
23
|
+
if (node.payload.responseModel)
|
|
24
|
+
continue;
|
|
25
|
+
findings.push({
|
|
26
|
+
source: 'kern',
|
|
27
|
+
ruleId: 'missing-response-model',
|
|
28
|
+
severity: 'warning',
|
|
29
|
+
category: 'bug',
|
|
30
|
+
message: `Route \`${node.payload.name}\` has no FastAPI response_model. Add response_model=... so backend response-shape drift is caught at the contract boundary.`,
|
|
31
|
+
primarySpan: node.primarySpan,
|
|
32
|
+
fingerprint: createFingerprint('missing-response-model', node.primarySpan.startLine, node.primarySpan.startCol),
|
|
33
|
+
confidence: node.confidence * CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return findings;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=missing-response-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-response-model.js","sourceRoot":"","sources":["../../src/concept-rules/missing-response-model.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,gCAAgC,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG9F,MAAM,UAAU,oBAAoB,CAAC,GAAuB;IAC1D,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAEvD,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY;YAAE,SAAS;QACzC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;YAAE,SAAS;QACjD,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,OAAO;YAAE,SAAS;QAC/C,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;YAAE,SAAS;QACrC,IAAI,IAAI,CAAC,OAAO,CAAC,aAAa;YAAE,SAAS;QAEzC,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,wBAAwB;YAChC,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,WAAW,IAAI,CAAC,OAAO,CAAC,IAAI,8HAA8H;YACnK,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,WAAW,EAAE,iBAAiB,CAAC,wBAAwB,EAAE,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;YAC/G,UAAU,EAAE,IAAI,CAAC,UAAU,GAAG,gCAAgC;SAC/D,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: orphan-route
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — mirror of contract-drift. Fires when a server-side route
|
|
5
|
+
* exists that no client call in the reviewed graph targets. Real-bug classes:
|
|
6
|
+
* - Handler was kept live after the frontend renamed the URL it hits.
|
|
7
|
+
* - Endpoint was written before the UI that would call it (forgotten TODO).
|
|
8
|
+
* - A dev-only test endpoint made it into production code.
|
|
9
|
+
*
|
|
10
|
+
* v1 scope: path-only match (any client call whose normalized path matches
|
|
11
|
+
* the route template suppresses the finding, regardless of HTTP method). The
|
|
12
|
+
* method axis is left to `contract-method-drift` — compounding both here
|
|
13
|
+
* would double-fire on the same line.
|
|
14
|
+
*
|
|
15
|
+
* Requires graph mode: silent in single-file review. Single-file can't know
|
|
16
|
+
* "no one calls this" because callers live in other files by definition.
|
|
17
|
+
*/
|
|
18
|
+
import type { ReviewFinding } from '../types.js';
|
|
19
|
+
import type { ConceptRuleContext } from './index.js';
|
|
20
|
+
export declare function orphanRoute(ctx: ConceptRuleContext): ReviewFinding[];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: orphan-route
|
|
3
|
+
*
|
|
4
|
+
* Cross-stack rule — mirror of contract-drift. Fires when a server-side route
|
|
5
|
+
* exists that no client call in the reviewed graph targets. Real-bug classes:
|
|
6
|
+
* - Handler was kept live after the frontend renamed the URL it hits.
|
|
7
|
+
* - Endpoint was written before the UI that would call it (forgotten TODO).
|
|
8
|
+
* - A dev-only test endpoint made it into production code.
|
|
9
|
+
*
|
|
10
|
+
* v1 scope: path-only match (any client call whose normalized path matches
|
|
11
|
+
* the route template suppresses the finding, regardless of HTTP method). The
|
|
12
|
+
* method axis is left to `contract-method-drift` — compounding both here
|
|
13
|
+
* would double-fire on the same line.
|
|
14
|
+
*
|
|
15
|
+
* Requires graph mode: silent in single-file review. Single-file can't know
|
|
16
|
+
* "no one calls this" because callers live in other files by definition.
|
|
17
|
+
*/
|
|
18
|
+
import { createFingerprint } from '../types.js';
|
|
19
|
+
import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutesAcrossGraph, findRoutesAtPath, normalizeClientUrl, } from './cross-stack-utils.js';
|
|
20
|
+
export function orphanRoute(ctx) {
|
|
21
|
+
if (!ctx.allConcepts || ctx.allConcepts.size === 0)
|
|
22
|
+
return [];
|
|
23
|
+
const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
|
|
24
|
+
if (serverRoutes.length === 0)
|
|
25
|
+
return [];
|
|
26
|
+
// Collect every client-call path in the graph once so each route checks
|
|
27
|
+
// against a shared set rather than re-walking allConcepts.
|
|
28
|
+
//
|
|
29
|
+
// Codex review: if ANY network effect has an unresolved target (imported
|
|
30
|
+
// constant, URL builder, variable expression), the rule MUST abstain —
|
|
31
|
+
// the unresolved call could be hitting any of the "orphan" routes and
|
|
32
|
+
// we'd fire a false positive. Only run the rule when every client call
|
|
33
|
+
// is statically resolvable.
|
|
34
|
+
const clientPaths = new Set();
|
|
35
|
+
let hasUnresolvedTarget = false;
|
|
36
|
+
for (const [, conceptMap] of ctx.allConcepts) {
|
|
37
|
+
for (const node of conceptMap.nodes) {
|
|
38
|
+
if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
|
|
39
|
+
continue;
|
|
40
|
+
const target = node.payload.target;
|
|
41
|
+
if (typeof target !== 'string') {
|
|
42
|
+
hasUnresolvedTarget = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const normalized = normalizeClientUrl(target);
|
|
46
|
+
if (!normalized) {
|
|
47
|
+
hasUnresolvedTarget = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!API_PATH_RE.test(normalized))
|
|
51
|
+
continue;
|
|
52
|
+
clientPaths.add(normalized);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Gate: backend-only project (no client calls) — silent.
|
|
56
|
+
// Gate: any unresolved client targets — silent (Codex P2).
|
|
57
|
+
if (clientPaths.size === 0)
|
|
58
|
+
return [];
|
|
59
|
+
if (hasUnresolvedTarget)
|
|
60
|
+
return [];
|
|
61
|
+
const findings = [];
|
|
62
|
+
const seenFingerprints = new Set();
|
|
63
|
+
for (const route of serverRoutes) {
|
|
64
|
+
if (!route.node || route.node.primarySpan.file !== ctx.filePath)
|
|
65
|
+
continue;
|
|
66
|
+
if (clientCallMatches(route.path, clientPaths))
|
|
67
|
+
continue;
|
|
68
|
+
const fingerprint = createFingerprint('orphan-route', route.node.primarySpan.startLine, route.node.primarySpan.startCol);
|
|
69
|
+
// Router-mount expansion can cause the same per-file route to surface
|
|
70
|
+
// twice under different prefixes when two mounts share a router (rare
|
|
71
|
+
// but legal). Dedupe by fingerprint so we emit one finding per span.
|
|
72
|
+
if (seenFingerprints.has(fingerprint))
|
|
73
|
+
continue;
|
|
74
|
+
seenFingerprints.add(fingerprint);
|
|
75
|
+
const methodLabel = route.method ? `${route.method} ` : '';
|
|
76
|
+
findings.push({
|
|
77
|
+
source: 'kern',
|
|
78
|
+
ruleId: 'orphan-route',
|
|
79
|
+
severity: 'warning',
|
|
80
|
+
category: 'bug',
|
|
81
|
+
message: `Server defines \`${methodLabel}${route.path}\` but no client in the reviewed project calls this path. Either remove the handler or add the frontend caller.`,
|
|
82
|
+
primarySpan: route.node.primarySpan,
|
|
83
|
+
fingerprint,
|
|
84
|
+
confidence: route.node.confidence * CROSS_STACK_HEURISTIC_CONFIDENCE,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
89
|
+
function clientCallMatches(routePath, clientPaths) {
|
|
90
|
+
for (const cp of clientPaths) {
|
|
91
|
+
if (findRoutesAtPath(cp, [{ path: routePath, method: undefined }]).length > 0)
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=orphan-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orphan-route.js","sourceRoot":"","sources":["../../src/concept-rules/orphan-route.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: sync-handler-does-io
|
|
3
|
+
*
|
|
4
|
+
* Fires when a route handler is explicitly synchronous and performs
|
|
5
|
+
* network/db/fs I/O in the same function container.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReviewFinding } from '../types.js';
|
|
8
|
+
import type { ConceptRuleContext } from './index.js';
|
|
9
|
+
export declare function syncHandlerDoesIo(ctx: ConceptRuleContext): ReviewFinding[];
|