@kernlang/review 3.4.3-canary.7.1.88c06dcc → 3.4.3

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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Rule: error-contract-drift
3
+ *
4
+ * Cross-stack rule — fires when a server route declares a literal HTTP error
5
+ * status that the matched frontend call-site does not branch on, AND the
6
+ * call-site already branches on at least one OTHER status this route emits
7
+ * (proving the dispatch is endpoint-specific, not incidental).
8
+ *
9
+ * Phase 2 of the error-contract work. Phase 1 (#208) shipped the client-side
10
+ * `handledErrorStatusCodes` extraction; this rule consumes it.
11
+ *
12
+ * The 0.9 evidence gate (campfire 2026-05-04 — Codex/Gemini/OpenCode unanimous):
13
+ * - Path + method match (one server route, exact verb).
14
+ * - Server `errorStatusCodes` is non-empty after the semantic-only filter.
15
+ * - Client `handledErrorStatusCodes` is non-empty (call-site has explicit
16
+ * literal-status dispatch — not generic `catch` or `response.ok` only).
17
+ * - Client already branches on at least ONE pre-existing server status for
18
+ * this endpoint. Without this overlap the rule would fire on every legacy
19
+ * mismatch where the client's specific status check happens to be unrelated
20
+ * to the server's set.
21
+ *
22
+ * Excluded statuses (v1):
23
+ * - 500 / 502 / 503 — server emits these via `next(err)` and unresolved
24
+ * `throw`, which are not stable client contracts. Codex called this out
25
+ * explicitly: "I would explicitly exclude inferred 500s from v1, because
26
+ * generic `throw` / `next(err)` is not a stable client contract."
27
+ * - Anything outside the 4xx/429 range. Phase-3 work will surface these
28
+ * once we have an `errorStatusCodesResolved` completeness bit on the server.
29
+ *
30
+ * Confidence: `CROSS_STACK_EXACT_CONFIDENCE` (0.9). The rule's static gates
31
+ * already approximate this; kern-guard's `isNew` ratchet handles "did this
32
+ * mismatch just appear in the PR diff" suppression.
33
+ */
34
+ import type { ReviewFinding } from '../types.js';
35
+ import type { ConceptRuleContext } from './index.js';
36
+ export declare function errorContractDrift(ctx: ConceptRuleContext): ReviewFinding[];
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Rule: error-contract-drift
3
+ *
4
+ * Cross-stack rule — fires when a server route declares a literal HTTP error
5
+ * status that the matched frontend call-site does not branch on, AND the
6
+ * call-site already branches on at least one OTHER status this route emits
7
+ * (proving the dispatch is endpoint-specific, not incidental).
8
+ *
9
+ * Phase 2 of the error-contract work. Phase 1 (#208) shipped the client-side
10
+ * `handledErrorStatusCodes` extraction; this rule consumes it.
11
+ *
12
+ * The 0.9 evidence gate (campfire 2026-05-04 — Codex/Gemini/OpenCode unanimous):
13
+ * - Path + method match (one server route, exact verb).
14
+ * - Server `errorStatusCodes` is non-empty after the semantic-only filter.
15
+ * - Client `handledErrorStatusCodes` is non-empty (call-site has explicit
16
+ * literal-status dispatch — not generic `catch` or `response.ok` only).
17
+ * - Client already branches on at least ONE pre-existing server status for
18
+ * this endpoint. Without this overlap the rule would fire on every legacy
19
+ * mismatch where the client's specific status check happens to be unrelated
20
+ * to the server's set.
21
+ *
22
+ * Excluded statuses (v1):
23
+ * - 500 / 502 / 503 — server emits these via `next(err)` and unresolved
24
+ * `throw`, which are not stable client contracts. Codex called this out
25
+ * explicitly: "I would explicitly exclude inferred 500s from v1, because
26
+ * generic `throw` / `next(err)` is not a stable client contract."
27
+ * - Anything outside the 4xx/429 range. Phase-3 work will surface these
28
+ * once we have an `errorStatusCodesResolved` completeness bit on the server.
29
+ *
30
+ * Confidence: `CROSS_STACK_EXACT_CONFIDENCE` (0.9). The rule's static gates
31
+ * already approximate this; kern-guard's `isNew` ratchet handles "did this
32
+ * mismatch just appear in the PR diff" suppression.
33
+ */
34
+ import { createFingerprint } from '../types.js';
35
+ import { API_PATH_RE, CROSS_STACK_EXACT_CONFIDENCE, collectRoutesAcrossGraph, findHighConfidenceRouteForMethod, normalizeClientUrl, } from './cross-stack-utils.js';
36
+ import { apiCallRootCause } from './root-cause.js';
37
+ // Codex/Gemini/OpenCode all converged on this set as the "stable" semantic
38
+ // statuses where a server contract change is meaningful for the client. 429
39
+ // is included because real audiofacets/web-viewer code branches on it (phase-1
40
+ // probe found 429×1) and rate-limit handling is a legitimate UX concern.
41
+ const SEMANTIC_ERROR_STATUSES = new Set([401, 403, 404, 409, 422, 429]);
42
+ export function errorContractDrift(ctx) {
43
+ if (!ctx.allConcepts || ctx.allConcepts.size === 0)
44
+ return [];
45
+ const serverRoutes = collectRoutesAcrossGraph(ctx.allConcepts);
46
+ if (serverRoutes.length === 0)
47
+ return [];
48
+ const clientCalls = collectExplicitDispatchCalls(ctx);
49
+ if (clientCalls.length === 0)
50
+ return [];
51
+ const findings = [];
52
+ for (const call of clientCalls) {
53
+ if (call.node.primarySpan.file !== ctx.filePath)
54
+ continue;
55
+ const route = findHighConfidenceRouteForMethod(call.normalizedPath, call.method, serverRoutes);
56
+ if (!route?.node)
57
+ continue;
58
+ if (route.node.payload.kind !== 'entrypoint')
59
+ continue;
60
+ const serverCodes = route.node.payload.errorStatusCodes;
61
+ if (!serverCodes || serverCodes.length === 0)
62
+ continue;
63
+ const semanticServer = serverCodes.filter((c) => SEMANTIC_ERROR_STATUSES.has(c));
64
+ if (semanticServer.length === 0)
65
+ continue;
66
+ const handledSet = new Set(call.handled);
67
+ // The strongest 0.9 gate from the buddy round: client must already
68
+ // branch on at least ONE server status for this endpoint. Without
69
+ // this, a client that handles 401 globally (auth interceptor) but
70
+ // never anything endpoint-specific would fire on every route the
71
+ // server adds a 404 to. The overlap proves the dispatch is wired
72
+ // to THIS endpoint's contract.
73
+ const overlap = semanticServer.filter((c) => handledSet.has(c));
74
+ if (overlap.length === 0)
75
+ continue;
76
+ const unhandled = semanticServer.filter((c) => !handledSet.has(c));
77
+ if (unhandled.length === 0)
78
+ continue;
79
+ const codeList = unhandled.join(', ');
80
+ const handledList = call.handled.filter((c) => SEMANTIC_ERROR_STATUSES.has(c)).join(', ');
81
+ findings.push({
82
+ source: 'kern',
83
+ ruleId: 'error-contract-drift',
84
+ severity: 'warning',
85
+ category: 'bug',
86
+ message: `Server route \`${call.method} ${route.path}\` emits status${unhandled.length === 1 ? '' : 'es'} ` +
87
+ `[${codeList}] but this client call-site only branches on [${handledList}]. ` +
88
+ `Add an explicit \`response.status === ${unhandled[0]}\` (or \`case ${unhandled[0]}:\`) branch, or ` +
89
+ `confirm the generic fallback handles ${unhandled.length === 1 ? 'it' : 'them'} the same way as ${handledList}.`,
90
+ primarySpan: call.node.primarySpan,
91
+ fingerprint: createFingerprint('error-contract-drift', call.node.primarySpan.startLine, call.node.primarySpan.startCol),
92
+ confidence: call.node.confidence * CROSS_STACK_EXACT_CONFIDENCE,
93
+ rootCause: apiCallRootCause(call.node, call.normalizedPath, call.method, route.node),
94
+ });
95
+ }
96
+ return findings;
97
+ }
98
+ function collectExplicitDispatchCalls(ctx) {
99
+ const calls = [];
100
+ if (!ctx.allConcepts)
101
+ return calls;
102
+ for (const [, conceptMap] of ctx.allConcepts) {
103
+ for (const node of conceptMap.nodes) {
104
+ if (node.kind !== 'effect')
105
+ continue;
106
+ if (node.payload.kind !== 'effect')
107
+ continue;
108
+ if (node.payload.subtype !== 'network')
109
+ continue;
110
+ const target = node.payload.target;
111
+ const method = node.payload.method;
112
+ const handled = node.payload.handledErrorStatusCodes;
113
+ if (typeof target !== 'string')
114
+ continue;
115
+ if (typeof method !== 'string')
116
+ continue;
117
+ if (!handled || handled.length === 0)
118
+ continue;
119
+ const normalized = normalizeClientUrl(target);
120
+ if (!normalized || !API_PATH_RE.test(normalized))
121
+ continue;
122
+ calls.push({ target, normalizedPath: normalized, method, handled, node });
123
+ }
124
+ }
125
+ return calls;
126
+ }
127
+ //# sourceMappingURL=error-contract-drift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-contract-drift.js","sourceRoot":"","sources":["../../src/concept-rules/error-contract-drift.ts"],"names":[],"mappings":"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"}
@@ -12,8 +12,10 @@ import { boundaryMutation } from './boundary-mutation.js';
12
12
  import { contractDrift } from './contract-drift.js';
13
13
  import { contractMethodDrift } from './contract-method-drift.js';
14
14
  import { duplicateRoute } from './duplicate-route.js';
15
+ import { errorContractDrift } from './error-contract-drift.js';
15
16
  import { ignoredError } from './ignored-error.js';
16
17
  import { missingResponseModel } from './missing-response-model.js';
18
+ import { mixedHostSameEndpoint } from './mixed-host-same-endpoint.js';
17
19
  import { mutationWithoutIdempotency } from './mutation-without-idempotency.js';
18
20
  import { orphanRoute } from './orphan-route.js';
19
21
  import { paramNameSwap } from './param-name-swap.js';
@@ -34,8 +36,10 @@ export const conceptRules = [
34
36
  contractDrift,
35
37
  contractMethodDrift,
36
38
  duplicateRoute,
39
+ errorContractDrift,
37
40
  ignoredError,
38
41
  missingResponseModel,
42
+ mixedHostSameEndpoint,
39
43
  mutationWithoutIdempotency,
40
44
  orphanRoute,
41
45
  paramNameSwap,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/concept-rules/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,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,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAe1E,MAAM,CAAC,MAAM,YAAY,GAAkB;IACzC,SAAS;IACT,oBAAoB;IACpB,cAAc;IACd,gBAAgB;IAChB,aAAa;IACb,mBAAmB;IACnB,cAAc;IACd,YAAY;IACZ,oBAAoB;IACpB,0BAA0B;IAC1B,WAAW;IACX,aAAa;IACb,sBAAsB;IACtB,iBAAiB;IACjB,iBAAiB;IACjB,wBAAwB;IACxB,eAAe;IACf,sBAAsB;IACtB,iBAAiB;IACjB,kBAAkB;IAClB,uBAAuB;CACxB,CAAC;AAEF,MAAM,UAAU,eAAe,CAC7B,QAAoB,EACpB,QAAgB,EAChB,WAAqC,EACrC,YAAoC,EACpC,MAA6C;IAE7C,MAAM,GAAG,GAAuB;QAC9B,QAAQ;QACR,QAAQ;QACR,WAAW;QACX,YAAY;QACZ,cAAc,EAAE,MAAM,EAAE,cAAc;KACvC,CAAC;IACF,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,qBAAqB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/concept-rules/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,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,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAe1E,MAAM,CAAC,MAAM,YAAY,GAAkB;IACzC,SAAS;IACT,oBAAoB;IACpB,cAAc;IACd,gBAAgB;IAChB,aAAa;IACb,mBAAmB;IACnB,cAAc;IACd,kBAAkB;IAClB,YAAY;IACZ,oBAAoB;IACpB,qBAAqB;IACrB,0BAA0B;IAC1B,WAAW;IACX,aAAa;IACb,sBAAsB;IACtB,iBAAiB;IACjB,iBAAiB;IACjB,wBAAwB;IACxB,eAAe;IACf,sBAAsB;IACtB,iBAAiB;IACjB,kBAAkB;IAClB,uBAAuB;CACxB,CAAC;AAEF,MAAM,UAAU,eAAe,CAC7B,QAAoB,EACpB,QAAgB,EAChB,WAAqC,EACrC,YAAoC,EACpC,MAA6C;IAE7C,MAAM,GAAG,GAAuB;QAC9B,QAAQ;QACR,QAAQ;QACR,WAAW;QACX,YAAY;QACZ,cAAc,EAAE,MAAM,EAAE,cAAc;KACvC,CAAC;IACF,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,qBAAqB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AACjD,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Rule: mixed-host-same-endpoint
3
+ *
4
+ * Cross-file rule (single source — runs against the reviewed repo, no
5
+ * cross-stack partner needed). Fires when the same `(method, path)` is
6
+ * fetched against two or more *different* non-dev hosts in the codebase.
7
+ *
8
+ * Concrete example:
9
+ *
10
+ * // src/lib/users.ts
11
+ * await fetch(`https://api.example.com/api/users/${id}`);
12
+ *
13
+ * // src/admin/legacy.ts
14
+ * await fetch(`https://beta-api.example.com/api/users/${id}`);
15
+ *
16
+ * Same path+method, different production hosts. Almost always a stale
17
+ * base-URL — someone copy-pasted across a host migration or hardcoded
18
+ * the old URL. The rule fires on every call-site that participates in
19
+ * the divergence and lives in the file currently under review.
20
+ *
21
+ * This rule is built on top of the +41pp `host` data unlocked by
22
+ * phase-1.5 (env-fallback const resolution). Without populated `host`,
23
+ * the rule has nothing to compare and stays silent.
24
+ *
25
+ * FP gates:
26
+ * - Both/all hosts must be populated and absolute (relative URLs are
27
+ * intentionally same-origin; can't be inconsistent).
28
+ * - Dev-shaped hosts (localhost, 127.0.0.1, 0.0.0.0, *.local, *.test,
29
+ * and explicit ports on loopback) are skipped — `localhost` vs
30
+ * `api.prod.com` is normal env-aware code, not a bug.
31
+ * - Path must look internal (`/api/…`) so that random third-party SDK
32
+ * calls don't produce noise.
33
+ * - At least 2 different non-dev hosts must agree on path + method.
34
+ * Doesn't fire on single-call endpoints.
35
+ *
36
+ * Confidence: CROSS_STACK_HEURISTIC_CONFIDENCE (0.7). The match is
37
+ * structural (same path + method, different host) but the intent
38
+ * (migration vs intentional cross-region routing) needs human review.
39
+ */
40
+ import type { ReviewFinding } from '../types.js';
41
+ import type { ConceptRuleContext } from './index.js';
42
+ export declare function mixedHostSameEndpoint(ctx: ConceptRuleContext): ReviewFinding[];
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Rule: mixed-host-same-endpoint
3
+ *
4
+ * Cross-file rule (single source — runs against the reviewed repo, no
5
+ * cross-stack partner needed). Fires when the same `(method, path)` is
6
+ * fetched against two or more *different* non-dev hosts in the codebase.
7
+ *
8
+ * Concrete example:
9
+ *
10
+ * // src/lib/users.ts
11
+ * await fetch(`https://api.example.com/api/users/${id}`);
12
+ *
13
+ * // src/admin/legacy.ts
14
+ * await fetch(`https://beta-api.example.com/api/users/${id}`);
15
+ *
16
+ * Same path+method, different production hosts. Almost always a stale
17
+ * base-URL — someone copy-pasted across a host migration or hardcoded
18
+ * the old URL. The rule fires on every call-site that participates in
19
+ * the divergence and lives in the file currently under review.
20
+ *
21
+ * This rule is built on top of the +41pp `host` data unlocked by
22
+ * phase-1.5 (env-fallback const resolution). Without populated `host`,
23
+ * the rule has nothing to compare and stays silent.
24
+ *
25
+ * FP gates:
26
+ * - Both/all hosts must be populated and absolute (relative URLs are
27
+ * intentionally same-origin; can't be inconsistent).
28
+ * - Dev-shaped hosts (localhost, 127.0.0.1, 0.0.0.0, *.local, *.test,
29
+ * and explicit ports on loopback) are skipped — `localhost` vs
30
+ * `api.prod.com` is normal env-aware code, not a bug.
31
+ * - Path must look internal (`/api/…`) so that random third-party SDK
32
+ * calls don't produce noise.
33
+ * - At least 2 different non-dev hosts must agree on path + method.
34
+ * Doesn't fire on single-call endpoints.
35
+ *
36
+ * Confidence: CROSS_STACK_HEURISTIC_CONFIDENCE (0.7). The match is
37
+ * structural (same path + method, different host) but the intent
38
+ * (migration vs intentional cross-region routing) needs human review.
39
+ */
40
+ import { createFingerprint } from '../types.js';
41
+ import { API_PATH_RE, CROSS_STACK_HEURISTIC_CONFIDENCE, normalizeClientUrl } from './cross-stack-utils.js';
42
+ import { apiCallRootCause } from './root-cause.js';
43
+ // Hosts we exclude from the divergence check. These are normal in
44
+ // dev/test code and not signs of a stale base URL.
45
+ const DEV_HOST_RE = /^(localhost|127\.0\.0\.1|0\.0\.0\.0|host\.docker\.internal)(:\d+)?$/i;
46
+ const DEV_TLD_RE = /\.(local|test|localhost)(:\d+)?$/i;
47
+ function isDevHost(host) {
48
+ return DEV_HOST_RE.test(host) || DEV_TLD_RE.test(host);
49
+ }
50
+ export function mixedHostSameEndpoint(ctx) {
51
+ if (!ctx.allConcepts || ctx.allConcepts.size === 0)
52
+ return [];
53
+ // Group every populated-host network call by `<METHOD> <path>`.
54
+ const byEndpoint = new Map();
55
+ for (const [, conceptMap] of ctx.allConcepts) {
56
+ for (const node of conceptMap.nodes) {
57
+ if (node.kind !== 'effect')
58
+ continue;
59
+ if (node.payload.kind !== 'effect')
60
+ continue;
61
+ if (node.payload.subtype !== 'network')
62
+ continue;
63
+ const host = node.payload.host;
64
+ if (!host)
65
+ continue;
66
+ if (isDevHost(host))
67
+ continue;
68
+ const method = node.payload.method;
69
+ if (!method)
70
+ continue;
71
+ const target = node.payload.target;
72
+ if (!target)
73
+ continue;
74
+ const path = normalizeClientUrl(target);
75
+ if (!path)
76
+ continue;
77
+ if (!API_PATH_RE.test(path))
78
+ continue;
79
+ const key = `${method.toUpperCase()} ${path}`;
80
+ const arr = byEndpoint.get(key);
81
+ if (arr)
82
+ arr.push({ node, host, method, path });
83
+ else
84
+ byEndpoint.set(key, [{ node, host, method, path }]);
85
+ }
86
+ }
87
+ const findings = [];
88
+ for (const [endpoint, calls] of byEndpoint) {
89
+ const distinctHosts = new Set(calls.map((c) => c.host));
90
+ if (distinctHosts.size < 2)
91
+ continue;
92
+ // Fire on the calls that live in the file currently being reviewed.
93
+ // A repo-wide rule still routes findings through the per-file context.
94
+ for (const call of calls) {
95
+ if (call.node.primarySpan.file !== ctx.filePath)
96
+ continue;
97
+ const otherHosts = [...distinctHosts].filter((h) => h !== call.host).sort();
98
+ const allHosts = [...distinctHosts].sort();
99
+ findings.push({
100
+ source: 'kern',
101
+ ruleId: 'mixed-host-same-endpoint',
102
+ severity: 'warning',
103
+ category: 'bug',
104
+ message: `\`${endpoint}\` is fetched against multiple hosts in this codebase: [${allHosts.join(', ')}]. ` +
105
+ `This call uses \`${call.host}\`; ${otherHosts.length === 1 ? 'another call uses' : 'other calls use'} ` +
106
+ `\`${otherHosts.join(', ')}\`. ` +
107
+ `Likely a stale base URL — confirm both hosts are intentional or unify them behind a single config const.`,
108
+ primarySpan: call.node.primarySpan,
109
+ fingerprint: createFingerprint('mixed-host-same-endpoint', call.node.primarySpan.startLine, call.node.primarySpan.startCol),
110
+ confidence: call.node.confidence * CROSS_STACK_HEURISTIC_CONFIDENCE,
111
+ rootCause: apiCallRootCause(call.node, call.path, call.method),
112
+ });
113
+ }
114
+ }
115
+ return findings;
116
+ }
117
+ //# sourceMappingURL=mixed-host-same-endpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mixed-host-same-endpoint.js","sourceRoot":"","sources":["../../src/concept-rules/mixed-host-same-endpoint.ts"],"names":[],"mappings":"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"}