weavatrix 0.2.15 → 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 (46) hide show
  1. package/README.md +53 -13
  2. package/docs/releases/v0.2.16.md +53 -0
  3. package/package.json +6 -2
  4. package/src/analysis/cargo-dependency-evidence.js +111 -0
  5. package/src/analysis/cargo-manifests.js +74 -0
  6. package/src/analysis/dep-check-ecosystems.js +3 -0
  7. package/src/analysis/endpoints-java.js +2 -5
  8. package/src/analysis/findings.js +12 -0
  9. package/src/analysis/go-dependency-evidence.js +68 -0
  10. package/src/analysis/health-capabilities.js +1 -1
  11. package/src/analysis/http-contracts/analysis.js +2 -0
  12. package/src/analysis/http-contracts/client-call-detection.js +1 -0
  13. package/src/analysis/http-contracts/client-call-parser.js +18 -4
  14. package/src/analysis/internal-audit/dependency-health.js +15 -13
  15. package/src/analysis/internal-audit/python-manifests.js +18 -5
  16. package/src/analysis/internal-audit/supply-chain.js +2 -0
  17. package/src/analysis/internal-audit.run.js +12 -7
  18. package/src/analysis/jvm-dependency-evidence.js +102 -56
  19. package/src/analysis/jvm-manifests.js +114 -0
  20. package/src/analysis/source-correctness.js +2 -5
  21. package/src/analysis/transport-contracts.js +157 -0
  22. package/src/graph/builder/lang-go.js +4 -2
  23. package/src/graph/builder/lang-java.js +12 -3
  24. package/src/graph/builder/lang-rust.js +22 -3
  25. package/src/graph/freshness-probe.js +1 -1
  26. package/src/graph/internal-builder.build.js +2 -2
  27. package/src/graph/internal-builder.resolvers.js +18 -8
  28. package/src/mcp/actions/advisories.mjs +2 -3
  29. package/src/mcp/catalog.mjs +7 -4
  30. package/src/mcp/company-contract-verdict.mjs +42 -0
  31. package/src/mcp/tools-company.mjs +28 -53
  32. package/src/mcp/tools-impact-precision.mjs +89 -0
  33. package/src/mcp/tools-impact.mjs +52 -94
  34. package/src/precision/lsp-overlay/build.js +10 -0
  35. package/src/precision/lsp-overlay/contract.js +1 -1
  36. package/src/precision/symbol-query.js +39 -0
  37. package/src/precision/typescript-provider/client.js +16 -3
  38. package/src/precision/typescript-provider/discovery.js +1 -1
  39. package/src/precision/typescript-provider/isolated-runtime.js +39 -0
  40. package/src/precision/typescript-provider/project-host.js +11 -6
  41. package/src/precision/typescript-provider/project-safety.js +5 -0
  42. package/src/security/advisory-store.js +2 -2
  43. package/src/security/installed-jvm-rust.js +46 -0
  44. package/src/security/installed.js +21 -1
  45. package/src/util.js +8 -0
  46. package/docs/releases/v0.2.15.md +0 -37
package/README.md CHANGED
@@ -117,10 +117,20 @@ Every dead-code, orphan, dependency and duplicate item remains review evidence.
117
117
  conventions and source use before editing; Weavatrix does not auto-delete or merge findings.
118
118
 
119
119
  `run_audit` also returns an explicit capability matrix. `STRUCTURE CHECKED` and a supported package
120
- ecosystem do not imply `RUNTIME_CORRECTNESS` or `CONCURRENCY` is complete. Maven/Gradle manifests are
121
- reported `NOT_SUPPORTED`/`PARTIAL` for import-to-artifact verification rather than as a false clean
122
- `0 declared / 0 external`; bounded Go/Java correctness checks disclose that they are neither compiler
123
- proof nor a race detector.
120
+ ecosystem do not imply `RUNTIME_CORRECTNESS` or `CONCURRENCY` is complete. npm, nested Python and Go
121
+ modules, Maven properties, common Gradle declarations/version catalogs, and Cargo workspace/renamed
122
+ packages are compared with indexed imports. JVM reflection/generated/runtime-only use, Cargo
123
+ features/proc macros and unresolved build logic remain review evidence; bounded Go/Java checks are
124
+ neither compiler proof nor a race detector.
125
+
126
+ ### Where this saves agent context — and where it does not
127
+
128
+ The largest saving comes from graph operations that replace repeated discovery: one
129
+ `change_impact`, `get_dependents`, `context_bundle`, contract trace or dependency audit can collapse
130
+ many search/read hops into a bounded evidence set. `read_source` is mainly a convenient exact window
131
+ and is not inherently cheaper than a client's native offset read. Database queries, runtime state,
132
+ disk forensics and background workflow investigation remain outside a static repository graph.
133
+ Typecheck, tests and runtime checks remain the release authority.
124
134
 
125
135
  ### Trace an event, queue, worker, cron or CLI flow
126
136
 
@@ -259,8 +269,9 @@ and builds a missing graph automatically. A normal
259
269
  probes without building and refuses a legacy graph. Retargeting is offline but intentionally changes
260
270
  the filesystem boundary for subsequent tools; select `pinned` when that boundary must not move.
261
271
 
262
- An agent skill with recipes ships in [skill/SKILL.md](skill/SKILL.md) — install as
263
- `~/.claude/skills/weavatrix/SKILL.md`.
272
+ An agent skill with recipes ships in [skill/SKILL.md](skill/SKILL.md) — install it as
273
+ `~/.claude/skills/weavatrix/SKILL.md` for Claude Code or
274
+ `~/.codex/skills/weavatrix/SKILL.md` for Codex.
264
275
 
265
276
  ## Tools
266
277
 
@@ -281,6 +292,13 @@ disabled and only static evidence is active. Java and Rust language-server provi
281
292
  their edges never become `EXACT_LSP`, even when a mixed repository reports a complete
282
293
  TypeScript/JavaScript overlay.
283
294
 
295
+ Configured TypeScript plugins (including Next.js) are recorded but suppressed; repository-local
296
+ plugin code is never loaded. The broad overlay is deliberately budgeted, so `PARTIAL` can mean the
297
+ candidate cap was reached. `get_dependents` then spends a bounded point query on the requested JS/TS
298
+ symbol and replaces direct heuristic references only when exact absence/presence is proven.
299
+ `change_impact` batches the same exact query for changed symbols. Further transitive hops remain
300
+ graph-backed and are labelled as such.
301
+
284
302
  The bounded JS/TS provider is enabled by default for new graphs. Set `WEAVATRIX_PRECISION=off`
285
303
  before starting the MCP server for parser-only operation from the first build, or pass
286
304
  `precision:"off"` to `rebuild_graph` / `open_repo`. The MCPB installer exposes the same `lsp` / `off`
@@ -302,8 +320,8 @@ functions, methods, and symbols, with evidence tiers, completed/remaining verifi
302
320
  public/framework/dynamic caveats),
303
321
  `run_audit` (an explicit capability matrix plus unused files/exports/dependencies with per-finding manifest/indexed-source/script/config verification;
304
322
  `category:dependencies` isolates missing/unused/duplicate declarations, unresolved imports and lockfile drift,
305
- missing npm/Go/Python deps, honest Maven/Gradle `NOT_SUPPORTED`/`PARTIAL` import verification, bounded
306
- Go/Java correctness candidates, runtime
323
+ missing npm/Go/Python/Maven/Gradle/Cargo deps, bounded import-to-artifact/crate verification, concrete
324
+ npm/PyPI/Go/Maven/crates.io advisory pins, Go/Java correctness candidates, runtime
307
325
  cycles, type-only/compile-only coupling, orphans, boundary rules, offline OSV vulnerabilities + typosquat +
308
326
  lockfile drift; accepts an immutable `base_ref`, `changed_files`, and `debt: new|existing|all` for
309
327
  review-scoped results; production paths are the default and `include_classified:true` opts into
@@ -392,6 +410,21 @@ metrics are not persisted or transmitted by Weavatrix. If a source checkout's pa
392
410
  while an old daemon remains alive, `initialize`, `tools/list`, and tool calls fail loudly with
393
411
  `STALE_RUNTIME` until the client reconnects; the opt-out is reserved for deliberate development.
394
412
 
413
+ ### 0.2.16 exact dependents, multi-ecosystem dependencies and transport contracts
414
+
415
+ - Safe TypeScript project plugins no longer disable semantic precision. Plugin loading remains
416
+ suppressed, while `get_dependents` and `change_impact` can obtain exact direct JS/TS references on
417
+ demand and identify graph-backed transitive hops separately.
418
+ - Dependency evidence covers nested npm/Python/Go scopes, Maven properties, Gradle catalogs/locks,
419
+ and Cargo workspace inheritance/renames/locks. Explicit OSV refresh accepts npm, PyPI, Go, Maven
420
+ and crates.io pins.
421
+ - Cross-repository tracing adds static GraphQL, gRPC and event-topic joins alongside HTTP and keeps
422
+ runtime configuration, reflection and other dynamic targets explicitly `UNKNOWN`.
423
+ - npm/MCPB metadata now calls out Codex/OpenAI Codex workflows, and Hosted distinguishes aggregate
424
+ folder-boundary feedback from proven file-level runtime cycles.
425
+
426
+ Full patch notes: [docs/releases/v0.2.16.md](docs/releases/v0.2.16.md).
427
+
395
428
  ### 0.2.15 self-audit precision patch
396
429
 
397
430
  - Dependency review now scopes nested manifests correctly, recognizes framework peer packages and
@@ -658,13 +691,19 @@ Java graphs:
658
691
  agent review queue. Public/exported methods, framework entries, decorators, dynamic loading and
659
692
  reflection lower confidence; the result always says `REVIEW_REQUIRED` and `autoDelete:false`.
660
693
 
661
- ### HTTP clients and wrappers
694
+ ### Cross-repository transports
662
695
 
663
696
  `trace_api_contract` recognizes built-in object clients such as `axios.get(...)`, explicit bare or
664
697
  object/member wrappers, and simple auto-discovered functions that forward a URL parameter directly
665
698
  to a known HTTP client. Auto-discovered wrappers are restricted to their bounded reverse-import
666
699
  scope; ambiguous same-name definitions are skipped and reported as incomplete evidence.
667
700
 
701
+ The same tool can select GraphQL, gRPC or event evidence. It joins static GraphQL schema fields and
702
+ operations, proto service methods and typed stub calls, and static Kafka/event-bus topic producers
703
+ and consumers across registered repositories. Computed URLs, runtime configuration, dynamic
704
+ GraphQL documents/topics, reflection and dynamic stubs remain `UNKNOWN`; they are not converted into
705
+ an absence claim.
706
+
668
707
  Persistent per-client-repository configuration lives in `.weavatrix.json`:
669
708
 
670
709
  ```json
@@ -826,16 +865,17 @@ loader) behind the thin stdio entry `src/mcp-server.mjs`.
826
865
  cross-repository wrapper/liveness fixture, framework/convention fixture, full MCP lifecycle gate
827
866
  and a portable real-repository runner. Six source-free 0.2.1 real-repository baselines are
828
867
  recorded; edge provenance is gated end-to-end and the strict six-repository release command passes.
829
- - **Wrapper-aware API contracts** shipped in 0.2.2: persistent/ad-hoc configuration,
830
- conservative discovery, cross-repository handler liveness and explicit unknown states. The next
831
- hosted increment joins privacy-safe contract identities across separately synced services.
868
+ - **Cross-transport contracts** cover bounded HTTP wrappers plus static GraphQL, gRPC and event-bus
869
+ identities across registered local repositories, with explicit runtime/dynamic unknown states.
832
870
  - **Hosted architecture workbench** at
833
871
  [app.weavatrix.com](https://app.weavatrix.com) is an access-controlled preview for
834
872
  owner-authenticated source-free evidence and revision history. Its UI and backend evolve
835
873
  independently from the public MCP release; local use remains fully optional.
836
874
  - **Semantic precision bridge** shipped for TypeScript/JavaScript in 0.2.4: a bounded, revision-bound
837
875
  local overlay validates references with the bundled language server while the parser graph remains
838
- the fallback. Java and Rust providers are not bundled yet and stay explicitly `UNAVAILABLE`.
876
+ the fallback. 0.2.16 adds safe configured-plugin suppression plus exact on-demand point and changed-
877
+ symbol batch queries. Java and Rust language-server providers are not bundled and stay explicitly
878
+ unavailable as semantic providers; their parser/dependency evidence is still active.
839
879
  - **Git-native architecture history** — bounded tag/ref timelines and branch
840
880
  reports built outside the worktree; graph artifacts stay out of Git.
841
881
  - **Cross-repository company evidence** — endpoints, events and internal
@@ -0,0 +1,53 @@
1
+ # Weavatrix 0.2.16
2
+
3
+ 0.2.16 is a semantic-precision, dependency-coverage, and cross-transport contract release. It keeps
4
+ the 38-tool MCP surface and local-first defaults while fixing the critical case where a safe
5
+ TypeScript project plugin could disable every exact reference edge.
6
+
7
+ ## Exact dependents for real TypeScript projects
8
+
9
+ - TypeScript project plugins such as Next.js are recorded and suppressed instead of making the
10
+ semantic overlay unavailable. Repository-local plugin code is never loaded.
11
+ - `get_dependents` performs a bounded exact point query for JS/TS symbols by default. A complete LSP
12
+ result replaces heuristic direct references; an incomplete result retains graph evidence and says
13
+ why.
14
+ - `change_impact` batches exact direct-reference queries for changed JS/TS symbols. Transitive hops
15
+ remain explicitly graph-backed instead of being presented as LSP proof.
16
+ - Real-repository validation moved an affected Next.js repository from `UNAVAILABLE / 0 EXACT_LSP`
17
+ to a bounded partial overlay with 154 exact edges; a point query outside the initial budget found
18
+ 16 exact direct references and then served the same result from cache.
19
+
20
+ ## Multi-ecosystem dependency and advisory evidence
21
+
22
+ - Python checks nested requirements, pyproject/Poetry/Pipfile scopes; Go resolves nested modules,
23
+ direct/indirect requirements and replacements; both compare declarations with indexed imports.
24
+ - Maven resolves properties and dependency declarations; Gradle resolves common Groovy/Kotlin
25
+ declarations, version catalogs and lock evidence. Import-to-artifact mapping uses bounded group
26
+ prefixes and artifact tokens and keeps reflection/generated/runtime-only cases review-only.
27
+ - Cargo resolves workspace inheritance, renamed packages and lockfile pins, compares crate paths to
28
+ the nearest manifest, and reports feature/proc-macro uncertainty rather than claiming compiler
29
+ proof.
30
+ - Explicit advisory refresh now includes concrete Maven and crates.io pins in addition to npm,
31
+ PyPI and Go. It remains opt-in and sends only package identities and versions to OSV.
32
+
33
+ ## Contracts beyond static HTTP
34
+
35
+ - `trace_api_contract` can select HTTP, GraphQL, gRPC or event contracts. Static GraphQL
36
+ operations/schema fields, proto service methods/stub calls and Kafka/event topics join across
37
+ registered local repositories.
38
+ - Runtime-computed URLs, operations/topics, reflection and dynamic stubs remain `UNKNOWN` with
39
+ bounded evidence. Runtime configuration is never converted into a false clean result.
40
+
41
+ ## Agent and Hosted fixes
42
+
43
+ - Package metadata now names Codex/OpenAI Codex alongside other MCP and coding-agent workflows.
44
+ - The Hosted Flow view distinguishes an aggregate folder-boundary feedback loop from a proven
45
+ file-level runtime import cycle. Current Weavatrix evidence has zero file-level runtime cycles.
46
+ - The release checklist includes a fresh preview-confirmed private graph sync, so public demo history
47
+ and authenticated repository maps cannot silently drift apart.
48
+
49
+ ## Verification
50
+
51
+ The release is gated by the full Node suite, six-language fixture benchmark, real-repository
52
+ semantic checks, exact LSP dogfooding, release-manifest verification, packed npm and MCPB stdio
53
+ checks, npm audit, Hosted tests/build/lint, explicit private sync, and live browser verification.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "type": "module",
5
5
  "description": "Local repository intelligence MCP for AI coding agents: reusable architecture graph for fast application understanding, Health, dead code, clones, history, blast radius and architecture safeguards.",
6
6
  "author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
@@ -26,7 +26,7 @@
26
26
  "skill",
27
27
  "scripts/run-agent-task-benchmark.mjs",
28
28
  "docs/agent-task-benchmark.md",
29
- "docs/releases/v0.2.15.md",
29
+ "docs/releases/v0.2.16.md",
30
30
  "README.md",
31
31
  "SECURITY.md",
32
32
  "LICENSE"
@@ -39,6 +39,7 @@
39
39
  "benchmark:real:release": "node scripts/run-real-benchmark.mjs --require-all",
40
40
  "benchmark:agent": "node scripts/run-agent-task-benchmark.mjs",
41
41
  "benchmark:agent:release": "node scripts/run-agent-task-benchmark.mjs --require-independent",
42
+ "sync:private": "node scripts/sync-private.mjs",
42
43
  "build:mcpb": "node scripts/build-mcpb.mjs",
43
44
  "verify:release": "node scripts/verify-release.mjs"
44
45
  },
@@ -56,6 +57,9 @@
56
57
  "dependency-graph",
57
58
  "tree-sitter",
58
59
  "claude",
60
+ "codex",
61
+ "openai-codex",
62
+ "coding-agents",
59
63
  "ai-agents"
60
64
  ],
61
65
  "dependencies": {
@@ -0,0 +1,111 @@
1
+ import { dirname } from "node:path";
2
+ import { createRepoBoundary } from "../repo-path.js";
3
+ import { cargoName, parseCargoToml } from "./cargo-manifests.js";
4
+ import { dependencyVerification, makeFinding } from "./findings.js";
5
+ import { listRepoFiles, readRepoText } from "./internal-audit.collect.js";
6
+
7
+ const rootOf = (file) => {
8
+ const root = dirname(String(file || "").replace(/\\/g, "/"));
9
+ return root === "." ? "" : root;
10
+ };
11
+ const owns = (root, file) => !root || file === root || String(file || "").replace(/\\/g, "/").startsWith(`${root}/`);
12
+
13
+ export function collectCargoDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot), externalImports = [] } = {}) {
14
+ const boundary = createRepoBoundary(repoRoot);
15
+ const manifests = files.filter((file) => /(^|\/)Cargo\.toml$/i.test(file));
16
+ const scopes = manifests.map((file) => ({ file, root: rootOf(file), ...parseCargoToml(readRepoText(boundary, file)) }))
17
+ .sort((left, right) => right.root.length - left.root.length || left.root.localeCompare(right.root));
18
+ const workspaceDeps = new Map();
19
+ for (const scope of scopes) for (const dependency of scope.workspaceDependencies) workspaceDeps.set(cargoName(dependency.alias), dependency);
20
+ const issues = [];
21
+ for (const scope of scopes) {
22
+ scope.dependencies = scope.dependencies.map((dependency) => {
23
+ if (!dependency.inherited) return dependency;
24
+ const inherited = workspaceDeps.get(cargoName(dependency.alias));
25
+ if (!inherited) { issues.push(`${scope.file}: workspace dependency ${dependency.alias} is unresolved`); return dependency; }
26
+ return { ...dependency, name: inherited.name, version: inherited.version, inherited: true };
27
+ });
28
+ }
29
+ const importsByScope = new Map(scopes.map((scope) => [scope, []]));
30
+ for (const entry of externalImports) {
31
+ if (entry.ecosystem !== "crates.io" || !entry.pkg || entry.builtin || entry.unresolved) continue;
32
+ const owner = scopes.find((scope) => owns(scope.root, entry.file));
33
+ if (owner) importsByScope.get(owner).push(entry);
34
+ }
35
+ const findings = [], declared = new Set();
36
+ let mappedImports = 0, unmappedImports = 0;
37
+ if (!scopes.length) {
38
+ const missing = new Map();
39
+ for (const entry of externalImports.filter((item) => item.ecosystem === "crates.io" && item.pkg && !item.builtin && !item.unresolved)) {
40
+ if (!missing.has(cargoName(entry.pkg))) missing.set(cargoName(entry.pkg), entry);
41
+ }
42
+ for (const [name, entry] of missing) findings.push(makeFinding({
43
+ category: "unused", rule: "missing-dep", severity: "low", confidence: "high",
44
+ title: `Rust crate path without Cargo.toml: ${name}`,
45
+ reason: "An indexed external Rust crate path exists, but no Cargo.toml was discovered.",
46
+ detail: `"${entry.spec || name}" is used by ${entry.file}, but no Cargo manifest owns the file.`,
47
+ package: name, file: entry.file, line: entry.line || 0, source: "internal",
48
+ verification: { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: "NOT_PRESENT" }, indexedSourceImports: { status: "FOUND", count: 1, files: [entry.file] }, mapping: "Cargo crate name normalization" },
49
+ fixHint: "initialize or restore the owning Cargo manifest",
50
+ }));
51
+ }
52
+ for (const scope of scopes) {
53
+ const imports = importsByScope.get(scope);
54
+ const used = new Map(), missingSeen = new Set();
55
+ for (const entry of imports) {
56
+ const imported = cargoName(entry.pkg);
57
+ const dependency = scope.dependencies.find((item) => cargoName(item.alias) === imported || cargoName(item.name) === imported);
58
+ if (dependency) {
59
+ const evidence = used.get(dependency.alias) || [];
60
+ evidence.push(entry); used.set(dependency.alias, evidence); mappedImports++;
61
+ continue;
62
+ }
63
+ unmappedImports++;
64
+ if (missingSeen.has(imported)) continue;
65
+ missingSeen.add(imported);
66
+ findings.push(makeFinding({
67
+ category: "unused", rule: "missing-dep", severity: "medium", confidence: "high",
68
+ title: `Missing Cargo dependency: ${entry.pkg}`,
69
+ reason: "An indexed external Rust crate path has no matching dependency alias or package name in the nearest Cargo.toml.",
70
+ detail: `"${entry.spec || entry.pkg}" is used by ${entry.file}, but ${scope.file} does not declare crate "${entry.pkg}" (including workspace inheritance and renamed packages).`,
71
+ package: entry.pkg, file: entry.file, line: entry.line || 0, manifest: scope.file,
72
+ evidence: [{ file: entry.file, line: entry.line || 0, snippet: entry.spec || "" }], source: "internal",
73
+ verification: { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: "NOT_FOUND", file: scope.file }, indexedSourceImports: { status: "FOUND", count: 1, files: [entry.file] }, mapping: "Cargo alias/package normalization" },
74
+ fixHint: `cargo add ${entry.pkg}`,
75
+ }));
76
+ }
77
+ for (const dependency of scope.dependencies) {
78
+ const identity = cargoName(dependency.name);
79
+ if (!identity) continue;
80
+ declared.add(`${scope.root || "."}:${identity}`);
81
+ const evidence = used.get(dependency.alias) || [];
82
+ if (evidence.length || dependency.optional || dependency.build) continue;
83
+ findings.push(makeFinding({
84
+ category: "unused", rule: "unused-dep", severity: dependency.dev ? "info" : "low", confidence: "low",
85
+ title: `Unused Cargo dependency: ${dependency.name}`,
86
+ reason: "No indexed Rust use/path/extern-crate evidence maps to this dependency; feature-only, proc-macro, generated and reflective registration remain possible.",
87
+ detail: `"${dependency.name}" (${dependency.alias}) is declared in ${scope.file}, but no indexed .rs file in that Cargo scope references its crate path. Confirm with cargo check/test and feature combinations before removal.`,
88
+ package: dependency.name, version: dependency.version, manifest: scope.file, source: "internal",
89
+ verification: dependencyVerification(scope.file, [], "REVIEW_REQUIRED", "Cargo alias/package normalization"),
90
+ fixHint: `cargo remove ${dependency.alias} after checking all features and targets`,
91
+ }));
92
+ }
93
+ }
94
+ const present = manifests.length > 0;
95
+ return {
96
+ present,
97
+ status: present ? "CHECKED" : "NOT_PRESENT",
98
+ completeness: present ? (issues.length ? "PARTIAL" : "COMPLETE") : "NOT_APPLICABLE",
99
+ manifests,
100
+ declared,
101
+ mappedImports,
102
+ unmappedImports,
103
+ findings,
104
+ reasons: issues,
105
+ reason: !present
106
+ ? "No Cargo.toml was discovered."
107
+ : issues.length
108
+ ? `Cargo declarations and indexed crate paths were checked, but ${issues.length} workspace inheritance reference(s) remain unresolved.`
109
+ : `Cargo declarations from ${manifests.length} scope(s), workspace inheritance, renamed packages and every indexed external crate path were compared.`,
110
+ };
111
+ }
@@ -0,0 +1,74 @@
1
+ const clean = (value) => String(value || "").trim().replace(/^['"]|['"]$/g, "");
2
+ export const cargoName = (value) => clean(value).toLowerCase().replace(/_/g, "-");
3
+
4
+ function inlineTable(value) {
5
+ const fields = new Map();
6
+ for (const match of String(value || "").matchAll(/([A-Za-z][\w.-]*)\s*=\s*("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|true|false)/g)) {
7
+ fields.set(match[1], clean(match[2]));
8
+ }
9
+ return fields;
10
+ }
11
+
12
+ // Bounded Cargo.toml parser for package/workspace dependency tables. It intentionally owns
13
+ // dependency identity, aliases and concrete versions; feature activation is not treated as usage.
14
+ export function parseCargoToml(text) {
15
+ const source = String(text || "");
16
+ const dependencies = [];
17
+ const workspaceDependencies = [];
18
+ let section = "", packageName = "", workspace = false;
19
+ for (const raw of source.split(/\r?\n/)) {
20
+ const line = raw.replace(/(^|\s)#.*$/, "").trim();
21
+ if (!line) continue;
22
+ const header = /^\[([^\]]+)]$/.exec(line);
23
+ if (header) { section = header[1].trim(); workspace ||= section === "workspace"; continue; }
24
+ const kv = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
25
+ if (!kv) continue;
26
+ if (section === "package" && kv[1] === "name") { packageName = clean(kv[2]); continue; }
27
+ const dependencyTable = /^(?:target\..+\.)?(dependencies|dev-dependencies|build-dependencies)$/.exec(section);
28
+ const workspaceTable = section === "workspace.dependencies";
29
+ if (!dependencyTable && !workspaceTable) continue;
30
+ const alias = kv[1], value = kv[2].trim();
31
+ const table = value.startsWith("{") ? inlineTable(value) : new Map();
32
+ const version = value.startsWith("{") ? clean(table.get("version")) : clean(value);
33
+ const name = clean(table.get("package")) || alias;
34
+ const record = {
35
+ alias,
36
+ name,
37
+ version: /^\d/.test(version) || /^=?\s*\d/.test(version) ? version.replace(/^=\s*/, "") : "",
38
+ dev: dependencyTable?.[1] === "dev-dependencies",
39
+ build: dependencyTable?.[1] === "build-dependencies",
40
+ optional: table.get("optional") === "true",
41
+ inherited: table.get("workspace") === "true",
42
+ };
43
+ (workspaceTable ? workspaceDependencies : dependencies).push(record);
44
+ }
45
+ return { packageName, workspace, dependencies, workspaceDependencies };
46
+ }
47
+
48
+ // Cargo.lock v3/v4 [[package]] records. Registry packages are exact installed crates; workspace
49
+ // and git packages are excluded because OSV crates.io versions must not be inferred for them.
50
+ export function parseCargoLockPackages(text) {
51
+ const packages = [];
52
+ let current = null;
53
+ const flush = () => {
54
+ if (current?.name && current.version && /^registry\+https:\/\/github\.com\/rust-lang\/crates\.io-index|^registry\+https:\/\/index\.crates\.io\//.test(current.source)) {
55
+ packages.push({ ecosystem: "crates.io", name: current.name, version: current.version, dev: false, integrity: current.checksum || "", source: "cargo-lock" });
56
+ }
57
+ };
58
+ for (const raw of String(text || "").split(/\r?\n/)) {
59
+ const line = raw.trim();
60
+ if (line === "[[package]]") { flush(); current = { name: "", version: "", source: "", checksum: "" }; continue; }
61
+ if (!current) continue;
62
+ if (/^\[/.test(line)) { flush(); current = null; continue; }
63
+ const match = /^(name|version|source|checksum)\s*=\s*"([^"]+)"/.exec(line);
64
+ if (match) current[match[1]] = match[2];
65
+ }
66
+ flush();
67
+ const seen = new Set();
68
+ return packages.filter((pkg) => {
69
+ const key = `${pkg.name}@${pkg.version}`;
70
+ if (seen.has(key)) return false;
71
+ seen.add(key);
72
+ return true;
73
+ });
74
+ }
@@ -194,6 +194,9 @@ export function computePyDepFindings(options = {}) {
194
194
  findings.push(...result.findings.map((finding) => ({
195
195
  ...finding,
196
196
  ...(scope.manifests?.length ? {manifest: scope.manifests[0]} : {}),
197
+ verification: finding.rule === "missing-dep"
198
+ ? { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: scope.present ? "NOT_FOUND" : "NOT_PRESENT", files: scope.manifests || [] }, indexedSourceImports: { status: "FOUND", count: finding.evidence?.length || 1, files: (finding.evidence || []).map((item) => item.file) }, mapping: "PEP 503 plus bounded import-to-distribution aliases" }
199
+ : { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "REVIEW_REQUIRED", manifestDeclaration: { status: "FOUND", files: scope.manifests || [] }, indexedSourceImports: { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0, files: [] }, mapping: "PEP 503 plus bounded import-to-distribution aliases" },
197
200
  })));
198
201
  for (const name of result.declared) declared.add(`${scope.root || "."}:${name}`);
199
202
  for (const name of result.managed) managed.add(name);
@@ -2,16 +2,13 @@
2
2
  // literal annotation paths: inventing values for constants would make an architecture inventory look
3
3
  // more complete than the evidence permits.
4
4
  import { maskJavaNonCode } from "./java-source.js";
5
+ import { lineNumberAt } from "../util.js";
5
6
 
6
7
  const SPRING_MAPPING = /@(?:org\.springframework\.web\.bind\.annotation\.)?(RequestMapping|GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping)\b/g;
7
8
  const SPRING_CONDITION = /@(?:org\.springframework\.boot\.autoconfigure\.condition\.)?(ConditionalOnExpression|ConditionalOnProperty)\b/g;
8
9
  const REQUEST_METHOD = /\bRequestMethod\s*\.\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\b/g;
9
10
 
10
- const lineAt = (text, index) => {
11
- let line = 1;
12
- for (let i = 0; i < index && i < text.length; i++) if (text[i] === "\n") line++;
13
- return line;
14
- };
11
+ const lineAt = lineNumberAt;
15
12
 
16
13
  function skipTrivia(text, start) {
17
14
  let i = start;
@@ -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);