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,65 +0,0 @@
|
|
|
1
|
-
// Reading a built graph.json and summarizing it for the UI cards: named communities and degree
|
|
2
|
-
// hotspots. Both read the graph file from disk and return [] on any failure.
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { communityTerritoryOf } from "../graph/community.js";
|
|
5
|
-
import { isStructuralRelation } from "../graph/relations.js";
|
|
6
|
-
|
|
7
|
-
// graph-builder labels communities "Community N" without an LLM. Derive a real name from each
|
|
8
|
-
// community's dominant folder + sample files so the UI shows modules, not bare numbers.
|
|
9
|
-
export function summarizeCommunities(graphJsonPath, max = 40) {
|
|
10
|
-
try {
|
|
11
|
-
const graph = JSON.parse(readFileSync(graphJsonPath, "utf8"));
|
|
12
|
-
const byCommunity = new Map();
|
|
13
|
-
for (const node of graph.nodes || []) {
|
|
14
|
-
if (node.file_type !== "code") continue;
|
|
15
|
-
const community = node.community;
|
|
16
|
-
if (community === undefined || community === null) continue;
|
|
17
|
-
if (!byCommunity.has(community)) byCommunity.set(community, { id: community, size: 0, dirs: new Map(), files: [] });
|
|
18
|
-
const entry = byCommunity.get(community);
|
|
19
|
-
entry.size += 1;
|
|
20
|
-
const parts = String(node.source_file || "").split(/[\\/]/).filter(Boolean);
|
|
21
|
-
const dir = communityTerritoryOf(node.source_file);
|
|
22
|
-
entry.dirs.set(dir, (entry.dirs.get(dir) || 0) + 1);
|
|
23
|
-
if (entry.files.length < 4) entry.files.push(parts[parts.length - 1] || node.source_file || "");
|
|
24
|
-
}
|
|
25
|
-
return [...byCommunity.values()]
|
|
26
|
-
.map((entry) => {
|
|
27
|
-
const dominant = [...entry.dirs.entries()].sort((left, right) => right[1] - left[1])[0];
|
|
28
|
-
return { id: entry.id, size: entry.size, name: dominant ? dominant[0] : "(mixed)", files: entry.files };
|
|
29
|
-
})
|
|
30
|
-
.sort((left, right) => right.size - left.size)
|
|
31
|
-
.slice(0, max);
|
|
32
|
-
} catch {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Top nodes by total degree (in+out) — the "load-bearing" / refactor-candidate hotspots.
|
|
38
|
-
export function summarizeHotspots(graphJsonPath, max = 15) {
|
|
39
|
-
try {
|
|
40
|
-
const graph = JSON.parse(readFileSync(graphJsonPath, "utf8"));
|
|
41
|
-
const nodes = graph.nodes || [];
|
|
42
|
-
const endpoint = (value) => (value && typeof value === "object" ? value.id : value);
|
|
43
|
-
const inDeg = new Map();
|
|
44
|
-
const outDeg = new Map();
|
|
45
|
-
for (const link of graph.links || []) {
|
|
46
|
-
if (isStructuralRelation(link.relation) || link.barrelProxy === true) continue;
|
|
47
|
-
const s = endpoint(link.source);
|
|
48
|
-
const t = endpoint(link.target);
|
|
49
|
-
outDeg.set(s, (outDeg.get(s) || 0) + 1);
|
|
50
|
-
inDeg.set(t, (inDeg.get(t) || 0) + 1);
|
|
51
|
-
}
|
|
52
|
-
return nodes
|
|
53
|
-
.filter((node) => node.file_type === "code")
|
|
54
|
-
.map((node) => {
|
|
55
|
-
const inbound = inDeg.get(node.id) || 0;
|
|
56
|
-
const outbound = outDeg.get(node.id) || 0;
|
|
57
|
-
return { label: node.label || node.norm_label || node.id, file: node.source_file || "", in: inbound, out: outbound, degree: inbound + outbound };
|
|
58
|
-
})
|
|
59
|
-
.filter((node) => node.degree > 0)
|
|
60
|
-
.sort((left, right) => right.degree - left.degree)
|
|
61
|
-
.slice(0, max);
|
|
62
|
-
} catch {
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
// Honest Health capability coverage. `status` says whether a check actually ran for this repository;
|
|
2
|
-
// `completeness` says whether its evidence can support a repository-wide conclusion.
|
|
3
|
-
const capability = (status, completeness, detail, extra = {}) => ({ status, completeness, detail, ...extra });
|
|
4
|
-
|
|
5
|
-
const sourceLanguages = (files) => {
|
|
6
|
-
const languages = new Set();
|
|
7
|
-
for (const file of files || []) {
|
|
8
|
-
if (/\.(?:[cm]?js|jsx|[cm]?ts|tsx)$/i.test(file)) languages.add("javascript/typescript");
|
|
9
|
-
else if (/\.py$/i.test(file)) languages.add("python");
|
|
10
|
-
else if (/\.go$/i.test(file)) languages.add("go");
|
|
11
|
-
else if (/\.java$/i.test(file)) languages.add("java");
|
|
12
|
-
else if (/\.rs$/i.test(file)) languages.add("rust");
|
|
13
|
-
else if (/\.cs$/i.test(file)) languages.add("csharp");
|
|
14
|
-
}
|
|
15
|
-
return languages;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const checkCapability = (check, supported, label) => {
|
|
19
|
-
if (!supported) return capability("NOT_SUPPORTED", "PARTIAL", `${label} is not supported for the discovered package ecosystem.`);
|
|
20
|
-
if (check?.status === "OK") return capability("CHECKED", "COMPLETE", check.detail || `${label} completed.`);
|
|
21
|
-
if (check?.status === "PARTIAL") return capability("NOT_CHECKED", "PARTIAL", check.detail || `${label} evidence is partial.`);
|
|
22
|
-
if (check?.status === "ERROR") return capability("NOT_CHECKED", "PARTIAL", check.detail || `${label} failed.`);
|
|
23
|
-
return capability("NOT_CHECKED", "PARTIAL", check?.detail || `${label} was not requested.`);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export function buildHealthCapabilityMatrix({
|
|
27
|
-
graphComplete,
|
|
28
|
-
dependencyStatus,
|
|
29
|
-
dependencyEcosystems = {},
|
|
30
|
-
checks = {},
|
|
31
|
-
sourceFiles = [],
|
|
32
|
-
correctnessCoverage = {},
|
|
33
|
-
measuredCoverageFiles = 0,
|
|
34
|
-
} = {}) {
|
|
35
|
-
const languages = sourceLanguages(sourceFiles);
|
|
36
|
-
const ecosystemRows = Object.values(dependencyEcosystems).filter((item) => item?.present);
|
|
37
|
-
const supportedDependencyRows = ecosystemRows.filter((item) => item.status === "CHECKED");
|
|
38
|
-
const unsupportedDependencyRows = ecosystemRows.filter((item) => item.status === "NOT_SUPPORTED");
|
|
39
|
-
const noDependencyEvidence = ecosystemRows.length === 0;
|
|
40
|
-
const onlyUnsupportedDependencies = unsupportedDependencyRows.length > 0 && supportedDependencyRows.length === 0;
|
|
41
|
-
const advisorySupported = supportedDependencyRows.some((item) => ["npm", "go", "python", "rust", "maven", "gradle"].includes(item.ecosystem));
|
|
42
|
-
const onlyRustAdvisories = supportedDependencyRows.length > 0 && supportedDependencyRows.every((item) => item.ecosystem === "rust");
|
|
43
|
-
const malwareSupported = supportedDependencyRows.some((item) => ["npm", "go", "python"].includes(item.ecosystem));
|
|
44
|
-
const coverageSupported = [...languages].some((language) => ["javascript/typescript", "python", "go"].includes(language));
|
|
45
|
-
const runtimeFiles = Number(correctnessCoverage.runtimeCorrectnessFiles || 0);
|
|
46
|
-
const concurrencyFiles = Number(correctnessCoverage.concurrencyFiles || 0);
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
capabilityMatrixV: 1,
|
|
50
|
-
structure: capability(
|
|
51
|
-
"CHECKED",
|
|
52
|
-
graphComplete ? "COMPLETE" : "PARTIAL",
|
|
53
|
-
graphComplete
|
|
54
|
-
? "Graph cycles, orphans and configured boundaries were checked over the complete graph."
|
|
55
|
-
: "Structure checks ran, but the graph is scoped or excludes part of the repository.",
|
|
56
|
-
),
|
|
57
|
-
dependencies: capability(
|
|
58
|
-
noDependencyEvidence ? "NOT_CHECKED" : onlyUnsupportedDependencies ? "NOT_SUPPORTED" : "CHECKED",
|
|
59
|
-
!noDependencyEvidence && dependencyStatus === "COMPLETE" ? "COMPLETE" : "PARTIAL",
|
|
60
|
-
noDependencyEvidence
|
|
61
|
-
? "No dependency manifest was discovered, so manifest-to-import verification did not run."
|
|
62
|
-
: onlyUnsupportedDependencies
|
|
63
|
-
? "Dependency manifests were detected, but every discovered build ecosystem lacks import-to-artifact verification."
|
|
64
|
-
: unsupportedDependencyRows.length
|
|
65
|
-
? `Supported ecosystems were checked; ${unsupportedDependencyRows.map((item) => item.ecosystem).join(", ")} remains NOT_SUPPORTED.`
|
|
66
|
-
: "Discovered supported manifests were compared with indexed imports at their documented evidence level.",
|
|
67
|
-
{ ecosystems: dependencyEcosystems },
|
|
68
|
-
),
|
|
69
|
-
runtimeCorrectness: runtimeFiles
|
|
70
|
-
? capability("CHECKED", "PARTIAL", `Checked ${runtimeFiles} product source file(s) for the bounded Go/Java/retry correctness patterns. This is not compiler or runtime proof.`, { checks: correctnessCoverage.checks || {} })
|
|
71
|
-
: capability("NOT_SUPPORTED", "PARTIAL", "No source file matched the currently supported bounded correctness patterns."),
|
|
72
|
-
concurrency: concurrencyFiles
|
|
73
|
-
? capability("CHECKED", "PARTIAL", `Checked ${concurrencyFiles} Java source file(s) for direct InterruptedException restore/rethrow evidence. No race detector ran; race freedom is not claimed.`)
|
|
74
|
-
: capability("NOT_SUPPORTED", "PARTIAL", "No supported Java interruption check applied. No race detector ran; race freedom is not claimed."),
|
|
75
|
-
advisories: checkCapability(onlyRustAdvisories && checks.rustsec?.status === "OK" ? checks.rustsec : checks.osv, advisorySupported, onlyRustAdvisories ? "RustSec advisory matching" : "OSV advisory matching"),
|
|
76
|
-
malware: checkCapability(checks.malware, malwareSupported, "Installed-package malware scanning"),
|
|
77
|
-
coverage: measuredCoverageFiles > 0
|
|
78
|
-
? capability("CHECKED", "COMPLETE", `Mapped an existing supported coverage report to ${measuredCoverageFiles} file(s).`)
|
|
79
|
-
: coverageSupported
|
|
80
|
-
? capability("NOT_CHECKED", "PARTIAL", "No supported measured coverage report was found; static test reachability is not coverage.")
|
|
81
|
-
: capability("NOT_SUPPORTED", "PARTIAL", `No supported coverage format applies to the discovered language set${languages.size ? ` (${[...languages].join(", ")})` : ""}.`),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
@@ -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', 'vendored', '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
|
-
}
|