weavatrix 0.3.14 → 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 -66
- 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,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
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import {isWeavatrixIgnored} from '../../path-ignore.js'
|
|
2
|
-
import {
|
|
3
|
-
boundedHistoryInteger,
|
|
4
|
-
GIT_HISTORY_DEFAULTS as DEFAULTS,
|
|
5
|
-
GIT_HISTORY_HARD_CAPS as HARD_CAPS,
|
|
6
|
-
safeHistoryPath,
|
|
7
|
-
} from './options.js'
|
|
8
|
-
|
|
9
|
-
const HEADER_SEPARATOR = '\x1e'
|
|
10
|
-
const FIELD_SEPARATOR = '\x1f'
|
|
11
|
-
|
|
12
|
-
// git subprocess execution (sync runGit + streaming boundedGitCommand) lives in ../../git-exec.js.
|
|
13
|
-
|
|
14
|
-
const statNumber = (value) => value === '-'
|
|
15
|
-
? {value: 0, binary: true}
|
|
16
|
-
: {value: Number(value), binary: false}
|
|
17
|
-
|
|
18
|
-
export function parseGitNumstatLog(raw, options = {}) {
|
|
19
|
-
const maxFiles = boundedHistoryInteger(options.maxFilesPerCommit, DEFAULTS.maxFilesPerCommit, 2, HARD_CAPS.maxFilesPerCommit)
|
|
20
|
-
const ignoreRules = options.ignoreRules || []
|
|
21
|
-
const text = Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw || '')
|
|
22
|
-
const segments = text.split(HEADER_SEPARATOR).slice(1)
|
|
23
|
-
if (options.dropLastIncomplete && segments.length) segments.pop()
|
|
24
|
-
const commits = []
|
|
25
|
-
for (const segment of segments) {
|
|
26
|
-
const firstNul = segment.indexOf('\0')
|
|
27
|
-
if (firstNul < 0) continue
|
|
28
|
-
const header = segment.slice(0, firstNul).replace(/^[\r\n]+/, '')
|
|
29
|
-
const separator = header.indexOf(FIELD_SEPARATOR)
|
|
30
|
-
if (separator < 0) continue
|
|
31
|
-
const hash = header.slice(0, separator), timestamp = Number(header.slice(separator + 1))
|
|
32
|
-
if (!/^[a-f0-9]{40,64}$/i.test(hash) || !Number.isInteger(timestamp) || timestamp < 0) continue
|
|
33
|
-
const tokens = segment.slice(firstNul + 1).split('\0'), files = new Map()
|
|
34
|
-
let fileCount = 0, ignoredFiles = 0, invalidPaths = 0, oversized = false
|
|
35
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
36
|
-
const token = tokens[index].replace(/^[\r\n]+/, '')
|
|
37
|
-
const match = /^(\d+|-)\t(\d+|-)\t(.*)$/s.exec(token)
|
|
38
|
-
if (!match) continue
|
|
39
|
-
let rawPath = match[3], renamedFrom = null
|
|
40
|
-
if (!rawPath) {
|
|
41
|
-
renamedFrom = safeHistoryPath(tokens[index + 1])
|
|
42
|
-
rawPath = tokens[index + 2]
|
|
43
|
-
index += 2
|
|
44
|
-
}
|
|
45
|
-
const path = safeHistoryPath(rawPath)
|
|
46
|
-
if (!path) { invalidPaths++; continue }
|
|
47
|
-
if (isWeavatrixIgnored(path, ignoreRules)) { ignoredFiles++; continue }
|
|
48
|
-
const additions = statNumber(match[1]), deletions = statNumber(match[2])
|
|
49
|
-
fileCount++
|
|
50
|
-
if (fileCount > maxFiles) { oversized = true; files.clear(); continue }
|
|
51
|
-
if (oversized) continue
|
|
52
|
-
const previous = files.get(path)
|
|
53
|
-
files.set(path, {
|
|
54
|
-
file: path,
|
|
55
|
-
additions: (previous?.additions || 0) + additions.value,
|
|
56
|
-
deletions: (previous?.deletions || 0) + deletions.value,
|
|
57
|
-
binary: Boolean(previous?.binary || additions.binary || deletions.binary),
|
|
58
|
-
...(renamedFrom ? {renamedFrom} : previous?.renamedFrom ? {renamedFrom: previous.renamedFrom} : {}),
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
commits.push({
|
|
62
|
-
hash, timestamp, fileCount, ignoredFiles, invalidPaths, oversized,
|
|
63
|
-
files: oversized ? [] : [...files.values()].sort((a, b) => a.file.localeCompare(b.file)),
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
return commits
|
|
67
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import {isAbsolute} from 'node:path'
|
|
2
|
-
import {boundedInteger} from '../../util.js'
|
|
3
|
-
|
|
4
|
-
export const GIT_HISTORY_V = 1
|
|
5
|
-
export const GIT_HISTORY_WINDOWS = Object.freeze([3, 6, 12])
|
|
6
|
-
export const GIT_HISTORY_DEFAULTS = Object.freeze({
|
|
7
|
-
months: 6,
|
|
8
|
-
maxCommits: 500,
|
|
9
|
-
maxFilesPerCommit: 80,
|
|
10
|
-
maxPairs: 100,
|
|
11
|
-
minPairCount: 2,
|
|
12
|
-
maxPairCandidates: 100_000,
|
|
13
|
-
maxOutputBytes: 16 * 1024 * 1024,
|
|
14
|
-
timeoutMs: 20_000,
|
|
15
|
-
})
|
|
16
|
-
export const GIT_HISTORY_HARD_CAPS = Object.freeze({
|
|
17
|
-
maxCommits: 2_000,
|
|
18
|
-
maxFilesPerCommit: 200,
|
|
19
|
-
maxPairs: 500,
|
|
20
|
-
maxPairCandidates: 250_000,
|
|
21
|
-
maxOutputBytes: 64 * 1024 * 1024,
|
|
22
|
-
timeoutMs: 60_000,
|
|
23
|
-
})
|
|
24
|
-
export const GIT_FORMAT = '%x1e%H%x1f%ct'
|
|
25
|
-
|
|
26
|
-
export const graphEndpoint = (value) => value && typeof value === 'object' ? value.id : value
|
|
27
|
-
export const roundHistoryNumber = (value, digits = 4) => {
|
|
28
|
-
if (!Number.isFinite(value)) return 0
|
|
29
|
-
const scale = 10 ** digits
|
|
30
|
-
return Math.round(value * scale) / scale
|
|
31
|
-
}
|
|
32
|
-
export const boundedHistoryInteger = boundedInteger
|
|
33
|
-
|
|
34
|
-
export function safeHistoryPath(value) {
|
|
35
|
-
const path = String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
|
|
36
|
-
if (!path || path.includes('\0') || isAbsolute(path) || /^[a-z]:\//i.test(path)) return null
|
|
37
|
-
if (/[\x00-\x1f\x7f]/.test(path)) return null
|
|
38
|
-
const parts = path.split('/')
|
|
39
|
-
return parts.some((part) => !part || part === '.' || part === '..') ? null : path
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function utcMonthsBefore(date, months) {
|
|
43
|
-
const source = new Date(date)
|
|
44
|
-
if (!Number.isFinite(source.getTime())) throw new Error('now must be a valid date')
|
|
45
|
-
const targetMonth = source.getUTCMonth() - months
|
|
46
|
-
const first = new Date(Date.UTC(source.getUTCFullYear(), targetMonth, 1, source.getUTCHours(), source.getUTCMinutes(), source.getUTCSeconds(), source.getUTCMilliseconds()))
|
|
47
|
-
const lastDay = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate()
|
|
48
|
-
first.setUTCDate(Math.min(source.getUTCDate(), lastDay))
|
|
49
|
-
return first
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function normalizeGitHistoryOptions(input = {}) {
|
|
53
|
-
const defaults = GIT_HISTORY_DEFAULTS, caps = GIT_HISTORY_HARD_CAPS
|
|
54
|
-
const months = Number(input.months ?? defaults.months)
|
|
55
|
-
if (!GIT_HISTORY_WINDOWS.includes(months)) throw new Error('months must be one of 3, 6 or 12')
|
|
56
|
-
return {
|
|
57
|
-
months,
|
|
58
|
-
maxCommits: boundedHistoryInteger(input.maxCommits, defaults.maxCommits, 1, caps.maxCommits),
|
|
59
|
-
maxFilesPerCommit: boundedHistoryInteger(input.maxFilesPerCommit, defaults.maxFilesPerCommit, 2, caps.maxFilesPerCommit),
|
|
60
|
-
maxPairs: boundedHistoryInteger(input.maxPairs, defaults.maxPairs, 1, caps.maxPairs),
|
|
61
|
-
minPairCount: boundedHistoryInteger(input.minPairCount, defaults.minPairCount, 1, 100),
|
|
62
|
-
maxPairCandidates: boundedHistoryInteger(input.maxPairCandidates, defaults.maxPairCandidates, 100, caps.maxPairCandidates),
|
|
63
|
-
maxOutputBytes: boundedHistoryInteger(input.maxOutputBytes, defaults.maxOutputBytes, 64 * 1024, caps.maxOutputBytes),
|
|
64
|
-
timeoutMs: boundedHistoryInteger(input.timeoutMs, defaults.timeoutMs, 1_000, caps.timeoutMs),
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
// Public facade and bounded local collector for behavioral Git-history intelligence.
|
|
2
|
-
import {childProcessEnv} from '../child-env.js'
|
|
3
|
-
import {createRepoBoundary} from '../repo-path.js'
|
|
4
|
-
import {loadWeavatrixIgnore} from '../path-ignore.js'
|
|
5
|
-
import {buildGitHistoryAnalytics} from './git-history/analytics.js'
|
|
6
|
-
import {parseGitNumstatLog} from './git-history/collector.js'
|
|
7
|
-
import {boundedGitCommand} from '../git-exec.js'
|
|
8
|
-
import {
|
|
9
|
-
boundedHistoryInteger,
|
|
10
|
-
GIT_FORMAT,
|
|
11
|
-
GIT_HISTORY_V,
|
|
12
|
-
normalizeGitHistoryOptions,
|
|
13
|
-
utcMonthsBefore,
|
|
14
|
-
} from './git-history/options.js'
|
|
15
|
-
|
|
16
|
-
export {buildGitHistoryAnalytics} from './git-history/analytics.js'
|
|
17
|
-
export {parseGitNumstatLog} from './git-history/collector.js'
|
|
18
|
-
export {GIT_HISTORY_V, GIT_HISTORY_WINDOWS} from './git-history/options.js'
|
|
19
|
-
|
|
20
|
-
const unavailableResult = (window, limits, reason) => ({
|
|
21
|
-
gitHistoryV: GIT_HISTORY_V,
|
|
22
|
-
status: 'unavailable',
|
|
23
|
-
window,
|
|
24
|
-
limits,
|
|
25
|
-
completeness: {complete: false, reasons: [reason]},
|
|
26
|
-
totals: {
|
|
27
|
-
commitsRead: 0, commitsAnalyzed: 0, oversizedCommitsSkipped: 0, files: 0,
|
|
28
|
-
additions: 0, deletions: 0, churn: 0, binaryChanges: 0, ignoredFiles: 0,
|
|
29
|
-
invalidPaths: 0, graphFiles: 0,
|
|
30
|
-
},
|
|
31
|
-
fileChurn: [],
|
|
32
|
-
hotspots: [],
|
|
33
|
-
coupling: {eligibleCommits: 0, totalCandidates: 0, candidatesTruncated: false, observed: [], expectedTestSource: [], hidden: []},
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
export async function analyzeGitHistory(input = {}) {
|
|
37
|
-
const options = normalizeGitHistoryOptions(input)
|
|
38
|
-
const now = new Date(input.now ?? Date.now())
|
|
39
|
-
if (!Number.isFinite(now.getTime())) throw new Error('now must be a valid date')
|
|
40
|
-
const since = utcMonthsBefore(now, options.months)
|
|
41
|
-
const window = {months: options.months, since: since.toISOString(), until: now.toISOString()}
|
|
42
|
-
const limits = {
|
|
43
|
-
maxCommits: options.maxCommits,
|
|
44
|
-
maxFilesPerCommit: options.maxFilesPerCommit,
|
|
45
|
-
maxPairs: options.maxPairs,
|
|
46
|
-
minPairCount: options.minPairCount,
|
|
47
|
-
maxPairCandidates: options.maxPairCandidates,
|
|
48
|
-
}
|
|
49
|
-
const boundary = createRepoBoundary(input.repoRoot)
|
|
50
|
-
if (!boundary.root) return unavailableResult(window, limits, 'repository root is unavailable')
|
|
51
|
-
const args = [
|
|
52
|
-
'log', '--no-merges', '--numstat', '-z', `--format=${GIT_FORMAT}`,
|
|
53
|
-
`--since=${window.since}`, `--until=${window.until}`, `--max-count=${options.maxCommits + 1}`,
|
|
54
|
-
'--', '.',
|
|
55
|
-
]
|
|
56
|
-
let execution
|
|
57
|
-
try {
|
|
58
|
-
execution = await (input.runner || boundedGitCommand)('git', args, {
|
|
59
|
-
cwd: boundary.root,
|
|
60
|
-
env: childProcessEnv(),
|
|
61
|
-
timeoutMs: options.timeoutMs,
|
|
62
|
-
maxOutputBytes: options.maxOutputBytes,
|
|
63
|
-
})
|
|
64
|
-
} catch (error) {
|
|
65
|
-
return unavailableResult(window, limits, String(error?.message || 'git history collection failed').slice(0, 200))
|
|
66
|
-
}
|
|
67
|
-
if (execution.exitCode !== 0 && !execution.truncated) return unavailableResult(window, limits, 'git log failed')
|
|
68
|
-
const raw = Buffer.isBuffer(execution.stdout) ? execution.stdout : Buffer.from(String(execution.stdout || ''))
|
|
69
|
-
const tooLarge = raw.length > options.maxOutputBytes
|
|
70
|
-
const truncated = Boolean(execution.truncated || tooLarge)
|
|
71
|
-
const bounded = tooLarge ? raw.subarray(0, options.maxOutputBytes) : raw
|
|
72
|
-
const parsed = parseGitNumstatLog(bounded, {
|
|
73
|
-
maxFilesPerCommit: options.maxFilesPerCommit,
|
|
74
|
-
ignoreRules: loadWeavatrixIgnore(boundary.root),
|
|
75
|
-
dropLastIncomplete: truncated,
|
|
76
|
-
})
|
|
77
|
-
const commitsTruncated = parsed.length > options.maxCommits
|
|
78
|
-
return buildGitHistoryAnalytics({
|
|
79
|
-
commits: parsed.slice(0, options.maxCommits),
|
|
80
|
-
graph: input.graph || {},
|
|
81
|
-
window,
|
|
82
|
-
limits,
|
|
83
|
-
status: truncated || commitsTruncated ? 'partial' : 'complete',
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function formatGitHistoryAnalytics(result, options = {}) {
|
|
88
|
-
const topN = boundedHistoryInteger(options.topN, 10, 1, 50)
|
|
89
|
-
if (!result || result.status === 'unavailable') {
|
|
90
|
-
return `Git history intelligence: UNAVAILABLE — ${result?.completeness?.reasons?.[0] || 'history is unavailable'}`
|
|
91
|
-
}
|
|
92
|
-
const window = result.window
|
|
93
|
-
? `${result.window.months} months (${result.window.since.slice(0, 10)} → ${result.window.until.slice(0, 10)})`
|
|
94
|
-
: 'configured window'
|
|
95
|
-
const lines = [
|
|
96
|
-
`Git history intelligence — ${window}`,
|
|
97
|
-
`Status: ${String(result.status).toUpperCase()} · ${result.totals.commitsAnalyzed}/${result.totals.commitsRead} commits analyzed · ${result.totals.files} files · ${result.totals.churn} changed lines`,
|
|
98
|
-
'',
|
|
99
|
-
'Hotspots (churn percentile × graph-connectivity percentile):',
|
|
100
|
-
]
|
|
101
|
-
const hotspots = result.hotspots.slice(0, topN)
|
|
102
|
-
if (!hotspots.length) lines.push('- none')
|
|
103
|
-
for (const item of hotspots) lines.push(`- ${item.file}: score ${item.hotspotScore.toFixed(4)} · churn ${item.churn} in ${item.commits} commits · connectivity ${item.connectivity}`)
|
|
104
|
-
lines.push('', 'Hidden co-change coupling (no graph path within 2 hops):')
|
|
105
|
-
const hidden = result.coupling.hidden.slice(0, topN)
|
|
106
|
-
if (!hidden.length) lines.push('- none')
|
|
107
|
-
for (const pair of hidden) lines.push(`- ${pair.left} ↔ ${pair.right}: ${pair.count} commits · Jaccard ${pair.jaccard.toFixed(4)} · lift ${pair.lift.toFixed(4)} · confidence ${pair.confidence.toFixed(4)}`)
|
|
108
|
-
lines.push('', 'Expected test/source co-change:')
|
|
109
|
-
const expected = result.coupling.expectedTestSource.slice(0, topN)
|
|
110
|
-
if (!expected.length) lines.push('- none')
|
|
111
|
-
for (const pair of expected) lines.push(`- ${pair.test} ↔ ${pair.source}: ${pair.count} commits · confidence ${pair.confidence.toFixed(4)}`)
|
|
112
|
-
if (result.completeness.reasons.length) lines.push('', `Partial: ${result.completeness.reasons.join('; ')}.`)
|
|
113
|
-
return lines.join('\n')
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function boundGitHistoryAnalytics(result, options = {}) {
|
|
117
|
-
const topN = boundedHistoryInteger(options.topN, 10, 1, 50)
|
|
118
|
-
const source = result && typeof result === 'object' ? result : {}
|
|
119
|
-
const coupling = source.coupling && typeof source.coupling === 'object' ? source.coupling : {}
|
|
120
|
-
const collections = {}
|
|
121
|
-
const cap = (name, value) => {
|
|
122
|
-
const items = Array.isArray(value) ? value : []
|
|
123
|
-
const bounded = items.slice(0, topN)
|
|
124
|
-
collections[name] = {total: items.length, returned: bounded.length, truncated: items.length > bounded.length}
|
|
125
|
-
return bounded
|
|
126
|
-
}
|
|
127
|
-
const bounded = {
|
|
128
|
-
...source,
|
|
129
|
-
limits: {...(source.limits || {}), topN},
|
|
130
|
-
fileChurn: cap('fileChurn', source.fileChurn),
|
|
131
|
-
hotspots: cap('hotspots', source.hotspots),
|
|
132
|
-
coupling: {
|
|
133
|
-
...coupling,
|
|
134
|
-
observed: cap('coupling.observed', coupling.observed),
|
|
135
|
-
expectedTestSource: cap('coupling.expectedTestSource', coupling.expectedTestSource),
|
|
136
|
-
hidden: cap('coupling.hidden', coupling.hidden),
|
|
137
|
-
},
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
result: bounded,
|
|
141
|
-
page: {limit: topN, truncated: Object.values(collections).some((entry) => entry.truncated), collections},
|
|
142
|
-
}
|
|
143
|
-
}
|