weavatrix 0.3.13 → 1.0.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 +231 -199
- 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 +8 -65
- 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.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.js +0 -235
- 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 -62
- package/src/analysis/endpoints/extract.js +0 -107
- 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 -295
- 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 -154
- 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 -173
- 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 -174
- package/src/mcp/health/dead-code.mjs +0 -110
- package/src/mcp/health/duplicates.mjs +0 -85
- package/src/mcp/health/endpoints.mjs +0 -71
- package/src/mcp/health/structure.mjs +0 -219
- 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 -220
- 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,266 +0,0 @@
|
|
|
1
|
-
// Bounded local hot-path review over parser-derived complexity facts. This deliberately keeps
|
|
2
|
-
// local syntax cost, graph coupling and test evidence separate: it is a review queue, not a runtime
|
|
3
|
-
// profiler or an interprocedural Big-O proof.
|
|
4
|
-
import {readCoverageForRepo, normalizeRepoParts} from './coverage-reports.js'
|
|
5
|
-
import {computeStaticTestReachability} from './static-test-reachability.js'
|
|
6
|
-
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
7
|
-
import {isStructuralRelation} from '../graph/relations.js'
|
|
8
|
-
import {boundedInteger} from '../util.js'
|
|
9
|
-
|
|
10
|
-
const NON_PRODUCT = ['generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
11
|
-
const endpoint = (value) => String(value && typeof value === 'object' ? value.id : value || '')
|
|
12
|
-
const normalize = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
|
|
13
|
-
const boundedInt = boundedInteger
|
|
14
|
-
const round = (value) => Math.round(Number(value || 0) * 100) / 100
|
|
15
|
-
const clamp = (value, min = 0, max = 100) => Math.max(min, Math.min(max, Number(value) || 0))
|
|
16
|
-
|
|
17
|
-
function normalizeScope(value) {
|
|
18
|
-
const path = normalize(value)
|
|
19
|
-
if (!path) return {ok: true, path: ''}
|
|
20
|
-
if (path.includes('\0') || /^[a-z]:\//i.test(path) || path.startsWith('/') || path.split('/').includes('..')) {
|
|
21
|
-
return {ok: false, error: 'path must be repository-relative and cannot contain traversal segments'}
|
|
22
|
-
}
|
|
23
|
-
return {ok: true, path}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function coverageForRange(record, startLine, endLine) {
|
|
27
|
-
if (!record) return null
|
|
28
|
-
if (!(record.lines instanceof Map) || !record.lines.size) return Number.isFinite(record.pct) ? record.pct : null
|
|
29
|
-
let total = 0
|
|
30
|
-
let covered = 0
|
|
31
|
-
for (let line = Math.max(1, startLine); line <= Math.max(startLine, endLine); line++) {
|
|
32
|
-
if (!record.lines.has(line)) continue
|
|
33
|
-
total++
|
|
34
|
-
if (Number(record.lines.get(line)) > 0) covered++
|
|
35
|
-
}
|
|
36
|
-
return total ? covered / total : Number.isFinite(record.pct) ? record.pct : null
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function graphRiskBySymbol(graph, symbolIds, testCallerIds) {
|
|
40
|
-
const state = new Map([...symbolIds].map((id) => [id, {
|
|
41
|
-
incoming: 0, outgoing: 0, callers: new Set(), callees: new Set(),
|
|
42
|
-
}]))
|
|
43
|
-
for (const link of graph?.links || []) {
|
|
44
|
-
if (!link || link.typeOnly === true || link.compileOnly === true || link.barrelProxy === true || isStructuralRelation(link.relation)) continue
|
|
45
|
-
const source = endpoint(link.source)
|
|
46
|
-
const target = endpoint(link.target)
|
|
47
|
-
if (!source || !target || source === target) continue
|
|
48
|
-
if (state.has(source)) {
|
|
49
|
-
const item = state.get(source)
|
|
50
|
-
item.outgoing++
|
|
51
|
-
item.callees.add(target)
|
|
52
|
-
}
|
|
53
|
-
// A test caller (inline #[cfg(test)] symbol or a test-path file) must not inflate a production
|
|
54
|
-
// symbol's fan-in — the same reason test symbols are excluded as candidates. Skipped only for the
|
|
55
|
-
// incoming/coupling side; unless the caller opted tests in (testCallerIds is then null).
|
|
56
|
-
if (state.has(target) && !(testCallerIds && testCallerIds.has(source))) {
|
|
57
|
-
const item = state.get(target)
|
|
58
|
-
item.incoming++
|
|
59
|
-
item.callers.add(source)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return new Map([...state].map(([id, item]) => [id, {
|
|
63
|
-
fanIn: item.callers.size,
|
|
64
|
-
fanOut: item.callees.size,
|
|
65
|
-
incomingEdges: item.incoming,
|
|
66
|
-
outgoingEdges: item.outgoing,
|
|
67
|
-
}]))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function localSyntaxScore(complexity, thresholds) {
|
|
71
|
-
const insideLoop = Number(complexity.allocationsInLoops || 0)
|
|
72
|
-
+ Number(complexity.copiesInLoops || 0)
|
|
73
|
-
+ Number(complexity.linearOpsInLoops || 0)
|
|
74
|
-
+ Number(complexity.sortsInLoops || 0) * 2
|
|
75
|
-
+ Number(complexity.recursionInLoops || 0) * 2
|
|
76
|
-
const score = Number(complexity.timeRank || 0) * 12
|
|
77
|
-
+ Number(complexity.memoryRank || 0) * 5
|
|
78
|
-
+ Math.min(18, Number(complexity.cyclomatic || 1) / thresholds.cyclomatic * 10)
|
|
79
|
-
+ Math.min(12, Number(complexity.callCount || 0) / thresholds.calls * 6)
|
|
80
|
-
+ Math.min(18, Number(complexity.maxLoopDepth || 0) * 5)
|
|
81
|
-
+ Math.min(24, insideLoop * 5)
|
|
82
|
-
+ (complexity.recursion ? 6 : 0)
|
|
83
|
-
return round(clamp(score))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function couplingScore(risk) {
|
|
87
|
-
return round(clamp(Math.log2(1 + risk.fanIn) * 20 + Math.log2(1 + risk.fanOut) * 10))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function reasonsFor(complexity, thresholds) {
|
|
91
|
-
const reasons = []
|
|
92
|
-
if (Number(complexity.timeRank || 0) >= thresholds.timeRank) reasons.push(String(complexity.timeLabel || `local time rank ${complexity.timeRank}`))
|
|
93
|
-
if (Number(complexity.cyclomatic || 0) >= thresholds.cyclomatic) reasons.push(`cyclomatic ${complexity.cyclomatic} >= ${thresholds.cyclomatic}`)
|
|
94
|
-
if (Number(complexity.callCount || 0) >= thresholds.calls) reasons.push(`${complexity.callCount} local call sites >= ${thresholds.calls}`)
|
|
95
|
-
if (Number(complexity.maxLoopDepth || 0) >= thresholds.loopDepth) reasons.push(`loop depth ${complexity.maxLoopDepth} >= ${thresholds.loopDepth}`)
|
|
96
|
-
if (complexity.allocationsInLoops) reasons.push(`${complexity.allocationsInLoops} allocation(s) inside iteration`)
|
|
97
|
-
if (complexity.copiesInLoops) reasons.push(`${complexity.copiesInLoops} copy/copies inside iteration`)
|
|
98
|
-
if (complexity.linearOpsInLoops) reasons.push(`${complexity.linearOpsInLoops} linear operation(s) inside iteration`)
|
|
99
|
-
if (complexity.sortsInLoops) reasons.push(`${complexity.sortsInLoops} sort(s) inside iteration`)
|
|
100
|
-
if (complexity.recursionInLoops) reasons.push(`${complexity.recursionInLoops} recursive call(s) inside iteration`)
|
|
101
|
-
if (complexity.recursion) reasons.push('direct recursion; bound remains unknown')
|
|
102
|
-
return [...new Set(reasons)].slice(0, 12)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function computeHotPathReview(graph, options = {}) {
|
|
106
|
-
const scope = normalizeScope(options.path)
|
|
107
|
-
if (!scope.ok) return {ok: false, error: scope.error}
|
|
108
|
-
const thresholds = {
|
|
109
|
-
cyclomatic: boundedInt(options.cyclomaticThreshold, 8, 2, 1000),
|
|
110
|
-
calls: boundedInt(options.callThreshold, 12, 1, 10000),
|
|
111
|
-
loopDepth: boundedInt(options.loopDepthThreshold, 2, 1, 10),
|
|
112
|
-
timeRank: boundedInt(options.timeRankThreshold, 2, 0, 5),
|
|
113
|
-
minScore: boundedInt(options.minScore, 85, 0, 100),
|
|
114
|
-
}
|
|
115
|
-
const defaultFocus = options.minScore == null
|
|
116
|
-
const topN = boundedInt(options.topN, 20, 1, 100)
|
|
117
|
-
const classifier = createPathClassifier(options.repoRoot || null)
|
|
118
|
-
const knownFiles = [...new Set((graph?.nodes || []).map((node) => normalize(node?.source_file)).filter(Boolean))]
|
|
119
|
-
const measuredCoverage = options.repoRoot ? readCoverageForRepo(options.repoRoot, knownFiles) : new Map()
|
|
120
|
-
const coverageSources = [...new Set([...measuredCoverage.values()].map((item) => item?.source).filter(Boolean))].sort()
|
|
121
|
-
const staticTests = measuredCoverage.size || !options.repoRoot
|
|
122
|
-
? null
|
|
123
|
-
: computeStaticTestReachability(graph, {repoRoot: options.repoRoot, path: scope.path})
|
|
124
|
-
const staticByFile = new Map((staticTests?.reachable || []).map((item) => [normalize(item.file), item]))
|
|
125
|
-
|
|
126
|
-
const eligible = []
|
|
127
|
-
const excluded = {tests: 0, classified: 0, outOfScope: 0}
|
|
128
|
-
for (const node of graph?.nodes || []) {
|
|
129
|
-
const complexity = node?.complexity
|
|
130
|
-
const file = normalize(node?.source_file)
|
|
131
|
-
if (!complexity || !file || !String(node?.id || '').includes('#')) continue
|
|
132
|
-
if (scope.path && file !== scope.path && !file.startsWith(`${scope.path}/`)) { excluded.outOfScope++; continue }
|
|
133
|
-
const classification = classifier.explain(file)
|
|
134
|
-
const test = node?.test_surface === true || hasPathClass(classification, 'test', 'e2e')
|
|
135
|
-
const classified = classification.excluded || hasPathClass(classification, ...NON_PRODUCT)
|
|
136
|
-
if (test && options.includeTests !== true) { excluded.tests++; continue }
|
|
137
|
-
if (classified && options.includeClassified !== true) { excluded.classified++; continue }
|
|
138
|
-
eligible.push({node, complexity, file, classification: test ? 'test' : classified ? 'classified' : 'production'})
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Test callers (inline test_surface symbols and test-path files) that must not count toward a
|
|
142
|
-
// production symbol's fan-in. Built over ALL nodes because a caller may sit outside the review scope.
|
|
143
|
-
const testCallerIds = options.includeTests === true ? null : (() => {
|
|
144
|
-
const ids = new Set()
|
|
145
|
-
const classifyCache = new Map()
|
|
146
|
-
for (const node of graph?.nodes || []) {
|
|
147
|
-
const id = String(node?.id || '')
|
|
148
|
-
if (!id.includes('#')) continue
|
|
149
|
-
let isTest = node?.test_surface === true
|
|
150
|
-
const file = normalize(node?.source_file)
|
|
151
|
-
if (!isTest && file) {
|
|
152
|
-
let info = classifyCache.get(file)
|
|
153
|
-
if (info === undefined) classifyCache.set(file, (info = classifier.explain(file)))
|
|
154
|
-
isTest = hasPathClass(info, 'test', 'e2e')
|
|
155
|
-
}
|
|
156
|
-
if (isTest) ids.add(id)
|
|
157
|
-
}
|
|
158
|
-
return ids
|
|
159
|
-
})()
|
|
160
|
-
const riskById = graphRiskBySymbol(graph, new Set(eligible.map(({node}) => String(node.id))), testCallerIds)
|
|
161
|
-
const candidates = []
|
|
162
|
-
for (const entry of eligible) {
|
|
163
|
-
const {node, complexity, file} = entry
|
|
164
|
-
const reasons = reasonsFor(complexity, thresholds)
|
|
165
|
-
if (!reasons.length) continue
|
|
166
|
-
const risk = riskById.get(String(node.id)) || {fanIn: 0, fanOut: 0, incomingEdges: 0, outgoingEdges: 0}
|
|
167
|
-
const syntaxScore = localSyntaxScore(complexity, thresholds)
|
|
168
|
-
const graphScore = couplingScore(risk)
|
|
169
|
-
const startLine = Number(complexity.startLine) || Number(String(node.source_location || '').replace(/^L/, '')) || 0
|
|
170
|
-
const endLine = Number(complexity.endLine) || Number(String(node.source_end || '').replace(/^L/, '')) || startLine
|
|
171
|
-
const coverageRecord = measuredCoverage.get(normalizeRepoParts(file)) || null
|
|
172
|
-
const actualCoverage = coverageForRange(coverageRecord, startLine, endLine)
|
|
173
|
-
const nearest = staticByFile.get(file)?.nearestTests?.[0] || null
|
|
174
|
-
const coverageRisk = actualCoverage == null ? 0 : (1 - actualCoverage) * 100
|
|
175
|
-
const score = round(clamp(syntaxScore * 0.72 + graphScore * 0.2 + coverageRisk * 0.08))
|
|
176
|
-
const directHotEvidence = Array.isArray(complexity.hotEvidence) ? complexity.hotEvidence.slice(0, 12) : []
|
|
177
|
-
// The default queue is intentionally narrow. A small, locally expensive function can still be
|
|
178
|
-
// important even with little graph fan-in, so retain only a bounded strong-local fallback. An
|
|
179
|
-
// explicit minScore disables this fallback and gives the caller a strict numeric gate.
|
|
180
|
-
const bodyLines = startLine > 0 && endLine >= startLine ? endLine - startLine + 1 : Number.POSITIVE_INFINITY
|
|
181
|
-
const strongLocalEvidence = defaultFocus && directHotEvidence.length > 0 && bodyLines <= 80 && (
|
|
182
|
-
Number(complexity.recursionInLoops || 0) > 0
|
|
183
|
-
|| (bodyLines <= 40 && (
|
|
184
|
-
Number(complexity.sortsInLoops || 0) > 0
|
|
185
|
-
|| (Number(complexity.timeRank || 0) >= 4 && Number(complexity.maxLoopDepth || 0) >= 2)
|
|
186
|
-
))
|
|
187
|
-
)
|
|
188
|
-
if (score < thresholds.minScore && !strongLocalEvidence) continue
|
|
189
|
-
const confidence = directHotEvidence.length ? 'HIGH' : complexity.recursion ? 'LOW' : 'MEDIUM'
|
|
190
|
-
candidates.push({
|
|
191
|
-
id: String(node.id),
|
|
192
|
-
label: String(node.label || node.norm_label || node.id),
|
|
193
|
-
kind: String(node.symbol_kind || 'symbol'),
|
|
194
|
-
file,
|
|
195
|
-
startLine,
|
|
196
|
-
endLine,
|
|
197
|
-
classification: entry.classification,
|
|
198
|
-
score,
|
|
199
|
-
selection: score >= thresholds.minScore ? 'SCORE_THRESHOLD' : 'STRONG_LOCAL_EVIDENCE',
|
|
200
|
-
confidence,
|
|
201
|
-
localSyntax: {
|
|
202
|
-
score: syntaxScore,
|
|
203
|
-
timeRank: Number(complexity.timeRank || 0),
|
|
204
|
-
timeLabel: String(complexity.timeLabel || ''),
|
|
205
|
-
memoryRank: Number(complexity.memoryRank || 0),
|
|
206
|
-
memoryLabel: String(complexity.memoryLabel || ''),
|
|
207
|
-
cyclomatic: Number(complexity.cyclomatic || 0),
|
|
208
|
-
calls: Number(complexity.callCount || 0),
|
|
209
|
-
loops: Number(complexity.loops || 0),
|
|
210
|
-
maxLoopDepth: Number(complexity.maxLoopDepth || 0),
|
|
211
|
-
allocationsInLoops: Number(complexity.allocationsInLoops || 0),
|
|
212
|
-
copiesInLoops: Number(complexity.copiesInLoops || 0),
|
|
213
|
-
linearOpsInLoops: Number(complexity.linearOpsInLoops || 0),
|
|
214
|
-
sortsInLoops: Number(complexity.sortsInLoops || 0),
|
|
215
|
-
recursionInLoops: Number(complexity.recursionInLoops || 0),
|
|
216
|
-
recursion: complexity.recursion === true,
|
|
217
|
-
},
|
|
218
|
-
graphRisk: {...risk, score: graphScore},
|
|
219
|
-
testEvidence: actualCoverage != null
|
|
220
|
-
? {actualCoverage, source: coverageRecord?.source || 'coverage report'}
|
|
221
|
-
: nearest
|
|
222
|
-
? {actualCoverage: 'NOT_AVAILABLE', staticReachable: true, nearestTest: nearest.test, distance: nearest.distance, confidence: nearest.confidence}
|
|
223
|
-
: {actualCoverage: 'NOT_AVAILABLE', staticReachable: false},
|
|
224
|
-
reasons,
|
|
225
|
-
sourceEvidence: directHotEvidence,
|
|
226
|
-
})
|
|
227
|
-
}
|
|
228
|
-
candidates.sort((left, right) => right.score - left.score
|
|
229
|
-
|| right.localSyntax.score - left.localSyntax.score
|
|
230
|
-
|| right.graphRisk.fanIn - left.graphRisk.fanIn
|
|
231
|
-
|| left.id.localeCompare(right.id))
|
|
232
|
-
const hotspots = candidates.slice(0, topN)
|
|
233
|
-
return {
|
|
234
|
-
ok: true,
|
|
235
|
-
modelVersion: 1,
|
|
236
|
-
complexityVersion: Number(graph?.complexityV) || 0,
|
|
237
|
-
scope: {
|
|
238
|
-
path: scope.path || null,
|
|
239
|
-
includeTests: options.includeTests === true,
|
|
240
|
-
includeClassified: options.includeClassified === true,
|
|
241
|
-
},
|
|
242
|
-
thresholds,
|
|
243
|
-
selectionPolicy: {
|
|
244
|
-
mode: defaultFocus ? 'FOCUSED_DEFAULT' : 'EXPLICIT_SCORE_THRESHOLD',
|
|
245
|
-
strongLocalFallback: defaultFocus,
|
|
246
|
-
broadenWith: 'Set min_score lower (0 restores the full diagnostic candidate set).',
|
|
247
|
-
},
|
|
248
|
-
analyzedSymbols: eligible.length,
|
|
249
|
-
candidateSymbols: candidates.length,
|
|
250
|
-
coverage: measuredCoverage.size
|
|
251
|
-
? {actualCoverage: 'AVAILABLE', measuredFiles: measuredCoverage.size, sources: coverageSources}
|
|
252
|
-
: {actualCoverage: 'NOT_AVAILABLE', staticReachability: staticTests ? {
|
|
253
|
-
reachableFiles: staticTests.reachableFiles,
|
|
254
|
-
productFiles: staticTests.productFiles,
|
|
255
|
-
truncated: staticTests.bounds.truncated,
|
|
256
|
-
} : null},
|
|
257
|
-
excluded,
|
|
258
|
-
hotspots,
|
|
259
|
-
bounds: {topN, returned: hotspots.length, totalCandidates: candidates.length, truncated: candidates.length > hotspots.length},
|
|
260
|
-
caveats: [
|
|
261
|
-
'Scores rank parser-derived local syntax cost; they are not profiler measurements.',
|
|
262
|
-
'Graph risk is reported separately and does not propagate loop complexity through callees.',
|
|
263
|
-
'Recursion bounds, CFG reachability, dead stores and taint flow are not inferred by this model.',
|
|
264
|
-
],
|
|
265
|
-
}
|
|
266
|
-
}
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
// Bounded HTTP-wrapper configuration and conservative auto-discovery. A wrapper is accepted only
|
|
2
|
-
// when its URL position and HTTP method are statically known; no source is evaluated.
|
|
3
|
-
import { statSync } from "node:fs";
|
|
4
|
-
import { createRepoBoundary } from "../repo-path.js";
|
|
5
|
-
import { safeRead } from "../util.js";
|
|
6
|
-
|
|
7
|
-
export const DEFAULT_HTTP_CLIENT_NAMES = Object.freeze([
|
|
8
|
-
"axios", "http", "https", "$http", "httpClient", "apiClient", "restClient",
|
|
9
|
-
]);
|
|
10
|
-
|
|
11
|
-
const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
12
|
-
const MAX_CONFIG_BYTES = 64 * 1024;
|
|
13
|
-
const MAX_WRAPPERS = 100;
|
|
14
|
-
const MAX_CLIENT_NAMES = 40;
|
|
15
|
-
const MAX_DISCOVERY_BODY = 2_000;
|
|
16
|
-
const identifier = (value) => {
|
|
17
|
-
const text = String(value || "").trim();
|
|
18
|
-
return /^[A-Za-z_$][\w$]{0,127}$/.test(text) ? text : null;
|
|
19
|
-
};
|
|
20
|
-
const method = (value) => {
|
|
21
|
-
const text = String(value || "").trim().toUpperCase();
|
|
22
|
-
return HTTP_METHODS.has(text) ? text : null;
|
|
23
|
-
};
|
|
24
|
-
const argumentIndex = (value) => {
|
|
25
|
-
const number = Number(value ?? 0);
|
|
26
|
-
return Number.isInteger(number) && number >= 0 && number <= 5 ? number : null;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export function normalizeHttpClientNames(values) {
|
|
30
|
-
return [...new Set((Array.isArray(values) ? values : [])
|
|
31
|
-
.slice(0, MAX_CLIENT_NAMES)
|
|
32
|
-
.map(identifier)
|
|
33
|
-
.filter(Boolean))];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function normalizeHttpWrapperDescriptors(values, source = "input") {
|
|
37
|
-
const wrappers = [];
|
|
38
|
-
for (const raw of (Array.isArray(values) ? values : []).slice(0, MAX_WRAPPERS)) {
|
|
39
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
40
|
-
const urlArgument = argumentIndex(raw.urlArgument ?? raw.url_argument);
|
|
41
|
-
const fixedMethod = method(raw.method);
|
|
42
|
-
if (urlArgument == null || !fixedMethod) continue;
|
|
43
|
-
const call = identifier(raw.call);
|
|
44
|
-
const object = identifier(raw.object);
|
|
45
|
-
const member = identifier(raw.member);
|
|
46
|
-
if (call && !object && !member) {
|
|
47
|
-
wrappers.push({ kind: "function", call, method: fixedMethod, urlArgument, source });
|
|
48
|
-
} else if (!call && object && member) {
|
|
49
|
-
wrappers.push({ kind: "member", object, member, method: fixedMethod, urlArgument, source });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const seen = new Set();
|
|
53
|
-
return wrappers.filter((wrapper) => {
|
|
54
|
-
const key = `${wrapper.kind}\0${wrapper.call || wrapper.object}\0${wrapper.member || ""}\0${wrapper.method}\0${wrapper.urlArgument}`;
|
|
55
|
-
if (seen.has(key)) return false;
|
|
56
|
-
seen.add(key);
|
|
57
|
-
return true;
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function loadHttpContractConfig(repoRoot) {
|
|
62
|
-
const empty = { clientNames: [], wrappers: [], autoDiscoverWrappers: true, loaded: false, warnings: [] };
|
|
63
|
-
const resolved = createRepoBoundary(repoRoot).resolve(".weavatrix.json");
|
|
64
|
-
if (!resolved.ok) return empty;
|
|
65
|
-
try {
|
|
66
|
-
if (statSync(resolved.path).size > MAX_CONFIG_BYTES) return { ...empty, error: "config-too-large" };
|
|
67
|
-
const raw = JSON.parse(safeRead(resolved.path));
|
|
68
|
-
const config = raw?.httpContracts;
|
|
69
|
-
if (config == null) return { ...empty, loaded: true };
|
|
70
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) return { ...empty, error: "invalid-http-contract-config" };
|
|
71
|
-
const rawNames = Array.isArray(config.clientNames) ? config.clientNames : [];
|
|
72
|
-
const rawWrappers = Array.isArray(config.wrappers) ? config.wrappers : [];
|
|
73
|
-
const clientNames = normalizeHttpClientNames(rawNames);
|
|
74
|
-
const wrappers = normalizeHttpWrapperDescriptors(rawWrappers, "config");
|
|
75
|
-
const warnings = [];
|
|
76
|
-
if (config.clientNames != null && !Array.isArray(config.clientNames)) warnings.push("httpContracts.clientNames must be an array");
|
|
77
|
-
if (rawNames.length > MAX_CLIENT_NAMES) warnings.push("httpContracts.clientNames cap reached");
|
|
78
|
-
if (clientNames.length < Math.min(rawNames.length, MAX_CLIENT_NAMES)) warnings.push("invalid httpContracts.clientNames entries skipped");
|
|
79
|
-
if (config.wrappers != null && !Array.isArray(config.wrappers)) warnings.push("httpContracts.wrappers must be an array");
|
|
80
|
-
if (rawWrappers.length > MAX_WRAPPERS) warnings.push("httpContracts.wrappers cap reached");
|
|
81
|
-
if (wrappers.length < Math.min(rawWrappers.length, MAX_WRAPPERS)) warnings.push("invalid or duplicate httpContracts.wrappers entries skipped");
|
|
82
|
-
if (config.autoDiscoverWrappers != null && typeof config.autoDiscoverWrappers !== "boolean") warnings.push("httpContracts.autoDiscoverWrappers must be boolean");
|
|
83
|
-
return {
|
|
84
|
-
clientNames,
|
|
85
|
-
wrappers,
|
|
86
|
-
autoDiscoverWrappers: config.autoDiscoverWrappers !== false,
|
|
87
|
-
loaded: true,
|
|
88
|
-
warnings,
|
|
89
|
-
};
|
|
90
|
-
} catch {
|
|
91
|
-
return { ...empty, error: "invalid-config" };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Masks comments and strings while retaining indices and newlines. Wrapper discovery needs only
|
|
96
|
-
// declarations and a direct `client.verb(urlParam)` delegation, never literal contents.
|
|
97
|
-
function codeMask(text) {
|
|
98
|
-
const chars = String(text || "").split("");
|
|
99
|
-
let index = 0;
|
|
100
|
-
while (index < chars.length) {
|
|
101
|
-
const char = chars[index], next = chars[index + 1];
|
|
102
|
-
if (char === "/" && next === "/") {
|
|
103
|
-
chars[index++] = " "; chars[index++] = " ";
|
|
104
|
-
while (index < chars.length && chars[index] !== "\n") chars[index++] = " ";
|
|
105
|
-
} else if (char === "/" && next === "*") {
|
|
106
|
-
chars[index++] = " "; chars[index++] = " ";
|
|
107
|
-
while (index < chars.length) {
|
|
108
|
-
if (chars[index] === "*" && chars[index + 1] === "/") { chars[index++] = " "; chars[index++] = " "; break; }
|
|
109
|
-
if (chars[index] !== "\n") chars[index] = " ";
|
|
110
|
-
index += 1;
|
|
111
|
-
}
|
|
112
|
-
} else if (char === "'" || char === '"' || char === "`") {
|
|
113
|
-
const quote = char;
|
|
114
|
-
chars[index++] = " ";
|
|
115
|
-
while (index < chars.length) {
|
|
116
|
-
if (chars[index] === "\\") {
|
|
117
|
-
chars[index++] = " ";
|
|
118
|
-
if (index < chars.length && chars[index] !== "\n") chars[index] = " ";
|
|
119
|
-
index += 1;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const closes = chars[index] === quote;
|
|
123
|
-
if (chars[index] !== "\n") chars[index] = " ";
|
|
124
|
-
index += 1;
|
|
125
|
-
if (closes) break;
|
|
126
|
-
}
|
|
127
|
-
} else index += 1;
|
|
128
|
-
}
|
|
129
|
-
return chars.join("");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function matchingBrace(mask, open) {
|
|
133
|
-
let depth = 0;
|
|
134
|
-
for (let index = open; index < Math.min(mask.length, open + MAX_DISCOVERY_BODY); index += 1) {
|
|
135
|
-
if (mask[index] === "{") depth += 1;
|
|
136
|
-
else if (mask[index] === "}" && --depth === 0) return index + 1;
|
|
137
|
-
}
|
|
138
|
-
return Math.min(mask.length, open + MAX_DISCOVERY_BODY);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function parameterNames(raw) {
|
|
142
|
-
const parts = [];
|
|
143
|
-
let start = 0, round = 0, square = 0, curly = 0, angle = 0;
|
|
144
|
-
const text = String(raw || "");
|
|
145
|
-
for (let index = 0; index <= text.length; index += 1) {
|
|
146
|
-
const char = text[index];
|
|
147
|
-
if (char === "(") round += 1;
|
|
148
|
-
else if (char === ")") round -= 1;
|
|
149
|
-
else if (char === "[") square += 1;
|
|
150
|
-
else if (char === "]") square -= 1;
|
|
151
|
-
else if (char === "{") curly += 1;
|
|
152
|
-
else if (char === "}") curly -= 1;
|
|
153
|
-
else if (char === "<") angle += 1;
|
|
154
|
-
else if (char === ">") angle = Math.max(0, angle - 1);
|
|
155
|
-
if ((char === "," && round === 0 && square === 0 && curly === 0 && angle === 0) || index === text.length) {
|
|
156
|
-
parts.push(text.slice(start, index));
|
|
157
|
-
start = index + 1;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const names = [];
|
|
161
|
-
for (const part of parts.slice(0, 6)) {
|
|
162
|
-
const match = /^\s*([A-Za-z_$][\w$]*)(?=\s*(?:[?:=]|$))/.exec(part);
|
|
163
|
-
if (!match) return [];
|
|
164
|
-
names.push(match[1]);
|
|
165
|
-
}
|
|
166
|
-
return names;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function escaped(value) {
|
|
170
|
-
return String(value).replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function delegatedWrapper(mask, definition, clientNames) {
|
|
174
|
-
const body = mask.slice(definition.bodyStart, definition.bodyEnd);
|
|
175
|
-
const clients = clientNames.map(escaped).join("|");
|
|
176
|
-
if (!clients) return null;
|
|
177
|
-
const delegate = new RegExp(`\\b(?:${clients})\\s*(?:\\?\\.|\\.)\\s*(get|post|put|patch|delete|head|options)\\s*(?:<[^>\\n]{1,200}>)?\\s*\\(\\s*([A-Za-z_$][\\w$]*)\\s*(?=[,)])`, "gi");
|
|
178
|
-
const matches = [];
|
|
179
|
-
let found;
|
|
180
|
-
while ((found = delegate.exec(body)) && matches.length < 3) {
|
|
181
|
-
const urlArgument = definition.parameters.indexOf(found[2]);
|
|
182
|
-
if (urlArgument >= 0) matches.push({ method: found[1].toUpperCase(), urlArgument });
|
|
183
|
-
}
|
|
184
|
-
// Common typed clients keep response/error handling in one helper and pass the concrete
|
|
185
|
-
// axios/http method plus its argument array: api(axios.get, [url, options]). The HTTP method and
|
|
186
|
-
// first transport argument are still statically fixed, so this is as deterministic as a direct
|
|
187
|
-
// axios.get(url) delegation without evaluating the helper.
|
|
188
|
-
const methodReference = new RegExp(`\\b[A-Za-z_$][\\w$]*\\s*\\(\\s*(?:${clients})\\s*(?:\\?\\.|\\.)\\s*(get|post|put|patch|delete|head|options)\\s*,\\s*\\[\\s*([A-Za-z_$][\\w$]*)\\s*(?=[,\\]])`, "gi");
|
|
189
|
-
while ((found = methodReference.exec(body)) && matches.length < 3) {
|
|
190
|
-
const urlArgument = definition.parameters.indexOf(found[2]);
|
|
191
|
-
if (urlArgument >= 0) matches.push({ method: found[1].toUpperCase(), urlArgument });
|
|
192
|
-
}
|
|
193
|
-
const unique = [...new Map(matches.map((item) => [`${item.method}\0${item.urlArgument}`, item])).values()];
|
|
194
|
-
return unique.length === 1 ? unique[0] : null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function discoverHttpWrappers(sources, clientNames = []) {
|
|
198
|
-
const allowedClients = [...new Set([...DEFAULT_HTTP_CLIENT_NAMES, ...normalizeHttpClientNames(clientNames)])];
|
|
199
|
-
const candidates = [];
|
|
200
|
-
let truncated = false;
|
|
201
|
-
for (const source of Array.isArray(sources) ? sources : []) {
|
|
202
|
-
if (candidates.length >= MAX_WRAPPERS) { truncated = true; break; }
|
|
203
|
-
const mask = codeMask(source.text);
|
|
204
|
-
const definitions = [];
|
|
205
|
-
const functions = /\b(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]{0,300})\)\s*(?::[^\n{]{1,200})?\s*\{/g;
|
|
206
|
-
let match;
|
|
207
|
-
while ((match = functions.exec(mask)) && definitions.length < MAX_WRAPPERS) {
|
|
208
|
-
const open = functions.lastIndex - 1;
|
|
209
|
-
definitions.push({ call: match[1], parameters: parameterNames(match[2]), bodyStart: open + 1, bodyEnd: matchingBrace(mask, open) - 1 });
|
|
210
|
-
}
|
|
211
|
-
const arrows = /\b(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:<[^>\n]{1,200}>\s*)?(?:\(([^)]{0,300})\)\s*(?::[^\n=]{1,200})?|([A-Za-z_$][\w$]*))\s*=>\s*/g;
|
|
212
|
-
while ((match = arrows.exec(mask)) && definitions.length < MAX_WRAPPERS) {
|
|
213
|
-
const parameters = parameterNames(match[2] ?? match[3]);
|
|
214
|
-
const open = mask.indexOf("{", arrows.lastIndex);
|
|
215
|
-
const block = open >= 0 && /^\s*\{/.test(mask.slice(arrows.lastIndex, open + 1));
|
|
216
|
-
const bodyStart = block ? open + 1 : arrows.lastIndex;
|
|
217
|
-
const bodyEnd = block ? matchingBrace(mask, open) - 1 : Math.min(mask.length, bodyStart + MAX_DISCOVERY_BODY, mask.indexOf(";", bodyStart) >= 0 ? mask.indexOf(";", bodyStart) : mask.length);
|
|
218
|
-
definitions.push({ call: match[1], parameters, bodyStart, bodyEnd });
|
|
219
|
-
}
|
|
220
|
-
for (const definition of definitions) {
|
|
221
|
-
if (!definition.parameters.length) continue;
|
|
222
|
-
const delegated = delegatedWrapper(mask, definition, allowedClients);
|
|
223
|
-
if (delegated && candidates.length < MAX_WRAPPERS) candidates.push({ kind: "function", call: definition.call, ...delegated, source: "auto", definitionFile: source.file });
|
|
224
|
-
else if (delegated) truncated = true;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const byCall = new Map();
|
|
229
|
-
for (const candidate of candidates) {
|
|
230
|
-
const entries = byCall.get(candidate.call) || [];
|
|
231
|
-
entries.push(candidate);
|
|
232
|
-
byCall.set(candidate.call, entries);
|
|
233
|
-
}
|
|
234
|
-
const wrappers = [], ambiguous = [];
|
|
235
|
-
for (const [call, entries] of [...byCall.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
236
|
-
const unique = [...new Map(entries.map((entry) => [`${entry.method}\0${entry.urlArgument}\0${entry.definitionFile}`, entry])).values()];
|
|
237
|
-
if (unique.length === 1) wrappers.push(unique[0]);
|
|
238
|
-
else ambiguous.push({ call, definitions: unique.length });
|
|
239
|
-
}
|
|
240
|
-
return { wrappers, ambiguous, truncated };
|
|
241
|
-
}
|