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.
- package/README.md +53 -13
- package/docs/releases/v0.2.16.md +53 -0
- package/package.json +6 -2
- package/src/analysis/cargo-dependency-evidence.js +111 -0
- package/src/analysis/cargo-manifests.js +74 -0
- package/src/analysis/dep-check-ecosystems.js +3 -0
- package/src/analysis/endpoints-java.js +2 -5
- package/src/analysis/findings.js +12 -0
- package/src/analysis/go-dependency-evidence.js +68 -0
- package/src/analysis/health-capabilities.js +1 -1
- 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/internal-audit/dependency-health.js +15 -13
- package/src/analysis/internal-audit/python-manifests.js +18 -5
- package/src/analysis/internal-audit/supply-chain.js +2 -0
- package/src/analysis/internal-audit.run.js +12 -7
- package/src/analysis/jvm-dependency-evidence.js +102 -56
- package/src/analysis/jvm-manifests.js +114 -0
- package/src/analysis/source-correctness.js +2 -5
- 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.resolvers.js +18 -8
- package/src/mcp/actions/advisories.mjs +2 -3
- package/src/mcp/catalog.mjs +7 -4
- package/src/mcp/company-contract-verdict.mjs +42 -0
- package/src/mcp/tools-company.mjs +28 -53
- 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/util.js +8 -0
- 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.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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,
|
|
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
|
-
###
|
|
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
|
-
- **
|
|
830
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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;
|
package/src/analysis/findings.js
CHANGED
|
@@ -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);
|