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,260 +0,0 @@
|
|
|
1
|
-
// Spring MVC/WebFlux endpoint extraction. This stays source-only and deliberately resolves only
|
|
2
|
-
// literal annotation paths: inventing values for constants would make an architecture inventory look
|
|
3
|
-
// more complete than the evidence permits.
|
|
4
|
-
import { maskJavaNonCode } from "./java-source.js";
|
|
5
|
-
import { lineNumberAt } from "../util.js";
|
|
6
|
-
|
|
7
|
-
const SPRING_MAPPING = /@(?:org\.springframework\.web\.bind\.annotation\.)?(RequestMapping|GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping)\b/g;
|
|
8
|
-
const SPRING_CONDITION = /@(?:org\.springframework\.boot\.autoconfigure\.condition\.)?(ConditionalOnExpression|ConditionalOnProperty)\b/g;
|
|
9
|
-
const REQUEST_METHOD = /\bRequestMethod\s*\.\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\b/g;
|
|
10
|
-
|
|
11
|
-
const lineAt = lineNumberAt;
|
|
12
|
-
|
|
13
|
-
function skipTrivia(text, start) {
|
|
14
|
-
let i = start;
|
|
15
|
-
while (i < text.length) {
|
|
16
|
-
if (/\s/.test(text[i])) { i++; continue; }
|
|
17
|
-
if (text.startsWith("//", i)) {
|
|
18
|
-
const nl = text.indexOf("\n", i + 2);
|
|
19
|
-
i = nl < 0 ? text.length : nl + 1;
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
if (text.startsWith("/*", i)) {
|
|
23
|
-
const end = text.indexOf("*/", i + 2);
|
|
24
|
-
i = end < 0 ? text.length : end + 2;
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
break;
|
|
28
|
-
}
|
|
29
|
-
return i;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function balancedEnd(text, start, open = "(", close = ")") {
|
|
33
|
-
if (text[start] !== open) return start;
|
|
34
|
-
let depth = 0;
|
|
35
|
-
let quote = "";
|
|
36
|
-
let escaped = false;
|
|
37
|
-
for (let i = start; i < text.length; i++) {
|
|
38
|
-
const ch = text[i];
|
|
39
|
-
if (quote) {
|
|
40
|
-
if (escaped) escaped = false;
|
|
41
|
-
else if (ch === "\\") escaped = true;
|
|
42
|
-
else if (ch === quote) quote = "";
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (ch === '"' || ch === "'") { quote = ch; continue; }
|
|
46
|
-
if (ch === open) depth++;
|
|
47
|
-
else if (ch === close && --depth === 0) return i + 1;
|
|
48
|
-
}
|
|
49
|
-
return text.length;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function annotationInvocation(text, nameEnd) {
|
|
53
|
-
const start = skipTrivia(text, nameEnd);
|
|
54
|
-
if (text[start] !== "(") return { args: "", end: start };
|
|
55
|
-
const end = balancedEnd(text, start);
|
|
56
|
-
return { args: text.slice(start + 1, Math.max(start + 1, end - 1)), end };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function skipAnnotations(text, start) {
|
|
60
|
-
let i = skipTrivia(text, start);
|
|
61
|
-
while (text[i] === "@") {
|
|
62
|
-
const name = /^@[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*/.exec(text.slice(i));
|
|
63
|
-
if (!name) break;
|
|
64
|
-
i += name[0].length;
|
|
65
|
-
i = annotationInvocation(text, i).end;
|
|
66
|
-
i = skipTrivia(text, i);
|
|
67
|
-
}
|
|
68
|
-
return i;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function stringLiterals(value) {
|
|
72
|
-
const out = [];
|
|
73
|
-
const re = /"((?:\\.|[^"\\])*)"/g;
|
|
74
|
-
let match;
|
|
75
|
-
while ((match = re.exec(String(value || "")))) {
|
|
76
|
-
out.push(match[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"));
|
|
77
|
-
}
|
|
78
|
-
return out;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function initializer(args, equalsAt) {
|
|
82
|
-
let i = skipTrivia(args, equalsAt + 1);
|
|
83
|
-
if (args[i] === "{") {
|
|
84
|
-
const end = balancedEnd(args, i, "{", "}");
|
|
85
|
-
return args.slice(i, end);
|
|
86
|
-
}
|
|
87
|
-
if (args[i] === '"') {
|
|
88
|
-
let escaped = false;
|
|
89
|
-
for (let end = i + 1; end < args.length; end++) {
|
|
90
|
-
if (escaped) escaped = false;
|
|
91
|
-
else if (args[end] === "\\") escaped = true;
|
|
92
|
-
else if (args[end] === '"') return args.slice(i, end + 1);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const comma = args.indexOf(",", i);
|
|
96
|
-
return args.slice(i, comma < 0 ? args.length : comma);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function mappingPaths(args) {
|
|
100
|
-
const source = String(args || "");
|
|
101
|
-
const named = /\b(?:path|value)\s*=/g.exec(source);
|
|
102
|
-
if (named) return stringLiterals(initializer(source, source.indexOf("=", named.index)));
|
|
103
|
-
const leading = source.trimStart();
|
|
104
|
-
if (!leading) return [""];
|
|
105
|
-
if (leading[0] === '"' || leading[0] === "{") return stringLiterals(leading);
|
|
106
|
-
// `method=`, `produces=` and friends do not specify a path, so they map the class/method root.
|
|
107
|
-
if (/^(?:method|produces|consumes|headers|params|name)\s*=/.test(leading)) return [""];
|
|
108
|
-
return []; // unresolved positional constant: do not invent a root route
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function mappingMethods(name, args) {
|
|
112
|
-
if (name !== "RequestMapping") return [name.replace(/Mapping$/, "").toUpperCase()];
|
|
113
|
-
const methods = [];
|
|
114
|
-
let match;
|
|
115
|
-
REQUEST_METHOD.lastIndex = 0;
|
|
116
|
-
while ((match = REQUEST_METHOD.exec(args))) if (!methods.includes(match[1])) methods.push(match[1]);
|
|
117
|
-
return methods.length ? methods : ["ANY"];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function namedInitializer(args, name) {
|
|
121
|
-
const source = String(args || "");
|
|
122
|
-
const match = new RegExp(`\\b${name}\\s*=`).exec(source);
|
|
123
|
-
return match ? initializer(source, source.indexOf("=", match.index)) : "";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function namedStrings(args, name) {
|
|
127
|
-
return stringLiterals(namedInitializer(args, name));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function namedBoolean(args, name, fallback) {
|
|
131
|
-
const value = namedInitializer(args, name).trim();
|
|
132
|
-
if (/^true\b/i.test(value)) return true;
|
|
133
|
-
if (/^false\b/i.test(value)) return false;
|
|
134
|
-
return fallback;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function positionalStrings(args) {
|
|
138
|
-
const source = String(args || "");
|
|
139
|
-
const start = source.trimStart()[0];
|
|
140
|
-
return start === '"' || start === "{" ? stringLiterals(initializer(source, -1)) : [];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function expressionDefaultActive(expression) {
|
|
144
|
-
const value = String(expression || "").trim();
|
|
145
|
-
if (/^(?:true|false)$/i.test(value)) return value.toLowerCase() === "true";
|
|
146
|
-
const match = /^\$\{[^{}:]+:(true|false)\}$/i.exec(value);
|
|
147
|
-
return match ? match[1].toLowerCase() === "true" : null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function conditionMetadata(annotation, args) {
|
|
151
|
-
if (annotation === "ConditionalOnExpression") {
|
|
152
|
-
const expression = namedStrings(args, "value")[0] || stringLiterals(args)[0] || "";
|
|
153
|
-
return {
|
|
154
|
-
type: annotation,
|
|
155
|
-
expression,
|
|
156
|
-
defaultActive: expressionDefaultActive(expression),
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
const namedProperties = [...namedStrings(args, "name"), ...namedStrings(args, "value")];
|
|
160
|
-
const properties = [...new Set(namedProperties.length ? namedProperties : positionalStrings(args))];
|
|
161
|
-
const matchIfMissing = namedBoolean(args, "matchIfMissing", false);
|
|
162
|
-
return {
|
|
163
|
-
type: annotation,
|
|
164
|
-
prefix: namedStrings(args, "prefix")[0] || "",
|
|
165
|
-
properties,
|
|
166
|
-
havingValue: namedStrings(args, "havingValue")[0] || "",
|
|
167
|
-
matchIfMissing,
|
|
168
|
-
defaultActive: matchIfMissing,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function joinPaths(prefix, path) {
|
|
173
|
-
const left = String(prefix || "").trim().replace(/^\/+|\/+$/g, "");
|
|
174
|
-
const right = String(path || "").trim().replace(/^\/+|\/+$/g, "");
|
|
175
|
-
return `/${[left, right].filter(Boolean).join("/")}`.replace(/\/{2,}/g, "/");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function declarationAfter(text, start) {
|
|
179
|
-
const declarationStart = skipAnnotations(text, start);
|
|
180
|
-
const bounded = text.slice(declarationStart, declarationStart + 2_500);
|
|
181
|
-
const terminators = [bounded.indexOf("{"), bounded.indexOf(";")].filter((i) => i >= 0);
|
|
182
|
-
const end = terminators.length ? Math.min(...terminators) : bounded.length;
|
|
183
|
-
const head = bounded.slice(0, end);
|
|
184
|
-
const classMatch = /\b(class|interface|record|enum)\s+([A-Za-z_$][\w$]*)/.exec(head);
|
|
185
|
-
if (classMatch) {
|
|
186
|
-
const open = bounded.indexOf("{");
|
|
187
|
-
const bodyOpen = open < 0 ? -1 : declarationStart + open;
|
|
188
|
-
return { kind: "class", name: classMatch[2], start: declarationStart, bodyOpen, bodyClose: bodyOpen < 0 ? text.length : balancedEnd(text, bodyOpen, "{", "}") };
|
|
189
|
-
}
|
|
190
|
-
const methodMatch = /\b([A-Za-z_$][\w$]*)\s*\(/.exec(head);
|
|
191
|
-
return methodMatch ? { kind: "method", name: methodMatch[1], start: declarationStart } : { kind: "unknown", name: "", start: declarationStart };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function extractSpringEndpoints(text, file) {
|
|
195
|
-
if (!/\.java$/i.test(file)) return [];
|
|
196
|
-
const code = maskJavaNonCode(text);
|
|
197
|
-
if (!/@(?:[\w$]+\.)*(?:Request|Get|Post|Put|Patch|Delete)Mapping\b/.test(code)) return [];
|
|
198
|
-
const mappings = [];
|
|
199
|
-
let match;
|
|
200
|
-
SPRING_MAPPING.lastIndex = 0;
|
|
201
|
-
while ((match = SPRING_MAPPING.exec(code))) {
|
|
202
|
-
const invocation = annotationInvocation(text, SPRING_MAPPING.lastIndex);
|
|
203
|
-
const declaration = declarationAfter(code, invocation.end);
|
|
204
|
-
mappings.push({
|
|
205
|
-
annotation: match[1],
|
|
206
|
-
args: invocation.args,
|
|
207
|
-
index: match.index,
|
|
208
|
-
line: lineAt(text, match.index),
|
|
209
|
-
declaration,
|
|
210
|
-
});
|
|
211
|
-
SPRING_MAPPING.lastIndex = Math.max(SPRING_MAPPING.lastIndex, invocation.end);
|
|
212
|
-
}
|
|
213
|
-
const conditions = [];
|
|
214
|
-
SPRING_CONDITION.lastIndex = 0;
|
|
215
|
-
while ((match = SPRING_CONDITION.exec(code))) {
|
|
216
|
-
const invocation = annotationInvocation(text, SPRING_CONDITION.lastIndex);
|
|
217
|
-
const declaration = declarationAfter(code, invocation.end);
|
|
218
|
-
conditions.push({
|
|
219
|
-
index: match.index,
|
|
220
|
-
line: lineAt(text, match.index),
|
|
221
|
-
declaration,
|
|
222
|
-
...conditionMetadata(match[1], invocation.args),
|
|
223
|
-
});
|
|
224
|
-
SPRING_CONDITION.lastIndex = Math.max(SPRING_CONDITION.lastIndex, invocation.end);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const classMappings = mappings
|
|
228
|
-
.filter((item) => item.declaration.kind === "class")
|
|
229
|
-
.map((item) => ({ ...item, paths: mappingPaths(item.args) }));
|
|
230
|
-
const out = [];
|
|
231
|
-
for (const item of mappings) {
|
|
232
|
-
if (item.declaration.kind !== "method") continue;
|
|
233
|
-
const owner = classMappings
|
|
234
|
-
.filter((candidate) => candidate.declaration.bodyOpen >= 0
|
|
235
|
-
&& item.index > candidate.declaration.bodyOpen
|
|
236
|
-
&& item.index < candidate.declaration.bodyClose)
|
|
237
|
-
.sort((a, b) => b.declaration.bodyOpen - a.declaration.bodyOpen)[0];
|
|
238
|
-
if (owner && !owner.paths.length) continue; // unresolved class prefix makes the full route unknowable
|
|
239
|
-
const prefixes = owner?.paths?.length ? owner.paths : [""];
|
|
240
|
-
const paths = mappingPaths(item.args);
|
|
241
|
-
if (!paths.length) continue; // literal path was requested but could not be resolved
|
|
242
|
-
const activationConditions = conditions.filter((condition) => condition.declaration.start === item.declaration.start
|
|
243
|
-
|| (owner && condition.declaration.start === owner.declaration.start));
|
|
244
|
-
const activation = activationConditions.length
|
|
245
|
-
? {
|
|
246
|
-
conditional: true,
|
|
247
|
-
defaultActive: activationConditions.some((condition) => condition.defaultActive === false)
|
|
248
|
-
? false
|
|
249
|
-
: activationConditions.every((condition) => condition.defaultActive === true) ? true : null,
|
|
250
|
-
conditions: activationConditions.map(({ declaration: _declaration, index: _index, ...condition }) => condition),
|
|
251
|
-
}
|
|
252
|
-
: {};
|
|
253
|
-
for (const method of mappingMethods(item.annotation, item.args)) {
|
|
254
|
-
for (const prefix of prefixes) for (const path of paths) {
|
|
255
|
-
out.push({ method, path: joinPaths(prefix, path), handler: item.declaration.name, file, line: item.line, ...activation });
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return out;
|
|
260
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
const RUST_ROUTE_METHOD = "get|post|put|patch|delete|head|options|trace|connect|any";
|
|
2
|
-
|
|
3
|
-
// Find a call's closing parenthesis without mistaking parens inside strings for syntax. This is deliberately
|
|
4
|
-
// small (not a Rust parser), but keeps route-handler expressions bounded instead of using a cross-statement
|
|
5
|
-
// regex that could turn an unrelated function call into a route.
|
|
6
|
-
function closingParen(text, openAt) {
|
|
7
|
-
let depth = 0, quote = "", escaped = false, blockComment = 0;
|
|
8
|
-
for (let i = openAt; i < text.length; i++) {
|
|
9
|
-
const ch = text[i];
|
|
10
|
-
if (quote) {
|
|
11
|
-
if (escaped) escaped = false;
|
|
12
|
-
else if (ch === "\\") escaped = true;
|
|
13
|
-
else if (ch === quote) quote = "";
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
if (blockComment) {
|
|
17
|
-
if (ch === "/" && text[i + 1] === "*") { blockComment++; i++; }
|
|
18
|
-
else if (ch === "*" && text[i + 1] === "/") { blockComment--; i++; }
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
if (ch === "/" && text[i + 1] === "/") {
|
|
22
|
-
const newline = text.indexOf("\n", i + 2);
|
|
23
|
-
if (newline < 0) return -1;
|
|
24
|
-
i = newline;
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (ch === "/" && text[i + 1] === "*") { blockComment = 1; i++; continue; }
|
|
28
|
-
if (ch === '"') { quote = ch; continue; }
|
|
29
|
-
if (ch === "'") {
|
|
30
|
-
// Skip a Rust character literal, but do not mistake a lifetime (`'a`) for an unterminated string.
|
|
31
|
-
const char = /^'(?:\\.|[^\\'\r\n])'/.exec(text.slice(i));
|
|
32
|
-
if (char) { i += char[0].length - 1; continue; }
|
|
33
|
-
}
|
|
34
|
-
if (ch === "(") depth++;
|
|
35
|
-
else if (ch === ")" && --depth === 0) return i;
|
|
36
|
-
}
|
|
37
|
-
return -1;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Parse axum's MethodRouter expression, including direct method chains:
|
|
41
|
-
// get(list).post(create) / axum::routing::get(show).delete(remove)
|
|
42
|
-
// Calls inside a handler closure are not scanned and therefore cannot become fake endpoint methods.
|
|
43
|
-
function axumRoutes(expr) {
|
|
44
|
-
const found = [];
|
|
45
|
-
let rest = String(expr || ""), offset = 0;
|
|
46
|
-
const firstRe = new RegExp(`^\\s*(?:(?:axum\\s*::\\s*)?routing\\s*::\\s*)?(${RUST_ROUTE_METHOD})\\s*\\(`, "i");
|
|
47
|
-
let match = firstRe.exec(rest);
|
|
48
|
-
if (!match) return found;
|
|
49
|
-
|
|
50
|
-
while (match) {
|
|
51
|
-
const openAt = offset + match.index + match[0].lastIndexOf("(");
|
|
52
|
-
const closeAt = closingParen(expr, openAt);
|
|
53
|
-
if (closeAt < 0) break;
|
|
54
|
-
found.push({ method: match[1], handler: expr.slice(openAt + 1, closeAt), index: openAt });
|
|
55
|
-
offset = closeAt + 1;
|
|
56
|
-
rest = expr.slice(offset);
|
|
57
|
-
match = new RegExp(`^\\s*\\.\\s*(${RUST_ROUTE_METHOD})\\s*\\(`, "i").exec(rest);
|
|
58
|
-
}
|
|
59
|
-
return found;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Actix's builder form is structurally distinct from axum and from HTTP clients:
|
|
63
|
-
// web::get().to(handler) / actix_web::web::post().to(handler)
|
|
64
|
-
function actixRoutes(expr) {
|
|
65
|
-
const found = [];
|
|
66
|
-
const re = new RegExp(`(?:actix_web\\s*::\\s*)?web\\s*::\\s*(${RUST_ROUTE_METHOD})\\s*\\(\\s*\\)\\s*\\.\\s*to\\s*\\(`, "gi");
|
|
67
|
-
let match;
|
|
68
|
-
while ((match = re.exec(expr))) {
|
|
69
|
-
const openAt = re.lastIndex - 1;
|
|
70
|
-
const closeAt = closingParen(expr, openAt);
|
|
71
|
-
if (closeAt < 0) continue;
|
|
72
|
-
found.push({ method: match[1], handler: expr.slice(openAt + 1, closeAt), index: match.index });
|
|
73
|
-
re.lastIndex = closeAt + 1;
|
|
74
|
-
}
|
|
75
|
-
return found;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function extractRustEndpoints(text, add) {
|
|
79
|
-
let match;
|
|
80
|
-
|
|
81
|
-
// Axum and actix both expose a path-first `.route(path, ...)` builder. Requiring either an axum
|
|
82
|
-
// MethodRouter or actix's web::<method>().to(...) avoids treating arbitrary Rust APIs named `route` as HTTP.
|
|
83
|
-
const routeRe = /\.\s*route\s*\(\s*"(\/(?:\\.|[^"\\])*)"\s*,/g;
|
|
84
|
-
while ((match = routeRe.exec(text))) {
|
|
85
|
-
const openAt = text.indexOf("(", match.index);
|
|
86
|
-
const closeAt = closingParen(text, openAt);
|
|
87
|
-
if (closeAt < 0) continue;
|
|
88
|
-
const exprStart = routeRe.lastIndex;
|
|
89
|
-
const expr = text.slice(exprStart, closeAt);
|
|
90
|
-
const routes = [...axumRoutes(expr), ...actixRoutes(expr)];
|
|
91
|
-
for (const route of routes) add(route.method, match[1], route.handler, exprStart + route.index);
|
|
92
|
-
routeRe.lastIndex = closeAt + 1;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// actix `web::resource(path)` associates several method routes with one path.
|
|
96
|
-
const resourceRe = /(?:actix_web\s*::\s*)?web\s*::\s*resource\s*\(\s*"(\/(?:\\.|[^"\\])*)"\s*\)/g;
|
|
97
|
-
while ((match = resourceRe.exec(text))) {
|
|
98
|
-
const tail = text.slice(resourceRe.lastIndex);
|
|
99
|
-
const nextResource = tail.search(/(?:actix_web\s*::\s*)?web\s*::\s*resource\s*\(/);
|
|
100
|
-
const semicolon = text.indexOf(";", resourceRe.lastIndex);
|
|
101
|
-
let end = Math.min(text.length, resourceRe.lastIndex + 4000);
|
|
102
|
-
if (nextResource >= 0) end = Math.min(end, resourceRe.lastIndex + nextResource);
|
|
103
|
-
if (semicolon >= 0) end = Math.min(end, semicolon);
|
|
104
|
-
const chain = text.slice(resourceRe.lastIndex, end);
|
|
105
|
-
for (const route of actixRoutes(chain)) add(route.method, match[1], route.handler, resourceRe.lastIndex + route.index);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Actix attribute macros. The route macro can declare more than one method for the same handler.
|
|
109
|
-
const macroRe = new RegExp(`#\\s*\\[\\s*(?:(?:actix_web|actix)\\s*::\\s*)?(${RUST_ROUTE_METHOD})\\s*\\(\\s*"(\\/(?:\\\\.|[^"\\\\])*)"(?:\\s*,[^\\]]*)?\\s*\\)\\s*\\]`, "gi");
|
|
110
|
-
while ((match = macroRe.exec(text))) {
|
|
111
|
-
const after = text.slice(macroRe.lastIndex, macroRe.lastIndex + 600);
|
|
112
|
-
const fn = /\b(?:pub(?:\s*\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z_][\w]*)/.exec(after);
|
|
113
|
-
add(match[1], match[2], fn ? fn[1] : "", match.index);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const multiMacroRe = /#\s*\[\s*(?:(?:actix_web|actix)\s*::\s*)?route\s*\(\s*"(\/(?:\\.|[^"\\])*)"([\s\S]*?)\)\s*\]/gi;
|
|
117
|
-
while ((match = multiMacroRe.exec(text))) {
|
|
118
|
-
const after = text.slice(multiMacroRe.lastIndex, multiMacroRe.lastIndex + 600);
|
|
119
|
-
const fn = /\b(?:pub(?:\s*\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z_][\w]*)/.exec(after);
|
|
120
|
-
const methodRe = /\bmethod\s*=\s*"(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|CONNECT)"/gi;
|
|
121
|
-
let method;
|
|
122
|
-
while ((method = methodRe.exec(match[2]))) add(method[1], match[1], fn ? fn[1] : "", match.index);
|
|
123
|
-
}
|
|
124
|
-
}
|
package/src/analysis/findings.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
// Unified Finding factory + rollups for the internal dependency/security engine (DEPS_SECURITY_PLAN.md §2.3).
|
|
2
|
-
// Every analyzer (unused / structure / vulnerability / malware) and the external-tool adapter emit THIS shape,
|
|
3
|
-
// so the renderer and the AI summarizer consume one contract regardless of engine.
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
|
|
6
|
-
const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
|
|
7
|
-
|
|
8
|
-
export function dependencyVerification(manifest, imports, decision, mapping) {
|
|
9
|
-
return {
|
|
10
|
-
evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
|
|
11
|
-
decision,
|
|
12
|
-
manifestDeclaration: { status: "FOUND", file: manifest },
|
|
13
|
-
indexedSourceImports: imports.length
|
|
14
|
-
? { status: "FOUND", count: imports.length, files: [...new Set(imports.map((item) => item.file))].slice(0, 10) }
|
|
15
|
-
: { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0, files: [] },
|
|
16
|
-
mapping,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Stable id: survives re-runs so the UI can persist expand/dismiss state per finding.
|
|
21
|
-
export function makeFinding(f) {
|
|
22
|
-
const cycleIdentity = Array.isArray(f.cycleMembers)
|
|
23
|
-
? [...new Set(f.cycleMembers.map(String))].sort().join("\0")
|
|
24
|
-
: "";
|
|
25
|
-
const id = createHash("sha1")
|
|
26
|
-
.update([
|
|
27
|
-
f.category, f.rule, f.file || "", f.manifest || "", f.scope || "",
|
|
28
|
-
f.package || "", f.symbol || "", cycleIdentity, f.title || "",
|
|
29
|
-
].join("|"))
|
|
30
|
-
.digest("hex")
|
|
31
|
-
.slice(0, 16);
|
|
32
|
-
return {
|
|
33
|
-
id,
|
|
34
|
-
severity: "info",
|
|
35
|
-
confidence: "medium",
|
|
36
|
-
title: "",
|
|
37
|
-
detail: "",
|
|
38
|
-
reason: "",
|
|
39
|
-
file: "",
|
|
40
|
-
line: 0,
|
|
41
|
-
symbol: "",
|
|
42
|
-
package: "",
|
|
43
|
-
version: "",
|
|
44
|
-
graphNodeId: f.graphNodeId || f.file || "",
|
|
45
|
-
evidence: [],
|
|
46
|
-
source: "internal",
|
|
47
|
-
fixHint: "",
|
|
48
|
-
...f,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function summarizeFindings(findings) {
|
|
53
|
-
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
54
|
-
const byCategory = { unused: 0, structure: 0, vulnerability: 0, malware: 0 };
|
|
55
|
-
for (const f of findings || []) {
|
|
56
|
-
if (f.severity in bySeverity) bySeverity[f.severity]++;
|
|
57
|
-
if (f.category in byCategory) byCategory[f.category]++;
|
|
58
|
-
}
|
|
59
|
-
return { bySeverity, byCategory };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function sortFindings(findings) {
|
|
63
|
-
const sev = new Map(SEVERITY_ORDER.map((s, i) => [s, i]));
|
|
64
|
-
return [...(findings || [])].sort(
|
|
65
|
-
(a, b) =>
|
|
66
|
-
(sev.get(a.severity) ?? 9) - (sev.get(b.severity) ?? 9) ||
|
|
67
|
-
String(a.category).localeCompare(String(b.category)) ||
|
|
68
|
-
String(a.file || a.package || "").localeCompare(String(b.file || b.package || ""))
|
|
69
|
-
);
|
|
70
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import {isTestPath} from '../../graph/graph-filter.js'
|
|
2
|
-
import {isStructuralRelation} from '../../graph/relations.js'
|
|
3
|
-
import {
|
|
4
|
-
boundedHistoryInteger,
|
|
5
|
-
GIT_HISTORY_DEFAULTS as DEFAULTS,
|
|
6
|
-
GIT_HISTORY_HARD_CAPS as HARD_CAPS,
|
|
7
|
-
GIT_HISTORY_V,
|
|
8
|
-
graphEndpoint,
|
|
9
|
-
roundHistoryNumber as round,
|
|
10
|
-
safeHistoryPath,
|
|
11
|
-
} from './options.js'
|
|
12
|
-
|
|
13
|
-
function graphFilesAndAdjacency(graph = {}) {
|
|
14
|
-
const byId = new Map(), files = new Set()
|
|
15
|
-
for (const node of graph.nodes || []) {
|
|
16
|
-
const file = safeHistoryPath(node?.source_file)
|
|
17
|
-
if (!file) continue
|
|
18
|
-
byId.set(String(node.id), file)
|
|
19
|
-
files.add(file)
|
|
20
|
-
}
|
|
21
|
-
const adjacency = new Map([...files].map((file) => [file, new Set()]))
|
|
22
|
-
for (const link of graph.links || []) {
|
|
23
|
-
if (isStructuralRelation(link?.relation) || link?.barrelProxy === true) continue
|
|
24
|
-
const left = byId.get(String(graphEndpoint(link?.source)))
|
|
25
|
-
const right = byId.get(String(graphEndpoint(link?.target)))
|
|
26
|
-
if (!left || !right || left === right) continue
|
|
27
|
-
adjacency.get(left)?.add(right)
|
|
28
|
-
adjacency.get(right)?.add(left)
|
|
29
|
-
}
|
|
30
|
-
return {files, adjacency}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function graphDistanceAtMostTwo(left, right, adjacency) {
|
|
34
|
-
if (left === right) return 0
|
|
35
|
-
const neighbors = adjacency.get(left)
|
|
36
|
-
if (!neighbors) return null
|
|
37
|
-
if (neighbors.has(right)) return 1
|
|
38
|
-
for (const middle of neighbors) if (adjacency.get(middle)?.has(right)) return 2
|
|
39
|
-
return null
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function percentile(value, positiveValues) {
|
|
43
|
-
if (!(value > 0) || !positiveValues.length) return 0
|
|
44
|
-
let atOrBelow = 0
|
|
45
|
-
for (const candidate of positiveValues) if (candidate <= value) atOrBelow++
|
|
46
|
-
return round(atOrBelow / positiveValues.length)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const pairSort = (left, right) => right.count - left.count
|
|
50
|
-
|| right.confidence - left.confidence
|
|
51
|
-
|| right.lift - left.lift
|
|
52
|
-
|| left.left.localeCompare(right.left)
|
|
53
|
-
|| left.right.localeCompare(right.right)
|
|
54
|
-
|
|
55
|
-
const publicPair = (pair, graphDistance) => ({
|
|
56
|
-
left: pair.left,
|
|
57
|
-
right: pair.right,
|
|
58
|
-
count: pair.count,
|
|
59
|
-
jaccard: pair.jaccard,
|
|
60
|
-
lift: pair.lift,
|
|
61
|
-
confidence: pair.confidence,
|
|
62
|
-
leftConfidence: pair.leftConfidence,
|
|
63
|
-
rightConfidence: pair.rightConfidence,
|
|
64
|
-
graphDistance,
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
function fileActivity(commits, graph) {
|
|
68
|
-
const activity = new Map(), fileCommits = new Map()
|
|
69
|
-
let additions = 0, deletions = 0, binaryChanges = 0
|
|
70
|
-
for (const commit of commits) {
|
|
71
|
-
const seen = new Set()
|
|
72
|
-
for (const stat of commit.files) {
|
|
73
|
-
if (seen.has(stat.file)) continue
|
|
74
|
-
seen.add(stat.file)
|
|
75
|
-
const entry = activity.get(stat.file) || {file: stat.file, commits: 0, additions: 0, deletions: 0, binaryChanges: 0}
|
|
76
|
-
entry.commits++
|
|
77
|
-
entry.additions += stat.additions
|
|
78
|
-
entry.deletions += stat.deletions
|
|
79
|
-
entry.binaryChanges += stat.binary ? 1 : 0
|
|
80
|
-
activity.set(stat.file, entry)
|
|
81
|
-
fileCommits.set(stat.file, (fileCommits.get(stat.file) || 0) + 1)
|
|
82
|
-
additions += stat.additions
|
|
83
|
-
deletions += stat.deletions
|
|
84
|
-
if (stat.binary) binaryChanges++
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const {files: graphFiles, adjacency} = graphFilesAndAdjacency(graph)
|
|
88
|
-
const raw = [...activity.values()].map((entry) => ({...entry, churn: entry.additions + entry.deletions}))
|
|
89
|
-
const churnValues = raw.map((entry) => entry.churn).filter((value) => value > 0)
|
|
90
|
-
const connectivityValues = [...graphFiles].map((file) => adjacency.get(file)?.size || 0).filter((value) => value > 0)
|
|
91
|
-
const fileChurn = raw.map((entry) => {
|
|
92
|
-
const connectivity = adjacency.get(entry.file)?.size || 0
|
|
93
|
-
const churnPercentile = percentile(entry.churn, churnValues)
|
|
94
|
-
const connectivityPercentile = percentile(connectivity, connectivityValues)
|
|
95
|
-
return {...entry, connectivity, churnPercentile, connectivityPercentile, hotspotScore: round(Math.sqrt(churnPercentile * connectivityPercentile))}
|
|
96
|
-
}).sort((a, b) => b.churn - a.churn || b.commits - a.commits || a.file.localeCompare(b.file))
|
|
97
|
-
const hotspots = fileChurn.filter((entry) => entry.connectivity > 0)
|
|
98
|
-
.sort((a, b) => b.hotspotScore - a.hotspotScore || b.churn - a.churn || b.connectivity - a.connectivity || a.file.localeCompare(b.file))
|
|
99
|
-
return {fileCommits, graphFiles, adjacency, fileChurn, hotspots, additions, deletions, binaryChanges}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function coChangePairs(commits, fileCommits, limits) {
|
|
103
|
-
const counts = new Map()
|
|
104
|
-
let truncated = false
|
|
105
|
-
for (const commit of commits) {
|
|
106
|
-
const files = [...new Set(commit.files.map((entry) => entry.file))].sort()
|
|
107
|
-
for (let left = 0; left < files.length; left++) for (let right = left + 1; right < files.length; right++) {
|
|
108
|
-
const key = `${files[left]}\0${files[right]}`
|
|
109
|
-
if (!counts.has(key) && counts.size >= limits.maxPairCandidates) { truncated = true; continue }
|
|
110
|
-
counts.set(key, (counts.get(key) || 0) + 1)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const pairs = []
|
|
114
|
-
for (const [key, count] of counts) {
|
|
115
|
-
if (count < limits.minPairCount || !commits.length) continue
|
|
116
|
-
const split = key.indexOf('\0'), left = key.slice(0, split), right = key.slice(split + 1)
|
|
117
|
-
const leftCount = fileCommits.get(left) || 0, rightCount = fileCommits.get(right) || 0
|
|
118
|
-
const leftConfidence = count / leftCount, rightConfidence = count / rightCount
|
|
119
|
-
pairs.push({
|
|
120
|
-
left, right, count,
|
|
121
|
-
jaccard: round(count / (leftCount + rightCount - count)),
|
|
122
|
-
lift: round((count * commits.length) / (leftCount * rightCount)),
|
|
123
|
-
confidence: round(Math.max(leftConfidence, rightConfidence)),
|
|
124
|
-
leftConfidence: round(leftConfidence),
|
|
125
|
-
rightConfidence: round(rightConfidence),
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
return {pairs: pairs.sort(pairSort), totalCandidates: counts.size, truncated}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function buildGitHistoryAnalytics({commits = [], graph = {}, window, limits = {}, status = 'complete'} = {}) {
|
|
132
|
-
const maxPairs = boundedHistoryInteger(limits.maxPairs, DEFAULTS.maxPairs, 1, HARD_CAPS.maxPairs)
|
|
133
|
-
const minPairCount = boundedHistoryInteger(limits.minPairCount, DEFAULTS.minPairCount, 1, 100)
|
|
134
|
-
const maxPairCandidates = boundedHistoryInteger(limits.maxPairCandidates, DEFAULTS.maxPairCandidates, 100, HARD_CAPS.maxPairCandidates)
|
|
135
|
-
const eligible = commits.filter((commit) => !commit.oversized && commit.files.length > 0)
|
|
136
|
-
const skipped = commits.filter((commit) => commit.oversized)
|
|
137
|
-
const activity = fileActivity(eligible, graph)
|
|
138
|
-
const cochange = coChangePairs(eligible, activity.fileCommits, {minPairCount, maxPairCandidates})
|
|
139
|
-
const observed = cochange.pairs.slice(0, maxPairs).map((pair) => publicPair(pair, graphDistanceAtMostTwo(pair.left, pair.right, activity.adjacency)))
|
|
140
|
-
const expectedTestSource = cochange.pairs.filter((pair) => isTestPath(pair.left) !== isTestPath(pair.right)).slice(0, maxPairs).map((pair) => {
|
|
141
|
-
const test = isTestPath(pair.left) ? pair.left : pair.right
|
|
142
|
-
const source = test === pair.left ? pair.right : pair.left
|
|
143
|
-
return {
|
|
144
|
-
source, test, count: pair.count, jaccard: pair.jaccard, lift: pair.lift, confidence: pair.confidence,
|
|
145
|
-
sourceConfidence: source === pair.left ? pair.leftConfidence : pair.rightConfidence,
|
|
146
|
-
testConfidence: test === pair.left ? pair.leftConfidence : pair.rightConfidence,
|
|
147
|
-
graphDistance: graphDistanceAtMostTwo(source, test, activity.adjacency),
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
const hidden = cochange.pairs.filter((pair) => !isTestPath(pair.left) && !isTestPath(pair.right)
|
|
151
|
-
&& activity.graphFiles.has(pair.left) && activity.graphFiles.has(pair.right)
|
|
152
|
-
&& graphDistanceAtMostTwo(pair.left, pair.right, activity.adjacency) === null)
|
|
153
|
-
.slice(0, maxPairs).map((pair) => publicPair(pair, null))
|
|
154
|
-
const reasons = [
|
|
155
|
-
skipped.length ? `${skipped.length} oversized change-set(s) excluded` : null,
|
|
156
|
-
cochange.truncated ? 'co-change candidate cap reached' : null,
|
|
157
|
-
status === 'partial' ? 'git output or commit window was truncated' : null,
|
|
158
|
-
].filter(Boolean)
|
|
159
|
-
return {
|
|
160
|
-
gitHistoryV: GIT_HISTORY_V,
|
|
161
|
-
status: reasons.length ? 'partial' : status,
|
|
162
|
-
window: window || null,
|
|
163
|
-
limits: {maxCommits: limits.maxCommits ?? null, maxFilesPerCommit: limits.maxFilesPerCommit ?? null, maxPairs, minPairCount, maxPairCandidates},
|
|
164
|
-
completeness: {complete: reasons.length === 0, reasons},
|
|
165
|
-
totals: {
|
|
166
|
-
commitsRead: commits.length, commitsAnalyzed: eligible.length, oversizedCommitsSkipped: skipped.length,
|
|
167
|
-
files: activity.fileChurn.length, additions: activity.additions, deletions: activity.deletions,
|
|
168
|
-
churn: activity.additions + activity.deletions, binaryChanges: activity.binaryChanges,
|
|
169
|
-
ignoredFiles: commits.reduce((sum, commit) => sum + (commit.ignoredFiles || 0), 0),
|
|
170
|
-
invalidPaths: commits.reduce((sum, commit) => sum + (commit.invalidPaths || 0), 0),
|
|
171
|
-
graphFiles: activity.graphFiles.size,
|
|
172
|
-
},
|
|
173
|
-
fileChurn: activity.fileChurn,
|
|
174
|
-
hotspots: activity.hotspots,
|
|
175
|
-
coupling: {eligibleCommits: eligible.length, totalCandidates: cochange.totalCandidates, candidatesTruncated: cochange.truncated, observed, expectedTestSource, hidden},
|
|
176
|
-
}
|
|
177
|
-
}
|