@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.
Files changed (62) hide show
  1. package/dist/cache.js +1 -1
  2. package/dist/concept-rules/auth-drift.d.ts +29 -0
  3. package/dist/concept-rules/auth-drift.js +127 -0
  4. package/dist/concept-rules/auth-drift.js.map +1 -0
  5. package/dist/concept-rules/contract-drift.d.ts +21 -0
  6. package/dist/concept-rules/contract-drift.js +65 -0
  7. package/dist/concept-rules/contract-drift.js.map +1 -0
  8. package/dist/concept-rules/contract-method-drift.d.ts +22 -0
  9. package/dist/concept-rules/contract-method-drift.js +105 -0
  10. package/dist/concept-rules/contract-method-drift.js.map +1 -0
  11. package/dist/concept-rules/cross-stack-utils.d.ts +96 -0
  12. package/dist/concept-rules/cross-stack-utils.js +259 -0
  13. package/dist/concept-rules/cross-stack-utils.js.map +1 -0
  14. package/dist/concept-rules/duplicate-route.d.ts +20 -0
  15. package/dist/concept-rules/duplicate-route.js +112 -0
  16. package/dist/concept-rules/duplicate-route.js.map +1 -0
  17. package/dist/concept-rules/index.js +26 -1
  18. package/dist/concept-rules/index.js.map +1 -1
  19. package/dist/concept-rules/missing-response-model.d.ts +10 -0
  20. package/dist/concept-rules/missing-response-model.js +38 -0
  21. package/dist/concept-rules/missing-response-model.js.map +1 -0
  22. package/dist/concept-rules/orphan-route.d.ts +20 -0
  23. package/dist/concept-rules/orphan-route.js +96 -0
  24. package/dist/concept-rules/orphan-route.js.map +1 -0
  25. package/dist/concept-rules/sync-handler-does-io.d.ts +9 -0
  26. package/dist/concept-rules/sync-handler-does-io.js +56 -0
  27. package/dist/concept-rules/sync-handler-does-io.js.map +1 -0
  28. package/dist/concept-rules/tainted-across-wire.d.ts +33 -0
  29. package/dist/concept-rules/tainted-across-wire.js +95 -0
  30. package/dist/concept-rules/tainted-across-wire.js.map +1 -0
  31. package/dist/concept-rules/untyped-api-response.d.ts +30 -0
  32. package/dist/concept-rules/untyped-api-response.js +73 -0
  33. package/dist/concept-rules/untyped-api-response.js.map +1 -0
  34. package/dist/concept-rules/untyped-both-ends-response.d.ts +10 -0
  35. package/dist/concept-rules/untyped-both-ends-response.js +55 -0
  36. package/dist/concept-rules/untyped-both-ends-response.js.map +1 -0
  37. package/dist/external-tools.d.ts +17 -4
  38. package/dist/external-tools.js +12 -1
  39. package/dist/external-tools.js.map +1 -1
  40. package/dist/index.d.ts +2 -1
  41. package/dist/index.js +115 -9
  42. package/dist/index.js.map +1 -1
  43. package/dist/llm-bridge.d.ts +38 -1
  44. package/dist/llm-bridge.js +172 -12
  45. package/dist/llm-bridge.js.map +1 -1
  46. package/dist/llm-review.js +29 -11
  47. package/dist/llm-review.js.map +1 -1
  48. package/dist/mappers/ts-concepts.js +650 -11
  49. package/dist/mappers/ts-concepts.js.map +1 -1
  50. package/dist/rules/index.js +17 -1
  51. package/dist/rules/index.js.map +1 -1
  52. package/dist/rules/kern-source.js +37 -5
  53. package/dist/rules/kern-source.js.map +1 -1
  54. package/dist/rules/set-setter-collision.d.ts +21 -0
  55. package/dist/rules/set-setter-collision.js +74 -0
  56. package/dist/rules/set-setter-collision.js.map +1 -0
  57. package/dist/rules/suggest-kern-primitive.d.ts +30 -0
  58. package/dist/rules/suggest-kern-primitive.js +543 -0
  59. package/dist/rules/suggest-kern-primitive.js.map +1 -0
  60. package/dist/types.d.ts +2 -0
  61. package/dist/types.js.map +1 -1
  62. 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-2';
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":"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"}
@@ -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[];