weavatrix 0.2.12 → 0.2.14

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 (132) hide show
  1. package/README.md +90 -21
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.14.md +93 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +108 -43
  6. package/src/analysis/allowed-test-runner.js +20 -9
  7. package/src/analysis/architecture/contract-graph.js +119 -0
  8. package/src/analysis/architecture/contract-schema.js +110 -0
  9. package/src/analysis/architecture/contract-storage.js +35 -0
  10. package/src/analysis/architecture/contract-verification.js +168 -0
  11. package/src/analysis/architecture-contract.js +16 -343
  12. package/src/analysis/change-classification/diff-parser.js +103 -0
  13. package/src/analysis/change-classification/options.js +40 -0
  14. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  15. package/src/analysis/change-classification.js +120 -519
  16. package/src/analysis/dead-code-review/policy.js +23 -0
  17. package/src/analysis/dead-code-review.js +9 -19
  18. package/src/analysis/dep-check.js +10 -57
  19. package/src/analysis/dep-rules.js +10 -303
  20. package/src/analysis/dependency/scoped-dependencies.js +40 -0
  21. package/src/analysis/endpoints/common.js +62 -0
  22. package/src/analysis/endpoints/extract.js +107 -0
  23. package/src/analysis/endpoints/inventory.js +84 -0
  24. package/src/analysis/endpoints/mounts.js +129 -0
  25. package/src/analysis/endpoints-java.js +80 -3
  26. package/src/analysis/endpoints.js +3 -448
  27. package/src/analysis/git-history/analytics.js +177 -0
  28. package/src/analysis/git-history/collector.js +109 -0
  29. package/src/analysis/git-history/options.js +68 -0
  30. package/src/analysis/git-history.js +128 -577
  31. package/src/analysis/health-capabilities.js +82 -0
  32. package/src/analysis/http-contracts/analysis.js +169 -0
  33. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  34. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  35. package/src/analysis/http-contracts/graph-context.js +143 -0
  36. package/src/analysis/http-contracts/matching.js +61 -0
  37. package/src/analysis/http-contracts/shared.js +86 -0
  38. package/src/analysis/http-contracts.js +6 -822
  39. package/src/analysis/internal-audit/dependency-health.js +111 -0
  40. package/src/analysis/internal-audit/python-manifests.js +65 -0
  41. package/src/analysis/internal-audit/repo-files.js +55 -0
  42. package/src/analysis/internal-audit/supply-chain.js +132 -0
  43. package/src/analysis/internal-audit.collect.js +5 -106
  44. package/src/analysis/internal-audit.run.js +113 -200
  45. package/src/analysis/jvm-dependency-evidence.js +69 -0
  46. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  47. package/src/analysis/source-correctness.js +225 -0
  48. package/src/analysis/structure/dependency-graph.js +156 -0
  49. package/src/analysis/structure/findings.js +142 -0
  50. package/src/graph/builder/go-receiver-resolution.js +112 -0
  51. package/src/graph/builder/js/queries.js +48 -0
  52. package/src/graph/builder/lang-go.js +89 -4
  53. package/src/graph/builder/lang-js.js +4 -44
  54. package/src/graph/builder/pass2-resolution.js +68 -0
  55. package/src/graph/freshness-probe.js +2 -1
  56. package/src/graph/incremental-refresh.js +3 -2
  57. package/src/graph/internal-builder.build.js +22 -183
  58. package/src/graph/internal-builder.pass2.js +161 -0
  59. package/src/graph/internal-builder.resolvers.js +2 -105
  60. package/src/graph/resolvers/rust.js +117 -0
  61. package/src/mcp/actions/advisories.mjs +18 -0
  62. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  63. package/src/mcp/actions/graph-sync.mjs +195 -0
  64. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  65. package/src/mcp/architecture-bootstrap.mjs +168 -0
  66. package/src/mcp/architecture-starter.mjs +234 -0
  67. package/src/mcp/catalog.mjs +22 -6
  68. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  69. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  70. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  71. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  72. package/src/mcp/graph/context-core.mjs +111 -0
  73. package/src/mcp/graph/context-seeds.mjs +208 -0
  74. package/src/mcp/graph/context-state.mjs +86 -0
  75. package/src/mcp/graph/tools-core.mjs +143 -0
  76. package/src/mcp/graph/tools-query.mjs +223 -0
  77. package/src/mcp/graph-context.mjs +4 -496
  78. package/src/mcp/health/audit-format.mjs +172 -0
  79. package/src/mcp/health/audit.mjs +171 -0
  80. package/src/mcp/health/dead-code.mjs +110 -0
  81. package/src/mcp/health/duplicates.mjs +83 -0
  82. package/src/mcp/health/endpoints.mjs +42 -0
  83. package/src/mcp/health/structure.mjs +219 -0
  84. package/src/mcp/runtime-version.mjs +32 -0
  85. package/src/mcp/server/auto-refresh.mjs +66 -0
  86. package/src/mcp/server/runtime-config.mjs +43 -0
  87. package/src/mcp/server/shutdown.mjs +40 -0
  88. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  89. package/src/mcp/sync/evidence-common.mjs +88 -0
  90. package/src/mcp/sync/evidence-duplicates.mjs +100 -0
  91. package/src/mcp/sync/evidence-health.mjs +56 -0
  92. package/src/mcp/sync/evidence-packages.mjs +95 -0
  93. package/src/mcp/sync/payload-common.mjs +97 -0
  94. package/src/mcp/sync/payload-v2.mjs +53 -0
  95. package/src/mcp/sync/payload-v3.mjs +79 -0
  96. package/src/mcp/sync-evidence.mjs +7 -402
  97. package/src/mcp/sync-payload.mjs +10 -244
  98. package/src/mcp/tools-actions.mjs +18 -414
  99. package/src/mcp/tools-architecture.mjs +18 -70
  100. package/src/mcp/tools-context.mjs +56 -15
  101. package/src/mcp/tools-endpoints.mjs +4 -1
  102. package/src/mcp/tools-graph.mjs +17 -330
  103. package/src/mcp/tools-health.mjs +24 -705
  104. package/src/mcp/tools-verified-change.mjs +12 -4
  105. package/src/mcp-server.mjs +49 -146
  106. package/src/precision/lsp-client/constants.js +12 -0
  107. package/src/precision/lsp-client/environment.js +20 -0
  108. package/src/precision/lsp-client/errors.js +15 -0
  109. package/src/precision/lsp-client/lifecycle.js +127 -0
  110. package/src/precision/lsp-client/message-parser.js +81 -0
  111. package/src/precision/lsp-client/protocol.js +212 -0
  112. package/src/precision/lsp-client/registry.js +58 -0
  113. package/src/precision/lsp-client/repo-uri.js +60 -0
  114. package/src/precision/lsp-client/stdio-client.js +151 -0
  115. package/src/precision/lsp-client.js +10 -682
  116. package/src/precision/lsp-overlay/build.js +238 -0
  117. package/src/precision/lsp-overlay/contract.js +61 -0
  118. package/src/precision/lsp-overlay/reference-results.js +108 -0
  119. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  120. package/src/precision/lsp-overlay/source-session.js +134 -0
  121. package/src/precision/lsp-overlay/store.js +150 -0
  122. package/src/precision/lsp-overlay/target-index.js +147 -0
  123. package/src/precision/lsp-overlay/target-query.js +55 -0
  124. package/src/precision/lsp-overlay.js +11 -868
  125. package/src/precision/typescript-lsp-provider.js +12 -682
  126. package/src/precision/typescript-provider/client.js +69 -0
  127. package/src/precision/typescript-provider/discovery.js +124 -0
  128. package/src/precision/typescript-provider/project-host.js +249 -0
  129. package/src/precision/typescript-provider/project-safety.js +161 -0
  130. package/src/precision/typescript-provider/reference-usage.js +53 -0
  131. package/src/version.js +7 -0
  132. package/docs/releases/v0.2.12.md +0 -45
@@ -1,822 +1,6 @@
1
- // Cross-repository HTTP contract evidence: join routes exposed by backend repositories to literal or
2
- // bounded-template HTTP calls in client repositories. Results contain metadata only (path/method and
3
- // file:line), never source snippets or URL expressions.
4
- import { folderModuleOf } from "./graph-analysis.edges.js";
5
- import { detectEndpoints } from "./endpoints.js";
6
- import { isStructuralRelation } from "../graph/relations.js";
7
- import { createPathClassifier, hasPathClass } from "../path-classification.js";
8
- import { isWeavatrixIgnored, loadWeavatrixIgnore } from "../path-ignore.js";
9
- import { createRepoBoundary } from "../repo-path.js";
10
- import { safeRead } from "../util.js";
11
- import {
12
- DEFAULT_HTTP_CLIENT_NAMES,
13
- discoverHttpWrappers,
14
- loadHttpContractConfig,
15
- normalizeHttpClientNames,
16
- normalizeHttpWrapperDescriptors,
17
- } from "./http-contract-wrappers.js";
18
-
19
- export const HTTP_CONTRACTS_V = 2;
20
-
21
- const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
22
- const DEFAULTS = Object.freeze({
23
- maxBackendFiles: 3_000,
24
- maxClientFiles: 3_000,
25
- maxEndpoints: 250,
26
- maxCallsPerClient: 2_000,
27
- maxMatches: 1_000,
28
- maxCallsitesPerEndpoint: 100,
29
- maxUncertain: 200,
30
- maxImpactDepth: 2,
31
- maxAffectedFiles: 100,
32
- maxScreens: 50,
33
- maxModules: 50,
34
- });
35
- const HARD = Object.freeze({
36
- maxBackendFiles: 3_000,
37
- maxClientFiles: 10_000,
38
- maxEndpoints: 500,
39
- maxCallsPerClient: 5_000,
40
- maxMatches: 5_000,
41
- maxCallsitesPerEndpoint: 500,
42
- maxUncertain: 1_000,
43
- maxImpactDepth: 5,
44
- maxAffectedFiles: 500,
45
- maxScreens: 200,
46
- maxModules: 200,
47
- });
48
-
49
- const endpointId = (value) => value && typeof value === "object" ? value.id : value;
50
- const normalizeFile = (value) => {
51
- const raw = String(value || "").replace(/\\/g, "/");
52
- if (!raw || raw.startsWith("/") || /^[a-z]:\//i.test(raw) || /[\x00-\x1f\x7f]/.test(raw)) return "";
53
- const normalized = raw.replace(/^\.\//, "");
54
- return normalized.split("/").some((part) => !part || part === "." || part === "..") ? "" : normalized;
55
- };
56
- const boundedInteger = (value, fallback, min, max) => {
57
- const number = Number(value);
58
- return Number.isInteger(number) ? Math.max(min, Math.min(max, number)) : fallback;
59
- };
60
- const safeName = (value, fallback) => {
61
- const text = String(value || "").trim();
62
- return /^[a-z0-9][a-z0-9._-]{0,79}$/i.test(text) ? text : fallback;
63
- };
64
- const lineAt = (text, index) => {
65
- let line = 1;
66
- for (let cursor = 0; cursor < index; cursor += 1) if (text.charCodeAt(cursor) === 10) line += 1;
67
- return line;
68
- };
69
-
70
- function normalizeLimits(input = {}) {
71
- const result = {};
72
- for (const key of Object.keys(DEFAULTS)) result[key] = boundedInteger(input[key], DEFAULTS[key], key === "maxImpactDepth" ? 0 : 1, HARD[key]);
73
- return result;
74
- }
75
-
76
- export function normalizeHttpContractPath(value) {
77
- let path = String(value || "").trim();
78
- if (!path || path.length > 2_048) return null;
79
- try {
80
- if (/^https?:\/\//i.test(path)) path = new URL(path).pathname;
81
- else if (/^\/\//.test(path)) path = new URL(`http:${path}`).pathname;
82
- } catch { return null; }
83
- const queryAt = path.search(/[?#]/);
84
- if (queryAt >= 0) path = path.slice(0, queryAt);
85
- path = path.replace(/\\/g, "/").replace(/\/+/g, "/");
86
- if (!path.startsWith("/")) path = `/${path}`;
87
- path = path.replace(/\/\{[^/}]+\}/g, "/:param")
88
- .replace(/\/:([A-Za-z_$][\w$-]*)(?:\?)?/g, "/:param")
89
- .replace(/\/\*[^/]*/g, "/*")
90
- .replace(/\[(?:\.\.\.)?[^\]]+\]/g, "/:param")
91
- .replace(/\/+$/g, "") || "/";
92
- if (/[\x00-\x1f\x7f]/.test(path) || path.includes("..")) return null;
93
- return path;
94
- }
95
-
96
- function maskNonCode(text) {
97
- // Split by UTF-16 code unit so indices stay aligned with the original JS string even when a file
98
- // contains astral Unicode before a callsite.
99
- const chars = String(text || "").split("");
100
- let index = 0;
101
- while (index < chars.length) {
102
- const char = chars[index];
103
- const next = chars[index + 1];
104
- if (char === "/" && next === "/") {
105
- chars[index++] = " "; chars[index++] = " ";
106
- while (index < chars.length && chars[index] !== "\n") chars[index++] = " ";
107
- continue;
108
- }
109
- if (char === "/" && next === "*") {
110
- chars[index++] = " "; chars[index++] = " ";
111
- while (index < chars.length) {
112
- if (chars[index] === "*" && chars[index + 1] === "/") { chars[index++] = " "; chars[index++] = " "; break; }
113
- if (chars[index] !== "\n") chars[index] = " ";
114
- index += 1;
115
- }
116
- continue;
117
- }
118
- if (char === "'" || char === '"' || char === "`") {
119
- const quote = char;
120
- chars[index++] = " ";
121
- while (index < chars.length) {
122
- if (chars[index] === "\\") {
123
- chars[index++] = " ";
124
- if (index < chars.length && chars[index] !== "\n") chars[index] = " ";
125
- index += 1;
126
- continue;
127
- }
128
- const closes = chars[index] === quote;
129
- if (chars[index] !== "\n") chars[index] = " ";
130
- index += 1;
131
- if (closes) break;
132
- }
133
- continue;
134
- }
135
- index += 1;
136
- }
137
- return chars.join("");
138
- }
139
-
140
- function quotedArgument(text, start, quote) {
141
- let value = "";
142
- for (let index = start + 1; index < text.length; index += 1) {
143
- const char = text[index];
144
- if (char === "\\") {
145
- if (index + 1 >= text.length) break;
146
- value += text[index + 1];
147
- index += 1;
148
- continue;
149
- }
150
- if (char === quote) return { value, endIndex: index + 1 };
151
- if (char === "\n" || char === "\r") break;
152
- value += char;
153
- }
154
- return null;
155
- }
156
-
157
- function templateArgument(text, start, constants = null, requireStatic = false) {
158
- let value = "";
159
- let dynamicSegments = 0;
160
- let unknownPrefix = false;
161
- let partialDynamic = false;
162
- for (let index = start + 1; index < text.length; index += 1) {
163
- const char = text[index];
164
- if (char === "\\") {
165
- if (index + 1 >= text.length) break;
166
- value += text[index + 1];
167
- index += 1;
168
- continue;
169
- }
170
- if (char === "`") return { value, endIndex: index + 1, dynamicSegments, unknownPrefix, partialDynamic };
171
- if (char !== "$" || text[index + 1] !== "{") {
172
- value += char;
173
- continue;
174
- }
175
- let cursor = index + 2;
176
- let depth = 1;
177
- let quote = null;
178
- while (cursor < text.length && depth > 0) {
179
- const token = text[cursor];
180
- if (quote) {
181
- if (token === "\\") cursor += 2;
182
- else { if (token === quote) quote = null; cursor += 1; }
183
- continue;
184
- }
185
- if (token === "'" || token === '"' || token === "`") { quote = token; cursor += 1; continue; }
186
- if (token === "{") depth += 1;
187
- else if (token === "}") depth -= 1;
188
- cursor += 1;
189
- }
190
- if (depth !== 0) return null;
191
- const expression = text.slice(index + 2, cursor - 1).trim();
192
- if (/^[A-Za-z_$][\w$]*$/.test(expression) && constants?.has(expression)) {
193
- value += constants.get(expression);
194
- index = cursor - 1;
195
- continue;
196
- }
197
- if (requireStatic) return null;
198
- const next = text[cursor];
199
- const leftBoundary = value === "" || value.endsWith("/");
200
- const rightBoundary = !next || next === "/" || next === "?" || next === "#" || next === "`";
201
- dynamicSegments += 1;
202
- if (value === "" && next === "/") unknownPrefix = true;
203
- else if (leftBoundary && rightBoundary) value += ":param";
204
- else { value += ":dynamic"; partialDynamic = true; }
205
- index = cursor - 1;
206
- }
207
- return null;
208
- }
209
-
210
- function extractStaticStringConstants(text) {
211
- const source = String(text || "");
212
- const mask = maskNonCode(source);
213
- const declarations = [];
214
- const declaration = /\bconst\s+([A-Za-z_$][\w$]*)\s*=/g;
215
- let match;
216
- while ((match = declaration.exec(mask)) && declarations.length < 500) {
217
- let start = match.index + match[0].length;
218
- while (/\s/.test(source[start] || "")) start += 1;
219
- declarations.push({ name: match[1], start });
220
- }
221
-
222
- const constants = new Map();
223
- for (const item of declarations) {
224
- const quote = source[item.start];
225
- if (quote !== "'" && quote !== '"') continue;
226
- const parsed = quotedArgument(source, item.start, quote);
227
- if (parsed && parsed.value.length <= 2_048) constants.set(item.name, parsed.value);
228
- }
229
- // Resolve bounded chains such as `const route = `${API_ROOT}/query`` without evaluating code.
230
- for (let pass = 0; pass < Math.min(8, declarations.length); pass += 1) {
231
- let changed = false;
232
- for (const item of declarations) {
233
- if (constants.has(item.name) || source[item.start] !== "`") continue;
234
- const parsed = templateArgument(source, item.start, constants, true);
235
- if (!parsed || parsed.value.length > 2_048) continue;
236
- constants.set(item.name, parsed.value);
237
- changed = true;
238
- }
239
- if (!changed) break;
240
- }
241
- return constants;
242
- }
243
-
244
- function argumentStart(text, openParen, target) {
245
- let current = 0, round = 0, square = 0, curly = 0;
246
- for (let index = openParen + 1; index < text.length; index += 1) {
247
- const char = text[index];
248
- if (current === target && !/\s|,/.test(char)) return index;
249
- if (char === "'" || char === '"' || char === "`") {
250
- const quote = char;
251
- index += 1;
252
- while (index < text.length) {
253
- if (text[index] === "\\") index += 2;
254
- else if (text[index] === quote) break;
255
- else index += 1;
256
- }
257
- continue;
258
- }
259
- if (char === "(" ) round += 1;
260
- else if (char === "[" ) square += 1;
261
- else if (char === "{" ) curly += 1;
262
- else if (char === ")") {
263
- if (round === 0 && square === 0 && curly === 0) return null;
264
- round -= 1;
265
- } else if (char === "]") square -= 1;
266
- else if (char === "}") curly -= 1;
267
- else if (char === "," && round === 0 && square === 0 && curly === 0) current += 1;
268
- }
269
- return null;
270
- }
271
-
272
- function parseUrlArgument(text, openParen, constants, argument = 0) {
273
- let start = argumentStart(text, openParen, argument);
274
- if (start == null) return { path: null, endIndex: openParen, kind: "dynamic", dynamic: true, unknownPrefix: false, partialDynamic: true, reason: `URL argument ${argument} is missing` };
275
- while (/\s/.test(text[start] || "")) start += 1;
276
- const quote = text[start];
277
- if (quote === "'" || quote === '"') {
278
- const parsed = quotedArgument(text, start, quote);
279
- if (!parsed) return { path: null, endIndex: start, kind: "dynamic", dynamic: true, reason: "unterminated URL literal" };
280
- return { path: normalizeHttpContractPath(parsed.value), endIndex: parsed.endIndex, kind: "literal", dynamic: false, unknownPrefix: false, partialDynamic: false, reason: null };
281
- }
282
- if (quote === "`") {
283
- const parsed = templateArgument(text, start, constants);
284
- if (!parsed) return { path: null, endIndex: start, kind: "dynamic", dynamic: true, reason: "unterminated URL template" };
285
- return {
286
- path: normalizeHttpContractPath(parsed.value),
287
- endIndex: parsed.endIndex,
288
- kind: parsed.dynamicSegments ? "template" : "literal",
289
- dynamic: parsed.dynamicSegments > 0,
290
- unknownPrefix: parsed.unknownPrefix,
291
- partialDynamic: parsed.partialDynamic,
292
- reason: parsed.partialDynamic ? "URL template contains a partial dynamic segment" : null,
293
- };
294
- }
295
- return { path: null, endIndex: start, kind: "dynamic", dynamic: true, unknownPrefix: false, partialDynamic: true, reason: "URL argument is not a string or template literal" };
296
- }
297
-
298
- function fetchMethod(text, argumentEnd) {
299
- const tail = text.slice(argumentEnd, argumentEnd + 500);
300
- if (!/^\s*,/.test(tail)) return { method: "GET", uncertain: false };
301
- const config = tail.replace(/^\s*,\s*/, "");
302
- if (!config.startsWith("{") || /^\{\s*\.\.\./.test(config)) return { method: "UNKNOWN", uncertain: true };
303
- const literal = /\bmethod\s*:\s*(["'`])(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\1/i.exec(tail);
304
- if (literal) return { method: literal[2].toUpperCase(), uncertain: false };
305
- if (/\bmethod\s*:/.test(tail)) return { method: "UNKNOWN", uncertain: true };
306
- return { method: "GET", uncertain: false };
307
- }
308
-
309
- function normalizedClientNames(values) {
310
- return new Set([...DEFAULT_HTTP_CLIENT_NAMES, ...normalizeHttpClientNames(values)]
311
- .map((value) => String(value || "").trim().toLowerCase())
312
- .filter((value) => /^[a-z_$][\w$]*$/i.test(value)));
313
- }
314
-
315
- const escapeRegex = (value) => String(value).replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&");
316
-
317
- export function extractHttpClientCallsFromText(text, file, options = {}) {
318
- const source = String(text || "");
319
- const mask = maskNonCode(source);
320
- const constants = extractStaticStringConstants(source);
321
- const allowed = normalizedClientNames(options.clientNames);
322
- const wrappers = normalizeHttpWrapperDescriptors(options.wrappers, "input")
323
- .concat((Array.isArray(options.normalizedWrappers) ? options.normalizedWrappers : []));
324
- const maxCalls = boundedInteger(options.maxCalls, DEFAULTS.maxCallsPerClient, 1, HARD.maxCallsPerClient);
325
- const calls = [];
326
- const seen = new Set();
327
- let truncated = false;
328
- const add = (clientName, method, openParen, fetch = false, urlArgument = 0, detector = "builtin", wrapper = null) => {
329
- const key = `${openParen}\0${method}\0${urlArgument}`;
330
- if (seen.has(key)) return;
331
- seen.add(key);
332
- if (calls.length >= maxCalls) { truncated = true; return; }
333
- const parsed = parseUrlArgument(source, openParen, constants, urlArgument);
334
- const fetchInfo = fetch ? fetchMethod(source, parsed.endIndex) : { method: method.toUpperCase(), uncertain: false };
335
- calls.push({
336
- file: normalizeFile(file),
337
- line: lineAt(source, openParen),
338
- client: clientName,
339
- method: fetchInfo.method,
340
- path: parsed.path,
341
- kind: parsed.kind,
342
- dynamic: parsed.dynamic,
343
- unknownPrefix: Boolean(parsed.unknownPrefix),
344
- partialDynamic: Boolean(parsed.partialDynamic || fetchInfo.uncertain),
345
- reason: fetchInfo.uncertain ? "HTTP method is dynamic" : parsed.reason,
346
- detector,
347
- wrapper,
348
- });
349
- };
350
-
351
- const member = /(^|[^\w$])([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*(get|post|put|patch|delete|head|options)\s*(?:<[^>\n]{1,200}>)?\s*\(/gim;
352
- let match;
353
- while ((match = member.exec(mask))) {
354
- if (!allowed.has(match[2].toLowerCase())) continue;
355
- add(match[2], match[3], member.lastIndex - 1, false);
356
- }
357
- const fetchCall = /(^|[^\w$])fetch\s*\(/gim;
358
- while ((match = fetchCall.exec(mask))) add("fetch", "GET", fetchCall.lastIndex - 1, true);
359
- for (const wrapper of wrappers) {
360
- if (wrapper.allowedFiles instanceof Set && !wrapper.allowedFiles.has(normalizeFile(file))) continue;
361
- if (wrapper.kind === "function") {
362
- const bare = new RegExp(`(^|[^\\w$.?])${escapeRegex(wrapper.call)}\\s*(?:<[^>\\n]{1,200}>)?\\s*\\(`, "gim");
363
- while ((match = bare.exec(mask))) {
364
- const nameAt = bare.lastIndex - match[0].length + match[1].length;
365
- if (/\bfunction\s*$/i.test(mask.slice(Math.max(0, nameAt - 30), nameAt))) continue;
366
- add(wrapper.call, wrapper.method, bare.lastIndex - 1, false, wrapper.urlArgument, `${wrapper.source}-wrapper`, {
367
- kind: wrapper.kind, call: wrapper.call, definitionFile: wrapper.definitionFile || null,
368
- });
369
- }
370
- } else if (wrapper.kind === "member") {
371
- const memberCall = new RegExp(`(^|[^\\w$])${escapeRegex(wrapper.object)}\\s*(?:\\?\\.|\\.)\\s*${escapeRegex(wrapper.member)}\\s*(?:<[^>\\n]{1,200}>)?\\s*\\(`, "gim");
372
- while ((match = memberCall.exec(mask))) add(`${wrapper.object}.${wrapper.member}`, wrapper.method, memberCall.lastIndex - 1, false, wrapper.urlArgument, `${wrapper.source}-wrapper`, {
373
- kind: wrapper.kind, object: wrapper.object, member: wrapper.member, definitionFile: wrapper.definitionFile || null,
374
- });
375
- }
376
- }
377
- calls.sort((left, right) => left.file.localeCompare(right.file) || left.line - right.line || left.method.localeCompare(right.method) || String(left.path).localeCompare(String(right.path)));
378
- return { calls, truncated };
379
- }
380
-
381
- function filesFromGraph(graph) {
382
- return [...new Set((graph?.nodes || []).map((node) => normalizeFile(node?.source_file)).filter(Boolean))].sort();
383
- }
384
-
385
- export function detectHttpClientCalls(repoRoot, codeFiles, options = {}) {
386
- const boundary = createRepoBoundary(repoRoot);
387
- if (!boundary.root) return { calls: [], truncated: false, filesScanned: 0, discovery: { enabled: false, configured: 0, discovered: 0, ambiguous: [] }, reasons: [] };
388
- const maxFiles = boundedInteger(options.maxFiles, DEFAULTS.maxClientFiles, 1, HARD.maxClientFiles);
389
- const maxCalls = boundedInteger(options.maxCalls, DEFAULTS.maxCallsPerClient, 1, HARD.maxCallsPerClient);
390
- const ignoreRules = loadWeavatrixIgnore(boundary.root);
391
- const classifier = createPathClassifier(boundary.root);
392
- const candidates = [...new Set((codeFiles || []).map((entry) => normalizeFile(entry?.path || entry)).filter(Boolean))].sort();
393
- let truncated = candidates.length > maxFiles;
394
- let filesScanned = 0;
395
- const sources = [];
396
- const calls = [];
397
- for (const file of candidates.slice(0, maxFiles)) {
398
- if (!/\.(?:[cm]?[jt]sx?|vue|svelte)$/i.test(file) || isWeavatrixIgnored(file, ignoreRules)) continue;
399
- const classification = classifier.explain(file, { content: "" });
400
- if (classification.excluded || (!options.includeTests && hasPathClass(classification, "test", "e2e"))) continue;
401
- const resolved = boundary.resolve(file);
402
- if (!resolved.ok) continue;
403
- const text = safeRead(resolved.path);
404
- if (!text) continue;
405
- filesScanned += 1;
406
- sources.push({ file, text });
407
- }
408
- const config = loadHttpContractConfig(boundary.root);
409
- const clientNames = [...new Set([
410
- ...normalizeHttpClientNames(options.clientNames),
411
- ...config.clientNames,
412
- ])];
413
- const configured = [
414
- ...normalizeHttpWrapperDescriptors(options.wrappers, "input"),
415
- ...config.wrappers,
416
- ];
417
- const discoveryEnabled = options.autoDiscoverWrappers !== false && config.autoDiscoverWrappers !== false;
418
- const discovered = discoveryEnabled ? discoverHttpWrappers(sources, clientNames) : { wrappers: [], ambiguous: [], truncated: false };
419
- const scopedDiscovered = discovered.wrappers.map((wrapper) => ({
420
- ...wrapper,
421
- allowedFiles: wrapperScopeFiles(wrapper.definitionFile, options.graph),
422
- }));
423
- for (const { file, text } of sources) {
424
- const remaining = maxCalls - calls.length;
425
- if (remaining <= 0) { truncated = true; break; }
426
- const extracted = extractHttpClientCallsFromText(text, file, {
427
- clientNames,
428
- normalizedWrappers: [...configured, ...scopedDiscovered],
429
- maxCalls: remaining,
430
- });
431
- calls.push(...extracted.calls);
432
- if (extracted.truncated) truncated = true;
433
- }
434
- calls.sort((left, right) => left.file.localeCompare(right.file) || left.line - right.line || left.method.localeCompare(right.method) || String(left.path).localeCompare(String(right.path)));
435
- const reasons = [];
436
- if (config.error) reasons.push(`HTTP contract config ${config.error}`);
437
- reasons.push(...(config.warnings || []));
438
- if (discovered.truncated) reasons.push("auto-discovered wrapper cap reached");
439
- if (discovered.ambiguous.length) reasons.push(`${discovered.ambiguous.length} ambiguous auto-discovered wrapper name(s) skipped`);
440
- return {
441
- calls,
442
- truncated,
443
- filesScanned,
444
- discovery: {
445
- enabled: discoveryEnabled,
446
- configured: configured.length,
447
- discovered: scopedDiscovered.length,
448
- ambiguous: discovered.ambiguous,
449
- truncated: discovered.truncated,
450
- },
451
- reasons,
452
- };
453
- }
454
-
455
- function pathSegments(path) {
456
- return String(path || "").split("/").filter(Boolean);
457
- }
458
- const parameter = (segment) => segment === ":param";
459
- const wildcard = (segment) => segment === "*";
460
-
461
- function routeShapeMatches(endpointSegments, callSegments) {
462
- const catchAll = wildcard(endpointSegments.at(-1));
463
- if (catchAll ? callSegments.length < endpointSegments.length - 1 : endpointSegments.length !== callSegments.length) return false;
464
- const compared = catchAll ? endpointSegments.length - 1 : endpointSegments.length;
465
- for (let index = 0; index < compared; index += 1) {
466
- const expected = endpointSegments[index];
467
- const actual = callSegments[index];
468
- if (parameter(expected)) continue;
469
- if (parameter(actual) || wildcard(actual) || expected !== actual) return false;
470
- }
471
- return true;
472
- }
473
-
474
- function suffixShapeMatch(endpointSegments, callSegments) {
475
- if (endpointSegments.length === callSegments.length || Math.min(endpointSegments.length, callSegments.length) < 2) return false;
476
- if (endpointSegments.length < callSegments.length) {
477
- return routeShapeMatches(endpointSegments, callSegments.slice(callSegments.length - endpointSegments.length));
478
- }
479
- return routeShapeMatches(endpointSegments.slice(endpointSegments.length - callSegments.length), callSegments);
480
- }
481
-
482
- function routeShapeContains(endpointSegments, requestedSegments) {
483
- if (!requestedSegments.length || requestedSegments.length > endpointSegments.length) return false;
484
- for (let start = 0; start <= endpointSegments.length - requestedSegments.length; start += 1) {
485
- if (routeShapeMatches(endpointSegments.slice(start, start + requestedSegments.length), requestedSegments)) return true;
486
- }
487
- return false;
488
- }
489
-
490
- function methodMatches(endpointMethod, callMethod) {
491
- return endpointMethod === "ANY" || endpointMethod === "ALL" || endpointMethod === callMethod;
492
- }
493
-
494
- export function matchHttpContract(endpoint, call) {
495
- if (!call?.path || !methodMatches(String(endpoint?.method || "").toUpperCase(), call.method)) return null;
496
- const expected = pathSegments(normalizeHttpContractPath(endpoint.path));
497
- const actual = pathSegments(call.path);
498
- if (routeShapeMatches(expected, actual)) {
499
- if (call.unknownPrefix || call.partialDynamic) return { kind: "exact-dynamic", confidence: "medium", score: 0.78, reason: "method and normalized route shape match, but the client URL retains a dynamic component" };
500
- const concreteParameter = expected.some((segment, index) => parameter(segment) && !parameter(actual[index]));
501
- return {
502
- kind: "exact",
503
- confidence: "high",
504
- score: concreteParameter ? 0.96 : 1,
505
- reason: concreteParameter ? "method matches and a backend parameter accepts the concrete client segment" : "method and normalized route shape match exactly",
506
- };
507
- }
508
- if (suffixShapeMatch(expected, actual)) {
509
- const dynamic = call.dynamic || call.unknownPrefix || call.partialDynamic;
510
- return {
511
- kind: "suffix",
512
- confidence: dynamic ? "low" : "medium",
513
- score: dynamic ? 0.55 : 0.72,
514
- reason: dynamic ? "method matches and at least two trailing route segments match; the client prefix is dynamic" : "method matches and at least two trailing route segments match after a client/backend base-path difference",
515
- };
516
- }
517
- return null;
518
- }
519
-
520
- function reverseImports(graph = {}) {
521
- const byId = new Map();
522
- const files = new Set();
523
- for (const node of graph.nodes || []) {
524
- const file = normalizeFile(node?.source_file);
525
- if (!file) continue;
526
- byId.set(String(node.id), file);
527
- files.add(file);
528
- }
529
- const reverse = new Map([...files].map((file) => [file, new Set()]));
530
- for (const link of graph.links || []) {
531
- if (isStructuralRelation(link?.relation) || !["imports", "re_exports"].includes(link?.relation) || link?.typeOnly === true || link?.compileOnly === true || link?.barrelProxy === true) continue;
532
- const importer = byId.get(String(endpointId(link.source)));
533
- const imported = byId.get(String(endpointId(link.target)));
534
- if (!importer || !imported || importer === imported) continue;
535
- reverse.get(imported)?.add(importer);
536
- }
537
- return reverse;
538
- }
539
-
540
- function wrapperScopeFiles(sourceFile, graph) {
541
- const source = normalizeFile(sourceFile);
542
- if (!source || !Array.isArray(graph?.nodes)) return null;
543
- const reverse = reverseImports(graph);
544
- const allowed = new Set([source]);
545
- const queue = [{ file: source, depth: 0 }];
546
- while (queue.length && allowed.size < 2_000) {
547
- const current = queue.shift();
548
- if (current.depth >= 4) continue;
549
- for (const importer of [...(reverse.get(current.file) || [])].sort()) {
550
- if (allowed.has(importer)) continue;
551
- allowed.add(importer);
552
- queue.push({ file: importer, depth: current.depth + 1 });
553
- }
554
- }
555
- return allowed;
556
- }
557
-
558
- function isScreen(file) {
559
- const path = normalizeFile(file);
560
- const base = path.split("/").at(-1) || "";
561
- return /(^|\/)(pages?|screens?|views?|routes?)(\/|$)/i.test(path)
562
- || /(^|\/)(?:page|layout)\.[cm]?[jt]sx?$/i.test(path)
563
- || /^(?:App|Root)\.[cm]?[jt]sx?$/i.test(base);
564
- }
565
-
566
- function affectedForEndpoint(callsites, clientContexts, limits) {
567
- const collected = new Map();
568
- let traversalTruncated = false;
569
- for (const context of clientContexts) {
570
- const seeds = callsites.filter((call) => call.clientRepo === context.id).map((call) => call.file).sort();
571
- if (!seeds.length) continue;
572
- const queue = [];
573
- const distance = new Map();
574
- for (const seed of seeds) if (!distance.has(seed)) { distance.set(seed, 0); queue.push(seed); }
575
- while (queue.length) {
576
- const file = queue.shift();
577
- const depth = distance.get(file);
578
- const key = `${context.id}\0${file}`;
579
- const previous = collected.get(key);
580
- if (!previous || depth < previous.distance) collected.set(key, { client: context.id, file, distance: depth });
581
- if (depth >= limits.maxImpactDepth) continue;
582
- for (const importer of [...(context.reverse.get(file) || [])].sort()) {
583
- if (distance.has(importer)) continue;
584
- if (distance.size >= limits.maxAffectedFiles * 2) { traversalTruncated = true; continue; }
585
- distance.set(importer, depth + 1);
586
- queue.push(importer);
587
- }
588
- }
589
- }
590
- const allFiles = [...collected.values()].sort((left, right) => left.distance - right.distance || left.client.localeCompare(right.client) || left.file.localeCompare(right.file));
591
- const filesTruncated = allFiles.length > limits.maxAffectedFiles;
592
- const files = allFiles.slice(0, limits.maxAffectedFiles);
593
- const allScreens = files.filter((entry) => isScreen(entry.file));
594
- const screensTruncated = allScreens.length > limits.maxScreens;
595
- const screens = allScreens.slice(0, limits.maxScreens);
596
- const moduleMap = new Map();
597
- for (const entry of files) {
598
- const module = folderModuleOf(entry.file);
599
- const key = `${entry.client}\0${module}`;
600
- const item = moduleMap.get(key) || { client: entry.client, module, files: 0, nearestDistance: entry.distance };
601
- item.files += 1;
602
- item.nearestDistance = Math.min(item.nearestDistance, entry.distance);
603
- moduleMap.set(key, item);
604
- }
605
- const allModules = [...moduleMap.values()].sort((left, right) => left.nearestDistance - right.nearestDistance || right.files - left.files || left.client.localeCompare(right.client) || left.module.localeCompare(right.module));
606
- const modulesTruncated = allModules.length > limits.maxModules;
607
- return {
608
- complete: !(traversalTruncated || filesTruncated || screensTruncated || modulesTruncated),
609
- files,
610
- screens,
611
- modules: allModules.slice(0, limits.maxModules),
612
- truncated: { traversal: traversalTruncated, files: filesTruncated, screens: screensTruncated, modules: modulesTruncated },
613
- };
614
- }
615
-
616
- function descriptorFiles(descriptor) {
617
- return descriptor.codeFiles || filesFromGraph(descriptor.graph);
618
- }
619
-
620
- function endpointFilter(endpoint, backendId, options) {
621
- if (options.method && endpoint.method !== options.method && endpoint.method !== "ANY" && endpoint.method !== "ALL") return false;
622
- if (options.path) {
623
- const requested = normalizeHttpContractPath(options.path);
624
- const endpointSegments = pathSegments(normalizeHttpContractPath(endpoint.path));
625
- const requestedSegments = pathSegments(requested);
626
- if (!requested || (!routeShapeMatches(endpointSegments, requestedSegments) && !routeShapeContains(endpointSegments, requestedSegments))) return false;
627
- }
628
- if (options.changedFiles?.size) {
629
- const file = normalizeFile(endpoint.file);
630
- if (!options.changedFiles.has(file) && !options.changedFiles.has(`${backendId}::${file}`)) return false;
631
- }
632
- return true;
633
- }
634
-
635
- function bareGraphLabel(value) {
636
- return String(value || "").replace(/\s*\(.*$/, "").replace(/[()]/g, "").trim();
637
- }
638
-
639
- function handlerNodeEvidence(endpoint, graph) {
640
- const handler = /^[A-Za-z_$][\w$]{0,127}$/.test(String(endpoint?.handler || "")) ? String(endpoint.handler) : null;
641
- if (!handler) return { handler: null, handlerNodeId: null, handlerResolution: "inline-or-unresolved" };
642
- const nodes = graph?.nodes || [];
643
- const matches = nodes.filter((node) =>
644
- normalizeFile(node?.source_file) && bareGraphLabel(node?.label) === handler && String(node?.id || "") !== normalizeFile(node?.source_file));
645
- const endpointFile = normalizeFile(endpoint.file);
646
- const sameFile = matches.filter((node) => normalizeFile(node?.source_file) === endpointFile);
647
- const byId = new Map(nodes.map((node) => [endpointId(node?.id), node]));
648
- const linkFile = (value) => {
649
- const id = endpointId(value);
650
- return normalizeFile(byId.get(id)?.source_file || String(id || "").split("#")[0]);
651
- };
652
- const directlyImportedFiles = new Set((graph?.links || [])
653
- .filter((link) => ["imports", "re_exports"].includes(link?.relation) && linkFile(link?.source) === endpointFile)
654
- .map((link) => linkFile(link?.target))
655
- .filter(Boolean));
656
- const imported = matches.filter((node) => directlyImportedFiles.has(normalizeFile(node?.source_file)));
657
- const resolved = sameFile.length === 1 ? sameFile[0] : imported.length === 1 ? imported[0] : matches.length === 1 ? matches[0] : null;
658
- return {
659
- handler,
660
- handlerNodeId: resolved ? String(resolved.id) : null,
661
- handlerResolution: resolved ? "resolved" : matches.length > 1 ? "ambiguous" : "unresolved",
662
- };
663
- }
664
-
665
- function externalUseLiveness(callsites, handlerEvidence) {
666
- const proven = callsites.filter((call) => call.match?.confidence === "high" || call.match?.confidence === "medium");
667
- const possible = callsites.filter((call) => call.match?.confidence === "low");
668
- const status = proven.length ? "NOT_DEAD_EXTERNAL_USE" : possible.length ? "POSSIBLE_EXTERNAL_USE" : "UNKNOWN";
669
- const evidence = proven.length ? proven : possible;
670
- return {
671
- status,
672
- subject: handlerEvidence.handlerNodeId ? "handler-node" : handlerEvidence.handler ? "endpoint-handler" : "endpoint",
673
- canSuppressDeadCandidate: proven.length > 0 && Boolean(handlerEvidence.handlerNodeId),
674
- staticEvidence: evidence.length,
675
- consumerRepositories: [...new Set(evidence.map((call) => call.clientRepo))].sort(),
676
- reason: proven.length
677
- ? "At least one selected external repository has a medium/high-confidence static HTTP contract match."
678
- : possible.length
679
- ? "Only low-confidence external HTTP contract matches were found; review before suppressing a dead-code candidate."
680
- : "No selected external repository proved a static caller; absence of evidence is not a dead-code verdict.",
681
- };
682
- }
683
-
684
- // `backends` and `clients` are arrays of {id, repoRoot, codeFiles?, graph?}. Backends may optionally
685
- // supply a precomputed `endpoints` array; otherwise the shared multi-language endpoint detector is used.
686
- export function analyzeHttpContracts(input = {}) {
687
- const limits = normalizeLimits(input);
688
- const method = input.method ? String(input.method).toUpperCase() : null;
689
- if (method && !METHODS.has(method)) throw new Error("method must be a concrete HTTP method");
690
- const changedFiles = new Set((Array.isArray(input.changedFiles) ? input.changedFiles : []).map((file) => normalizeFile(file)).filter(Boolean));
691
- const backendDescriptors = (Array.isArray(input.backends) ? input.backends : input.backend ? [input.backend] : []).slice(0, 20);
692
- const clientDescriptors = (Array.isArray(input.clients) ? input.clients : input.client ? [input.client] : []).slice(0, 20);
693
- const completeness = [];
694
- const backends = [];
695
- let endpointBudget = limits.maxEndpoints;
696
- for (let index = 0; index < backendDescriptors.length; index += 1) {
697
- const descriptor = backendDescriptors[index] || {};
698
- const id = safeName(descriptor.id, `backend-${index + 1}`);
699
- const candidates = descriptorFiles(descriptor).slice().sort();
700
- if (candidates.length > limits.maxBackendFiles) completeness.push(`${id}: backend file cap reached`);
701
- const detected = Array.isArray(descriptor.endpoints)
702
- ? descriptor.endpoints
703
- : detectEndpoints(descriptor.repoRoot, candidates.slice(0, limits.maxBackendFiles));
704
- const filtered = detected.filter((endpoint) => normalizeHttpContractPath(endpoint?.path) && normalizeFile(endpoint?.file) && endpointFilter(endpoint, id, { method, path: input.path, changedFiles })).sort((left, right) => left.path.localeCompare(right.path) || left.method.localeCompare(right.method) || String(left.file).localeCompare(String(right.file)) || Number(left.line) - Number(right.line));
705
- if (filtered.length > endpointBudget) completeness.push(`${id}: endpoint cap reached`);
706
- const accepted = filtered.slice(0, endpointBudget);
707
- endpointBudget -= accepted.length;
708
- backends.push({ id, endpoints: accepted, graph: descriptor.graph });
709
- }
710
-
711
- const clients = [];
712
- for (let index = 0; index < clientDescriptors.length; index += 1) {
713
- const descriptor = clientDescriptors[index] || {};
714
- const id = safeName(descriptor.id, `client-${index + 1}`);
715
- const detected = detectHttpClientCalls(descriptor.repoRoot, descriptorFiles(descriptor), {
716
- maxFiles: limits.maxClientFiles,
717
- maxCalls: limits.maxCallsPerClient,
718
- clientNames: descriptor.clientNames || input.clientNames,
719
- wrappers: descriptor.wrappers || input.wrappers,
720
- autoDiscoverWrappers: descriptor.autoDiscoverWrappers ?? input.autoDiscoverWrappers,
721
- graph: descriptor.graph,
722
- includeTests: descriptor.includeTests ?? input.includeTests,
723
- });
724
- if (detected.truncated) completeness.push(`${id}: client scan cap reached`);
725
- for (const reason of detected.reasons || []) completeness.push(`${id}: ${reason}`);
726
- clients.push({
727
- id,
728
- calls: detected.calls,
729
- reverse: reverseImports(descriptor.graph),
730
- filesScanned: detected.filesScanned,
731
- wrapperDiscovery: detected.discovery,
732
- });
733
- }
734
-
735
- const results = [];
736
- let matches = 0;
737
- let methodMismatches = 0;
738
- let callsiteCapReached = false;
739
- for (const backend of backends) {
740
- for (const endpoint of backend.endpoints) {
741
- const callsites = [];
742
- for (const client of clients) {
743
- for (const call of client.calls) {
744
- if (call.path && !methodMatches(endpoint.method, call.method)) {
745
- const expected = pathSegments(normalizeHttpContractPath(endpoint.path));
746
- const actual = pathSegments(call.path);
747
- if (routeShapeMatches(expected, actual) || suffixShapeMatch(expected, actual)) methodMismatches += 1;
748
- continue;
749
- }
750
- const match = matchHttpContract(endpoint, call);
751
- if (!match) continue;
752
- if (matches >= limits.maxMatches || callsites.length >= limits.maxCallsitesPerEndpoint) {
753
- callsiteCapReached = true;
754
- continue;
755
- }
756
- matches += 1;
757
- callsites.push({
758
- clientRepo: client.id,
759
- file: call.file,
760
- line: call.line,
761
- method: call.method,
762
- path: call.path,
763
- dynamic: call.dynamic,
764
- detector: call.detector,
765
- wrapper: call.wrapper,
766
- match,
767
- });
768
- }
769
- }
770
- callsites.sort((left, right) => right.match.score - left.match.score || left.clientRepo.localeCompare(right.clientRepo) || left.file.localeCompare(right.file) || left.line - right.line);
771
- const handlerEvidence = handlerNodeEvidence(endpoint, backend.graph);
772
- results.push({
773
- backend: backend.id,
774
- method: endpoint.method,
775
- path: endpoint.path,
776
- normalizedPath: normalizeHttpContractPath(endpoint.path),
777
- ...handlerEvidence,
778
- file: normalizeFile(endpoint.file) || null,
779
- line: Number(endpoint.line) || null,
780
- callsites,
781
- liveness: externalUseLiveness(callsites, handlerEvidence),
782
- affected: affectedForEndpoint(callsites, clients, limits),
783
- });
784
- }
785
- }
786
- if (callsiteCapReached) completeness.push("match or per-endpoint callsite cap reached");
787
-
788
- const uncertainAll = clients.flatMap((client) => client.calls.filter((call) => !call.path || call.unknownPrefix || call.partialDynamic || call.method === "UNKNOWN").map((call) => ({
789
- clientRepo: client.id,
790
- file: call.file,
791
- line: call.line,
792
- method: call.method,
793
- reason: call.reason || "URL retains an unresolved dynamic component",
794
- }))).sort((left, right) => left.clientRepo.localeCompare(right.clientRepo) || left.file.localeCompare(right.file) || left.line - right.line);
795
- if (uncertainAll.length > limits.maxUncertain) completeness.push("uncertain callsite cap reached");
796
- const affectedPartial = results.some((endpoint) => !endpoint.affected.complete);
797
- if (affectedPartial) completeness.push("affected-file traversal cap reached");
798
-
799
- return {
800
- httpContractsV: HTTP_CONTRACTS_V,
801
- status: completeness.length ? "partial" : "complete",
802
- filters: { method, path: input.path ? normalizeHttpContractPath(input.path) : null, changedFiles: [...changedFiles].sort() },
803
- limits,
804
- completeness: { complete: completeness.length === 0, reasons: [...new Set(completeness)] },
805
- totals: {
806
- backends: backends.length,
807
- clients: clients.length,
808
- endpoints: results.length,
809
- clientCalls: clients.reduce((sum, client) => sum + client.calls.length, 0),
810
- matches,
811
- methodMismatches,
812
- uncertainCalls: uncertainAll.length,
813
- notDeadExternalUse: results.filter((endpoint) => endpoint.liveness.status === "NOT_DEAD_EXTERNAL_USE").length,
814
- notDeadExternalHandlers: results.filter((endpoint) => endpoint.liveness.canSuppressDeadCandidate).length,
815
- possibleExternalUse: results.filter((endpoint) => endpoint.liveness.status === "POSSIBLE_EXTERNAL_USE").length,
816
- unknownLiveness: results.filter((endpoint) => endpoint.liveness.status === "UNKNOWN").length,
817
- },
818
- wrapperDiscovery: clients.map((client) => ({ clientRepo: client.id, ...client.wrapperDiscovery })),
819
- endpoints: results,
820
- uncertain: uncertainAll.slice(0, limits.maxUncertain),
821
- };
822
- }
1
+ // Stable public facade for cross-repository HTTP contract evidence.
2
+ export { HTTP_CONTRACTS_V, normalizeHttpContractPath } from "./http-contracts/shared.js";
3
+ export { extractHttpClientCallsFromText } from "./http-contracts/client-call-parser.js";
4
+ export { detectHttpClientCalls } from "./http-contracts/client-call-detection.js";
5
+ export { matchHttpContract } from "./http-contracts/matching.js";
6
+ export { analyzeHttpContracts } from "./http-contracts/analysis.js";