@kernlang/review 3.3.5 → 3.3.7

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 (50) hide show
  1. package/dist/concept-rules/auth-drift.d.ts +29 -0
  2. package/dist/concept-rules/auth-drift.js +127 -0
  3. package/dist/concept-rules/auth-drift.js.map +1 -0
  4. package/dist/concept-rules/contract-drift.js +2 -3
  5. package/dist/concept-rules/contract-drift.js.map +1 -1
  6. package/dist/concept-rules/contract-method-drift.d.ts +22 -0
  7. package/dist/concept-rules/contract-method-drift.js +105 -0
  8. package/dist/concept-rules/contract-method-drift.js.map +1 -0
  9. package/dist/concept-rules/cross-stack-utils.d.ts +46 -0
  10. package/dist/concept-rules/cross-stack-utils.js +161 -0
  11. package/dist/concept-rules/cross-stack-utils.js.map +1 -1
  12. package/dist/concept-rules/duplicate-route.d.ts +20 -0
  13. package/dist/concept-rules/duplicate-route.js +112 -0
  14. package/dist/concept-rules/duplicate-route.js.map +1 -0
  15. package/dist/concept-rules/index.js +14 -0
  16. package/dist/concept-rules/index.js.map +1 -1
  17. package/dist/concept-rules/missing-response-model.d.ts +10 -0
  18. package/dist/concept-rules/missing-response-model.js +38 -0
  19. package/dist/concept-rules/missing-response-model.js.map +1 -0
  20. package/dist/concept-rules/orphan-route.d.ts +20 -0
  21. package/dist/concept-rules/orphan-route.js +96 -0
  22. package/dist/concept-rules/orphan-route.js.map +1 -0
  23. package/dist/concept-rules/sync-handler-does-io.d.ts +9 -0
  24. package/dist/concept-rules/sync-handler-does-io.js +56 -0
  25. package/dist/concept-rules/sync-handler-does-io.js.map +1 -0
  26. package/dist/concept-rules/tainted-across-wire.js +2 -5
  27. package/dist/concept-rules/tainted-across-wire.js.map +1 -1
  28. package/dist/concept-rules/untyped-api-response.js +8 -6
  29. package/dist/concept-rules/untyped-api-response.js.map +1 -1
  30. package/dist/concept-rules/untyped-both-ends-response.d.ts +10 -0
  31. package/dist/concept-rules/untyped-both-ends-response.js +55 -0
  32. package/dist/concept-rules/untyped-both-ends-response.js.map +1 -0
  33. package/dist/index.js +40 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/llm-bridge.d.ts +12 -0
  36. package/dist/llm-bridge.js +131 -7
  37. package/dist/llm-bridge.js.map +1 -1
  38. package/dist/mappers/ts-concepts.js +406 -13
  39. package/dist/mappers/ts-concepts.js.map +1 -1
  40. package/dist/rules/index.js +16 -0
  41. package/dist/rules/index.js.map +1 -1
  42. package/dist/rules/kern-source.js +2 -0
  43. package/dist/rules/kern-source.js.map +1 -1
  44. package/dist/rules/set-setter-collision.d.ts +21 -0
  45. package/dist/rules/set-setter-collision.js +74 -0
  46. package/dist/rules/set-setter-collision.js.map +1 -0
  47. package/dist/rules/suggest-kern-primitive.d.ts +30 -0
  48. package/dist/rules/suggest-kern-primitive.js +543 -0
  49. package/dist/rules/suggest-kern-primitive.js.map +1 -0
  50. package/package.json +2 -2
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,4BAA4B,EAC5B,wBAAwB,EACxB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,wBAAwB,CAAC;AAGhC,MAAM,UAAU,SAAS,CAAC,GAAuB;IAC/C,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,uEAAuE;IACvE,uEAAuE;IACvE,uEAAuE;IACvE,yEAAyE;IACzE,yEAAyE;IACzE,sEAAsE;IACtE,+DAA+D;IAC/D,MAAM,mBAAmB,GAAG,0BAA0B,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACxE,IAAI,mBAAmB,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAE9C,MAAM,QAAQ,GAAoB,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,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,KAAK,GAAG,CAAC,QAAQ;gBAAE,SAAS;YACrD,+EAA+E;YAC/E,IAAI,IAAI,CAAC,OAAO,CAAC,aAAa,KAAK,KAAK;gBAAE,SAAS;YACnD,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;YAE3D,MAAM,KAAK,GAAG,iBAAiB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;YAC1D,IAAI,CAAC,KAAK,EAAE,IAAI;gBAAE,SAAS;YAE3B,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;YAC/C,MAAM,UAAU,GAAG,mBAAmB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACvD,IAAI,CAAC,UAAU;gBAAE,SAAS;YAE1B,6CAA6C;YAC7C,iEAAiE;YACjE,kEAAkE;YAClE,0BAA0B;YAC1B,mEAAmE;YACnE,gEAAgE;YAChE,oEAAoE;YACpE,4BAA4B;YAC5B,kEAAkE;YAClE,8DAA8D;YAC9D,MAAM,cAAc,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC;YAC9C,MAAM,yBAAyB,GAAG,cAAc,KAAK,SAAS,IAAI,UAAU,CAAC,eAAe,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YACjH,MAAM,+BAA+B,GAAG,UAAU,CAAC,iBAAiB,KAAK,CAAC,CAAC;YAC3E,IAAI,CAAC,yBAAyB,IAAI,CAAC,+BAA+B;gBAAE,SAAS;YAE7E,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,MAAM;gBACd,MAAM,EAAE,YAAY;gBACpB,QAAQ,EAAE,SAAS;gBACnB,QAAQ,EAAE,KAAK;gBACf,OAAO,EAAE,oBAAoB,MAAM,uGAAuG,SAAS,CAAC,UAAU,CAAC,6DAA6D;gBAC5N,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,WAAW,EAAE,iBAAiB,CAAC,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;gBACnG,UAAU,EAAE,IAAI,CAAC,UAAU,GAAG,4BAA4B;aAC3D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AASD,SAAS,0BAA0B,CAAC,WAA4C;IAC9E,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC/C,KAAK,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,WAAW,EAAE,CAAC;QAC1C,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;QAC1C,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;gBACzG,UAAU,EAAE,CAAC;gBACb,SAAS;YACX,CAAC;YACD,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,OAAO;gBAAE,SAAS;YACrE,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,MAAM;gBAAE,SAAS;YAC9C,kEAAkE;YAClE,+DAA+D;YAC/D,WAAW;YACX,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;gBAAE,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC5E,CAAC;QACD,IAAI,eAAe,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,eAAe,EAAE,iBAAiB,EAAE,UAAU,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB;IACjC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACnC,CAAC"}
@@ -17,15 +17,14 @@
17
17
  * review silently returns no findings (can't correlate from one file).
18
18
  */
19
19
  import { createFingerprint } from '../types.js';
20
- import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutes, hasMatchingRoute, normalizeClientUrl, } from './cross-stack-utils.js';
20
+ import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, collectRoutesAcrossGraph, hasMatchingRoute, normalizeClientUrl, } from './cross-stack-utils.js';
21
21
  export function contractDrift(ctx) {
22
22
  // Graph mode only — URL correlation is useless within a single file.
23
23
  if (!ctx.allConcepts || ctx.allConcepts.size === 0)
24
24
  return [];
25
- const serverRoutes = [];
25
+ const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
26
26
  const clientCalls = [];
27
27
  for (const [, conceptMap] of ctx.allConcepts) {
28
- collectRoutes(conceptMap, serverRoutes);
29
28
  for (const node of conceptMap.nodes) {
30
29
  if (node.kind !== 'effect' || node.payload.kind !== 'effect' || node.payload.subtype !== 'network')
31
30
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"contract-drift.js","sourceRoot":"","sources":["../../src/concept-rules/contract-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,gCAAgC,EAChC,aAAa,EACb,gBAAgB,EAChB,kBAAkB,GAEnB,MAAM,wBAAwB,CAAC;AAShC,MAAM,UAAU,aAAa,CAAC,GAAuB;IACnD,qEAAqE;IACrE,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAE9D,MAAM,YAAY,GAAkB,EAAE,CAAC;IACvC,MAAM,WAAW,GAAiB,EAAE,CAAC;IAErC,KAAK,MAAM,CAAC,EAAE,UAAU,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;QAC7C,aAAa,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QACxC,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS;gBAAE,SAAS;YAC7G,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;YACnC,IAAI,OAAO,MAAM,KAAK,QAAQ;gBAAE,SAAS;YACzC,MAAM,UAAU,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;YAC9C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC;gBAAE,SAAS;YAC3D,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IAED,wEAAwE;IACxE,0EAA0E;IAC1E,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAErE,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,wEAAwE;QACxE,4CAA4C;QAC5C,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,KAAK,GAAG,CAAC,QAAQ;YAAE,SAAS;QAC1D,IAAI,gBAAgB,CAAC,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC;YAAE,SAAS;QAElE,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,gBAAgB;YACxB,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,oBAAoB,IAAI,CAAC,MAAM,2KAA2K;YACnN,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW;YAClC,WAAW,EAAE,iBAAiB,CAAC,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;YACjH,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,gCAAgC;SACpE,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
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"}
@@ -17,6 +17,14 @@ import type { ConceptMap, ConceptNode } from '@kernlang/core';
17
17
  * untyped-api-response can bump its own multiplier).
18
18
  */
19
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;
20
28
  /** Client URLs we consider "internal" to the reviewed project. */
21
29
  export declare const API_PATH_RE: RegExp;
22
30
  export interface ServerRoute {
@@ -25,11 +33,41 @@ export interface ServerRoute {
25
33
  /** Present when the caller needs to cite the server route in a finding. */
26
34
  node?: ConceptNode;
27
35
  }
36
+ export declare function hasFastApiEvidence(map: ConceptMap): boolean;
37
+ export declare function isFastApiRouteMissingResponseModel(node: ConceptNode, map?: ConceptMap): boolean;
28
38
  /**
29
39
  * Pull every server-side route out of a concept map. Callers typically fold
30
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.
31
50
  */
32
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[];
33
71
  /**
34
72
  * Strip scheme/host, query string, and fragment from a client URL so it can
35
73
  * match against a server route template. Returns undefined when the input
@@ -48,3 +86,11 @@ export declare function normalizeClientUrl(raw: string): string | undefined;
48
86
  export declare function findMatchingRoute(clientPath: string, routes: readonly ServerRoute[]): ServerRoute | undefined;
49
87
  /** Boolean-returning thin wrapper preserved for callers that just need a yes/no. */
50
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[];
@@ -16,11 +16,48 @@
16
16
  * untyped-api-response can bump its own multiplier).
17
17
  */
18
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;
19
27
  /** Client URLs we consider "internal" to the reviewed project. */
20
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
+ }
21
49
  /**
22
50
  * Pull every server-side route out of a concept map. Callers typically fold
23
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.
24
61
  */
25
62
  export function collectRoutes(map, routes) {
26
63
  for (const node of map.nodes) {
@@ -32,6 +69,100 @@ export function collectRoutes(map, routes) {
32
69
  routes.push({ path, method: node.payload.httpMethod, node });
33
70
  }
34
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
+ }
35
166
  /**
36
167
  * Strip scheme/host, query string, and fragment from a client URL so it can
37
168
  * match against a server route template. Returns undefined when the input
@@ -89,6 +220,36 @@ export function findMatchingRoute(clientPath, routes) {
89
220
  export function hasMatchingRoute(clientPath, routes) {
90
221
  return findMatchingRoute(clientPath, routes) !== undefined;
91
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
+ }
92
253
  function trimTrailing(path) {
93
254
  return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path;
94
255
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cross-stack-utils.js","sourceRoot":"","sources":["../../src/concept-rules/cross-stack-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,CAAC;AAEpD,kEAAkE;AAClE,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AAStC;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,GAAe,EAAE,MAAqB;IAClE,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,OAAO;YAAE,SAAS;QACnH,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;QAC/B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAChE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,IAAI,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACrB,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACnE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAChC,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5D,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3D,GAAG,GAAG,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC;IACD,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,CAAC,KAAK,CAAC,CAAC;QAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpC,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,CAAC,KAAK,CAAC,CAAC;QAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpC,OAAO,GAAG,IAAI,SAAS,CAAC;AAC1B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAkB,EAAE,MAA8B;IAClF,MAAM,cAAc,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3D,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,aAAa,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1D,IAAI,aAAa,CAAC,MAAM,KAAK,cAAc,CAAC,MAAM;YAAE,SAAS;QAC7D,IAAI,OAAO,GAAG,IAAI,CAAC;QACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9C,MAAM,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;YAC5B,MAAM,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,cAAc,CAAC,EAAE,CAAC;gBAAE,SAAS;YACjC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACd,OAAO,GAAG,KAAK,CAAC;gBAChB,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,OAAO;YAAE,OAAO,KAAK,CAAC;IAC5B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,MAA8B;IACjF,OAAO,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC,KAAK,SAAS,CAAC;AAC7D,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1E,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,OAAO,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3E,CAAC"}
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[];