weavatrix 0.2.14 → 0.2.16

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.
Files changed (65) hide show
  1. package/README.md +69 -13
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.16.md +53 -0
  4. package/package.json +6 -2
  5. package/src/analysis/cargo-dependency-evidence.js +111 -0
  6. package/src/analysis/cargo-manifests.js +74 -0
  7. package/src/analysis/dep-check-ecosystems.js +3 -0
  8. package/src/analysis/dep-check.js +8 -18
  9. package/src/analysis/dependency/conventions.js +16 -0
  10. package/src/analysis/dependency/scoped-dependencies.js +7 -2
  11. package/src/analysis/dependency/source-references.js +21 -0
  12. package/src/analysis/duplicates.tokenize.js +12 -2
  13. package/src/analysis/endpoints-java.js +2 -5
  14. package/src/analysis/findings.js +12 -0
  15. package/src/analysis/go-dependency-evidence.js +68 -0
  16. package/src/analysis/health-capabilities.js +1 -1
  17. package/src/analysis/http-contracts/analysis.js +2 -0
  18. package/src/analysis/http-contracts/client-call-detection.js +1 -0
  19. package/src/analysis/http-contracts/client-call-parser.js +18 -4
  20. package/src/analysis/internal-audit/dependency-health.js +15 -13
  21. package/src/analysis/internal-audit/python-manifests.js +18 -5
  22. package/src/analysis/internal-audit/supply-chain.js +2 -0
  23. package/src/analysis/internal-audit.reach.js +20 -0
  24. package/src/analysis/internal-audit.run.js +13 -7
  25. package/src/analysis/jvm-dependency-evidence.js +102 -56
  26. package/src/analysis/jvm-manifests.js +114 -0
  27. package/src/analysis/source-correctness.js +2 -5
  28. package/src/analysis/task-retrieval.js +3 -14
  29. package/src/analysis/transport-contracts.js +157 -0
  30. package/src/graph/builder/lang-go.js +4 -2
  31. package/src/graph/builder/lang-java.js +12 -3
  32. package/src/graph/builder/lang-rust.js +22 -3
  33. package/src/graph/freshness-probe.js +1 -1
  34. package/src/graph/internal-builder.build.js +2 -2
  35. package/src/graph/internal-builder.resolvers.js +18 -8
  36. package/src/mcp/actions/advisories.mjs +2 -3
  37. package/src/mcp/catalog.mjs +7 -4
  38. package/src/mcp/company-contract-verdict.mjs +42 -0
  39. package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
  40. package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
  41. package/src/mcp/git-output.mjs +10 -0
  42. package/src/mcp/graph/context-seeds.mjs +3 -14
  43. package/src/mcp/graph/reverse-reach.mjs +42 -0
  44. package/src/mcp/sync/evidence-duplicates.mjs +1 -4
  45. package/src/mcp/tools-company.mjs +28 -53
  46. package/src/mcp/tools-impact-change.mjs +2 -38
  47. package/src/mcp/tools-impact-precision.mjs +89 -0
  48. package/src/mcp/tools-impact.mjs +53 -136
  49. package/src/path-classification.js +14 -0
  50. package/src/precision/lsp-overlay/build.js +10 -0
  51. package/src/precision/lsp-overlay/contract.js +1 -1
  52. package/src/precision/symbol-query.js +39 -0
  53. package/src/precision/typescript-provider/client.js +16 -3
  54. package/src/precision/typescript-provider/discovery.js +1 -1
  55. package/src/precision/typescript-provider/isolated-runtime.js +39 -0
  56. package/src/precision/typescript-provider/project-host.js +11 -6
  57. package/src/precision/typescript-provider/project-safety.js +5 -0
  58. package/src/security/advisory-store.js +2 -2
  59. package/src/security/installed-jvm-rust.js +46 -0
  60. package/src/security/installed.js +21 -1
  61. package/src/security/malware-heuristics.sweep.js +1 -1
  62. package/src/security/registry-sig.classify.js +2 -1
  63. package/src/security/registry-sig.rules.js +3 -3
  64. package/src/util.js +8 -0
  65. package/docs/releases/v0.2.14.md +0 -93
@@ -6,6 +6,18 @@ import { createHash } from "node:crypto";
6
6
  export const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
7
7
  export const FINDING_CATEGORIES = ["unused", "structure", "vulnerability", "malware"];
8
8
 
9
+ export function dependencyVerification(manifest, imports, decision, mapping) {
10
+ return {
11
+ evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
12
+ decision,
13
+ manifestDeclaration: { status: "FOUND", file: manifest },
14
+ indexedSourceImports: imports.length
15
+ ? { status: "FOUND", count: imports.length, files: [...new Set(imports.map((item) => item.file))].slice(0, 10) }
16
+ : { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0, files: [] },
17
+ mapping,
18
+ };
19
+ }
20
+
9
21
  // Stable id: survives re-runs so the UI can persist expand/dismiss state per finding.
10
22
  export function makeFinding(f) {
11
23
  const cycleIdentity = Array.isArray(f.cycleMembers)
@@ -0,0 +1,68 @@
1
+ import { dirname } from "node:path";
2
+ import { createRepoBoundary } from "../repo-path.js";
3
+ import { computeGoDepFindings } from "./dep-check-ecosystems.js";
4
+ import { listRepoFiles, readRepoText } from "./internal-audit.collect.js";
5
+ import { parseGoMod } from "./manifests.js";
6
+ import { makeFinding } from "./findings.js";
7
+
8
+ const rootOf = (file) => {
9
+ const root = dirname(String(file || "").replace(/\\/g, "/"));
10
+ return root === "." ? "" : root;
11
+ };
12
+ const owns = (root, file) => !root || file === root || String(file || "").replace(/\\/g, "/").startsWith(`${root}/`);
13
+
14
+ export function collectGoDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot), externalImports = [], nonRuntimeRoots = [] } = {}) {
15
+ const boundary = createRepoBoundary(repoRoot);
16
+ const manifests = files.filter((file) => /(^|\/)go\.mod$/i.test(file));
17
+ const scopes = manifests.map((file) => ({ file, root: rootOf(file), parsed: parseGoMod(readRepoText(boundary, file)) }))
18
+ .sort((left, right) => right.root.length - left.root.length || left.root.localeCompare(right.root));
19
+ const importsByScope = new Map(scopes.map((scope) => [scope, []]));
20
+ for (const entry of externalImports) {
21
+ if (entry.ecosystem !== "Go") continue;
22
+ const owner = scopes.find((scope) => owns(scope.root, entry.file));
23
+ if (owner) importsByScope.get(owner).push(entry);
24
+ }
25
+ const findings = [], declared = new Set(), issues = [];
26
+ if (!scopes.length) {
27
+ const missing = new Map();
28
+ for (const entry of externalImports.filter((item) => item.ecosystem === "Go" && item.pkg && !item.builtin && !item.unresolved)) {
29
+ if (!missing.has(entry.pkg)) missing.set(entry.pkg, entry);
30
+ }
31
+ for (const [name, entry] of missing) findings.push(makeFinding({
32
+ category: "unused", rule: "missing-dep", severity: "low", confidence: "high",
33
+ title: `Go import without a module manifest: ${name}`,
34
+ reason: "An indexed external Go import exists, but no go.mod was discovered.",
35
+ detail: `"${entry.spec || name}" is imported by ${entry.file}, but no go.mod exists in the repository.`,
36
+ package: name, file: entry.file, line: entry.line || 0, source: "internal",
37
+ 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" },
38
+ fixHint: "initialize or restore the owning Go module manifest",
39
+ }));
40
+ }
41
+ for (const scope of scopes) {
42
+ if (!scope.parsed.module) issues.push(`${scope.file}: module directive is missing or unreadable`);
43
+ const result = computeGoDepFindings({ externalImports: importsByScope.get(scope), goMod: scope.parsed, nonRuntimeRoots });
44
+ findings.push(...result.findings.map((finding) => ({
45
+ ...finding,
46
+ manifest: scope.file,
47
+ verification: finding.rule === "missing-dep"
48
+ ? { 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" }
49
+ : { 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" },
50
+ })));
51
+ for (const name of result.declared) declared.add(`${scope.root || "."}:${name}`);
52
+ }
53
+ const present = manifests.length > 0;
54
+ return {
55
+ present,
56
+ status: present ? "CHECKED" : "NOT_PRESENT",
57
+ completeness: present ? (issues.length ? "PARTIAL" : "COMPLETE") : "NOT_APPLICABLE",
58
+ manifests,
59
+ declared,
60
+ findings,
61
+ reasons: issues,
62
+ reason: !present
63
+ ? "No go.mod was discovered."
64
+ : issues.length
65
+ ? `Go imports and requirements were checked across ${manifests.length} module(s), but ${issues.length} module descriptor(s) were incomplete.`
66
+ : `Every discovered go.mod scope (${manifests.length}) was compared with indexed Go imports, including direct/indirect requirements and replace directives.`,
67
+ };
68
+ }
@@ -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);
@@ -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);
@@ -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: goManifests.length > 0,
40
- status: goManifests.length ? "CHECKED" : "NOT_PRESENT",
41
- completeness: goManifests.length ? "PARTIAL" : "NOT_APPLICABLE",
42
- manifests: goManifests,
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: goManifests.length
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
- ? "Python declarations were compared with indexed imports; environment markers, extras and dynamic imports remain bounded static evidence."
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 parsed = parsePyprojectDeps(readRepoText(boundary, file))
39
- addManifest(dirname(file), file, parsed.deps, parsed.present)
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 parsed = parsePipfileDeps(readRepoText(boundary, file))
43
- addManifest(dirname(file), file, parsed.deps, parsed.present)
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,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
 
@@ -98,6 +98,26 @@ export function entryFiles(graph, pkgOrScopes, dynamicTargets = new Set(), { dec
98
98
  const f = n.source_file;
99
99
  if (ENTRY_FILE.test(f) || isFrameworkEntryFile(f) || /\.d\.[cm]?ts$/i.test(f) || TEST_FILE_RE.test(f) || /\.html?$/i.test(f) || pe.has(f) || /(^|\/)[^/]*\.config\.[a-z]+$/i.test(f)) entries.add(f);
100
100
  }
101
+ // Classic/static HTML pages load scripts, styles and other assets without a
102
+ // JavaScript import edge. Treat an exact local src/href target as an entry
103
+ // surface so a deployed asset is not mislabeled orphan/dead.
104
+ for (const [rawFile, rawText] of sources || []) {
105
+ const htmlFile = String(rawFile || "").replace(/\\/g, "/");
106
+ if (!/\.html?$/i.test(htmlFile)) continue;
107
+ const htmlDir = posix.dirname(htmlFile);
108
+ const attr = /\b(?:src|href)\s*=\s*["']([^"']+)["']/gi;
109
+ let match;
110
+ while ((match = attr.exec(String(rawText || "")))) {
111
+ const rawTarget = String(match[1] || "").trim().split(/[?#]/, 1)[0];
112
+ if (!rawTarget || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(rawTarget)) continue;
113
+ const relative = rawTarget.replace(/^\/+/, "");
114
+ const candidates = rawTarget.startsWith("/")
115
+ ? [posix.normalize(posix.join(htmlDir, relative)), posix.normalize(relative)]
116
+ : [posix.normalize(posix.join(htmlDir, relative))];
117
+ const target = candidates.find((candidate) => fileSet.has(candidate));
118
+ if (target) entries.add(target);
119
+ }
120
+ }
101
121
  for (const evidence of springConventionEntries(sources, fileSet)) {
102
122
  entries.add(evidence.file);
103
123
  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 { computeGoDepFindings, computePyDepFindings, computeScopedDepFindings } from "./dep-check.js";
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,
@@ -86,11 +86,11 @@ export async function runInternalAudit(repoPath, {
86
86
  packageScopes,
87
87
  workspacePkgNames: workspacePkgNames(repoPath, pkg),
88
88
  configTexts,
89
+ sourceTexts: sources,
89
90
  nonRuntimeRoots,
90
91
  sourceFiles: [...sources.keys()],
91
92
  });
92
- const goModText = readRepoText(boundary, "go.mod");
93
- const goDep = computeGoDepFindings({ externalImports, goMod: goModText != null ? parseGoMod(goModText) : null, nonRuntimeRoots });
93
+ const goDep = collectGoDependencyEvidence(repoPath, { files: repoFiles, externalImports, nonRuntimeRoots });
94
94
  const asList = (value) => Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
95
95
  const pyRules = rules.python || {}, depRules = rules.dependencies || {};
96
96
  const managedPython = [...new Set([
@@ -108,12 +108,17 @@ export async function runInternalAudit(repoPath, {
108
108
  ignoredDependencies: ignoredPython,
109
109
  nonRuntimeRoots,
110
110
  });
111
- const jvmDependencies = collectJvmDependencyEvidence(repoPath, { files: repoFiles });
111
+ const jvmDependencies = collectJvmDependencyEvidence(repoPath, { files: repoFiles, externalImports });
112
+ const cargoDep = collectCargoDependencyEvidence(repoPath, { files: repoFiles, externalImports });
112
113
 
113
114
  const externalImportFiles = new Set(externalImports.filter((entry) => entry.pkg && !entry.builtin).map((entry) => entry.file));
114
115
  const structure = computeStructureFindings(graph, { rules, entrySet: entries, externalImportFiles });
115
116
  const correctness = analyzeSourceCorrectness(sources, { isNonProductPath });
116
- const findings = [...dep.findings, ...goDep.findings, ...pyDep.findings, ...correctness.findings];
117
+ const findings = [
118
+ ...dep.findings, ...goDep.findings, ...pyDep.findings,
119
+ ...jvmDependencies.maven.findings, ...jvmDependencies.gradle.findings, ...cargoDep.findings,
120
+ ...correctness.findings,
121
+ ];
117
122
  const deadFileSet = new Set(dead.deadFiles.map((finding) => finding.file));
118
123
  for (const finding of structure.findings) {
119
124
  if (!(finding.rule === "orphan-file" && deadFileSet.has(finding.file))) findings.push(finding);
@@ -189,6 +194,7 @@ export async function runInternalAudit(repoPath, {
189
194
  goDep,
190
195
  pyDep,
191
196
  jvmDependencies,
197
+ cargoDep,
192
198
  externalImports,
193
199
  findings: sorted,
194
200
  packageScopes,
@@ -1,69 +1,115 @@
1
- // Maven/Gradle manifest presence and bounded declaration counts. We intentionally do not map Java
2
- // imports to artifacts: package-to-artifact resolution needs a real build model and claiming 0/0 from
3
- // source regexes would be worse than an explicit NOT_SUPPORTED/PARTIAL state.
4
1
  import { createRepoBoundary } from "../repo-path.js";
2
+ import { dependencyVerification, makeFinding } from "./findings.js";
3
+ import { parseGradleDependencies, parseGradleVersionCatalog, parseMavenPom } from "./jvm-manifests.js";
5
4
  import { listRepoFiles, readRepoText } from "./internal-audit.collect.js";
6
5
 
7
- function mavenDeclarations(text) {
8
- const source = String(text || "")
9
- .replace(/<!--[\s\S]*?-->/g, " ")
10
- .replace(/<dependencyManagement\b[\s\S]*?<\/dependencyManagement>/gi, " ");
11
- const identities = [];
12
- const re = /<dependency\b[^>]*>([\s\S]*?)<\/dependency>/gi;
13
- let match;
14
- while ((match = re.exec(source))) {
15
- const group = /<groupId>\s*([^<]+?)\s*<\/groupId>/i.exec(match[1])?.[1]?.trim() || "";
16
- const artifact = /<artifactId>\s*([^<]+?)\s*<\/artifactId>/i.exec(match[1])?.[1]?.trim() || "";
17
- if (artifact) identities.push(group ? `${group}:${artifact}` : artifact);
18
- }
19
- return identities;
6
+ const normalize = (value) => String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
7
+ const javaBuiltin = (name) => /^(?:java|jdk|sun)\.|^(?:org\.w3c\.dom|org\.xml\.sax)(?:\.|$)/.test(name);
8
+
9
+ function mappingScore(spec, dependency) {
10
+ const imported = String(spec || "").replace(/\.\*$/, "");
11
+ if (!imported || !dependency.group) return 0;
12
+ if (imported === dependency.group || imported.startsWith(`${dependency.group}.`)) return 1_000 + dependency.group.length;
13
+ const compactImport = normalize(imported), compactArtifact = normalize(dependency.artifact);
14
+ if (compactArtifact.length >= 4 && compactImport.includes(compactArtifact)) return 500 + compactArtifact.length;
15
+ const artifactTokens = String(dependency.artifact).toLowerCase().split(/[-_.]+/).filter((part) => part.length >= 4 && !["core", "java", "client", "common", "api"].includes(part));
16
+ const hits = artifactTokens.filter((part) => imported.toLowerCase().split(".").some((segment) => normalize(segment) === normalize(part))).length;
17
+ return hits ? 100 + hits : 0;
18
+ }
19
+
20
+ function bestDependency(spec, dependencies) {
21
+ const ranked = dependencies.map((dependency) => ({ dependency, score: mappingScore(spec, dependency) }))
22
+ .filter((item) => item.score > 0).sort((left, right) => right.score - left.score || left.dependency.name.localeCompare(right.dependency.name));
23
+ if (!ranked.length || (ranked[1] && ranked[1].score === ranked[0].score)) return null;
24
+ return ranked[0].dependency;
20
25
  }
21
26
 
22
- function gradleDeclarations(text) {
23
- const source = String(text || "")
24
- .replace(/\/\*[\s\S]*?\*\//g, " ")
25
- .replace(/(^|\s)\/\/.*$/gm, "$1");
26
- const declarations = [];
27
- const configurations = "api|implementation|compileOnly|runtimeOnly|annotationProcessor|kapt|testImplementation|testCompileOnly|testRuntimeOnly|androidTestImplementation";
28
- const line = new RegExp(`^\\s*(?:${configurations})\\s*(?:\\(\\s*)?([^\\r\\n]+)`, "gmi");
29
- let match;
30
- while ((match = line.exec(source))) {
31
- const expression = match[1].trim().replace(/[),;]+\s*$/, "");
32
- const coordinate = /["']([^"']+:[^"']+)["']/.exec(expression)?.[1] || "";
33
- const catalog = /\blibs(?:\.[A-Za-z_]\w*)+/.exec(expression)?.[0] || "";
34
- declarations.push(coordinate || catalog || "unresolved-declaration");
27
+ function analyze(ecosystem, manifests, dependencies, imports, unresolvedDeclarations, mappingDependencies = dependencies, includeMissing = true) {
28
+ const findings = [], used = new Map(), missing = new Map();
29
+ const owned = new Set(dependencies.map((dependency) => dependency.name));
30
+ for (const item of imports) {
31
+ const dependency = bestDependency(item.spec || item.pkg, mappingDependencies);
32
+ if (dependency) {
33
+ if (owned.has(dependency.name)) {
34
+ const list = used.get(dependency.name) || [];
35
+ list.push(item); used.set(dependency.name, list);
36
+ }
37
+ } else {
38
+ const key = item.spec || item.pkg;
39
+ const list = missing.get(key) || [];
40
+ list.push(item); missing.set(key, list);
41
+ }
35
42
  }
36
- return declarations;
43
+ const runtimeOnly = /runtime|provided|classpath|annotationProcessor|kapt/i;
44
+ for (const dependency of dependencies) {
45
+ const evidence = used.get(dependency.name) || [];
46
+ if (evidence.length || dependency.optional || runtimeOnly.test(dependency.scope || "")) continue;
47
+ findings.push(makeFinding({
48
+ category: "unused", rule: "unused-dep", severity: "low", confidence: "low",
49
+ title: `Unused ${ecosystem} dependency: ${dependency.name}`,
50
+ reason: "No indexed Java import mapped to this declared artifact; reflection, service loading, generated code and runtime-only use remain possible.",
51
+ detail: `"${dependency.name}" is declared in ${dependency.file}, but no indexed Java import maps to it. Review framework, reflection, ServiceLoader and generated-source use before removal.`,
52
+ package: dependency.name, version: dependency.version, manifest: dependency.file, source: "internal",
53
+ verification: dependencyVerification(dependency.file, [], "REVIEW_REQUIRED", "group-prefix/artifact-token"),
54
+ fixHint: `remove ${dependency.name} only after the ${ecosystem} build and tests confirm it is unused`,
55
+ }));
56
+ }
57
+ for (const [spec, evidence] of includeMissing ? missing : []) {
58
+ if (javaBuiltin(spec)) continue;
59
+ findings.push(makeFinding({
60
+ category: "unused", rule: "missing-dep", severity: "medium", confidence: "medium",
61
+ title: `Unmapped Java import: ${spec}`,
62
+ reason: `The indexed Java import did not map to any declared ${ecosystem} artifact.`,
63
+ detail: `"${spec}" is imported by ${evidence.length} file(s), but no ${ecosystem} declaration has a matching group prefix or artifact token. Add the owning artifact or configure/build the source that supplies it.`,
64
+ package: spec, file: evidence[0].file, line: evidence[0].line || 0,
65
+ evidence: evidence.slice(0, 5).map((item) => ({ file: item.file, line: item.line || 0, snippet: item.spec || "" })),
66
+ source: "internal",
67
+ verification: { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: "NOT_FOUND", files: manifests }, indexedSourceImports: { status: "FOUND", count: evidence.length, files: evidence.map((item) => item.file).slice(0, 10) }, mapping: "group-prefix/artifact-token" },
68
+ fixHint: `identify the artifact that owns ${spec} and declare it in the nearest ${ecosystem} manifest`,
69
+ }));
70
+ }
71
+ const present = manifests.length > 0;
72
+ return {
73
+ present,
74
+ status: present ? "CHECKED" : "NOT_PRESENT",
75
+ completeness: present ? (unresolvedDeclarations ? "PARTIAL" : "COMPLETE") : "NOT_APPLICABLE",
76
+ manifests,
77
+ declared: dependencies.length,
78
+ mappedImports: [...used.values()].reduce((sum, list) => sum + list.length, 0),
79
+ unmappedImports: [...missing.values()].reduce((sum, list) => sum + list.length, 0),
80
+ unresolvedDeclarations,
81
+ sample: dependencies.slice(0, 20).map(({ file, name, version }) => ({ file, identity: name, version })),
82
+ reason: !present
83
+ ? `No ${ecosystem === "maven" ? "pom.xml" : "Gradle build file"} was discovered.`
84
+ : unresolvedDeclarations
85
+ ? `${dependencies.length} declarations and all indexed Java imports were checked, but ${unresolvedDeclarations} dynamic/property/catalog declaration(s) could not be resolved statically.`
86
+ : `${dependencies.length} declarations were compared with every indexed non-JDK Java import using bounded group-prefix and artifact-token evidence. Missing and unused results remain review evidence, not compiler proof.`,
87
+ findings,
88
+ };
37
89
  }
38
90
 
39
- export function collectJvmDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot) } = {}) {
91
+ export function collectJvmDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot), externalImports = [] } = {}) {
40
92
  const boundary = createRepoBoundary(repoRoot);
41
93
  const mavenFiles = files.filter((file) => /(^|\/)pom\.xml$/i.test(file));
42
- const gradleFiles = files.filter((file) => /(^|\/)build\.gradle(?:\.kts)?$/i.test(file));
43
- const maven = mavenFiles.flatMap((file) => mavenDeclarations(readRepoText(boundary, file)).map((identity) => ({ file, identity })));
44
- const gradle = gradleFiles.flatMap((file) => gradleDeclarations(readRepoText(boundary, file)).map((identity) => ({ file, identity })));
94
+ const gradleFiles = files.filter((file) => /(^|\/)(?:build|settings|[^/]+)\.gradle(?:\.kts)?$/i.test(file));
95
+ const catalogFiles = files.filter((file) => /(^|\/)libs\.versions\.toml$/i.test(file));
96
+ const catalog = new Map();
97
+ for (const file of catalogFiles) for (const [alias, entry] of parseGradleVersionCatalog(readRepoText(boundary, file))) catalog.set(alias, entry);
98
+ let mavenUnresolved = 0, gradleUnresolved = 0;
99
+ const mavenDependencies = mavenFiles.flatMap((file) => {
100
+ const parsed = parseMavenPom(readRepoText(boundary, file));
101
+ mavenUnresolved += parsed.unresolvedDeclarations;
102
+ return parsed.dependencies.map((dependency) => ({ ...dependency, file }));
103
+ });
104
+ const gradleDependencies = gradleFiles.flatMap((file) => {
105
+ const parsed = parseGradleDependencies(readRepoText(boundary, file), catalog);
106
+ gradleUnresolved += parsed.unresolvedDeclarations || 0;
107
+ return parsed.map((dependency) => ({ ...dependency, file }));
108
+ });
109
+ const javaImports = externalImports.filter((entry) => entry.ecosystem === "Maven" && entry.pkg && !entry.builtin && !entry.unresolved);
110
+ const allDependencies = [...mavenDependencies, ...gradleDependencies];
45
111
  return {
46
- maven: {
47
- present: mavenFiles.length > 0,
48
- status: mavenFiles.length ? "NOT_SUPPORTED" : "NOT_PRESENT",
49
- completeness: mavenFiles.length ? "PARTIAL" : "NOT_APPLICABLE",
50
- manifests: mavenFiles,
51
- declared: maven.length,
52
- sample: maven.slice(0, 20),
53
- reason: mavenFiles.length
54
- ? "Maven manifests and declaration counts were detected, but Java package imports were not mapped to Maven artifacts; unused/missing dependency verdicts are not supported."
55
- : "No pom.xml was discovered.",
56
- },
57
- gradle: {
58
- present: gradleFiles.length > 0,
59
- status: gradleFiles.length ? "NOT_SUPPORTED" : "NOT_PRESENT",
60
- completeness: gradleFiles.length ? "PARTIAL" : "NOT_APPLICABLE",
61
- manifests: gradleFiles,
62
- declared: gradle.length,
63
- sample: gradle.slice(0, 20),
64
- reason: gradleFiles.length
65
- ? "Gradle manifests and bounded dependency declarations were detected, but version catalogs/build logic and Java package-to-artifact mapping were not resolved; unused/missing dependency verdicts are not supported."
66
- : "No build.gradle/build.gradle.kts was discovered.",
67
- },
112
+ maven: analyze("maven", mavenFiles, mavenDependencies, javaImports, mavenUnresolved, allDependencies, true),
113
+ gradle: analyze("gradle", [...gradleFiles, ...catalogFiles], gradleDependencies, javaImports, gradleUnresolved, allDependencies, mavenFiles.length === 0),
68
114
  };
69
115
  }