weavatrix 0.3.14 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +132 -200
- package/bin/native/darwin-arm64/weavatrix +0 -0
- package/bin/native/darwin-x64/weavatrix +0 -0
- package/bin/native/linux-arm64/weavatrix +0 -0
- package/bin/native/linux-x64/weavatrix +0 -0
- package/bin/native/win32-arm64/weavatrix.exe +0 -0
- package/bin/native/win32-x64/weavatrix.exe +0 -0
- package/bin/resolve-binary.mjs +73 -0
- package/bin/run-native.mjs +26 -0
- package/bin/weavatrix-mcp.mjs +7 -6
- package/bin/weavatrix.mjs +5 -0
- package/package.json +10 -67
- package/server.json +38 -0
- package/skill/SKILL.md +62 -374
- package/skill/agents/openai.yaml +4 -0
- package/skill/references/tool-routing.md +45 -0
- package/SECURITY.md +0 -101
- package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
- package/docs/agent-task-benchmark.md +0 -58
- package/docs/releases/v0.2.19.md +0 -33
- package/docs/releases/v0.3.0.md +0 -43
- package/docs/releases/v0.3.1.md +0 -30
- package/docs/releases/v0.3.10.md +0 -76
- package/docs/releases/v0.3.11.md +0 -55
- package/docs/releases/v0.3.12.md +0 -44
- package/docs/releases/v0.3.13.md +0 -34
- package/docs/releases/v0.3.14.md +0 -37
- package/docs/releases/v0.3.2.md +0 -15
- package/docs/releases/v0.3.3.md +0 -25
- package/docs/releases/v0.3.4.md +0 -40
- package/docs/releases/v0.3.5.md +0 -50
- package/docs/releases/v0.3.6.md +0 -44
- package/docs/releases/v0.3.7.md +0 -37
- package/docs/releases/v0.3.8.md +0 -21
- package/docs/releases/v0.3.9.md +0 -36
- package/scripts/run-agent-task-benchmark.mjs +0 -118
- package/src/analysis/allowed-test-runner.js +0 -70
- package/src/analysis/architecture/contract-graph.js +0 -119
- package/src/analysis/architecture/contract-schema.js +0 -110
- package/src/analysis/architecture/contract-storage.js +0 -35
- package/src/analysis/architecture/contract-verification.js +0 -168
- package/src/analysis/architecture-contract.js +0 -16
- package/src/analysis/audit-debt.js +0 -94
- package/src/analysis/audit-extensions.js +0 -60
- package/src/analysis/cargo-dependency-evidence.js +0 -108
- package/src/analysis/cargo-manifests.js +0 -81
- package/src/analysis/change-classification/diff-parser.js +0 -103
- package/src/analysis/change-classification/options.js +0 -40
- package/src/analysis/change-classification/symbol-classifier.js +0 -177
- package/src/analysis/change-classification.js +0 -188
- package/src/analysis/coverage-reports-tarpaulin.js +0 -112
- package/src/analysis/coverage-reports.js +0 -246
- package/src/analysis/cycle-route.js +0 -14
- package/src/analysis/data-flow-evidence.js +0 -114
- package/src/analysis/dead-check.js +0 -300
- package/src/analysis/dead-code-review/candidates.js +0 -194
- package/src/analysis/dead-code-review/policy.js +0 -23
- package/src/analysis/dead-code-review.js +0 -119
- package/src/analysis/dep-check-ecosystems.js +0 -205
- package/src/analysis/dep-check.js +0 -290
- package/src/analysis/dep-rules.js +0 -10
- package/src/analysis/dependency/conventions.js +0 -16
- package/src/analysis/dependency/scoped-dependencies.js +0 -51
- package/src/analysis/dependency/source-references.js +0 -21
- package/src/analysis/duplicate-groups.js +0 -104
- package/src/analysis/duplicates-worker.js +0 -11
- package/src/analysis/duplicates.compute.js +0 -269
- package/src/analysis/duplicates.js +0 -15
- package/src/analysis/duplicates.run.js +0 -52
- package/src/analysis/duplicates.tokenize.js +0 -199
- package/src/analysis/endpoints/common.js +0 -79
- package/src/analysis/endpoints/extract.js +0 -120
- package/src/analysis/endpoints/inventory.js +0 -84
- package/src/analysis/endpoints/mounts.js +0 -129
- package/src/analysis/endpoints-java.js +0 -260
- package/src/analysis/endpoints-rust.js +0 -124
- package/src/analysis/endpoints.js +0 -3
- package/src/analysis/findings.js +0 -70
- package/src/analysis/git-history/analytics.js +0 -177
- package/src/analysis/git-history/collector.js +0 -67
- package/src/analysis/git-history/options.js +0 -66
- package/src/analysis/git-history.js +0 -143
- package/src/analysis/git-ref-graph.js +0 -76
- package/src/analysis/go-dependency-evidence.js +0 -62
- package/src/analysis/graph-analysis.aggregate.js +0 -296
- package/src/analysis/graph-analysis.edges.js +0 -24
- package/src/analysis/graph-analysis.js +0 -7
- package/src/analysis/graph-analysis.refs.js +0 -202
- package/src/analysis/graph-analysis.summaries.js +0 -65
- package/src/analysis/health-capabilities.js +0 -83
- package/src/analysis/hot-path-review.js +0 -266
- package/src/analysis/http-contract-wrappers.js +0 -241
- package/src/analysis/http-contracts/analysis.js +0 -179
- package/src/analysis/http-contracts/client-call-detection.js +0 -82
- package/src/analysis/http-contracts/client-call-parser.js +0 -273
- package/src/analysis/http-contracts/graph-context.js +0 -143
- package/src/analysis/http-contracts/matching.js +0 -61
- package/src/analysis/http-contracts/shared.js +0 -85
- package/src/analysis/http-contracts.js +0 -6
- package/src/analysis/internal-audit/dependency-health.js +0 -113
- package/src/analysis/internal-audit/python-manifests.js +0 -78
- package/src/analysis/internal-audit/repo-files.js +0 -50
- package/src/analysis/internal-audit/supply-chain.js +0 -149
- package/src/analysis/internal-audit.collect.js +0 -213
- package/src/analysis/internal-audit.js +0 -9
- package/src/analysis/internal-audit.reach.js +0 -201
- package/src/analysis/internal-audit.run.js +0 -256
- package/src/analysis/java-source.js +0 -36
- package/src/analysis/jvm-artifact-index.js +0 -87
- package/src/analysis/jvm-dependency-evidence.js +0 -138
- package/src/analysis/jvm-manifests.js +0 -114
- package/src/analysis/manifests.js +0 -123
- package/src/analysis/package-reachability.js +0 -39
- package/src/analysis/source-complexity.ast.js +0 -97
- package/src/analysis/source-complexity.constants.js +0 -55
- package/src/analysis/source-complexity.js +0 -14
- package/src/analysis/source-complexity.report.js +0 -81
- package/src/analysis/source-complexity.walk.js +0 -228
- package/src/analysis/source-correctness/retry-patterns.js +0 -93
- package/src/analysis/source-correctness.js +0 -222
- package/src/analysis/static-test-reachability.js +0 -150
- package/src/analysis/structure/dependency-graph.js +0 -151
- package/src/analysis/structure/findings.js +0 -162
- package/src/analysis/task-retrieval.js +0 -107
- package/src/analysis/transport-contracts.js +0 -224
- package/src/analysis/transport-runtime-evidence.js +0 -155
- package/src/analysis-kit.mjs +0 -17
- package/src/bounds.js +0 -4
- package/src/build-graph.js +0 -209
- package/src/child-env.js +0 -7
- package/src/config.js +0 -5
- package/src/extension/local-services.mjs +0 -75
- package/src/git-exec.js +0 -67
- package/src/graph/build-worker.js +0 -49
- package/src/graph/builder/go-receiver-resolution.js +0 -112
- package/src/graph/builder/js/dynamic-import.js +0 -26
- package/src/graph/builder/js/queries.js +0 -48
- package/src/graph/builder/lang-csharp.js +0 -40
- package/src/graph/builder/lang-css.js +0 -29
- package/src/graph/builder/lang-go.js +0 -140
- package/src/graph/builder/lang-html.js +0 -29
- package/src/graph/builder/lang-java-calls.js +0 -142
- package/src/graph/builder/lang-java.js +0 -215
- package/src/graph/builder/lang-js.js +0 -291
- package/src/graph/builder/lang-python.js +0 -235
- package/src/graph/builder/lang-rust-calls.js +0 -63
- package/src/graph/builder/lang-rust.js +0 -299
- package/src/graph/builder/lang-solidity.js +0 -126
- package/src/graph/builder/lang-sql.js +0 -263
- package/src/graph/builder/pass2-resolution.js +0 -81
- package/src/graph/builder/spec-pkg.js +0 -87
- package/src/graph/community.js +0 -27
- package/src/graph/edge-provenance.js +0 -31
- package/src/graph/file-lock.js +0 -69
- package/src/graph/freshness-probe.js +0 -142
- package/src/graph/graph-filter.js +0 -63
- package/src/graph/incremental-refresh.js +0 -235
- package/src/graph/internal-builder.barrels.js +0 -235
- package/src/graph/internal-builder.build.js +0 -300
- package/src/graph/internal-builder.java.js +0 -33
- package/src/graph/internal-builder.js +0 -12
- package/src/graph/internal-builder.langs.js +0 -157
- package/src/graph/internal-builder.pass2.js +0 -163
- package/src/graph/internal-builder.resolvers.js +0 -286
- package/src/graph/layout.js +0 -51
- package/src/graph/node-id.js +0 -7
- package/src/graph/parser-artifact-boundary.js +0 -55
- package/src/graph/relations.js +0 -4
- package/src/graph/repo-registry.js +0 -124
- package/src/graph/resolvers/rust.js +0 -117
- package/src/infra/infra-items.js +0 -255
- package/src/infra/infra-registry.js +0 -184
- package/src/infra/infra.detect.js +0 -119
- package/src/infra/infra.js +0 -38
- package/src/infra/infra.match.js +0 -102
- package/src/infra/infra.scan.js +0 -93
- package/src/mcp/actions/graph-lifecycle.mjs +0 -148
- package/src/mcp/architecture-bootstrap.mjs +0 -168
- package/src/mcp/architecture-starter.mjs +0 -234
- package/src/mcp/catalog.mjs +0 -233
- package/src/mcp/company-contract-page.mjs +0 -142
- package/src/mcp/company-contract-reconcile.mjs +0 -87
- package/src/mcp/company-contract-verdict.mjs +0 -42
- package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
- package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
- package/src/mcp/evidence/package-graph-common.mjs +0 -115
- package/src/mcp/evidence/package-lock-graph.mjs +0 -92
- package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
- package/src/mcp/evidence-snapshot.common.mjs +0 -158
- package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
- package/src/mcp/evidence-snapshot.health.mjs +0 -102
- package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
- package/src/mcp/evidence-snapshot.mjs +0 -50
- package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
- package/src/mcp/evidence-snapshot.structure.mjs +0 -84
- package/src/mcp/extension-api.mjs +0 -77
- package/src/mcp/git-output.mjs +0 -7
- package/src/mcp/graph/context-core.mjs +0 -111
- package/src/mcp/graph/context-seeds.mjs +0 -207
- package/src/mcp/graph/context-state.mjs +0 -85
- package/src/mcp/graph/reverse-reach.mjs +0 -42
- package/src/mcp/graph/tools-core.mjs +0 -143
- package/src/mcp/graph/tools-query.mjs +0 -276
- package/src/mcp/graph-context.mjs +0 -5
- package/src/mcp/graph-diff.mjs +0 -218
- package/src/mcp/health/audit-format.mjs +0 -199
- package/src/mcp/health/audit.mjs +0 -199
- package/src/mcp/health/dead-code.mjs +0 -110
- package/src/mcp/health/duplicates.mjs +0 -121
- package/src/mcp/health/endpoints.mjs +0 -71
- package/src/mcp/health/structure.mjs +0 -220
- package/src/mcp/runtime-version.mjs +0 -31
- package/src/mcp/server/auto-refresh.mjs +0 -66
- package/src/mcp/server/runtime-config.mjs +0 -43
- package/src/mcp/server/shutdown.mjs +0 -40
- package/src/mcp/staleness-notice.mjs +0 -20
- package/src/mcp/sync/evidence-architecture.mjs +0 -54
- package/src/mcp/sync/evidence-common.mjs +0 -88
- package/src/mcp/sync/evidence-duplicates.mjs +0 -97
- package/src/mcp/sync/evidence-health.mjs +0 -56
- package/src/mcp/sync/evidence-packages.mjs +0 -95
- package/src/mcp/sync/payload-common.mjs +0 -97
- package/src/mcp/sync/payload-v2.mjs +0 -54
- package/src/mcp/sync/payload-v3.mjs +0 -79
- package/src/mcp/sync-evidence.mjs +0 -23
- package/src/mcp/sync-payload.mjs +0 -70
- package/src/mcp/tool-result.mjs +0 -54
- package/src/mcp/tools-actions.mjs +0 -11
- package/src/mcp/tools-architecture.mjs +0 -177
- package/src/mcp/tools-company.mjs +0 -251
- package/src/mcp/tools-context.mjs +0 -222
- package/src/mcp/tools-endpoints.mjs +0 -207
- package/src/mcp/tools-graph-hubs.mjs +0 -78
- package/src/mcp/tools-graph.mjs +0 -17
- package/src/mcp/tools-health.mjs +0 -24
- package/src/mcp/tools-history.mjs +0 -28
- package/src/mcp/tools-impact-change.mjs +0 -220
- package/src/mcp/tools-impact-precision.mjs +0 -89
- package/src/mcp/tools-impact.mjs +0 -169
- package/src/mcp/tools-source.mjs +0 -238
- package/src/mcp/tools-verified-change.mjs +0 -200
- package/src/mcp-rg.mjs +0 -50
- package/src/mcp-runtime.mjs +0 -2
- package/src/mcp-server.mjs +0 -298
- package/src/mcp-source-tools.mjs +0 -163
- package/src/path-classification.js +0 -226
- package/src/path-ignore.js +0 -69
- package/src/precision/lsp-client/constants.js +0 -12
- package/src/precision/lsp-client/environment.js +0 -20
- package/src/precision/lsp-client/errors.js +0 -15
- package/src/precision/lsp-client/lifecycle.js +0 -114
- package/src/precision/lsp-client/message-parser.js +0 -81
- package/src/precision/lsp-client/protocol.js +0 -212
- package/src/precision/lsp-client/registry.js +0 -59
- package/src/precision/lsp-client/repo-uri.js +0 -60
- package/src/precision/lsp-client/stdio-client.js +0 -140
- package/src/precision/lsp-client.js +0 -10
- package/src/precision/lsp-overlay/budget-policy.js +0 -78
- package/src/precision/lsp-overlay/build.js +0 -261
- package/src/precision/lsp-overlay/contract.js +0 -61
- package/src/precision/lsp-overlay/reference-results.js +0 -108
- package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
- package/src/precision/lsp-overlay/source-session.js +0 -142
- package/src/precision/lsp-overlay/store.js +0 -150
- package/src/precision/lsp-overlay/target-index.js +0 -152
- package/src/precision/lsp-overlay/target-query.js +0 -55
- package/src/precision/lsp-overlay.js +0 -11
- package/src/precision/symbol-query.js +0 -224
- package/src/precision/typescript-lsp-provider.js +0 -12
- package/src/precision/typescript-provider/client.js +0 -89
- package/src/precision/typescript-provider/discovery.js +0 -124
- package/src/precision/typescript-provider/isolated-runtime.js +0 -39
- package/src/precision/typescript-provider/project-host.js +0 -254
- package/src/precision/typescript-provider/project-safety.js +0 -166
- package/src/precision/typescript-provider/reference-usage.js +0 -53
- package/src/process.js +0 -95
- package/src/repo-path.js +0 -53
- package/src/scan/discover.inventory.js +0 -78
- package/src/scan/discover.js +0 -5
- package/src/scan/discover.list.js +0 -79
- package/src/scan/discover.stack.js +0 -227
- package/src/scan/search.core.js +0 -24
- package/src/scan/search.git.js +0 -102
- package/src/scan/search.js +0 -9
- package/src/scan/search.node.js +0 -83
- package/src/scan/search.preview.js +0 -49
- package/src/scan/search.rg.js +0 -145
- package/src/security/advisory-store.js +0 -172
- package/src/security/installed-jvm-rust.js +0 -46
- package/src/security/installed.js +0 -280
- package/src/security/malware-file-heuristics.js +0 -121
- package/src/security/malware-heuristics.exclusions.js +0 -134
- package/src/security/malware-heuristics.js +0 -15
- package/src/security/malware-heuristics.roots.js +0 -145
- package/src/security/malware-heuristics.scan.js +0 -89
- package/src/security/malware-heuristics.sweep.js +0 -122
- package/src/security/malware-scoring.js +0 -105
- package/src/security/match.js +0 -85
- package/src/security/registry-sig.classify.js +0 -137
- package/src/security/registry-sig.js +0 -18
- package/src/security/registry-sig.rules.js +0 -192
- package/src/security/rust-advisory-report.js +0 -60
- package/src/security/typosquat.js +0 -81
- package/src/util.js +0 -40
- package/src/version.js +0 -7
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { detectEndpoints } from "../endpoints.js";
|
|
2
|
-
import { detectHttpClientCalls } from "./client-call-detection.js";
|
|
3
|
-
import { affectedForEndpoint, externalUseLiveness, handlerNodeEvidence, reverseRuntimeImports } from "./graph-context.js";
|
|
4
|
-
import { matchHttpContract, methodMatches, pathSegments, routeShapeContains, routeShapeMatches, suffixShapeMatch } from "./matching.js";
|
|
5
|
-
import {
|
|
6
|
-
HTTP_CONTRACTS_V,
|
|
7
|
-
HTTP_CONTRACT_METHODS,
|
|
8
|
-
filesFromGraph,
|
|
9
|
-
normalizeContractFile,
|
|
10
|
-
normalizeHttpContractLimits,
|
|
11
|
-
normalizeHttpContractPath,
|
|
12
|
-
safeContractName,
|
|
13
|
-
} from "./shared.js";
|
|
14
|
-
|
|
15
|
-
const descriptorFiles = (descriptor) => descriptor.codeFiles || filesFromGraph(descriptor.graph);
|
|
16
|
-
|
|
17
|
-
function endpointFilter(endpoint, backendId, options) {
|
|
18
|
-
if (options.method && endpoint.method !== options.method && endpoint.method !== "ANY" && endpoint.method !== "ALL") return false;
|
|
19
|
-
if (options.path) {
|
|
20
|
-
const requested = normalizeHttpContractPath(options.path);
|
|
21
|
-
const endpointSegments = pathSegments(normalizeHttpContractPath(endpoint.path));
|
|
22
|
-
const requestedSegments = pathSegments(requested);
|
|
23
|
-
if (!requested || (!routeShapeMatches(endpointSegments, requestedSegments) && !routeShapeContains(endpointSegments, requestedSegments))) return false;
|
|
24
|
-
}
|
|
25
|
-
if (options.changedFiles?.size) {
|
|
26
|
-
const file = normalizeContractFile(endpoint.file);
|
|
27
|
-
if (!options.changedFiles.has(file) && !options.changedFiles.has(`${backendId}::${file}`)) return false;
|
|
28
|
-
}
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function analyzeHttpContracts(input = {}) {
|
|
33
|
-
const limits = normalizeHttpContractLimits(input);
|
|
34
|
-
const method = input.method ? String(input.method).toUpperCase() : null;
|
|
35
|
-
if (method && !HTTP_CONTRACT_METHODS.has(method)) throw new Error("method must be a concrete HTTP method");
|
|
36
|
-
const changedFiles = new Set((Array.isArray(input.changedFiles) ? input.changedFiles : []).map((file) => normalizeContractFile(file)).filter(Boolean));
|
|
37
|
-
const backendDescriptors = (Array.isArray(input.backends) ? input.backends : input.backend ? [input.backend] : []).slice(0, 20);
|
|
38
|
-
const clientDescriptors = (Array.isArray(input.clients) ? input.clients : input.client ? [input.client] : []).slice(0, 20);
|
|
39
|
-
const completeness = [], backends = [];
|
|
40
|
-
let endpointBudget = limits.maxEndpoints;
|
|
41
|
-
for (let index = 0; index < backendDescriptors.length; index++) {
|
|
42
|
-
const descriptor = backendDescriptors[index] || {};
|
|
43
|
-
const id = safeContractName(descriptor.id, `backend-${index + 1}`);
|
|
44
|
-
const candidates = descriptorFiles(descriptor).slice().sort();
|
|
45
|
-
if (candidates.length > limits.maxBackendFiles) completeness.push(`${id}: backend file cap reached`);
|
|
46
|
-
const detected = Array.isArray(descriptor.endpoints)
|
|
47
|
-
? descriptor.endpoints
|
|
48
|
-
: detectEndpoints(descriptor.repoRoot, candidates.slice(0, limits.maxBackendFiles));
|
|
49
|
-
const filtered = detected
|
|
50
|
-
.filter((endpoint) => normalizeHttpContractPath(endpoint?.path) && normalizeContractFile(endpoint?.file) && endpointFilter(endpoint, id, { method, path: input.path, changedFiles }))
|
|
51
|
-
.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));
|
|
52
|
-
if (filtered.length > endpointBudget) completeness.push(`${id}: endpoint cap reached`);
|
|
53
|
-
const accepted = filtered.slice(0, endpointBudget);
|
|
54
|
-
endpointBudget -= accepted.length;
|
|
55
|
-
backends.push({ id, endpoints: accepted, graph: descriptor.graph });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const clients = [];
|
|
59
|
-
for (let index = 0; index < clientDescriptors.length; index++) {
|
|
60
|
-
const descriptor = clientDescriptors[index] || {};
|
|
61
|
-
const id = safeContractName(descriptor.id, `client-${index + 1}`);
|
|
62
|
-
const detected = detectHttpClientCalls(descriptor.repoRoot, descriptorFiles(descriptor), {
|
|
63
|
-
maxFiles: limits.maxClientFiles,
|
|
64
|
-
maxCalls: limits.maxCallsPerClient,
|
|
65
|
-
clientNames: descriptor.clientNames || input.clientNames,
|
|
66
|
-
wrappers: descriptor.wrappers || input.wrappers,
|
|
67
|
-
autoDiscoverWrappers: descriptor.autoDiscoverWrappers ?? input.autoDiscoverWrappers,
|
|
68
|
-
graph: descriptor.graph,
|
|
69
|
-
includeTests: descriptor.includeTests ?? input.includeTests,
|
|
70
|
-
runtimeValues: descriptor.runtimeValues || input.runtimeValues,
|
|
71
|
-
});
|
|
72
|
-
if (detected.truncated) completeness.push(`${id}: client scan cap reached`);
|
|
73
|
-
for (const reason of detected.reasons || []) completeness.push(`${id}: ${reason}`);
|
|
74
|
-
clients.push({
|
|
75
|
-
id,
|
|
76
|
-
calls: detected.calls,
|
|
77
|
-
reverse: reverseRuntimeImports(descriptor.graph),
|
|
78
|
-
filesScanned: detected.filesScanned,
|
|
79
|
-
wrapperDiscovery: detected.discovery,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const results = [];
|
|
84
|
-
let matches = 0, methodMismatches = 0, callsiteCapReached = false;
|
|
85
|
-
for (const backend of backends) {
|
|
86
|
-
for (const endpoint of backend.endpoints) {
|
|
87
|
-
const callsites = [];
|
|
88
|
-
let endpointMismatches = 0;
|
|
89
|
-
const methodMismatchSites = [];
|
|
90
|
-
for (const client of clients) {
|
|
91
|
-
for (const call of client.calls) {
|
|
92
|
-
if (call.path && !methodMatches(endpoint.method, call.method)) {
|
|
93
|
-
const expected = pathSegments(normalizeHttpContractPath(endpoint.path));
|
|
94
|
-
const actual = pathSegments(call.path);
|
|
95
|
-
if (routeShapeMatches(expected, actual) || suffixShapeMatch(expected, actual)) {
|
|
96
|
-
methodMismatches++;
|
|
97
|
-
endpointMismatches++;
|
|
98
|
-
if (methodMismatchSites.length < 3) methodMismatchSites.push({ clientRepo: client.id, file: call.file, line: call.line, method: call.method });
|
|
99
|
-
}
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
const match = matchHttpContract(endpoint, call);
|
|
103
|
-
if (!match) continue;
|
|
104
|
-
if (matches >= limits.maxMatches || callsites.length >= limits.maxCallsitesPerEndpoint) {
|
|
105
|
-
callsiteCapReached = true;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
matches++;
|
|
109
|
-
callsites.push({
|
|
110
|
-
clientRepo: client.id,
|
|
111
|
-
file: call.file,
|
|
112
|
-
line: call.line,
|
|
113
|
-
method: call.method,
|
|
114
|
-
path: call.path,
|
|
115
|
-
dynamic: call.dynamic,
|
|
116
|
-
detector: call.detector,
|
|
117
|
-
wrapper: call.wrapper,
|
|
118
|
-
match,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
callsites.sort((left, right) => right.match.score - left.match.score || left.clientRepo.localeCompare(right.clientRepo) || left.file.localeCompare(right.file) || left.line - right.line);
|
|
123
|
-
const handlerEvidence = handlerNodeEvidence(endpoint, backend.graph);
|
|
124
|
-
results.push({
|
|
125
|
-
backend: backend.id,
|
|
126
|
-
method: endpoint.method,
|
|
127
|
-
path: endpoint.path,
|
|
128
|
-
normalizedPath: normalizeHttpContractPath(endpoint.path),
|
|
129
|
-
...handlerEvidence,
|
|
130
|
-
file: normalizeContractFile(endpoint.file) || null,
|
|
131
|
-
line: Number(endpoint.line) || null,
|
|
132
|
-
callsites,
|
|
133
|
-
methodMismatches: endpointMismatches,
|
|
134
|
-
methodMismatchSites,
|
|
135
|
-
liveness: externalUseLiveness(callsites, handlerEvidence),
|
|
136
|
-
affected: affectedForEndpoint(callsites, clients, limits),
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (callsiteCapReached) completeness.push("match or per-endpoint callsite cap reached");
|
|
141
|
-
|
|
142
|
-
const uncertainAll = clients.flatMap((client) => client.calls
|
|
143
|
-
.filter((call) => !call.path || call.unknownPrefix || call.partialDynamic || call.method === "UNKNOWN")
|
|
144
|
-
.map((call) => ({
|
|
145
|
-
clientRepo: client.id,
|
|
146
|
-
file: call.file,
|
|
147
|
-
line: call.line,
|
|
148
|
-
method: call.method,
|
|
149
|
-
reason: call.reason || "URL retains an unresolved dynamic component",
|
|
150
|
-
})))
|
|
151
|
-
.sort((left, right) => left.clientRepo.localeCompare(right.clientRepo) || left.file.localeCompare(right.file) || left.line - right.line);
|
|
152
|
-
if (uncertainAll.length > limits.maxUncertain) completeness.push("uncertain callsite cap reached");
|
|
153
|
-
if (uncertainAll.length) completeness.push(`${uncertainAll.length} dynamic HTTP callsite(s) remain UNKNOWN`);
|
|
154
|
-
if (results.some((endpoint) => !endpoint.affected.complete)) completeness.push("affected-file traversal cap reached");
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
httpContractsV: HTTP_CONTRACTS_V,
|
|
158
|
-
status: completeness.length ? "partial" : "complete",
|
|
159
|
-
filters: { method, path: input.path ? normalizeHttpContractPath(input.path) : null, changedFiles: [...changedFiles].sort() },
|
|
160
|
-
limits,
|
|
161
|
-
completeness: { complete: completeness.length === 0, reasons: [...new Set(completeness)] },
|
|
162
|
-
totals: {
|
|
163
|
-
backends: backends.length,
|
|
164
|
-
clients: clients.length,
|
|
165
|
-
endpoints: results.length,
|
|
166
|
-
clientCalls: clients.reduce((sum, client) => sum + client.calls.length, 0),
|
|
167
|
-
matches,
|
|
168
|
-
methodMismatches,
|
|
169
|
-
uncertainCalls: uncertainAll.length,
|
|
170
|
-
notDeadExternalUse: results.filter((endpoint) => endpoint.liveness.status === "NOT_DEAD_EXTERNAL_USE").length,
|
|
171
|
-
notDeadExternalHandlers: results.filter((endpoint) => endpoint.liveness.canSuppressDeadCandidate).length,
|
|
172
|
-
possibleExternalUse: results.filter((endpoint) => endpoint.liveness.status === "POSSIBLE_EXTERNAL_USE").length,
|
|
173
|
-
unknownLiveness: results.filter((endpoint) => endpoint.liveness.status === "UNKNOWN").length,
|
|
174
|
-
},
|
|
175
|
-
wrapperDiscovery: clients.map((client) => ({ clientRepo: client.id, ...client.wrapperDiscovery })),
|
|
176
|
-
endpoints: results,
|
|
177
|
-
uncertain: uncertainAll.slice(0, limits.maxUncertain),
|
|
178
|
-
};
|
|
179
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { createPathClassifier, hasPathClass } from "../../path-classification.js";
|
|
2
|
-
import { isWeavatrixIgnored, loadWeavatrixIgnore } from "../../path-ignore.js";
|
|
3
|
-
import { createRepoBoundary } from "../../repo-path.js";
|
|
4
|
-
import { safeRead } from "../../util.js";
|
|
5
|
-
import {
|
|
6
|
-
discoverHttpWrappers,
|
|
7
|
-
loadHttpContractConfig,
|
|
8
|
-
normalizeHttpClientNames,
|
|
9
|
-
normalizeHttpWrapperDescriptors,
|
|
10
|
-
} from "../http-contract-wrappers.js";
|
|
11
|
-
import { wrapperScopeFiles } from "./graph-context.js";
|
|
12
|
-
import { extractHttpClientCallsFromText } from "./client-call-parser.js";
|
|
13
|
-
import {
|
|
14
|
-
HTTP_CONTRACT_DEFAULTS,
|
|
15
|
-
HTTP_CONTRACT_HARD_LIMITS,
|
|
16
|
-
boundedInteger,
|
|
17
|
-
normalizeContractFile,
|
|
18
|
-
} from "./shared.js";
|
|
19
|
-
|
|
20
|
-
export function detectHttpClientCalls(repoRoot, codeFiles, options = {}) {
|
|
21
|
-
const boundary = createRepoBoundary(repoRoot);
|
|
22
|
-
if (!boundary.root) return { calls: [], truncated: false, filesScanned: 0, discovery: { enabled: false, configured: 0, discovered: 0, ambiguous: [] }, reasons: [] };
|
|
23
|
-
const maxFiles = boundedInteger(options.maxFiles, HTTP_CONTRACT_DEFAULTS.maxClientFiles, 1, HTTP_CONTRACT_HARD_LIMITS.maxClientFiles);
|
|
24
|
-
const maxCalls = boundedInteger(options.maxCalls, HTTP_CONTRACT_DEFAULTS.maxCallsPerClient, 1, HTTP_CONTRACT_HARD_LIMITS.maxCallsPerClient);
|
|
25
|
-
const ignoreRules = loadWeavatrixIgnore(boundary.root);
|
|
26
|
-
const classifier = createPathClassifier(boundary.root);
|
|
27
|
-
const candidates = [...new Set((codeFiles || []).map((entry) => normalizeContractFile(entry?.path || entry)).filter(Boolean))].sort();
|
|
28
|
-
let truncated = candidates.length > maxFiles;
|
|
29
|
-
let filesScanned = 0;
|
|
30
|
-
const sources = [], calls = [];
|
|
31
|
-
for (const file of candidates.slice(0, maxFiles)) {
|
|
32
|
-
if (!/\.(?:[cm]?[jt]sx?|vue|svelte)$/i.test(file) || isWeavatrixIgnored(file, ignoreRules)) continue;
|
|
33
|
-
const classification = classifier.explain(file, { content: "" });
|
|
34
|
-
if (classification.excluded || (!options.includeTests && hasPathClass(classification, "test", "e2e"))) continue;
|
|
35
|
-
const resolved = boundary.resolve(file);
|
|
36
|
-
if (!resolved.ok) continue;
|
|
37
|
-
const text = safeRead(resolved.path);
|
|
38
|
-
if (!text) continue;
|
|
39
|
-
filesScanned++;
|
|
40
|
-
sources.push({ file, text });
|
|
41
|
-
}
|
|
42
|
-
const config = loadHttpContractConfig(boundary.root);
|
|
43
|
-
const clientNames = [...new Set([...normalizeHttpClientNames(options.clientNames), ...config.clientNames])];
|
|
44
|
-
const configured = [...normalizeHttpWrapperDescriptors(options.wrappers, "input"), ...config.wrappers];
|
|
45
|
-
const discoveryEnabled = options.autoDiscoverWrappers !== false && config.autoDiscoverWrappers !== false;
|
|
46
|
-
const discovered = discoveryEnabled ? discoverHttpWrappers(sources, clientNames) : { wrappers: [], ambiguous: [], truncated: false };
|
|
47
|
-
const scopedDiscovered = discovered.wrappers.map((wrapper) => ({
|
|
48
|
-
...wrapper,
|
|
49
|
-
allowedFiles: wrapperScopeFiles(wrapper.definitionFile, options.graph),
|
|
50
|
-
}));
|
|
51
|
-
for (const { file, text } of sources) {
|
|
52
|
-
const remaining = maxCalls - calls.length;
|
|
53
|
-
if (remaining <= 0) { truncated = true; break; }
|
|
54
|
-
const extracted = extractHttpClientCallsFromText(text, file, {
|
|
55
|
-
clientNames,
|
|
56
|
-
normalizedWrappers: [...configured, ...scopedDiscovered],
|
|
57
|
-
maxCalls: remaining,
|
|
58
|
-
runtimeValues: options.runtimeValues,
|
|
59
|
-
});
|
|
60
|
-
calls.push(...extracted.calls);
|
|
61
|
-
if (extracted.truncated) truncated = true;
|
|
62
|
-
}
|
|
63
|
-
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)));
|
|
64
|
-
const reasons = [];
|
|
65
|
-
if (config.error) reasons.push(`HTTP contract config ${config.error}`);
|
|
66
|
-
reasons.push(...(config.warnings || []));
|
|
67
|
-
if (discovered.truncated) reasons.push("auto-discovered wrapper cap reached");
|
|
68
|
-
if (discovered.ambiguous.length) reasons.push(`${discovered.ambiguous.length} ambiguous auto-discovered wrapper name(s) skipped`);
|
|
69
|
-
return {
|
|
70
|
-
calls,
|
|
71
|
-
truncated,
|
|
72
|
-
filesScanned,
|
|
73
|
-
discovery: {
|
|
74
|
-
enabled: discoveryEnabled,
|
|
75
|
-
configured: configured.length,
|
|
76
|
-
discovered: scopedDiscovered.length,
|
|
77
|
-
ambiguous: discovered.ambiguous,
|
|
78
|
-
truncated: discovered.truncated,
|
|
79
|
-
},
|
|
80
|
-
reasons,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_HTTP_CLIENT_NAMES, normalizeHttpClientNames, normalizeHttpWrapperDescriptors } from "../http-contract-wrappers.js";
|
|
2
|
-
import {
|
|
3
|
-
HTTP_CONTRACT_DEFAULTS,
|
|
4
|
-
HTTP_CONTRACT_HARD_LIMITS,
|
|
5
|
-
boundedInteger,
|
|
6
|
-
contractLineAt,
|
|
7
|
-
normalizeContractFile,
|
|
8
|
-
normalizeHttpContractPath,
|
|
9
|
-
} from "./shared.js";
|
|
10
|
-
|
|
11
|
-
function maskNonCode(text) {
|
|
12
|
-
const chars = String(text || "").split("");
|
|
13
|
-
let index = 0;
|
|
14
|
-
while (index < chars.length) {
|
|
15
|
-
const char = chars[index], next = chars[index + 1];
|
|
16
|
-
if (char === "/" && next === "/") {
|
|
17
|
-
chars[index++] = " "; chars[index++] = " ";
|
|
18
|
-
while (index < chars.length && chars[index] !== "\n") chars[index++] = " ";
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
if (char === "/" && next === "*") {
|
|
22
|
-
chars[index++] = " "; chars[index++] = " ";
|
|
23
|
-
while (index < chars.length) {
|
|
24
|
-
if (chars[index] === "*" && chars[index + 1] === "/") { chars[index++] = " "; chars[index++] = " "; break; }
|
|
25
|
-
if (chars[index] !== "\n") chars[index] = " ";
|
|
26
|
-
index++;
|
|
27
|
-
}
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (char === "'" || char === '"' || char === "`") {
|
|
31
|
-
const quote = char;
|
|
32
|
-
chars[index++] = " ";
|
|
33
|
-
while (index < chars.length) {
|
|
34
|
-
if (chars[index] === "\\") {
|
|
35
|
-
chars[index++] = " ";
|
|
36
|
-
if (index < chars.length && chars[index] !== "\n") chars[index] = " ";
|
|
37
|
-
index++;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
const closes = chars[index] === quote;
|
|
41
|
-
if (chars[index] !== "\n") chars[index] = " ";
|
|
42
|
-
index++;
|
|
43
|
-
if (closes) break;
|
|
44
|
-
}
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
index++;
|
|
48
|
-
}
|
|
49
|
-
return chars.join("");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function quotedArgument(text, start, quote) {
|
|
53
|
-
let value = "";
|
|
54
|
-
for (let index = start + 1; index < text.length; index++) {
|
|
55
|
-
const char = text[index];
|
|
56
|
-
if (char === "\\") {
|
|
57
|
-
if (index + 1 >= text.length) break;
|
|
58
|
-
value += text[++index];
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (char === quote) return { value, endIndex: index + 1 };
|
|
62
|
-
if (char === "\n" || char === "\r") break;
|
|
63
|
-
value += char;
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function templateArgument(text, start, constants = null, requireStatic = false) {
|
|
69
|
-
let value = "", dynamicSegments = 0, unknownPrefix = false, partialDynamic = false;
|
|
70
|
-
for (let index = start + 1; index < text.length; index++) {
|
|
71
|
-
const char = text[index];
|
|
72
|
-
if (char === "\\") {
|
|
73
|
-
if (index + 1 >= text.length) break;
|
|
74
|
-
value += text[++index];
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (char === "`") return { value, endIndex: index + 1, dynamicSegments, unknownPrefix, partialDynamic };
|
|
78
|
-
if (char !== "$" || text[index + 1] !== "{") { value += char; continue; }
|
|
79
|
-
let cursor = index + 2, depth = 1, quote = null;
|
|
80
|
-
while (cursor < text.length && depth > 0) {
|
|
81
|
-
const token = text[cursor];
|
|
82
|
-
if (quote) {
|
|
83
|
-
if (token === "\\") cursor += 2;
|
|
84
|
-
else { if (token === quote) quote = null; cursor++; }
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (token === "'" || token === '"' || token === "`") { quote = token; cursor++; continue; }
|
|
88
|
-
if (token === "{") depth++;
|
|
89
|
-
else if (token === "}") depth--;
|
|
90
|
-
cursor++;
|
|
91
|
-
}
|
|
92
|
-
if (depth !== 0) return null;
|
|
93
|
-
const expression = text.slice(index + 2, cursor - 1).trim();
|
|
94
|
-
if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(expression) && constants?.has(expression)) {
|
|
95
|
-
value += constants.get(expression);
|
|
96
|
-
index = cursor - 1;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (requireStatic) return null;
|
|
100
|
-
const next = text[cursor];
|
|
101
|
-
const leftBoundary = value === "" || value.endsWith("/");
|
|
102
|
-
const rightBoundary = !next || next === "/" || next === "?" || next === "#" || next === "`";
|
|
103
|
-
dynamicSegments++;
|
|
104
|
-
if (value === "" && next === "/") unknownPrefix = true;
|
|
105
|
-
else if (leftBoundary && rightBoundary) value += ":param";
|
|
106
|
-
else { value += ":dynamic"; partialDynamic = true; }
|
|
107
|
-
index = cursor - 1;
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function extractStaticStringConstants(text, runtimeValues = {}) {
|
|
113
|
-
const source = String(text || "");
|
|
114
|
-
const mask = maskNonCode(source);
|
|
115
|
-
const declarations = [];
|
|
116
|
-
const declaration = /\bconst\s+([A-Za-z_$][\w$]*)\s*=/g;
|
|
117
|
-
let match;
|
|
118
|
-
while ((match = declaration.exec(mask)) && declarations.length < 500) {
|
|
119
|
-
let start = match.index + match[0].length;
|
|
120
|
-
while (/\s/.test(source[start] || "")) start++;
|
|
121
|
-
declarations.push({ name: match[1], start });
|
|
122
|
-
}
|
|
123
|
-
const constants = new Map(Object.entries(runtimeValues || {})
|
|
124
|
-
.filter(([key, value]) => /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(key) && typeof value === "string" && value.length <= 2_048));
|
|
125
|
-
for (const item of declarations) {
|
|
126
|
-
const quote = source[item.start];
|
|
127
|
-
if (quote !== "'" && quote !== '"') continue;
|
|
128
|
-
const parsed = quotedArgument(source, item.start, quote);
|
|
129
|
-
if (parsed && parsed.value.length <= 2_048) constants.set(item.name, parsed.value);
|
|
130
|
-
}
|
|
131
|
-
for (let pass = 0; pass < Math.min(8, declarations.length); pass++) {
|
|
132
|
-
let changed = false;
|
|
133
|
-
for (const item of declarations) {
|
|
134
|
-
if (constants.has(item.name) || source[item.start] !== "`") continue;
|
|
135
|
-
const parsed = templateArgument(source, item.start, constants, true);
|
|
136
|
-
if (!parsed || parsed.value.length > 2_048) continue;
|
|
137
|
-
constants.set(item.name, parsed.value);
|
|
138
|
-
changed = true;
|
|
139
|
-
}
|
|
140
|
-
if (!changed) break;
|
|
141
|
-
}
|
|
142
|
-
for (const item of declarations) {
|
|
143
|
-
if (constants.has(item.name)) continue;
|
|
144
|
-
const rest = source.slice(item.start);
|
|
145
|
-
const expression = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*/.exec(rest)?.[0];
|
|
146
|
-
if (expression && constants.has(expression)) constants.set(item.name, constants.get(expression));
|
|
147
|
-
if (constants.has(item.name)) continue;
|
|
148
|
-
const fallback = /(?:\|\||\?\?)\s*(["'])/.exec(rest.slice(0, 500));
|
|
149
|
-
if (fallback) {
|
|
150
|
-
const start = item.start + fallback.index + fallback[0].lastIndexOf(fallback[1]);
|
|
151
|
-
const parsed = quotedArgument(source, start, fallback[1]);
|
|
152
|
-
if (parsed && parsed.value.length <= 2_048) constants.set(item.name, parsed.value);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return constants;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function argumentStart(text, openParen, target) {
|
|
159
|
-
let current = 0, round = 0, square = 0, curly = 0;
|
|
160
|
-
for (let index = openParen + 1; index < text.length; index++) {
|
|
161
|
-
const char = text[index];
|
|
162
|
-
if (current === target && !/\s|,/.test(char)) return index;
|
|
163
|
-
if (char === "'" || char === '"' || char === "`") {
|
|
164
|
-
const quote = char;
|
|
165
|
-
for (index++; index < text.length; index++) {
|
|
166
|
-
if (text[index] === "\\") index++;
|
|
167
|
-
else if (text[index] === quote) break;
|
|
168
|
-
}
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
if (char === "(") round++;
|
|
172
|
-
else if (char === "[") square++;
|
|
173
|
-
else if (char === "{") curly++;
|
|
174
|
-
else if (char === ")") {
|
|
175
|
-
if (round === 0 && square === 0 && curly === 0) return null;
|
|
176
|
-
round--;
|
|
177
|
-
} else if (char === "]") square--;
|
|
178
|
-
else if (char === "}") curly--;
|
|
179
|
-
else if (char === "," && round === 0 && square === 0 && curly === 0) current++;
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function parseUrlArgument(text, openParen, constants, argument = 0) {
|
|
185
|
-
let start = argumentStart(text, openParen, argument);
|
|
186
|
-
if (start == null) return { path: null, endIndex: openParen, kind: "dynamic", dynamic: true, unknownPrefix: false, partialDynamic: true, reason: `URL argument ${argument} is missing` };
|
|
187
|
-
while (/\s/.test(text[start] || "")) start++;
|
|
188
|
-
const quote = text[start];
|
|
189
|
-
if (quote === "'" || quote === '"') {
|
|
190
|
-
const parsed = quotedArgument(text, start, quote);
|
|
191
|
-
if (!parsed) return { path: null, endIndex: start, kind: "dynamic", dynamic: true, reason: "unterminated URL literal" };
|
|
192
|
-
return { path: normalizeHttpContractPath(parsed.value), endIndex: parsed.endIndex, kind: "literal", dynamic: false, unknownPrefix: false, partialDynamic: false, reason: null };
|
|
193
|
-
}
|
|
194
|
-
if (quote === "`") {
|
|
195
|
-
const parsed = templateArgument(text, start, constants);
|
|
196
|
-
if (!parsed) return { path: null, endIndex: start, kind: "dynamic", dynamic: true, reason: "unterminated URL template" };
|
|
197
|
-
return {
|
|
198
|
-
path: normalizeHttpContractPath(parsed.value), endIndex: parsed.endIndex,
|
|
199
|
-
kind: parsed.dynamicSegments ? "template" : "literal", dynamic: parsed.dynamicSegments > 0,
|
|
200
|
-
unknownPrefix: parsed.unknownPrefix, partialDynamic: parsed.partialDynamic,
|
|
201
|
-
reason: parsed.partialDynamic ? "URL template contains a partial dynamic segment" : null,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
return { path: null, endIndex: start, kind: "dynamic", dynamic: true, unknownPrefix: false, partialDynamic: true, reason: "URL argument is not a string or template literal" };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function fetchMethod(text, argumentEnd) {
|
|
208
|
-
const tail = text.slice(argumentEnd, argumentEnd + 500);
|
|
209
|
-
if (!/^\s*,/.test(tail)) return { method: "GET", uncertain: false };
|
|
210
|
-
const config = tail.replace(/^\s*,\s*/, "");
|
|
211
|
-
if (!config.startsWith("{") || /^\{\s*\.\.\./.test(config)) return { method: "UNKNOWN", uncertain: true };
|
|
212
|
-
const literal = /\bmethod\s*:\s*(["'`])(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\1/i.exec(tail);
|
|
213
|
-
if (literal) return { method: literal[2].toUpperCase(), uncertain: false };
|
|
214
|
-
return /\bmethod\s*:/.test(tail) ? { method: "UNKNOWN", uncertain: true } : { method: "GET", uncertain: false };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function normalizedClientNames(values) {
|
|
218
|
-
return new Set([...DEFAULT_HTTP_CLIENT_NAMES, ...normalizeHttpClientNames(values)]
|
|
219
|
-
.map((value) => String(value || "").trim().toLowerCase())
|
|
220
|
-
.filter((value) => /^[a-z_$][\w$]*$/i.test(value)));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const escapeRegex = (value) => String(value).replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&");
|
|
224
|
-
|
|
225
|
-
export function extractHttpClientCallsFromText(text, file, options = {}) {
|
|
226
|
-
const source = String(text || "");
|
|
227
|
-
const mask = maskNonCode(source);
|
|
228
|
-
const constants = extractStaticStringConstants(source, options.runtimeValues);
|
|
229
|
-
const allowed = normalizedClientNames(options.clientNames);
|
|
230
|
-
const wrappers = normalizeHttpWrapperDescriptors(options.wrappers, "input").concat(Array.isArray(options.normalizedWrappers) ? options.normalizedWrappers : []);
|
|
231
|
-
const maxCalls = boundedInteger(options.maxCalls, HTTP_CONTRACT_DEFAULTS.maxCallsPerClient, 1, HTTP_CONTRACT_HARD_LIMITS.maxCallsPerClient);
|
|
232
|
-
const calls = [], seen = new Set();
|
|
233
|
-
let truncated = false;
|
|
234
|
-
const add = (clientName, method, openParen, isFetch = false, urlArgument = 0, detector = "builtin", wrapper = null) => {
|
|
235
|
-
const key = `${openParen}\0${method}\0${urlArgument}`;
|
|
236
|
-
if (seen.has(key)) return;
|
|
237
|
-
seen.add(key);
|
|
238
|
-
if (calls.length >= maxCalls) { truncated = true; return; }
|
|
239
|
-
const parsed = parseUrlArgument(source, openParen, constants, urlArgument);
|
|
240
|
-
const fetchInfo = isFetch ? fetchMethod(source, parsed.endIndex) : { method: method.toUpperCase(), uncertain: false };
|
|
241
|
-
calls.push({
|
|
242
|
-
file: normalizeContractFile(file), line: contractLineAt(source, openParen), client: clientName, method: fetchInfo.method,
|
|
243
|
-
path: parsed.path, kind: parsed.kind, dynamic: parsed.dynamic, unknownPrefix: Boolean(parsed.unknownPrefix),
|
|
244
|
-
partialDynamic: Boolean(parsed.partialDynamic || fetchInfo.uncertain),
|
|
245
|
-
reason: fetchInfo.uncertain ? "HTTP method is dynamic" : parsed.reason, detector, wrapper,
|
|
246
|
-
});
|
|
247
|
-
};
|
|
248
|
-
const member = /(^|[^\w$])([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*(get|post|put|patch|delete|head|options)\s*(?:<[^>\n]{1,200}>)?\s*\(/gim;
|
|
249
|
-
let match;
|
|
250
|
-
while ((match = member.exec(mask))) if (allowed.has(match[2].toLowerCase())) add(match[2], match[3], member.lastIndex - 1);
|
|
251
|
-
// Detect where the fetch Web API is invoked in the ANALYZED source. The token is assembled
|
|
252
|
-
// at runtime so this static-analysis detector carries no literal call-shape of its own
|
|
253
|
-
// (this module performs no network I/O itself; enforced by offline-artifact-boundary).
|
|
254
|
-
const FETCH_CLIENT = "fetch";
|
|
255
|
-
const fetchCall = new RegExp(`(^|[^\\w$])${FETCH_CLIENT}\\s*\\(`, "gim");
|
|
256
|
-
while ((match = fetchCall.exec(mask))) add(FETCH_CLIENT, "GET", fetchCall.lastIndex - 1, true);
|
|
257
|
-
for (const wrapper of wrappers) {
|
|
258
|
-
if (wrapper.allowedFiles instanceof Set && !wrapper.allowedFiles.has(normalizeContractFile(file))) continue;
|
|
259
|
-
if (wrapper.kind === "function") {
|
|
260
|
-
const bare = new RegExp(`(^|[^\\w$.?])${escapeRegex(wrapper.call)}\\s*(?:<[^>\\n]{1,200}>)?\\s*\\(`, "gim");
|
|
261
|
-
while ((match = bare.exec(mask))) {
|
|
262
|
-
const nameAt = bare.lastIndex - match[0].length + match[1].length;
|
|
263
|
-
if (/\bfunction\s*$/i.test(mask.slice(Math.max(0, nameAt - 30), nameAt))) continue;
|
|
264
|
-
add(wrapper.call, wrapper.method, bare.lastIndex - 1, false, wrapper.urlArgument, `${wrapper.source}-wrapper`, { kind: wrapper.kind, call: wrapper.call, definitionFile: wrapper.definitionFile || null });
|
|
265
|
-
}
|
|
266
|
-
} else if (wrapper.kind === "member") {
|
|
267
|
-
const memberCall = new RegExp(`(^|[^\\w$])${escapeRegex(wrapper.object)}\\s*(?:\\?\\.|\\.)\\s*${escapeRegex(wrapper.member)}\\s*(?:<[^>\\n]{1,200}>)?\\s*\\(`, "gim");
|
|
268
|
-
while ((match = memberCall.exec(mask))) add(`${wrapper.object}.${wrapper.member}`, wrapper.method, memberCall.lastIndex - 1, false, wrapper.urlArgument, `${wrapper.source}-wrapper`, { kind: wrapper.kind, object: wrapper.object, member: wrapper.member, definitionFile: wrapper.definitionFile || null });
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
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)));
|
|
272
|
-
return { calls, truncated };
|
|
273
|
-
}
|