weavatrix 0.2.15 → 0.2.18
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/README.md +99 -13
- package/docs/adr/0001-v0.3-offline-online-split.md +58 -0
- package/docs/releases/v0.2.18.md +41 -0
- package/package.json +7 -2
- package/src/analysis/cargo-dependency-evidence.js +105 -0
- package/src/analysis/cargo-manifests.js +74 -0
- package/src/analysis/dep-check-ecosystems.js +3 -0
- package/src/analysis/dependency/scoped-dependencies.js +13 -7
- package/src/analysis/duplicates.tokenize.js +13 -6
- package/src/analysis/endpoints-java.js +2 -5
- package/src/analysis/findings.js +12 -0
- package/src/analysis/git-history/options.js +2 -4
- package/src/analysis/go-dependency-evidence.js +62 -0
- package/src/analysis/health-capabilities.js +1 -1
- package/src/analysis/hot-path-review.js +2 -4
- package/src/analysis/http-contracts/analysis.js +2 -0
- package/src/analysis/http-contracts/client-call-detection.js +1 -0
- package/src/analysis/http-contracts/client-call-parser.js +18 -4
- package/src/analysis/http-contracts/shared.js +3 -4
- package/src/analysis/internal-audit/dependency-health.js +15 -13
- package/src/analysis/internal-audit/python-manifests.js +18 -5
- package/src/analysis/internal-audit/repo-files.js +5 -6
- package/src/analysis/internal-audit/supply-chain.js +2 -0
- package/src/analysis/internal-audit.reach.js +14 -0
- package/src/analysis/internal-audit.run.js +15 -10
- package/src/analysis/jvm-dependency-evidence.js +102 -56
- package/src/analysis/jvm-manifests.js +114 -0
- package/src/analysis/manifests.js +0 -49
- package/src/analysis/source-correctness.js +2 -5
- package/src/analysis/structure/dependency-graph.js +3 -8
- package/src/analysis/transport-contracts.js +157 -0
- package/src/graph/builder/lang-go.js +4 -2
- package/src/graph/builder/lang-java.js +12 -3
- package/src/graph/builder/lang-rust.js +22 -3
- package/src/graph/freshness-probe.js +1 -1
- package/src/graph/internal-builder.build.js +2 -2
- package/src/graph/internal-builder.langs.js +19 -0
- package/src/graph/internal-builder.resolvers.js +18 -8
- package/src/graph/node-id.js +7 -0
- package/src/mcp/actions/advisories.mjs +2 -3
- package/src/mcp/architecture-bootstrap.mjs +3 -3
- package/src/mcp/catalog.mjs +7 -4
- package/src/mcp/company-contract-verdict.mjs +42 -0
- package/src/mcp/evidence-snapshot.common.mjs +1 -3
- package/src/mcp/evidence-snapshot.health.mjs +11 -4
- package/src/mcp/evidence-snapshot.mjs +1 -1
- package/src/mcp/graph-diff.mjs +2 -1
- package/src/mcp/tools-architecture.mjs +2 -6
- package/src/mcp/tools-company.mjs +28 -53
- package/src/mcp/tools-impact-change.mjs +3 -8
- package/src/mcp/tools-impact-precision.mjs +89 -0
- package/src/mcp/tools-impact.mjs +52 -94
- package/src/precision/lsp-overlay/build.js +10 -0
- package/src/precision/lsp-overlay/contract.js +1 -1
- package/src/precision/symbol-query.js +39 -0
- package/src/precision/typescript-provider/client.js +16 -3
- package/src/precision/typescript-provider/discovery.js +1 -1
- package/src/precision/typescript-provider/isolated-runtime.js +39 -0
- package/src/precision/typescript-provider/project-host.js +11 -6
- package/src/precision/typescript-provider/project-safety.js +5 -0
- package/src/security/advisory-store.js +2 -2
- package/src/security/installed-jvm-rust.js +46 -0
- package/src/security/installed.js +21 -1
- package/src/security/typosquat.js +15 -6
- package/src/util.js +13 -0
- package/docs/releases/v0.2.15.md +0 -37
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {isAbsolute} from 'node:path'
|
|
2
|
+
import {boundedInteger} from '../../util.js'
|
|
2
3
|
|
|
3
4
|
export const GIT_HISTORY_V = 1
|
|
4
5
|
export const GIT_HISTORY_WINDOWS = Object.freeze([3, 6, 12])
|
|
@@ -28,10 +29,7 @@ export const roundHistoryNumber = (value, digits = 4) => {
|
|
|
28
29
|
const scale = 10 ** digits
|
|
29
30
|
return Math.round(value * scale) / scale
|
|
30
31
|
}
|
|
31
|
-
export const boundedHistoryInteger =
|
|
32
|
-
const number = Number(value)
|
|
33
|
-
return Number.isInteger(number) ? Math.max(min, Math.min(max, number)) : fallback
|
|
34
|
-
}
|
|
32
|
+
export const boundedHistoryInteger = boundedInteger
|
|
35
33
|
|
|
36
34
|
export function safeHistoryPath(value) {
|
|
37
35
|
const path = String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createRepoBoundary } from "../repo-path.js";
|
|
2
|
+
import { computeGoDepFindings } from "./dep-check-ecosystems.js";
|
|
3
|
+
import { listRepoFiles, readRepoText } from "./internal-audit.collect.js";
|
|
4
|
+
import { parseGoMod } from "./manifests.js";
|
|
5
|
+
import { makeFinding } from "./findings.js";
|
|
6
|
+
import { dependencyScopeOwnsFile, dependencyScopeRoot } from "./dependency/scoped-dependencies.js";
|
|
7
|
+
|
|
8
|
+
export function collectGoDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot), externalImports = [], nonRuntimeRoots = [] } = {}) {
|
|
9
|
+
const boundary = createRepoBoundary(repoRoot);
|
|
10
|
+
const manifests = files.filter((file) => /(^|\/)go\.mod$/i.test(file));
|
|
11
|
+
const scopes = manifests.map((file) => ({ file, root: dependencyScopeRoot(file), parsed: parseGoMod(readRepoText(boundary, file)) }))
|
|
12
|
+
.sort((left, right) => right.root.length - left.root.length || left.root.localeCompare(right.root));
|
|
13
|
+
const importsByScope = new Map(scopes.map((scope) => [scope, []]));
|
|
14
|
+
for (const entry of externalImports) {
|
|
15
|
+
if (entry.ecosystem !== "Go") continue;
|
|
16
|
+
const owner = scopes.find((scope) => dependencyScopeOwnsFile(scope.root, entry.file));
|
|
17
|
+
if (owner) importsByScope.get(owner).push(entry);
|
|
18
|
+
}
|
|
19
|
+
const findings = [], declared = new Set(), issues = [];
|
|
20
|
+
if (!scopes.length) {
|
|
21
|
+
const missing = new Map();
|
|
22
|
+
for (const entry of externalImports.filter((item) => item.ecosystem === "Go" && item.pkg && !item.builtin && !item.unresolved)) {
|
|
23
|
+
if (!missing.has(entry.pkg)) missing.set(entry.pkg, entry);
|
|
24
|
+
}
|
|
25
|
+
for (const [name, entry] of missing) findings.push(makeFinding({
|
|
26
|
+
category: "unused", rule: "missing-dep", severity: "low", confidence: "high",
|
|
27
|
+
title: `Go import without a module manifest: ${name}`,
|
|
28
|
+
reason: "An indexed external Go import exists, but no go.mod was discovered.",
|
|
29
|
+
detail: `"${entry.spec || name}" is imported by ${entry.file}, but no go.mod exists in the repository.`,
|
|
30
|
+
package: name, file: entry.file, line: entry.line || 0, source: "internal",
|
|
31
|
+
verification: { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: "NOT_PRESENT" }, indexedSourceImports: { status: "FOUND", count: 1, files: [entry.file] }, mapping: "Go module prefix" },
|
|
32
|
+
fixHint: "initialize or restore the owning Go module manifest",
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
for (const scope of scopes) {
|
|
36
|
+
if (!scope.parsed.module) issues.push(`${scope.file}: module directive is missing or unreadable`);
|
|
37
|
+
const result = computeGoDepFindings({ externalImports: importsByScope.get(scope), goMod: scope.parsed, nonRuntimeRoots });
|
|
38
|
+
findings.push(...result.findings.map((finding) => ({
|
|
39
|
+
...finding,
|
|
40
|
+
manifest: scope.file,
|
|
41
|
+
verification: finding.rule === "missing-dep"
|
|
42
|
+
? { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: "NOT_FOUND", file: scope.file }, indexedSourceImports: { status: "FOUND", count: finding.evidence?.length || 1, files: (finding.evidence || []).map((item) => item.file) }, mapping: "longest go.mod module prefix" }
|
|
43
|
+
: { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "REVIEW_REQUIRED", manifestDeclaration: { status: "FOUND", file: scope.file }, indexedSourceImports: { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0, files: [] }, mapping: "longest go.mod module prefix" },
|
|
44
|
+
})));
|
|
45
|
+
for (const name of result.declared) declared.add(`${scope.root || "."}:${name}`);
|
|
46
|
+
}
|
|
47
|
+
const present = manifests.length > 0;
|
|
48
|
+
return {
|
|
49
|
+
present,
|
|
50
|
+
status: present ? "CHECKED" : "NOT_PRESENT",
|
|
51
|
+
completeness: present ? (issues.length ? "PARTIAL" : "COMPLETE") : "NOT_APPLICABLE",
|
|
52
|
+
manifests,
|
|
53
|
+
declared,
|
|
54
|
+
findings,
|
|
55
|
+
reasons: issues,
|
|
56
|
+
reason: !present
|
|
57
|
+
? "No go.mod was discovered."
|
|
58
|
+
: issues.length
|
|
59
|
+
? `Go imports and requirements were checked across ${manifests.length} module(s), but ${issues.length} module descriptor(s) were incomplete.`
|
|
60
|
+
: `Every discovered go.mod scope (${manifests.length}) was compared with indexed Go imports, including direct/indirect requirements and replace directives.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -38,7 +38,7 @@ export function buildHealthCapabilityMatrix({
|
|
|
38
38
|
const unsupportedDependencyRows = ecosystemRows.filter((item) => item.status === "NOT_SUPPORTED");
|
|
39
39
|
const noDependencyEvidence = ecosystemRows.length === 0;
|
|
40
40
|
const onlyUnsupportedDependencies = unsupportedDependencyRows.length > 0 && supportedDependencyRows.length === 0;
|
|
41
|
-
const advisorySupported = supportedDependencyRows.some((item) => ["npm", "go", "python"].includes(item.ecosystem));
|
|
41
|
+
const advisorySupported = supportedDependencyRows.some((item) => ["npm", "go", "python", "rust", "maven", "gradle"].includes(item.ecosystem));
|
|
42
42
|
const malwareSupported = supportedDependencyRows.some((item) => ["npm", "go", "python"].includes(item.ecosystem));
|
|
43
43
|
const coverageSupported = [...languages].some((language) => ["javascript/typescript", "python", "go"].includes(language));
|
|
44
44
|
const runtimeFiles = Number(correctnessCoverage.runtimeCorrectnessFiles || 0);
|
|
@@ -5,14 +5,12 @@ import {readCoverageForRepo, normalizeRepoParts} from './coverage-reports.js'
|
|
|
5
5
|
import {computeStaticTestReachability} from './static-test-reachability.js'
|
|
6
6
|
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
7
7
|
import {isStructuralRelation} from '../graph/relations.js'
|
|
8
|
+
import {boundedInteger} from '../util.js'
|
|
8
9
|
|
|
9
10
|
const NON_PRODUCT = ['generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
10
11
|
const endpoint = (value) => String(value && typeof value === 'object' ? value.id : value || '')
|
|
11
12
|
const normalize = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
|
|
12
|
-
const boundedInt =
|
|
13
|
-
const number = Number(value)
|
|
14
|
-
return Number.isInteger(number) ? Math.max(min, Math.min(max, number)) : fallback
|
|
15
|
-
}
|
|
13
|
+
const boundedInt = boundedInteger
|
|
16
14
|
const round = (value) => Math.round(Number(value || 0) * 100) / 100
|
|
17
15
|
const clamp = (value, min = 0, max = 100) => Math.max(min, Math.min(max, Number(value) || 0))
|
|
18
16
|
|
|
@@ -67,6 +67,7 @@ export function analyzeHttpContracts(input = {}) {
|
|
|
67
67
|
autoDiscoverWrappers: descriptor.autoDiscoverWrappers ?? input.autoDiscoverWrappers,
|
|
68
68
|
graph: descriptor.graph,
|
|
69
69
|
includeTests: descriptor.includeTests ?? input.includeTests,
|
|
70
|
+
runtimeValues: descriptor.runtimeValues || input.runtimeValues,
|
|
70
71
|
});
|
|
71
72
|
if (detected.truncated) completeness.push(`${id}: client scan cap reached`);
|
|
72
73
|
for (const reason of detected.reasons || []) completeness.push(`${id}: ${reason}`);
|
|
@@ -141,6 +142,7 @@ export function analyzeHttpContracts(input = {}) {
|
|
|
141
142
|
})))
|
|
142
143
|
.sort((left, right) => left.clientRepo.localeCompare(right.clientRepo) || left.file.localeCompare(right.file) || left.line - right.line);
|
|
143
144
|
if (uncertainAll.length > limits.maxUncertain) completeness.push("uncertain callsite cap reached");
|
|
145
|
+
if (uncertainAll.length) completeness.push(`${uncertainAll.length} dynamic HTTP callsite(s) remain UNKNOWN`);
|
|
144
146
|
if (results.some((endpoint) => !endpoint.affected.complete)) completeness.push("affected-file traversal cap reached");
|
|
145
147
|
|
|
146
148
|
return {
|
|
@@ -55,6 +55,7 @@ export function detectHttpClientCalls(repoRoot, codeFiles, options = {}) {
|
|
|
55
55
|
clientNames,
|
|
56
56
|
normalizedWrappers: [...configured, ...scopedDiscovered],
|
|
57
57
|
maxCalls: remaining,
|
|
58
|
+
runtimeValues: options.runtimeValues,
|
|
58
59
|
});
|
|
59
60
|
calls.push(...extracted.calls);
|
|
60
61
|
if (extracted.truncated) truncated = true;
|
|
@@ -91,7 +91,7 @@ function templateArgument(text, start, constants = null, requireStatic = false)
|
|
|
91
91
|
}
|
|
92
92
|
if (depth !== 0) return null;
|
|
93
93
|
const expression = text.slice(index + 2, cursor - 1).trim();
|
|
94
|
-
if (/^[A-Za-z_$][\w$]*$/.test(expression) && constants?.has(expression)) {
|
|
94
|
+
if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(expression) && constants?.has(expression)) {
|
|
95
95
|
value += constants.get(expression);
|
|
96
96
|
index = cursor - 1;
|
|
97
97
|
continue;
|
|
@@ -109,7 +109,7 @@ function templateArgument(text, start, constants = null, requireStatic = false)
|
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function extractStaticStringConstants(text) {
|
|
112
|
+
function extractStaticStringConstants(text, runtimeValues = {}) {
|
|
113
113
|
const source = String(text || "");
|
|
114
114
|
const mask = maskNonCode(source);
|
|
115
115
|
const declarations = [];
|
|
@@ -120,7 +120,8 @@ function extractStaticStringConstants(text) {
|
|
|
120
120
|
while (/\s/.test(source[start] || "")) start++;
|
|
121
121
|
declarations.push({ name: match[1], start });
|
|
122
122
|
}
|
|
123
|
-
const constants = new Map()
|
|
123
|
+
const constants = new Map(Object.entries(runtimeValues || {})
|
|
124
|
+
.filter(([key, value]) => /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(key) && typeof value === "string" && value.length <= 2_048));
|
|
124
125
|
for (const item of declarations) {
|
|
125
126
|
const quote = source[item.start];
|
|
126
127
|
if (quote !== "'" && quote !== '"') continue;
|
|
@@ -138,6 +139,19 @@ function extractStaticStringConstants(text) {
|
|
|
138
139
|
}
|
|
139
140
|
if (!changed) break;
|
|
140
141
|
}
|
|
142
|
+
for (const item of declarations) {
|
|
143
|
+
if (constants.has(item.name)) continue;
|
|
144
|
+
const rest = source.slice(item.start);
|
|
145
|
+
const expression = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*/.exec(rest)?.[0];
|
|
146
|
+
if (expression && constants.has(expression)) constants.set(item.name, constants.get(expression));
|
|
147
|
+
if (constants.has(item.name)) continue;
|
|
148
|
+
const fallback = /(?:\|\||\?\?)\s*(["'])/.exec(rest.slice(0, 500));
|
|
149
|
+
if (fallback) {
|
|
150
|
+
const start = item.start + fallback.index + fallback[0].lastIndexOf(fallback[1]);
|
|
151
|
+
const parsed = quotedArgument(source, start, fallback[1]);
|
|
152
|
+
if (parsed && parsed.value.length <= 2_048) constants.set(item.name, parsed.value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
141
155
|
return constants;
|
|
142
156
|
}
|
|
143
157
|
|
|
@@ -211,7 +225,7 @@ const escapeRegex = (value) => String(value).replace(/[|\\{}()[\]^$+*?.-]/g, "\\
|
|
|
211
225
|
export function extractHttpClientCallsFromText(text, file, options = {}) {
|
|
212
226
|
const source = String(text || "");
|
|
213
227
|
const mask = maskNonCode(source);
|
|
214
|
-
const constants = extractStaticStringConstants(source);
|
|
228
|
+
const constants = extractStaticStringConstants(source, options.runtimeValues);
|
|
215
229
|
const allowed = normalizedClientNames(options.clientNames);
|
|
216
230
|
const wrappers = normalizeHttpWrapperDescriptors(options.wrappers, "input").concat(Array.isArray(options.normalizedWrappers) ? options.normalizedWrappers : []);
|
|
217
231
|
const maxCalls = boundedInteger(options.maxCalls, HTTP_CONTRACT_DEFAULTS.maxCallsPerClient, 1, HTTP_CONTRACT_HARD_LIMITS.maxCallsPerClient);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { boundedInteger } from "../../util.js";
|
|
2
|
+
|
|
1
3
|
export const HTTP_CONTRACTS_V = 2;
|
|
2
4
|
export const HTTP_CONTRACT_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
3
5
|
|
|
@@ -38,10 +40,7 @@ export function normalizeContractFile(value) {
|
|
|
38
40
|
return normalized.split("/").some((part) => !part || part === "." || part === "..") ? "" : normalized;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
export
|
|
42
|
-
const number = Number(value);
|
|
43
|
-
return Number.isInteger(number) ? Math.max(min, Math.min(max, number)) : fallback;
|
|
44
|
-
}
|
|
43
|
+
export { boundedInteger };
|
|
45
44
|
|
|
46
45
|
export function safeContractName(value, fallback) {
|
|
47
46
|
const text = String(value || "").trim();
|
|
@@ -10,6 +10,7 @@ export function buildDependencyHealth({
|
|
|
10
10
|
goDep,
|
|
11
11
|
pyDep,
|
|
12
12
|
jvmDependencies,
|
|
13
|
+
cargoDep,
|
|
13
14
|
externalImports,
|
|
14
15
|
findings,
|
|
15
16
|
packageScopes,
|
|
@@ -20,8 +21,8 @@ export function buildDependencyHealth({
|
|
|
20
21
|
const dependencyFindings = findings.filter((finding) => ["unused-dep", "missing-dep", "duplicate-dep"].includes(finding.rule));
|
|
21
22
|
const graphComplete = !((graph.graphBuildMode && graph.graphBuildMode !== "full") || graph.graphBuildScope);
|
|
22
23
|
const npmManifests = repoFiles.filter((file) => /(^|\/)package\.json$/i.test(file));
|
|
23
|
-
const goManifests = repoFiles.filter((file) => /(^|\/)go\.mod$/i.test(file));
|
|
24
24
|
const pythonManifests = [...new Set((pyManifest.scopes || []).flatMap((scope) => scope.manifests || []))];
|
|
25
|
+
const publicEvidence = ({ findings: _findings, ...item }) => item;
|
|
25
26
|
const ecosystems = {
|
|
26
27
|
npm: {
|
|
27
28
|
ecosystem: "npm",
|
|
@@ -36,28 +37,29 @@ export function buildDependencyHealth({
|
|
|
36
37
|
},
|
|
37
38
|
go: {
|
|
38
39
|
ecosystem: "go",
|
|
39
|
-
present:
|
|
40
|
-
status:
|
|
41
|
-
completeness:
|
|
42
|
-
manifests:
|
|
40
|
+
present: goDep.present,
|
|
41
|
+
status: goDep.status,
|
|
42
|
+
completeness: graphComplete && goDep.completeness === "COMPLETE" ? "COMPLETE" : goDep.completeness,
|
|
43
|
+
manifests: goDep.manifests,
|
|
43
44
|
declared: goDep.declared.size,
|
|
44
|
-
reason:
|
|
45
|
-
? "Go dependency checks use indexed imports and the root go.mod; nested modules and build-tag-specific resolution are not fully modeled."
|
|
46
|
-
: "No go.mod was discovered.",
|
|
45
|
+
reason: goDep.reason,
|
|
47
46
|
},
|
|
48
47
|
python: {
|
|
49
48
|
ecosystem: "python",
|
|
50
49
|
present: pyManifest.present,
|
|
51
50
|
status: pyManifest.present ? "CHECKED" : "NOT_PRESENT",
|
|
52
|
-
completeness: pyManifest.present ? "PARTIAL" : "NOT_APPLICABLE",
|
|
51
|
+
completeness: pyManifest.present ? (graphComplete && pyManifest.completeness === "COMPLETE" ? "COMPLETE" : "PARTIAL") : "NOT_APPLICABLE",
|
|
53
52
|
manifests: pythonManifests,
|
|
54
53
|
declared: pyDep.declared.size,
|
|
55
54
|
reason: pyManifest.present
|
|
56
|
-
?
|
|
55
|
+
? pyManifest.reasons?.length
|
|
56
|
+
? `Python declarations were compared with indexed imports, but ${pyManifest.reasons.join("; ")}.`
|
|
57
|
+
: "Every discovered supported Python manifest scope was compared with indexed imports. Environment markers and extras are normalized as declarations; runtime-computed imports remain outside static proof."
|
|
57
58
|
: "No supported Python dependency manifest was discovered.",
|
|
58
59
|
},
|
|
59
|
-
maven: { ecosystem: "maven", ...jvmDependencies.maven },
|
|
60
|
-
gradle: { ecosystem: "gradle", ...jvmDependencies.gradle },
|
|
60
|
+
maven: { ecosystem: "maven", ...publicEvidence(jvmDependencies.maven), completeness: jvmDependencies.maven.present && !graphComplete ? "PARTIAL" : jvmDependencies.maven.completeness },
|
|
61
|
+
gradle: { ecosystem: "gradle", ...publicEvidence(jvmDependencies.gradle), completeness: jvmDependencies.gradle.present && !graphComplete ? "PARTIAL" : jvmDependencies.gradle.completeness },
|
|
62
|
+
rust: { ecosystem: "rust", ...publicEvidence(cargoDep), declared: cargoDep.declared.size, completeness: cargoDep.present && !graphComplete ? "PARTIAL" : cargoDep.completeness },
|
|
61
63
|
};
|
|
62
64
|
const present = Object.values(ecosystems).filter((item) => item.present);
|
|
63
65
|
const status = present.length === 0
|
|
@@ -68,7 +70,7 @@ export function buildDependencyHealth({
|
|
|
68
70
|
const importedPackages = new Set(externalImports
|
|
69
71
|
.filter((entry) => entry?.pkg && !entry.builtin && !entry.unresolved)
|
|
70
72
|
.map((entry) => `${entry.ecosystem || (/\.java$/i.test(entry.file || "") ? "maven-unresolved" : "npm")}:${entry.pkg}`));
|
|
71
|
-
const supportedDeclared = dep.declared.size + goDep.declared.size + pyDep.declared.size;
|
|
73
|
+
const supportedDeclared = dep.declared.size + goDep.declared.size + pyDep.declared.size + cargoDep.declared.size;
|
|
72
74
|
const jvmDeclared = jvmDependencies.maven.declared + jvmDependencies.gradle.declared;
|
|
73
75
|
const measuredCoverage = readCoverageForRepo(repoPath, sourceFiles);
|
|
74
76
|
const healthCapabilities = buildHealthCapabilityMatrix({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {dirname} from 'node:path'
|
|
1
|
+
import {dirname, posix} from 'node:path'
|
|
2
2
|
import {createRepoBoundary} from '../../repo-path.js'
|
|
3
3
|
import {parseRequirementsNames, parsePyprojectDeps, parsePipfileDeps, pep503} from '../manifests.js'
|
|
4
4
|
import {listRepoFiles, readRepoText} from './repo-files.js'
|
|
@@ -25,6 +25,8 @@ export function collectPyManifest(repoRoot) {
|
|
|
25
25
|
scope.deps.push(...parsedDeps.map((dep) => ({...dep, manifest})))
|
|
26
26
|
}
|
|
27
27
|
const files = listRepoFiles(repoRoot)
|
|
28
|
+
const fileSet = new Set(files.map((file) => file.replace(/\\/g, '/')))
|
|
29
|
+
const reasons = []
|
|
28
30
|
for (const file of files.filter((name) => /(^|\/)requirements[\w.-]*\.(?:txt|in)$/i.test(name)
|
|
29
31
|
|| /(^|\/)requirements\/[^/]+\.(?:txt|in)$/i.test(name))) {
|
|
30
32
|
const text = readRepoText(boundary, file)
|
|
@@ -33,14 +35,23 @@ export function collectPyManifest(repoRoot) {
|
|
|
33
35
|
const root = /(^|\/)requirements$/i.test(parent) ? normalizeRoot(dirname(parent)) : parent
|
|
34
36
|
const dev = /dev|test|lint|doc|ci/i.test(file.slice(file.lastIndexOf('/') + 1))
|
|
35
37
|
addManifest(root, file, parseRequirementsNames(text).map((dep) => ({...dep, dev})))
|
|
38
|
+
for (const match of text.matchAll(/^\s*(?:-r|--requirement|-c|--constraint)\s+(?:=\s*)?([^\s#]+)/gmi)) {
|
|
39
|
+
const parentDir = normalizeRoot(dirname(file))
|
|
40
|
+
const target = normalizeRoot(posix.normalize(`${parentDir ? `${parentDir}/` : ''}${match[1].replace(/\\/g, '/')}`))
|
|
41
|
+
if (!target || target.startsWith('../') || !fileSet.has(target)) reasons.push(`${file}: referenced requirements file ${match[1]} was not discovered`)
|
|
42
|
+
}
|
|
36
43
|
}
|
|
37
44
|
for (const file of files.filter((name) => /(^|\/)pyproject\.toml$/i.test(name))) {
|
|
38
|
-
const
|
|
39
|
-
|
|
45
|
+
const text = readRepoText(boundary, file)
|
|
46
|
+
const parsed = parsePyprojectDeps(text)
|
|
47
|
+
const dependencyManifest = parsed.present || /^\s*\[(?:project|tool\.poetry)\]/m.test(text || '')
|
|
48
|
+
addManifest(dirname(file), file, parsed.deps, dependencyManifest)
|
|
49
|
+
if (/\bdynamic\s*=\s*\[[^\]]*["']dependencies["']/s.test(text || '')) reasons.push(`${file}: project dependencies are dynamic`)
|
|
40
50
|
}
|
|
41
51
|
for (const file of files.filter((name) => /(^|\/)Pipfile$/i.test(name))) {
|
|
42
|
-
const
|
|
43
|
-
|
|
52
|
+
const text = readRepoText(boundary, file)
|
|
53
|
+
const parsed = parsePipfileDeps(text)
|
|
54
|
+
addManifest(dirname(file), file, parsed.deps, parsed.present || /^\s*\[(?:packages|dev-packages)\]/mi.test(text || ''))
|
|
44
55
|
}
|
|
45
56
|
const normalizedScopes = [...scopes.values()]
|
|
46
57
|
.map((scope) => {
|
|
@@ -61,5 +72,7 @@ export function collectPyManifest(repoRoot) {
|
|
|
61
72
|
present: normalizedScopes.some((scope) => scope.present),
|
|
62
73
|
deps: normalizedScopes.flatMap((scope) => scope.deps),
|
|
63
74
|
scopes: normalizedScopes,
|
|
75
|
+
completeness: reasons.length ? 'PARTIAL' : 'COMPLETE',
|
|
76
|
+
reasons: [...new Set(reasons)].sort(),
|
|
64
77
|
}
|
|
65
78
|
}
|
|
@@ -12,15 +12,14 @@ export const readJson = (path) => {
|
|
|
12
12
|
try { return JSON.parse(readFileSync(path, 'utf8')) } catch { return null }
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const readRepoFile = (boundary, relativePath, reader) => {
|
|
16
16
|
const resolved = boundary.resolve(relativePath)
|
|
17
|
-
return resolved.ok ?
|
|
17
|
+
return resolved.ok ? reader(resolved.path) : null
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
20
|
+
export const readRepoText = (boundary, relativePath) => readRepoFile(boundary, relativePath, readText)
|
|
21
|
+
|
|
22
|
+
export const readRepoJson = (boundary, relativePath) => readRepoFile(boundary, relativePath, readJson)
|
|
24
23
|
|
|
25
24
|
const SKIP_DIRS = new Set([
|
|
26
25
|
'.git', '.hg', '.svn', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', 'out',
|
|
@@ -12,6 +12,8 @@ const removalHint = (pkg) => {
|
|
|
12
12
|
if (pkg?.ecosystem === "npm") return `npm uninstall ${pkg.name} and audit what it touched`;
|
|
13
13
|
if (pkg?.ecosystem === "PyPI") return `remove ${pkg.name} from the Python environment/manifest and audit what it touched`;
|
|
14
14
|
if (pkg?.ecosystem === "Go") return `remove ${pkg.name} from the Go module graph, run go mod tidy, and audit what it touched`;
|
|
15
|
+
if (pkg?.ecosystem === "crates.io") return `cargo remove ${pkg.name}, rebuild every feature/target, and audit what it touched`;
|
|
16
|
+
if (pkg?.ecosystem === "Maven") return `remove or upgrade ${pkg.name} in Maven/Gradle, refresh the resolved graph, and audit what it touched`;
|
|
15
17
|
return `remove ${pkg?.name || "the package"} with its ecosystem's package manager and audit what it touched`;
|
|
16
18
|
};
|
|
17
19
|
|
|
@@ -118,6 +118,20 @@ export function entryFiles(graph, pkgOrScopes, dynamicTargets = new Set(), { dec
|
|
|
118
118
|
if (target) entries.add(target);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
// Drizzle Kit loads schema modules from configuration strings rather than a
|
|
122
|
+
// runtime import. Resolve the bounded `schema:` value so the schema and its
|
|
123
|
+
// exported tables are not reported as orphan/test-only production code.
|
|
124
|
+
for (const [rawFile, rawText] of sources || []) {
|
|
125
|
+
const configFile = String(rawFile || "").replace(/\\/g, "/");
|
|
126
|
+
if (!/(^|\/)drizzle(?:\.[^/]*)?\.config\.[cm]?[jt]s$/i.test(configFile)) continue;
|
|
127
|
+
const value = /\bschema\s*:\s*(\[[\s\S]{0,1000}?\]|["'][^"']+["'])/i.exec(String(rawText || ""))?.[1] || "";
|
|
128
|
+
const quoted = /["']([^"']+)["']/g;
|
|
129
|
+
let match;
|
|
130
|
+
while ((match = quoted.exec(value))) {
|
|
131
|
+
const candidate = posix.normalize(posix.join(posix.dirname(configFile), match[1].replace(/^\.\//, "")));
|
|
132
|
+
if (fileSet.has(candidate)) entries.add(candidate);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
121
135
|
for (const evidence of springConventionEntries(sources, fileSet)) {
|
|
122
136
|
entries.add(evidence.file);
|
|
123
137
|
conventionEvidence.push(evidence);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { computeDead, computeUnusedExports } from "./dead-check.js";
|
|
4
|
-
import {
|
|
4
|
+
import { computePyDepFindings, computeScopedDepFindings } from "./dep-check.js";
|
|
5
5
|
import { computeStructureFindings } from "./dep-rules.js";
|
|
6
6
|
import { makeFinding, sortFindings, summarizeFindings } from "./findings.js";
|
|
7
7
|
import { graphOutDirForRepo } from "../graph/layout.js";
|
|
@@ -15,17 +15,17 @@ import {
|
|
|
15
15
|
listRepoFiles,
|
|
16
16
|
readJson,
|
|
17
17
|
readRepoJson,
|
|
18
|
-
readRepoText,
|
|
19
18
|
workspacePkgNames,
|
|
20
19
|
} from "./internal-audit.collect.js";
|
|
21
20
|
import { computeReachability, entryFiles } from "./internal-audit.reach.js";
|
|
22
|
-
import { parseGoMod } from "./manifests.js";
|
|
23
21
|
import { PATH_CLASS_NAMES, createPathClassifier, hasPathClass } from "../path-classification.js";
|
|
24
22
|
import { createRepoBoundary } from "../repo-path.js";
|
|
25
23
|
import { analyzeSourceCorrectness } from "./source-correctness.js";
|
|
26
24
|
import { collectJvmDependencyEvidence } from "./jvm-dependency-evidence.js";
|
|
27
25
|
import { buildDependencyHealth } from "./internal-audit/dependency-health.js";
|
|
28
26
|
import { runSupplyChainChecks } from "./internal-audit/supply-chain.js";
|
|
27
|
+
import { collectGoDependencyEvidence } from "./go-dependency-evidence.js";
|
|
28
|
+
import { collectCargoDependencyEvidence } from "./cargo-dependency-evidence.js";
|
|
29
29
|
|
|
30
30
|
export async function runInternalAudit(repoPath, {
|
|
31
31
|
graph,
|
|
@@ -90,8 +90,7 @@ export async function runInternalAudit(repoPath, {
|
|
|
90
90
|
nonRuntimeRoots,
|
|
91
91
|
sourceFiles: [...sources.keys()],
|
|
92
92
|
});
|
|
93
|
-
const
|
|
94
|
-
const goDep = computeGoDepFindings({ externalImports, goMod: goModText != null ? parseGoMod(goModText) : null, nonRuntimeRoots });
|
|
93
|
+
const goDep = collectGoDependencyEvidence(repoPath, { files: repoFiles, externalImports, nonRuntimeRoots });
|
|
95
94
|
const asList = (value) => Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
96
95
|
const pyRules = rules.python || {}, depRules = rules.dependencies || {};
|
|
97
96
|
const managedPython = [...new Set([
|
|
@@ -109,15 +108,20 @@ export async function runInternalAudit(repoPath, {
|
|
|
109
108
|
ignoredDependencies: ignoredPython,
|
|
110
109
|
nonRuntimeRoots,
|
|
111
110
|
});
|
|
112
|
-
const jvmDependencies = collectJvmDependencyEvidence(repoPath, { files: repoFiles });
|
|
111
|
+
const jvmDependencies = collectJvmDependencyEvidence(repoPath, { files: repoFiles, externalImports });
|
|
112
|
+
const cargoDep = collectCargoDependencyEvidence(repoPath, { files: repoFiles, externalImports });
|
|
113
113
|
|
|
114
114
|
const externalImportFiles = new Set(externalImports.filter((entry) => entry.pkg && !entry.builtin).map((entry) => entry.file));
|
|
115
115
|
const structure = computeStructureFindings(graph, { rules, entrySet: entries, externalImportFiles });
|
|
116
116
|
const correctness = analyzeSourceCorrectness(sources, { isNonProductPath });
|
|
117
|
-
const findings = [
|
|
117
|
+
const findings = [
|
|
118
|
+
...dep.findings, ...goDep.findings, ...pyDep.findings,
|
|
119
|
+
...jvmDependencies.maven.findings, ...jvmDependencies.gradle.findings, ...cargoDep.findings,
|
|
120
|
+
...correctness.findings,
|
|
121
|
+
];
|
|
118
122
|
const deadFileSet = new Set(dead.deadFiles.map((finding) => finding.file));
|
|
119
123
|
for (const finding of structure.findings) {
|
|
120
|
-
if (!(finding.rule === "orphan-file" && deadFileSet.has(finding.file))) findings.push(finding);
|
|
124
|
+
if (!(finding.rule === "orphan-file" && (deadFileSet.has(finding.file) || entries.has(finding.file)))) findings.push(finding);
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
const actionableDeadFiles = dead.deadFiles.filter((finding) => !isNonProductPath(finding.file));
|
|
@@ -138,7 +142,7 @@ export async function runInternalAudit(repoPath, {
|
|
|
138
142
|
}
|
|
139
143
|
let unusedExportCount = 0;
|
|
140
144
|
for (const symbol of unusedExports) {
|
|
141
|
-
if (symbol.test || isNonProductPath(symbol.file)) continue;
|
|
145
|
+
if (symbol.test || isNonProductPath(symbol.file) || entries.has(symbol.file) || dynamicTargets.has(symbol.file)) continue;
|
|
142
146
|
if (/(^|\/)[^/]*\.config\.[a-z0-9]+$|(^|\/)\.[^/]+rc(\.[a-z]+)?$/i.test(symbol.file)) continue;
|
|
143
147
|
findings.push(makeFinding({
|
|
144
148
|
category: "unused",
|
|
@@ -155,7 +159,7 @@ export async function runInternalAudit(repoPath, {
|
|
|
155
159
|
unusedExportCount++;
|
|
156
160
|
}
|
|
157
161
|
for (const symbol of dead.testOnlySymbols || []) {
|
|
158
|
-
if (isNonProductPath(symbol.file)) continue;
|
|
162
|
+
if (isNonProductPath(symbol.file) || entries.has(symbol.file) || dynamicTargets.has(symbol.file)) continue;
|
|
159
163
|
findings.push(makeFinding({
|
|
160
164
|
category: "unused",
|
|
161
165
|
rule: "test-only-symbol",
|
|
@@ -190,6 +194,7 @@ export async function runInternalAudit(repoPath, {
|
|
|
190
194
|
goDep,
|
|
191
195
|
pyDep,
|
|
192
196
|
jvmDependencies,
|
|
197
|
+
cargoDep,
|
|
193
198
|
externalImports,
|
|
194
199
|
findings: sorted,
|
|
195
200
|
packageScopes,
|