weavatrix 0.3.9 → 0.3.11
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 +29 -4
- package/docs/releases/v0.3.10.md +76 -0
- package/docs/releases/v0.3.11.md +55 -0
- package/package.json +4 -2
- package/skill/SKILL.md +13 -6
- package/src/analysis/dead-check.js +10 -8
- package/src/analysis/dead-code-review.js +3 -0
- package/src/analysis/dep-rules.js +1 -1
- package/src/analysis/hot-path-review.js +1 -1
- package/src/analysis/http-contracts/analysis.js +9 -1
- package/src/analysis/structure/findings.js +21 -1
- package/src/analysis/task-retrieval.js +2 -0
- package/src/build-graph.js +9 -1
- package/src/graph/builder/lang-rust.js +36 -0
- package/src/graph/builder/lang-solidity.js +126 -0
- package/src/graph/builder/lang-sql.js +263 -0
- package/src/graph/builder/pass2-resolution.js +3 -1
- package/src/graph/freshness-probe.js +1 -1
- package/src/graph/graph-filter.js +5 -3
- package/src/graph/incremental-refresh.js +1 -1
- package/src/graph/internal-builder.build.js +18 -6
- package/src/graph/internal-builder.langs.js +3 -1
- package/src/graph/internal-builder.resolvers.js +31 -1
- package/src/graph/parser-artifact-boundary.js +1 -0
- package/src/mcp/actions/graph-lifecycle.mjs +3 -3
- package/src/mcp/architecture-starter.mjs +1 -1
- package/src/mcp/catalog.mjs +2 -2
- package/src/mcp/evidence-snapshot.structure.mjs +5 -2
- package/src/mcp/graph/context-seeds.mjs +6 -2
- package/src/mcp/graph/tools-query.mjs +28 -2
- package/src/mcp/health/endpoints.mjs +34 -5
- package/src/mcp/sync/payload-v2.mjs +1 -0
- package/src/mcp/tools-company.mjs +9 -0
- package/src/mcp/tools-endpoints.mjs +54 -12
- package/src/mcp/tools-graph-hubs.mjs +1 -0
- package/src/mcp/tools-impact.mjs +9 -1
- package/src/precision/lsp-overlay/build.js +9 -4
- package/src/precision/lsp-overlay/contract.js +1 -1
- package/src/precision/lsp-overlay/target-index.js +8 -3
- package/src/precision/symbol-query.js +2 -1
package/README.md
CHANGED
|
@@ -131,6 +131,18 @@ Every finding is review evidence, never an auto-delete verdict: `find_dead_code`
|
|
|
131
131
|
`run_audit category=unused` always return `REVIEW_REQUIRED` with `autoDelete:false`. Typecheck, tests
|
|
132
132
|
and runtime checks remain the release authority.
|
|
133
133
|
|
|
134
|
+
## Always-fresh graph
|
|
135
|
+
|
|
136
|
+
There is no watcher daemon to run and no manual refresh step: every graph/health call reconciles the
|
|
137
|
+
graph before answering. A Git-token freshness probe (HEAD + dirty/untracked content, debounced 2 s)
|
|
138
|
+
decides whether anything changed; when it did, a bounded incremental refresh reparses only the changed
|
|
139
|
+
files plus their reverse importers (≤ 24 changed / ≤ 80 reparsed JS/TS files) and merges the scoped
|
|
140
|
+
result into the previous graph under a file lock. Config/lockfile edits, export-surface changes,
|
|
141
|
+
barrel files and non-JS/TS languages fall back to a full rebuild — correctness always wins over speed.
|
|
142
|
+
Each refreshed answer carries a structured `refresh` record (`none` / `incremental` / `full`, changed
|
|
143
|
+
file count), so an agent can tell exactly which repository state it is reasoning about. The same
|
|
144
|
+
guarantees hold across concurrent MCP clients sharing one canonical graph.
|
|
145
|
+
|
|
134
146
|
## Benchmarks
|
|
135
147
|
|
|
136
148
|
Two gates ship in the repository:
|
|
@@ -174,10 +186,23 @@ realpath escapes. Report suspected vulnerabilities privately as described in [SE
|
|
|
174
186
|
|
|
175
187
|
## Languages
|
|
176
188
|
|
|
177
|
-
JavaScript · TypeScript · TSX · Python · Go · Java · C# · Rust · HTML · CSS — parsed with
|
|
189
|
+
JavaScript · TypeScript · TSX · Python · Go · Java · C# · Rust · Solidity · HTML · CSS — parsed with
|
|
178
190
|
[web-tree-sitter](https://github.com/tree-sitter/tree-sitter) WASM grammars; no Python install and no
|
|
179
191
|
native compilation.
|
|
180
192
|
|
|
193
|
+
SQL is indexed without a grammar: `.sql` files contribute tables, views, columns, functions, indexes
|
|
194
|
+
and triggers as first-class graph symbols, and SQL found in string literals of any other language
|
|
195
|
+
links the enclosing function to the table it queries. That makes schema objects visible to
|
|
196
|
+
`change_impact`/`get_dependents` (who touches this table?) and lets the dead-code check flag columns
|
|
197
|
+
no statement references — conservatively: verdicts require literal-SQL evidence in the repo, and
|
|
198
|
+
`SELECT *`-consumed tables never have their columns judged by name (ORM-generated SQL stays invisible
|
|
199
|
+
and is therefore never judged either).
|
|
200
|
+
|
|
201
|
+
Test surfaces are classified per file (path conventions plus `.weavatrix.json` overrides) and, for
|
|
202
|
+
Rust, per symbol: `#[cfg(test)]` modules and `#[test]`/`#[bench]` items inside production `.rs` files
|
|
203
|
+
carry a node-level `test_surface` flag, so dead-code, query, hot-path and hub tools treat them as
|
|
204
|
+
tests rather than production code.
|
|
205
|
+
|
|
181
206
|
## Development
|
|
182
207
|
|
|
183
208
|
```sh
|
|
@@ -187,9 +212,9 @@ npm run benchmark # full TS/JS/Python/Go/Java/Rust + MCP lifecycle gate
|
|
|
187
212
|
npm run benchmark:real # locally available real repos vs source-free 0.2.1 baselines
|
|
188
213
|
```
|
|
189
214
|
|
|
190
|
-
Maintained JavaScript/TypeScript under `src`, `bin`, `scripts`, `test`
|
|
191
|
-
|
|
192
|
-
|
|
215
|
+
Maintained JavaScript/TypeScript under `src`, `bin`, `scripts`, `test` has a hard 300-line physical
|
|
216
|
+
ceiling enforced by the release suite; larger concerns split into owner-focused modules behind slim
|
|
217
|
+
facades. The weavatrix.com landing site lives in its own repository (`weavatrix-site`).
|
|
193
218
|
|
|
194
219
|
## Release history
|
|
195
220
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Weavatrix 0.3.10
|
|
2
|
+
|
|
3
|
+
0.3.10 closes the honesty gaps a production evaluation surfaced: Rust inline
|
|
4
|
+
tests are no longer reported as production code, endpoint tools are
|
|
5
|
+
production-first everywhere, and semantic-precision statuses say exactly how
|
|
6
|
+
much was actually proven. Every fix landed with regression tests and survived
|
|
7
|
+
an adversarial multi-agent review of the full changeset.
|
|
8
|
+
|
|
9
|
+
## Node-level Rust test classification
|
|
10
|
+
|
|
11
|
+
Rust puts unit tests inside production files, so path classification alone
|
|
12
|
+
misreported them everywhere. The extractor now proves test-only compilation —
|
|
13
|
+
`#[cfg(test)]` / `#[cfg(all(test, ...))]` on items, impl blocks and modules,
|
|
14
|
+
`#[test]`/`#[tokio::test]`-style harness attributes and `#[bench]` functions,
|
|
15
|
+
inner `#![cfg(test)]` attributes at file and module-body scope, and attributes
|
|
16
|
+
separated from their item by comments — and stamps a `test_surface` flag on the
|
|
17
|
+
node. Deliberately conservative: `#[cfg(any(test, ...))]` also ships in
|
|
18
|
+
production builds and stays unclassified.
|
|
19
|
+
|
|
20
|
+
Every consumer honors the flag like a path-classified test: `find_dead_code`
|
|
21
|
+
(candidates and test-only consumer resolution), `query_graph` seeds and
|
|
22
|
+
traversal, `hot_path_review`, `god_nodes`, `verified_change` task anchors,
|
|
23
|
+
`no-tests`/`tests-only` build modes, hosted sync payloads and evidence
|
|
24
|
+
snapshots. A test-term question or `include_tests`/`include_classified` opts
|
|
25
|
+
back in. `extractorSchemaV` 5 → 6; cached graphs rebuild once, automatically.
|
|
26
|
+
|
|
27
|
+
## Production-first endpoint tools
|
|
28
|
+
|
|
29
|
+
- `list_endpoints` suppresses endpoints declared in classified
|
|
30
|
+
test/e2e/generated/mock/story/docs/benchmark/temp paths by default, with a
|
|
31
|
+
count note and `include_classified` opt-in (auto-enabled on tests-only
|
|
32
|
+
graphs). Opted-in rows carry `[classified:...]` tags.
|
|
33
|
+
- `trace_endpoint` filters classified handler twins before its ambiguity
|
|
34
|
+
check, ranks remaining same-named candidates by the import binding at the
|
|
35
|
+
route file (below same-directory proximity — a file-level edge proves
|
|
36
|
+
coupling, not identity), and reports `importBoundAtRoute`/`pathClasses` per
|
|
37
|
+
candidate when it still fails closed.
|
|
38
|
+
- New `handler_file` parameter resolves genuinely symmetric candidates: it
|
|
39
|
+
narrows the full candidate pool before scoring, prefers exact repo-relative
|
|
40
|
+
matches over path-suffix matches, and compares case-insensitively.
|
|
41
|
+
|
|
42
|
+
## Honest semantic-precision statuses
|
|
43
|
+
|
|
44
|
+
`COMPLETE` previously meant only "the bounded request finished", producing
|
|
45
|
+
`COMPLETE (0 EXACT_LSP edges)` on Rust repositories and `COMPLETE; exact
|
|
46
|
+
absence was not proven` in `get_dependents`. Now:
|
|
47
|
+
|
|
48
|
+
- A new `NONE` state covers the zero-eligible-targets outcome; `UNAVAILABLE`
|
|
49
|
+
stays reserved for provider failure. `PRECISION_OVERLAY_V` 4 → 5 discards
|
|
50
|
+
old-vocabulary sidecars.
|
|
51
|
+
- `COMPLETE` requires every selected target actually probed.
|
|
52
|
+
- `open_repo` and the build log print the `graph_stats`-style coverage
|
|
53
|
+
fraction: `PARTIAL — 32/1373 bounded target(s) queried (truncated)`.
|
|
54
|
+
- `get_dependents` never pairs a state word with a contradicting note, and
|
|
55
|
+
keeps `NONE`/`UNAVAILABLE` truthful instead of collapsing them into
|
|
56
|
+
`PARTIAL`.
|
|
57
|
+
|
|
58
|
+
## Output fidelity
|
|
59
|
+
|
|
60
|
+
- `trace_api_contract` records per-endpoint method mismatches with sample
|
|
61
|
+
callers and always shows a mismatch block, so `*_WITH_METHOD_MISMATCHES`
|
|
62
|
+
verdicts stay backed by visible rows under any `top_n`.
|
|
63
|
+
- Structure findings and hosted evidence snapshots stop reporting idiomatic
|
|
64
|
+
Rust `mod.rs`/`lib.rs`/`main.rs` parent-child module-tree cycles as
|
|
65
|
+
compile-time coupling (counted as `rustModuleTreeCouplings`; genuine
|
|
66
|
+
cross-directory cycles still report).
|
|
67
|
+
- `query_graph` edge lines and `shortest_path` hops widen repeated or generic
|
|
68
|
+
file basenames (`mod.rs`, `index.*`, `__init__.py`) to their last two path
|
|
69
|
+
segments.
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
The always-fresh graph contract — debounced Git freshness probe plus bounded
|
|
74
|
+
incremental refresh on every graph/health call, no watcher process required —
|
|
75
|
+
is now documented in the README and the agent skill, together with the Rust
|
|
76
|
+
test-surface classification rules.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Weavatrix 0.3.11
|
|
2
|
+
|
|
3
|
+
0.3.11 teaches the graph two new surfaces. SQL schemas become first-class graph
|
|
4
|
+
citizens with honest, evidence-gated dead-code verdicts, and Solidity joins the
|
|
5
|
+
parsed languages at zero dependency cost. The weavatrix.com site moved to its
|
|
6
|
+
own repository, so the engine repo now ships engine only.
|
|
7
|
+
|
|
8
|
+
## SQL: the graph reaches the database schema
|
|
9
|
+
|
|
10
|
+
No SQL grammar ships in the pinned tree-sitter-wasms, so `.sql` indexing runs
|
|
11
|
+
on a dependency-free statement scanner (a new `textOnly` language-module class)
|
|
12
|
+
built for the graph's needs — symbols and references, not SQL semantics.
|
|
13
|
+
String literals and comments are blanked before structural scanning, so quoted
|
|
14
|
+
text can never fabricate a reference.
|
|
15
|
+
|
|
16
|
+
- `CREATE TABLE`/`VIEW`/`FUNCTION`/`PROCEDURE`/`INDEX`/`TRIGGER` become graph
|
|
17
|
+
symbols; table columns (including `ALTER TABLE ADD COLUMN`) are member
|
|
18
|
+
symbols with `member_of`, and tables carry their columns as `field_types`.
|
|
19
|
+
- References resolve inside `.sql` files (a view's `SELECT`, an index's `ON`
|
|
20
|
+
table, a trigger's `EXECUTE FUNCTION`, foreign-key `REFERENCES`) — and, the
|
|
21
|
+
real point, from application code: literal SQL found in string literals of
|
|
22
|
+
any indexed language links the enclosing function to the table it queries.
|
|
23
|
+
`change_impact` and `get_dependents` on a table now answer "which code
|
|
24
|
+
touches this?"
|
|
25
|
+
- Dead-code verdicts stay honest. Schema objects are judged only when the
|
|
26
|
+
repository demonstrably uses literal SQL the scanner can read — an
|
|
27
|
+
ORM-driven repo produces silence, not guesses. Columns of a
|
|
28
|
+
`SELECT *`-consumed table are never judged by name, and indexes/triggers
|
|
29
|
+
(DB-engine surface) are never judged at all. A flagged column reports
|
|
30
|
+
"no SQL statement in the indexed sources references it".
|
|
31
|
+
|
|
32
|
+
## Solidity
|
|
33
|
+
|
|
34
|
+
The tree-sitter-solidity grammar already shipped inside the pinned
|
|
35
|
+
tree-sitter-wasms package — 0.3.11 registers it (and pins its SHA-256 in the
|
|
36
|
+
parser-artifact allowlist), so Solidity support adds zero new dependencies.
|
|
37
|
+
Contracts, interfaces, libraries, functions, constructors, modifiers, events,
|
|
38
|
+
errors, structs, enums and state variables are extracted with visibility and
|
|
39
|
+
membership; `is` inheritance, modifier invocations, `emit`, `new Contract()`
|
|
40
|
+
and `using X for Y` all feed the call/heritage resolvers. Imports resolve
|
|
41
|
+
through relative paths, Foundry remappings (`remappings.txt` / `foundry.toml`)
|
|
42
|
+
and root-anchored specs; npm-style specs (`@openzeppelin/...`) are recorded as
|
|
43
|
+
external dependencies. `.sol` files share directory scope like Go/C#/Rust,
|
|
44
|
+
which makes Solidity's plain `import "./Base.sol"` — a statement that names no
|
|
45
|
+
symbols — resolvable for siblings without guessing.
|
|
46
|
+
|
|
47
|
+
`extractorSchemaV` 6 → 7; cached graphs rebuild once, automatically.
|
|
48
|
+
|
|
49
|
+
## Repository
|
|
50
|
+
|
|
51
|
+
The weavatrix.com landing site (pages, Cloudflare Worker config, deploy
|
|
52
|
+
workflow and its asset checks) moved byte-identically to the separate
|
|
53
|
+
`weavatrix-site` repository, removing ~1.2 MB of page assets from the engine
|
|
54
|
+
repo. The MCPB icon the site used to own is vendored at `mcpb/icon.png`; the
|
|
55
|
+
published npm package contents are unchanged.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weavatrix",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Local repository intelligence MCP for AI coding agents:
|
|
5
|
+
"description": "Local repository intelligence MCP for AI coding agents: an always-fresh architecture graph with production-first evidence — blast radius, dead code, endpoints, clones, history, Health and architecture safeguards, with honestly labeled precision.",
|
|
6
6
|
"author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"homepage": "https://weavatrix.com",
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
"docs/releases/v0.3.7.md",
|
|
44
44
|
"docs/releases/v0.3.8.md",
|
|
45
45
|
"docs/releases/v0.3.9.md",
|
|
46
|
+
"docs/releases/v0.3.10.md",
|
|
47
|
+
"docs/releases/v0.3.11.md",
|
|
46
48
|
"docs/releases/v0.2.19.md",
|
|
47
49
|
"README.md",
|
|
48
50
|
"SECURITY.md",
|
package/skill/SKILL.md
CHANGED
|
@@ -142,12 +142,15 @@ interface dispatch, reflection, or runtime behavior.
|
|
|
142
142
|
- **Evidence, not verdicts**: treat audit, hub, orphan and duplicate output as hypotheses. Confirm a
|
|
143
143
|
finding in source and check framework/runtime conventions before deleting, merging or redesigning
|
|
144
144
|
code. A same-name/different-body pair is a divergence candidate, not proof of duplication.
|
|
145
|
-
- **Freshness**: graph
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`
|
|
145
|
+
- **Freshness**: the graph is always fresh at answer time — no watcher process and no manual refresh
|
|
146
|
+
step exists or is needed. Every graph/health call runs a debounced Git freshness probe and, when the
|
|
147
|
+
repository changed, refreshes before answering (bounded incremental reparse of changed files plus
|
|
148
|
+
reverse importers for JS/TS; full rebuild for config/export-surface/barrel changes and other
|
|
149
|
+
languages). Cross-repository tracing reconciles every selected registered graph the same way. Read
|
|
150
|
+
the structured `refresh` / `graphReconciliation` status: `none`, `incremental`, `full`, or
|
|
151
|
+
explicitly `PARTIAL`. Use `rebuild_graph` only when automatic reconciliation reports a
|
|
152
|
+
fallback/error or when intentionally changing build mode. A normal `open_repo` builds missing graphs
|
|
153
|
+
and upgrades legacy schemas; `build:false` deliberately refuses that upgrade.
|
|
151
154
|
- **Ambiguity**: `get_node`/`get_neighbors`/`get_dependents` disclose `matched N nodes; using the
|
|
152
155
|
best-connected` — read that note before trusting the answer; pass an exact node id to pin it.
|
|
153
156
|
- **Runtime versus compile time**: keep runtime cycles separate from TypeScript type-only and
|
|
@@ -169,6 +172,10 @@ interface dispatch, reflection, or runtime behavior.
|
|
|
169
172
|
roots. Dead-code/clone/audit review also suppresses `benchmarks/**` and `**/__temp/**` as classified
|
|
170
173
|
non-production surfaces. A verified production benchmark can opt back in narrowly through
|
|
171
174
|
`.weavatrix.json` `classify.product`, for example `{"classify":{"product":["benchmarks/core/**"]}}`.
|
|
175
|
+
Rust inline tests (`#[cfg(test)]` modules, `#[test]`/`#[bench]` items) are classified per symbol via
|
|
176
|
+
the node-level `test_surface` flag even though they live in production `.rs` files; dead-code,
|
|
177
|
+
query, hot-path and hub tools treat them like path-classified tests (`include_tests` /
|
|
178
|
+
`include_classified` and a test-term question opt back in).
|
|
172
179
|
- **Architectural queries**: broad bootstrap/tool-execution/routing questions rank conventional
|
|
173
180
|
production and graph-declared entry points ahead of classified docs, sites, benchmarks and
|
|
174
181
|
fixtures. Production-first classification also applies during traversal. A class term in the
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// and is fully testable with no filesystem. It only needs {nodes, links} + source text. See [[graph-builder-internalization]].
|
|
7
7
|
import { posix } from "node:path";
|
|
8
8
|
import { isStructuralRelation } from "../graph/relations.js";
|
|
9
|
+
import { createSqlDeadVerdict } from "../graph/builder/lang-sql.js";
|
|
9
10
|
import { createPathClassifier, hasPathClass } from "../path-classification.js";
|
|
10
11
|
|
|
11
12
|
const IDENT_RE = /[A-Za-z_$][\w$]*/g;
|
|
@@ -198,23 +199,24 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
|
|
|
198
199
|
return isDecorated(n); // framework-registered via decorator
|
|
199
200
|
};
|
|
200
201
|
|
|
202
|
+
const sqlVerdict = createSqlDeadVerdict({ nodes, links, ep, bareName }); // gated SQL verdicts — ORM blindness stays silent
|
|
201
203
|
const deadSymbols = [];
|
|
202
204
|
for (const n of symById.values()) {
|
|
203
|
-
if (isReferenced(n)) continue;
|
|
204
|
-
|
|
205
|
-
deadSymbols.push({ id: n.id, file: n.source_file, label: n.label, test, reason: "no inbound edge and name unreferenced outside its file" });
|
|
205
|
+
if (isReferenced(n) || sqlVerdict.veto(n)) continue;
|
|
206
|
+
deadSymbols.push({ id: n.id, file: n.source_file, label: n.label, test: isTestFile(n.source_file) || n.test_surface === true, reason: sqlVerdict.reason(n) || "no inbound edge and name unreferenced outside its file" });
|
|
206
207
|
}
|
|
207
208
|
const deadSet = new Set(deadSymbols.map((s) => s.id));
|
|
208
209
|
|
|
209
210
|
// A production declaration consumed only by tests is live to the raw graph but dead to production.
|
|
210
211
|
// Keep it in a separate review class: this is useful evidence, never an automatic delete verdict.
|
|
211
212
|
const testOnlySymbols = [];
|
|
213
|
+
const consumerFileOf = (id) => nodesById.get(id)?.source_file || (id.includes("#") ? id.split("#")[0] : id);
|
|
214
|
+
const isTestConsumer = (id) => nodesById.get(id)?.test_surface === true || isTestFile(consumerFileOf(id));
|
|
212
215
|
for (const n of symById.values()) {
|
|
213
|
-
if (deadSet.has(n.id) || isTestFile(n.source_file) || isDecorated(n)) continue;
|
|
216
|
+
if (deadSet.has(n.id) || isTestFile(n.source_file) || n.test_surface === true || isDecorated(n)) continue;
|
|
214
217
|
const sourcesForSymbol = inboundSources.get(String(n.id)) || [];
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
const hasProductionInbound = sourceFiles.some((file) => file && !isTestFile(file));
|
|
218
|
+
const hasTestInbound = sourcesForSymbol.some(isTestConsumer);
|
|
219
|
+
const hasProductionInbound = sourcesForSymbol.some((id) => consumerFileOf(id) && !isTestConsumer(id));
|
|
218
220
|
const name = bareName(n.label);
|
|
219
221
|
const occurrenceSet = occurrenceFiles.get(name) || new Set();
|
|
220
222
|
const externalOccurrences = [...occurrenceSet].filter((file) => file !== n.source_file);
|
|
@@ -232,7 +234,7 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
|
|
|
232
234
|
label: n.label,
|
|
233
235
|
test: false,
|
|
234
236
|
reason: "referenced only from test/e2e code; no production consumer was found",
|
|
235
|
-
testConsumerFiles: [...new Set(
|
|
237
|
+
testConsumerFiles: [...new Set(sourcesForSymbol.filter(isTestConsumer).map(consumerFileOf).filter(Boolean))].sort(),
|
|
236
238
|
evidence: hasTestInbound ? "graph" : hasExactTestInbound ? "exact-semantic" : "lexical",
|
|
237
239
|
publicApi: n.exported === true || ["public", "protected"].includes(String(n.visibility || "").toLowerCase()),
|
|
238
240
|
});
|
|
@@ -51,6 +51,9 @@ export function computeDeadCodeReview(graph, sources, options = {}) {
|
|
|
51
51
|
const suppressed = { tests: 0, classified: 0, confidence: 0, path: 0, kind: 0 };
|
|
52
52
|
const candidates = [];
|
|
53
53
|
for (const candidate of raw) {
|
|
54
|
+
// Node-level test surfaces (Rust #[cfg(test)] symbols in production files) follow the same
|
|
55
|
+
// include_tests policy as path-classified test files.
|
|
56
|
+
if (!includeTests && nodesById.get(String(candidate.id))?.test_surface === true) { suppressed.tests += 1; continue; }
|
|
54
57
|
const info = classify(candidate.file, sources.get(candidate.file));
|
|
55
58
|
const allowed = pathAllowed(info, { includeTests, includeClassified });
|
|
56
59
|
if (!allowed.ok) { suppressed[allowed.bucket] += 1; continue; }
|
|
@@ -128,7 +128,7 @@ export function computeHotPathReview(graph, options = {}) {
|
|
|
128
128
|
if (!complexity || !file || !String(node?.id || '').includes('#')) continue
|
|
129
129
|
if (scope.path && file !== scope.path && !file.startsWith(`${scope.path}/`)) { excluded.outOfScope++; continue }
|
|
130
130
|
const classification = classifier.explain(file)
|
|
131
|
-
const test = hasPathClass(classification, 'test', 'e2e')
|
|
131
|
+
const test = node?.test_surface === true || hasPathClass(classification, 'test', 'e2e')
|
|
132
132
|
const classified = classification.excluded || hasPathClass(classification, ...NON_PRODUCT)
|
|
133
133
|
if (test && options.includeTests !== true) { excluded.tests++; continue }
|
|
134
134
|
if (classified && options.includeClassified !== true) { excluded.classified++; continue }
|
|
@@ -85,12 +85,18 @@ export function analyzeHttpContracts(input = {}) {
|
|
|
85
85
|
for (const backend of backends) {
|
|
86
86
|
for (const endpoint of backend.endpoints) {
|
|
87
87
|
const callsites = [];
|
|
88
|
+
let endpointMismatches = 0;
|
|
89
|
+
const methodMismatchSites = [];
|
|
88
90
|
for (const client of clients) {
|
|
89
91
|
for (const call of client.calls) {
|
|
90
92
|
if (call.path && !methodMatches(endpoint.method, call.method)) {
|
|
91
93
|
const expected = pathSegments(normalizeHttpContractPath(endpoint.path));
|
|
92
94
|
const actual = pathSegments(call.path);
|
|
93
|
-
if (routeShapeMatches(expected, actual) || suffixShapeMatch(expected, actual))
|
|
95
|
+
if (routeShapeMatches(expected, actual) || suffixShapeMatch(expected, actual)) {
|
|
96
|
+
methodMismatches++;
|
|
97
|
+
endpointMismatches++;
|
|
98
|
+
if (methodMismatchSites.length < 3) methodMismatchSites.push({ clientRepo: client.id, file: call.file, line: call.line, method: call.method });
|
|
99
|
+
}
|
|
94
100
|
continue;
|
|
95
101
|
}
|
|
96
102
|
const match = matchHttpContract(endpoint, call);
|
|
@@ -124,6 +130,8 @@ export function analyzeHttpContracts(input = {}) {
|
|
|
124
130
|
file: normalizeContractFile(endpoint.file) || null,
|
|
125
131
|
line: Number(endpoint.line) || null,
|
|
126
132
|
callsites,
|
|
133
|
+
methodMismatches: endpointMismatches,
|
|
134
|
+
methodMismatchSites,
|
|
127
135
|
liveness: externalUseLiveness(callsites, handlerEvidence),
|
|
128
136
|
affected: affectedForEndpoint(callsites, clients, limits),
|
|
129
137
|
});
|
|
@@ -63,6 +63,23 @@ function compileTimeFinding(adjacency, component, runtimeComponents, edges) {
|
|
|
63
63
|
})
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Idiomatic Rust module trees close compile-time SCCs by construction: the parent (mod.rs, lib.rs,
|
|
67
|
+
// main.rs, or a 2018-edition foo.rs) declares `mod child` while children reach back via super::/crate::.
|
|
68
|
+
// Suppress only when every member is a .rs file sitting under one anchor's directory; genuine
|
|
69
|
+
// cross-directory .rs cycles keep their findings.
|
|
70
|
+
export function isRustModuleTreeComponent(component) {
|
|
71
|
+
if (!component.every((file) => String(file).endsWith('.rs'))) return false
|
|
72
|
+
return component.some((anchor) => {
|
|
73
|
+
const path = String(anchor)
|
|
74
|
+
const slash = path.lastIndexOf('/')
|
|
75
|
+
const base = path.slice(slash + 1)
|
|
76
|
+
const anchorDir = ['mod.rs', 'lib.rs', 'main.rs'].includes(base)
|
|
77
|
+
? (slash >= 0 ? path.slice(0, slash) : '')
|
|
78
|
+
: path.slice(0, -'.rs'.length)
|
|
79
|
+
return anchorDir !== '' && component.every((member) => member === anchor || String(member).startsWith(`${anchorDir}/`))
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
export function computeStructureFindings(graph, {rules = {}, entrySet = new Set(), externalImportFiles = new Set()} = {}) {
|
|
67
84
|
const imports = buildFileImportGraph(graph)
|
|
68
85
|
const findings = []
|
|
@@ -73,7 +90,9 @@ export function computeStructureFindings(graph, {rules = {}, entrySet = new Set(
|
|
|
73
90
|
|
|
74
91
|
const runtimeKeys = new Set(runtimeComponents.map((component) => [...component].sort().join('\0')))
|
|
75
92
|
const allComponents = findSccs(imports.allAdj).sort((a, b) => b.length - a.length)
|
|
76
|
-
const
|
|
93
|
+
const compileTimeCandidates = allComponents.filter((component) => !runtimeKeys.has([...component].sort().join('\0')))
|
|
94
|
+
const rustModuleTreeComponents = compileTimeCandidates.filter((component) => isRustModuleTreeComponent(component))
|
|
95
|
+
const compileTimeCouplings = compileTimeCandidates.filter((component) => !isRustModuleTreeComponent(component))
|
|
77
96
|
for (const component of compileTimeCouplings.slice(0, MAX_CYCLE_FINDINGS)) {
|
|
78
97
|
findings.push(compileTimeFinding(imports.allAdj, component, runtimeComponents, {
|
|
79
98
|
runtime: imports.edges,
|
|
@@ -135,6 +154,7 @@ export function computeStructureFindings(graph, {rules = {}, entrySet = new Set(
|
|
|
135
154
|
largestTypeCoupling: compileTimeCouplings[0]?.length || 0,
|
|
136
155
|
compileTimeCouplings: compileTimeCouplings.length,
|
|
137
156
|
largestCompileTimeCoupling: compileTimeCouplings[0]?.length || 0,
|
|
157
|
+
rustModuleTreeCouplings: rustModuleTreeComponents.length,
|
|
138
158
|
orphans: findings.filter((finding) => finding.rule === 'orphan-file').length,
|
|
139
159
|
boundaryViolations: violations.length,
|
|
140
160
|
},
|
|
@@ -64,6 +64,8 @@ export function retrieveTaskContext(g, {
|
|
|
64
64
|
const pathAllowed = (node) => {
|
|
65
65
|
const file = fileOf(node)
|
|
66
66
|
if (!file || changedFiles.has(file) || includeClassified === true) return true
|
|
67
|
+
// Node-level test surfaces (Rust #[cfg(test)]) follow the same production-first policy.
|
|
68
|
+
if (node?.test_surface === true && !requestedClasses.has('test')) return false
|
|
67
69
|
if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, {content: ''}))
|
|
68
70
|
const info = classificationCache.get(file)
|
|
69
71
|
const classes = PATH_CLASS_NAMES.filter((name) => hasPathClass(info, name))
|
package/src/build-graph.js
CHANGED
|
@@ -31,6 +31,14 @@ export function defaultPrecisionMode(env = process.env) {
|
|
|
31
31
|
return env?.WEAVATRIX_PRECISION === "off" ? "off" : "lsp";
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// One-line human status for a precision summary (precisionSummary shape) — the graph_stats
|
|
35
|
+
// composition, shared by the build log and open_repo so every surface reads the same fraction.
|
|
36
|
+
export function precisionStatusLine(precision) {
|
|
37
|
+
const p = precision || {};
|
|
38
|
+
if (p.state === "NONE") return `Semantic precision: NONE — ${p.reason || "no eligible JavaScript/TypeScript semantic targets"}`;
|
|
39
|
+
return `Semantic precision: ${p.state || "UNAVAILABLE"} — ${p.queried || 0}/${p.candidates || 0} bounded target(s) queried${p.truncated ? " (truncated)" : ""}; ${p.verifiedEdges || 0} EXACT_LSP edge(s)${p.state !== "COMPLETE" && p.reason ? `; ${p.reason}` : ""}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
function buildGraphInWorker(payload) {
|
|
35
43
|
return new Promise((resolve, reject) => {
|
|
36
44
|
let worker;
|
|
@@ -193,7 +201,7 @@ export async function buildGraphForRepo(repoPath, {
|
|
|
193
201
|
hotspots: built.hotspots,
|
|
194
202
|
refresh: built.refresh,
|
|
195
203
|
precision: built.precision,
|
|
196
|
-
log: `built-in builder: ${built.nodes} nodes, ${built.links} static links (${built.refresh?.kind || "full"}: ${built.refresh?.reason || "build"});
|
|
204
|
+
log: `built-in builder: ${built.nodes} nodes, ${built.links} static links (${built.refresh?.kind || "full"}: ${built.refresh?.reason || "build"}); ${precisionStatusLine(built.precision)}`
|
|
197
205
|
};
|
|
198
206
|
} catch (error) {
|
|
199
207
|
return { ok: false, error: `graph build failed: ${error.message}`, builder: "internal" };
|
|
@@ -59,6 +59,37 @@ function pathAttribute(modNode) {
|
|
|
59
59
|
return "";
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Test-only classification must mean "compiled exclusively under `cargo test`": #[cfg(test)] and
|
|
63
|
+
// #[cfg(all(test, ...))] guarantee that; #[cfg(any(test, ...))] also compiles into production builds
|
|
64
|
+
// and deliberately stays unclassified. #[test]/#[bench] (incl. #[tokio::test]-style harness paths)
|
|
65
|
+
// mark the annotated function itself.
|
|
66
|
+
const CFG_TEST_RE = /#\s*!?\s*\[\s*cfg\s*\(\s*(?:test\s*\)|all\s*\(\s*test\b)/;
|
|
67
|
+
const TEST_FN_ATTR_RE = /#\s*\[\s*(?:[\w]+(?:\s*::\s*[\w]+)*\s*::\s*)?(?:test|bench)\s*(?:\]|\()/;
|
|
68
|
+
const CFG_TEST_CARRIERS = new Set(["mod_item", "impl_item", "trait_item", "function_item"]);
|
|
69
|
+
const COMMENT_TYPES = new Set(["line_comment", "block_comment"]);
|
|
70
|
+
const itemAttributeText = (item) => {
|
|
71
|
+
// Comments are NAMED siblings in tree-sitter-rust — `#[test]` + `// note` + `fn` must still classify.
|
|
72
|
+
let text = "";
|
|
73
|
+
for (let prev = item?.previousNamedSibling; prev; prev = prev.previousNamedSibling) {
|
|
74
|
+
if (COMMENT_TYPES.has(prev.type)) continue;
|
|
75
|
+
if (prev.type !== "attribute_item") break;
|
|
76
|
+
text += prev.text + "\n";
|
|
77
|
+
}
|
|
78
|
+
return text;
|
|
79
|
+
};
|
|
80
|
+
// `#![cfg(test)]` at the top of a file or module body gates everything below it. tree-sitter-rust
|
|
81
|
+
// tokenizes a file-leading `#!...` as `shebang`; a real shebang line never matches CFG_TEST_RE.
|
|
82
|
+
const innerCfgTest = (body) => (body?.namedChildren || [])
|
|
83
|
+
.some((child) => (child.type === "inner_attribute_item" || child.type === "shebang") && CFG_TEST_RE.test(child.text));
|
|
84
|
+
const underCfgTest = (node) => {
|
|
85
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
86
|
+
if (parent.type === "declaration_list" && innerCfgTest(parent)) return true;
|
|
87
|
+
if (CFG_TEST_CARRIERS.has(parent.type) && CFG_TEST_RE.test(itemAttributeText(parent))) return true;
|
|
88
|
+
if (!parent.parent && innerCfgTest(parent)) return true; // source_file root
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
};
|
|
92
|
+
|
|
62
93
|
function inlineAncestors(node) {
|
|
63
94
|
const result = [];
|
|
64
95
|
for (let p = node?.parent; p; p = p.parent) {
|
|
@@ -129,10 +160,15 @@ export default {
|
|
|
129
160
|
const memberOf = ownerName(owner, field);
|
|
130
161
|
const symbolKind = cap.name === "function" ? (memberOf ? "method" : "function") : cap.name;
|
|
131
162
|
const visibility = publicVisibility(declaration, owner);
|
|
163
|
+
const attrs = itemAttributeText(declaration);
|
|
164
|
+
const testSurface = CFG_TEST_RE.test(attrs)
|
|
165
|
+
|| (cap.name === "function" && TEST_FN_ATTR_RE.test(attrs))
|
|
166
|
+
|| underCfgTest(declaration);
|
|
132
167
|
const id = addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "function", {
|
|
133
168
|
sourceNode: declaration,
|
|
134
169
|
selectionNode: cap.node,
|
|
135
170
|
symbolKind,
|
|
171
|
+
...(testSurface ? { testSurface: true } : {}),
|
|
136
172
|
...(memberOf ? { memberOf, visibility } : { moduleDeclaration: true }),
|
|
137
173
|
...(!memberOf && visibility === "public" ? { exported: true } : {}),
|
|
138
174
|
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Solidity extractor. Symbols: contract/interface/library (space "both" so `is Base` heritage and
|
|
2
|
+
// `new Vault()` both resolve through the value-space resolver), functions/constructors/modifiers/
|
|
3
|
+
// events/errors/structs/enums/state vars (members carry memberOf + visibility). Imports: relative
|
|
4
|
+
// paths, Foundry remappings (remappings.txt / foundry.toml), and root-anchored specs resolve to repo
|
|
5
|
+
// files; everything else (npm-style @openzeppelin/…, absent lib/ submodules) is RECORDED as an
|
|
6
|
+
// external import for dependency analysis. Calls: bare `f()`, modifier invocations, `emit Event(…)`,
|
|
7
|
+
// `new Contract(…)`, and `using Lib for T` all feed the calls resolver. Same-dir symbols share scope
|
|
8
|
+
// (see sharesDirScope): Solidity's flat project namespace makes plain `import "./Base.sol"` — which
|
|
9
|
+
// names no symbols — resolvable for siblings without guessing; cross-dir plain imports still produce
|
|
10
|
+
// the file-level import edge, so blast radius stays correct even where symbol edges are unknowable.
|
|
11
|
+
import { specToPkg } from "./spec-pkg.js";
|
|
12
|
+
|
|
13
|
+
const CONTAINERS = new Set(["contract_declaration", "interface_declaration", "library_declaration"]);
|
|
14
|
+
const KIND_BY_CONTAINER = {
|
|
15
|
+
contract_declaration: "contract",
|
|
16
|
+
interface_declaration: "interface",
|
|
17
|
+
library_declaration: "library",
|
|
18
|
+
};
|
|
19
|
+
// external/public functions are the deployed ABI — callable by any transaction, not just repo code.
|
|
20
|
+
const VISIBILITY = { external: "public", public: "public", internal: "protected", private: "private" };
|
|
21
|
+
|
|
22
|
+
function enclosingContainer(node) {
|
|
23
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
24
|
+
if (CONTAINERS.has(parent.type)) return parent;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const identifierOf = (node) => node?.namedChildren?.find((child) => child.type === "identifier") || null;
|
|
30
|
+
const childOfType = (node, type) => node?.namedChildren?.find((child) => child.type === type) || null;
|
|
31
|
+
const parameterCount = (node) => (node?.namedChildren || []).filter((child) => child.type === "parameter").length;
|
|
32
|
+
|
|
33
|
+
export default {
|
|
34
|
+
family: "solidity",
|
|
35
|
+
grammars: ["solidity"],
|
|
36
|
+
exts: { ".sol": "solidity" },
|
|
37
|
+
isWeb: false,
|
|
38
|
+
calls: `[(call_expression (identifier) @callee) (modifier_invocation (identifier) @callee) (emit_statement (identifier) @callee) (new_expression (type_name (user_defined_type (identifier) @callee))) (using_directive (type_alias (identifier) @callee))]`,
|
|
39
|
+
heritage: [`(inheritance_specifier (user_defined_type (identifier) @base))`],
|
|
40
|
+
|
|
41
|
+
pass1(ctx) {
|
|
42
|
+
const { grammar, tree, fileRel, caps, addSym, addImportEdge, addExternalImport, imports, links, nameToId, resolveSolidityImport } = ctx;
|
|
43
|
+
|
|
44
|
+
// ---- containers (contract/interface/library) ----
|
|
45
|
+
for (const cap of caps(grammar, `[(contract_declaration (identifier) @c) (interface_declaration (identifier) @c) (library_declaration (identifier) @c)]`, tree.rootNode)) {
|
|
46
|
+
addSym(cap.node.text, cap.node.startPosition.row + 1, false, {
|
|
47
|
+
sourceNode: cap.node.parent,
|
|
48
|
+
selectionNode: cap.node,
|
|
49
|
+
symbolKind: KIND_BY_CONTAINER[cap.node.parent.type],
|
|
50
|
+
symbolSpace: "both",
|
|
51
|
+
exported: true,
|
|
52
|
+
moduleDeclaration: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- functions (free + members), constructors, modifiers ----
|
|
57
|
+
const ownedMembers = [];
|
|
58
|
+
const addMember = (name, node, selection, extra) => {
|
|
59
|
+
const container = enclosingContainer(node);
|
|
60
|
+
const ownerName = container ? identifierOf(container)?.text || null : null;
|
|
61
|
+
const id = addSym(name, (selection || node).startPosition.row + 1, extra.callable !== false, {
|
|
62
|
+
sourceNode: node,
|
|
63
|
+
selectionNode: selection || undefined,
|
|
64
|
+
...(ownerName ? { memberOf: ownerName } : { exported: true, moduleDeclaration: true }),
|
|
65
|
+
...extra,
|
|
66
|
+
});
|
|
67
|
+
if (ownerName && id) ownedMembers.push({ ownerName, id });
|
|
68
|
+
return id;
|
|
69
|
+
};
|
|
70
|
+
for (const cap of caps(grammar, `(function_definition (identifier) @fn)`, tree.rootNode)) {
|
|
71
|
+
const definition = cap.node.parent;
|
|
72
|
+
const visibility = VISIBILITY[childOfType(definition, "visibility")?.text || ""];
|
|
73
|
+
addMember(cap.node.text, definition, cap.node, {
|
|
74
|
+
symbolKind: enclosingContainer(definition) ? "method" : "function",
|
|
75
|
+
...(visibility ? { visibility } : {}),
|
|
76
|
+
parameterCount: parameterCount(definition),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
for (const cap of caps(grammar, `(constructor_definition) @ctor`, tree.rootNode))
|
|
80
|
+
addMember("constructor", cap.node, null, { symbolKind: "constructor", parameterCount: parameterCount(cap.node) });
|
|
81
|
+
for (const cap of caps(grammar, `(modifier_definition (identifier) @m)`, tree.rootNode))
|
|
82
|
+
addMember(cap.node.text, cap.node.parent, cap.node, { symbolKind: "modifier", parameterCount: parameterCount(cap.node.parent) });
|
|
83
|
+
|
|
84
|
+
// ---- data/type declarations ----
|
|
85
|
+
for (const cap of caps(grammar, `[(event_definition (identifier) @n) (error_declaration (identifier) @n)]`, tree.rootNode))
|
|
86
|
+
addMember(cap.node.text, cap.node.parent, cap.node, { callable: false, symbolKind: cap.node.parent.type === "event_definition" ? "event" : "error" });
|
|
87
|
+
for (const cap of caps(grammar, `[(struct_declaration (identifier) @n) (enum_declaration (identifier) @n)]`, tree.rootNode))
|
|
88
|
+
addMember(cap.node.text, cap.node.parent, cap.node, { callable: false, symbolKind: cap.node.parent.type === "struct_declaration" ? "struct" : "enum", symbolSpace: "both" });
|
|
89
|
+
// capture the DECLARATION and take its first identifier child (the name): an initializer such as
|
|
90
|
+
// `uint x = OTHER_CONST;` can place a second bare identifier under the same declaration node.
|
|
91
|
+
for (const cap of caps(grammar, `(constant_variable_declaration) @d`, tree.rootNode)) {
|
|
92
|
+
const name = identifierOf(cap.node);
|
|
93
|
+
if (name) addMember(name.text, cap.node, name, { callable: false, symbolKind: "constant" });
|
|
94
|
+
}
|
|
95
|
+
for (const cap of caps(grammar, `(state_variable_declaration) @d`, tree.rootNode)) {
|
|
96
|
+
const name = identifierOf(cap.node);
|
|
97
|
+
const visibility = VISIBILITY[childOfType(cap.node, "visibility")?.text || ""] || "protected"; // Solidity default: internal
|
|
98
|
+
if (name) addMember(name.text, cap.node, name, { callable: false, symbolKind: "variable", visibility });
|
|
99
|
+
}
|
|
100
|
+
for (const member of ownedMembers) {
|
|
101
|
+
const ownerId = nameToId.get(member.ownerName);
|
|
102
|
+
if (ownerId) links.push({ source: ownerId, target: member.id, relation: "contains", confidence: "EXTRACTED" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- imports ----
|
|
106
|
+
for (const cap of caps(grammar, `(import_directive) @imp`, tree.rootNode)) {
|
|
107
|
+
const specNode = childOfType(cap.node, "string");
|
|
108
|
+
if (!specNode) continue;
|
|
109
|
+
const spec = specNode.text.replace(/^["']|["']$/g, "");
|
|
110
|
+
const line = specNode.startPosition.row + 1;
|
|
111
|
+
const target = resolveSolidityImport(fileRel, spec);
|
|
112
|
+
if (target) {
|
|
113
|
+
addImportEdge(target, { specifier: spec, line });
|
|
114
|
+
// `import {A, B} from "./x.sol"` — bind each named symbol. A lone identifier may instead be a
|
|
115
|
+
// `* as Alias` namespace name; binding it the same way is harmless (no same-named symbol → no edge).
|
|
116
|
+
for (const named of cap.node.namedChildren.filter((child) => child.type === "identifier"))
|
|
117
|
+
imports.set(named.text, { imported: named.text, targetFile: target });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (spec.startsWith(".")) { addExternalImport({ spec, kind: "sol-import", line, unresolved: true }); continue; }
|
|
121
|
+
const r = specToPkg(spec);
|
|
122
|
+
if (r) addExternalImport({ spec, pkg: r.pkg, builtin: false, kind: "sol-import", line });
|
|
123
|
+
else addExternalImport({ spec, kind: "sol-import", line, unresolved: true });
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|