weavatrix 0.3.10 → 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 CHANGED
@@ -186,10 +186,18 @@ realpath escapes. Report suspected vulnerabilities privately as described in [SE
186
186
 
187
187
  ## Languages
188
188
 
189
- 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
190
190
  [web-tree-sitter](https://github.com/tree-sitter/tree-sitter) WASM grammars; no Python install and no
191
191
  native compilation.
192
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
+
193
201
  Test surfaces are classified per file (path conventions plus `.weavatrix.json` overrides) and, for
194
202
  Rust, per symbol: `#[cfg(test)]` modules and `#[test]`/`#[bench]` items inside production `.rs` files
195
203
  carry a node-level `test_surface` flag, so dead-code, query, hot-path and hub tools treat them as
@@ -204,9 +212,9 @@ npm run benchmark # full TS/JS/Python/Go/Java/Rust + MCP lifecycle gate
204
212
  npm run benchmark:real # locally available real repos vs source-free 0.2.1 baselines
205
213
  ```
206
214
 
207
- Maintained JavaScript/TypeScript under `src`, `bin`, `scripts`, `test` (and HTML/CSS/JS under `site`)
208
- has a hard 300-line physical ceiling enforced by the release suite; larger concerns split into
209
- owner-focused modules behind slim facades.
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`).
210
218
 
211
219
  ## Release history
212
220
 
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "type": "module",
5
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>",
@@ -44,6 +44,7 @@
44
44
  "docs/releases/v0.3.8.md",
45
45
  "docs/releases/v0.3.9.md",
46
46
  "docs/releases/v0.3.10.md",
47
+ "docs/releases/v0.3.11.md",
47
48
  "docs/releases/v0.2.19.md",
48
49
  "README.md",
49
50
  "SECURITY.md",
@@ -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,12 +199,11 @@ 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
- // test_surface: extractor-proven test-only symbols (Rust #[cfg(test)]) live in production paths.
205
- const test = isTestFile(n.source_file) || n.test_surface === true;
206
- 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" });
207
207
  }
208
208
  const deadSet = new Set(deadSymbols.map((s) => s.id));
209
209
 
@@ -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
+ };
@@ -0,0 +1,263 @@
1
+ // SQL extractor. No SQL grammar ships in the pinned tree-sitter-wasms, so this module is textOnly:
2
+ // a dependency-free statement scanner built for the graph's needs (symbols + references), NOT a SQL
3
+ // parser. Symbols: CREATE TABLE/VIEW (+ columns as member symbols, ALTER TABLE ADD COLUMN included),
4
+ // FUNCTION/PROCEDURE, INDEX, TRIGGER. References: FROM/JOIN/INSERT INTO/UPDATE…SET/DELETE FROM/
5
+ // TRUNCATE/REFERENCES/EXECUTE FUNCTION/CALL inside .sql files, plus the same verbs found in string
6
+ // literals of every other indexed language (scanEmbeddedSql) — that is what ties the code graph to
7
+ // the database schema: a query in app code becomes a `references` edge onto the table it touches.
8
+ // Known blind spots (kept honest downstream, see dead-check.js): ORM-generated SQL is invisible,
9
+ // `SELECT *` consumes columns namelessly (tables get sql_star), DROP statements are not replayed.
10
+ // String literals and comments are blanked before structural scanning so a quoted "SELECT" can
11
+ // never fabricate a reference; dollar-quoted function bodies stay scannable on purpose.
12
+
13
+ const NAME = String.raw`((?:[\`"\[]?[A-Za-z_][\w$]*[\`"\]]?\.)*[\`"\[]?[A-Za-z_][\w$]*[\`"\]]?)`;
14
+ const cleanName = (raw) => String(raw || "").replace(/[`"\[\]]/g, "").split(".").pop();
15
+ const CONSTRAINT_START = /^(?:CONSTRAINT|PRIMARY|FOREIGN|UNIQUE|CHECK|EXCLUDE|KEY|INDEX|LIKE|PERIOD)\b/i;
16
+
17
+ // strings ('…' with '' doubling) and comments (-- …, /* … */) become spaces of equal length,
18
+ // so offsets/line numbers survive and quoted text can't match structural patterns.
19
+ function sanitizeSql(text) {
20
+ const out = text.split("");
21
+ let i = 0;
22
+ const blank = (from, to) => { for (let k = from; k < to; k++) if (out[k] !== "\n") out[k] = " "; };
23
+ while (i < text.length) {
24
+ const ch = text[i], next = text[i + 1];
25
+ if (ch === "-" && next === "-") { let j = i; while (j < text.length && text[j] !== "\n") j++; blank(i, j); i = j; continue; }
26
+ if (ch === "/" && next === "*") { let j = text.indexOf("*/", i + 2); j = j < 0 ? text.length : j + 2; blank(i, j); i = j; continue; }
27
+ if (ch === "'") {
28
+ let j = i + 1;
29
+ while (j < text.length) { if (text[j] === "'") { if (text[j + 1] === "'") { j += 2; continue; } break; } j++; }
30
+ blank(i + 1, Math.min(j, text.length)); i = j + 1; continue;
31
+ }
32
+ i++;
33
+ }
34
+ return out.join("");
35
+ }
36
+
37
+ const lineIndex = (text) => {
38
+ const starts = [0];
39
+ for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
40
+ return (offset) => {
41
+ let lo = 0, hi = starts.length - 1;
42
+ while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (starts[mid] <= offset) lo = mid; else hi = mid - 1; }
43
+ return lo + 1;
44
+ };
45
+ };
46
+
47
+ // split a CREATE TABLE body on top-level commas, keeping each part's offset
48
+ function splitColumns(body, base) {
49
+ const parts = [];
50
+ let depth = 0, start = 0;
51
+ for (let i = 0; i < body.length; i++) {
52
+ const ch = body[i];
53
+ if (ch === "(") depth++;
54
+ else if (ch === ")") depth--;
55
+ else if (ch === "," && depth === 0) { parts.push({ text: body.slice(start, i), offset: base + start }); start = i + 1; }
56
+ }
57
+ parts.push({ text: body.slice(start), offset: base + start });
58
+ return parts;
59
+ }
60
+
61
+ const REF_PATTERNS = [
62
+ new RegExp(String.raw`\bFROM\s+${NAME}`, "gi"),
63
+ new RegExp(String.raw`\bJOIN\s+${NAME}`, "gi"),
64
+ new RegExp(String.raw`\bINSERT\s+INTO\s+${NAME}`, "gi"),
65
+ new RegExp(String.raw`\bMERGE\s+INTO\s+${NAME}`, "gi"),
66
+ new RegExp(String.raw`\bUPDATE\s+(?:ONLY\s+)?${NAME}\s+SET\b`, "gi"),
67
+ new RegExp(String.raw`\bTRUNCATE\s+(?:TABLE\s+)?${NAME}`, "gi"),
68
+ new RegExp(String.raw`\bREFERENCES\s+${NAME}`, "gi"),
69
+ new RegExp(String.raw`\bEXECUTE\s+(?:FUNCTION|PROCEDURE)\s+${NAME}`, "gi"),
70
+ ];
71
+ const STAR_RE = /\bSELECT\s+(?:[A-Za-z_]\w*\.)?\*/i;
72
+
73
+ export default {
74
+ family: "sql",
75
+ grammars: [],
76
+ exts: { ".sql": "sql" },
77
+ isWeb: false,
78
+ textOnly: true,
79
+ calls: null,
80
+ heritage: [],
81
+
82
+ pass1(ctx) {
83
+ const { fileRel, code, addSym, links, sqlRefs } = ctx;
84
+ const text = sanitizeSql(String(code || ""));
85
+ const lineOf = lineIndex(text);
86
+ const fakeNode = (startOffset, endOffset) => ({
87
+ startPosition: { row: lineOf(startOffset) - 1, column: 0 },
88
+ endPosition: { row: lineOf(Math.max(startOffset, endOffset - 1)) - 1, column: 0 },
89
+ });
90
+
91
+ // statements with offsets (';' inside dollar-quoted bodies splits early — refs/lines still land right)
92
+ const statements = [];
93
+ let cursor = 0;
94
+ while (cursor <= text.length) {
95
+ let end = text.indexOf(";", cursor);
96
+ if (end < 0) end = text.length;
97
+ if (text.slice(cursor, end).trim()) statements.push({ text: text.slice(cursor, end), offset: cursor });
98
+ cursor = end + 1;
99
+ }
100
+
101
+ const addRef = (table, offset, star) => {
102
+ const name = cleanName(table);
103
+ if (/^[A-Za-z_][\w$]*$/.test(name)) sqlRefs.push({ file: fileRel, line: lineOf(offset), table: name, star: !!star });
104
+ };
105
+
106
+ for (const stmt of statements) {
107
+ const at = (m) => stmt.offset + m.index;
108
+ const definedHere = new Set();
109
+ const defineSym = (raw, m, callable, extra) => {
110
+ const name = cleanName(raw);
111
+ definedHere.add(name);
112
+ return addSym(name, lineOf(at(m)), callable, {
113
+ sourceNode: fakeNode(at(m), stmt.offset + stmt.text.length),
114
+ exported: true, moduleDeclaration: true, ...extra,
115
+ });
116
+ };
117
+
118
+ let m;
119
+ if ((m = stmt.text.match(new RegExp(String.raw`\bCREATE\s+(?:GLOBAL\s+|LOCAL\s+)?(?:TEMP(?:ORARY)?\s+|UNLOGGED\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?${NAME}\s*\(`, "i")))) {
120
+ const tableName = cleanName(m[1]);
121
+ const open = stmt.offset + m.index + m[0].length - 1;
122
+ let depth = 0, close = open;
123
+ for (let i = open; i < text.length; i++) { if (text[i] === "(") depth++; else if (text[i] === ")" && --depth === 0) { close = i; break; } }
124
+ const fieldTypes = {};
125
+ const columns = [];
126
+ for (const part of splitColumns(text.slice(open + 1, close), open + 1)) {
127
+ const trimmed = part.text.trim();
128
+ if (!trimmed || CONSTRAINT_START.test(trimmed)) continue;
129
+ const col = trimmed.match(new RegExp(String.raw`^${NAME}\s+([A-Za-z_]\w*(?:\([^)]*\))?)`));
130
+ if (!col) continue;
131
+ const colName = cleanName(col[1]);
132
+ if (!/^[A-Za-z_][\w$]*$/.test(colName)) continue;
133
+ fieldTypes[colName] = col[2];
134
+ columns.push({ name: colName, offset: part.offset + part.text.indexOf(col[1]) });
135
+ }
136
+ const tableId = defineSym(m[1], m, false, { symbolKind: "table", fieldTypes });
137
+ for (const column of columns) {
138
+ const columnId = addSym(column.name, lineOf(column.offset), false, {
139
+ sourceNode: fakeNode(column.offset, column.offset + 1),
140
+ symbolKind: "column", memberOf: tableName,
141
+ });
142
+ if (tableId && columnId) links.push({ source: tableId, target: columnId, relation: "contains", confidence: "EXTRACTED" });
143
+ }
144
+ } else if ((m = stmt.text.match(new RegExp(String.raw`\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:MATERIALIZED\s+)?VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?${NAME}`, "i")))) {
145
+ defineSym(m[1], m, false, { symbolKind: "view" });
146
+ } else if ((m = stmt.text.match(new RegExp(String.raw`\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:FUNCTION|PROCEDURE)\s+${NAME}`, "i")))) {
147
+ defineSym(m[1], m, true, { symbolKind: "function" });
148
+ } else if ((m = stmt.text.match(new RegExp(String.raw`\bCREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?${NAME}\s+ON\s+(?:ONLY\s+)?${NAME}`, "i")))) {
149
+ defineSym(m[1], m, false, { symbolKind: "index" });
150
+ addRef(m[2], at(m), false);
151
+ } else if ((m = stmt.text.match(new RegExp(String.raw`\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:CONSTRAINT\s+)?TRIGGER\s+${NAME}[\s\S]*?\bON\s+${NAME}`, "i")))) {
152
+ defineSym(m[1], m, false, { symbolKind: "trigger" });
153
+ addRef(m[2], at(m), false);
154
+ } else if ((m = stmt.text.match(new RegExp(String.raw`\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?${NAME}`, "i")))) {
155
+ const tableName = cleanName(m[1]);
156
+ addRef(m[1], at(m), false);
157
+ const addColumn = new RegExp(String.raw`\bADD\s+(?:COLUMN\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(?!CONSTRAINT\b|PRIMARY\b|FOREIGN\b|UNIQUE\b|CHECK\b|EXCLUDE\b)${NAME}\s+[A-Za-z_]`, "gi");
158
+ for (const colMatch of stmt.text.matchAll(addColumn)) {
159
+ const colName = cleanName(colMatch[1]);
160
+ if (!/^[A-Za-z_][\w$]*$/.test(colName)) continue;
161
+ addSym(colName, lineOf(stmt.offset + colMatch.index), false, {
162
+ sourceNode: fakeNode(stmt.offset + colMatch.index, stmt.offset + colMatch.index + 1),
163
+ symbolKind: "column", memberOf: tableName,
164
+ });
165
+ }
166
+ }
167
+
168
+ const star = STAR_RE.test(stmt.text);
169
+ for (const pattern of REF_PATTERNS) {
170
+ pattern.lastIndex = 0;
171
+ for (const refMatch of stmt.text.matchAll(pattern)) {
172
+ const name = cleanName(refMatch[1]);
173
+ if (!definedHere.has(name)) addRef(refMatch[1], stmt.offset + refMatch.index, star);
174
+ }
175
+ }
176
+ }
177
+ },
178
+ };
179
+
180
+ // SQL verbs inside string literals of any host language — the cross-link between app code and schema.
181
+ // Runs on RAW text (strings are exactly where the queries live); comments can over-match, which only
182
+ // errs toward "referenced" and carries INFERRED confidence.
183
+ const EMBEDDED = [
184
+ { re: new RegExp(String.raw`\bSELECT\b[^;]{0,2000}?\bFROM\s+${NAME}`, "gi"), star: true },
185
+ { re: new RegExp(String.raw`\bINSERT\s+INTO\s+${NAME}`, "gi") },
186
+ { re: new RegExp(String.raw`\bUPDATE\s+${NAME}\s+SET\b`, "gi") },
187
+ { re: new RegExp(String.raw`\bDELETE\s+FROM\s+${NAME}`, "gi") },
188
+ { re: new RegExp(String.raw`\bJOIN\s+${NAME}\b`, "gi") },
189
+ { re: new RegExp(String.raw`\bCALL\s+${NAME}\s*\(`, "gi") },
190
+ ];
191
+ const EMBEDDED_PREFILTER = /\b(?:select|insert|update|delete|join|call)\b/i;
192
+
193
+ export function scanEmbeddedSql(code, fileRel, sqlRefs) {
194
+ const text = String(code || "");
195
+ if (!EMBEDDED_PREFILTER.test(text)) return;
196
+ const lineOf = lineIndex(text);
197
+ for (const { re, star } of EMBEDDED) {
198
+ re.lastIndex = 0;
199
+ for (const m of text.matchAll(re)) {
200
+ const name = cleanName(m[1]);
201
+ if (!/^[A-Za-z_][\w$]*$/.test(name)) continue;
202
+ sqlRefs.push({ file: fileRel, line: lineOf(m.index), table: name, star: !!(star && STAR_RE.test(m[0])) });
203
+ }
204
+ }
205
+ }
206
+
207
+ // Dead-code policy for SQL schema objects, consumed by analysis/dead-check.js. Static liveness can
208
+ // only be judged where the repo demonstrably uses literal SQL the scanner can read: without a single
209
+ // embedded-SQL edge from host code, DB consumers (ORMs, external services) are invisible and every
210
+ // verdict would be a guess. Indexes and triggers are DB-engine surface — never judged. Columns of a
211
+ // `SELECT *`-consumed table are consumed namelessly — never judged by name.
212
+ export function createSqlDeadVerdict({ nodes, links, ep, bareName }) {
213
+ const usageFromCode = links.some((l) => l.usage === "sql" && !String(ep(l.source)).split("#")[0].endsWith(".sql"));
214
+ const starTables = new Set(nodes.filter((n) => n.sql_star).map((n) => bareName(n.label)));
215
+ const classify = (n) => {
216
+ if (!String(n.source_file || "").endsWith(".sql")) return null;
217
+ const kind = String(n.symbol_kind || "");
218
+ if (kind === "index" || kind === "trigger" || !usageFromCode) return "veto";
219
+ if (kind === "column" && starTables.has(String(n.member_of || ""))) return "veto";
220
+ return "flag";
221
+ };
222
+ return {
223
+ veto: (n) => classify(n) === "veto",
224
+ reason: (n) => (classify(n) === "flag" ? "no SQL statement in the indexed sources references it" : null),
225
+ };
226
+ }
227
+
228
+ // After both passes: resolve collected refs against every table/view/function DEFINED in .sql files.
229
+ // Unknown names drop silently — only schema objects the repo actually declares can gain edges.
230
+ export function resolveSqlReferences({ sqlRefs, links, nodeById, perFileSymbols }) {
231
+ if (!sqlRefs?.length) return;
232
+ const index = new Map();
233
+ for (const [file, syms] of perFileSymbols) {
234
+ if (!file.endsWith(".sql")) continue;
235
+ for (const sym of syms) {
236
+ if (!["table", "view", "function"].includes(sym.symbolKind)) continue;
237
+ (index.get(sym.name) || index.set(sym.name, []).get(sym.name)).push(sym.id);
238
+ }
239
+ }
240
+ if (!index.size) return;
241
+ const enclosing = (file, line) => {
242
+ let best = null;
243
+ for (const symbol of perFileSymbols.get(file) || []) {
244
+ if (line < symbol.start || line > symbol.end) continue;
245
+ if (!best || symbol.start > best.start || (symbol.start === best.start && symbol.end < best.end)) best = symbol;
246
+ }
247
+ return best;
248
+ };
249
+ const seen = new Set();
250
+ for (const ref of sqlRefs) {
251
+ const targets = index.get(ref.table);
252
+ if (!targets) continue;
253
+ const source = enclosing(ref.file, ref.line)?.id || ref.file;
254
+ for (const target of targets) {
255
+ if (ref.star) { const node = nodeById.get(target); if (node) node.sql_star = true; }
256
+ if (target === source) continue;
257
+ const key = source + ">" + target;
258
+ if (seen.has(key)) continue;
259
+ seen.add(key);
260
+ links.push({ source, target, relation: "references", confidence: "INFERRED", usage: "sql", line: ref.line });
261
+ }
262
+ }
263
+ }
@@ -1,6 +1,8 @@
1
1
  export function createPass2Resolution({symIdsByFileName, nodeById, importedLocals, symByFileName}) {
2
2
  const dirSymbols = new Map(), dirMethods = new Map(), dirMethodsByName = new Map(), dirTypes = new Map()
3
- const sharesDirScope = (file) => file.endsWith('.go') || file.endsWith('.cs') || file.endsWith('.rs')
3
+ // .sol shares dir scope because Solidity's project namespace is flat: `import "./Base.sol"` names no
4
+ // symbols yet pulls every declaration into scope, so same-dir name resolution is the honest static proxy.
5
+ const sharesDirScope = (file) => file.endsWith('.go') || file.endsWith('.cs') || file.endsWith('.rs') || file.endsWith('.sol')
4
6
  for (const [file, names] of symIdsByFileName) {
5
7
  if (!sharesDirScope(file)) continue
6
8
  const dir = file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : ''
@@ -27,7 +27,7 @@ const CURRENT_GRAPH_SCHEMA = Object.freeze({
27
27
  barrelResolutionV: 1,
28
28
  reExportOccurrencesV: 1,
29
29
  symbolSpacesV: 1,
30
- extractorSchemaV: 6,
30
+ extractorSchemaV: 7,
31
31
  })
32
32
  const CONTROL_FILES = ['.gitignore', '.weavatrixignore', '.weavatrix.json']
33
33
  const MAX_CONTROL_BYTES = 1_000_000
@@ -184,7 +184,7 @@ export async function refreshGraphIncrementally(repoDir, existingGraph, {
184
184
 
185
185
  if (!existingGraph || !existingGraph.fileHashes || !existingGraph.fileExportSignatures
186
186
  || !existingGraph.controlHashes || !existingGraph.jsExportRecords || Number(existingGraph.barrelResolutionV) < 1
187
- || Number(existingGraph.extractorSchemaV) < 6 || Number(existingGraph.physicalFileLocV) < 1
187
+ || Number(existingGraph.extractorSchemaV) < 7 || Number(existingGraph.physicalFileLocV) < 1
188
188
  || Number(existingGraph.reExportOccurrencesV) < 1
189
189
  || Number(existingGraph.symbolSpacesV) < 1 || Number(existingGraph.edgeProvenanceV) < 1) {
190
190
  return full("incremental-baseline-unavailable");
@@ -8,6 +8,7 @@ import { specToPkg } from "./builder/spec-pkg.js";
8
8
  import { analyzeSyntaxComplexity } from "../analysis/source-complexity.js";
9
9
  import { Parser, Query, GRAMMARS, LANGS, EXT_LANG, FAMILY, isDataFile, isDocFile, MAX_PARSE_BYTES, ensureParser, walk } from "./internal-builder.langs.js";
10
10
  import { buildResolvers } from "./internal-builder.resolvers.js";
11
+ import { scanEmbeddedSql, resolveSqlReferences } from "./builder/lang-sql.js";
11
12
  import { assignDeterministicCommunities } from "./community.js";
12
13
  import { snapshotRepository } from "./incremental-refresh.js";
13
14
  import { EDGE_PROVENANCE_V, stampEdgeProvenance } from "./edge-provenance.js";
@@ -72,6 +73,9 @@ export async function buildInternalGraph(repoDir, opts = {}) {
72
73
  // Bare-package imports (axios, node:fs, @scope/x) — the graph can't resolve them to a repo file, but
73
74
  // dependency analysis NEEDS them (unused/missing deps). Additive top-level array; nodes/links untouched.
74
75
  const externalImports = [];
76
+ // SQL references (from .sql statements AND string literals in host-language code) collect during
77
+ // pass 1 and resolve after both passes, once every schema object the repo declares is indexed.
78
+ const sqlRefs = [];
75
79
 
76
80
  const resolvers = buildResolvers(repoDir, fileSet); // aliases, go.mod, java index, href, selectors
77
81
  const { selectorIndex, htmlUsages } = resolvers;
@@ -90,13 +94,16 @@ export async function buildInternalGraph(repoDir, opts = {}) {
90
94
  } // config/infra/docs file-only node
91
95
  let code; try { code = readFileSync(abs, "utf8"); } catch { addNode(fileNode); continue; }
92
96
  addNode({ ...fileNode, physical_loc: physicalLineCount(code) });
93
- const grammar = EXT_LANG[ext]; const lang = LANGS[FAMILY[grammar]]; if (!lang || !langs[grammar]) continue;
97
+ const grammar = EXT_LANG[ext]; const lang = LANGS[FAMILY[grammar]]; if (!lang || (!langs[grammar] && !lang.textOnly)) continue;
94
98
  // giant / generated / minified file → keep the file NODE but don't parse symbols (avoids a wedged parse)
95
99
  if (code.length > MAX_PARSE_BYTES) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue; }
96
100
  if ((++_parsed % 24) === 0) await new Promise((r) => setImmediate(r)); // breathe: let the UI paint between chunks
97
101
  if (typeof opts.onParseFile === "function") opts.onParseFile(fileRel);
98
- const parser = new Parser(); parser.setLanguage(langs[grammar]);
99
- let tree; try { tree = parser.parse(code); } catch { continue; }
102
+ let tree = null; // textOnly languages (SQL) scan `code` directly — no grammar, no parse
103
+ if (!lang.textOnly) {
104
+ const parser = new Parser(); parser.setLanguage(langs[grammar]);
105
+ try { tree = parser.parse(code); } catch { continue; }
106
+ }
100
107
 
101
108
  const syms = []; const nameToId = new Map(); const nameToIds = new Map(); const moduleNameToId = new Map();
102
109
  const addSym = (name, line, callable, extra) => {
@@ -214,8 +221,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
214
221
  // Post-hoc export flag (export {a}, export default X, CJS module.exports) — declarations are flagged at addSym time.
215
222
  const markExported = (name) => { const id = moduleNameToId.get(name); const n = id && nodeById.get(id); if (n) n.exported = true; };
216
223
 
217
- try { lang.pass1({ grammar, tree, fileRel, code, caps, field, addSym, addNode, links, nodeIds, syms, nameToId, imports, addImportEdge, addExternalImport, markExported, recordJsExport, fileSet, ...resolvers }); }
224
+ try { lang.pass1({ grammar, tree, fileRel, code, caps, field, addSym, addNode, links, nodeIds, syms, nameToId, imports, addImportEdge, addExternalImport, markExported, recordJsExport, fileSet, sqlRefs, ...resolvers }); }
218
225
  catch (e) { /* one bad file never sinks the whole build */ void e; }
226
+ // string-literal SQL in host-language code → schema reference candidates (resolved post-pass-2)
227
+ if (!lang.textOnly && !lang.isWeb) try { scanEmbeddedSql(code, fileRel, sqlRefs); } catch { /* never sinks the build */ }
219
228
 
220
229
  syms.sort((a, b) => a.start - b.start);
221
230
  const eof = code.split("\n").length;
@@ -223,7 +232,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
223
232
  if (!syms[i].end || syms[i].end < syms[i].start) syms[i].end = i + 1 < syms.length ? syms[i + 1].start - 1 : eof;
224
233
  }
225
234
  perFileSymbols.set(fileRel, syms); symByFileName.set(fileRel, nameToId); symIdsByFileName.set(fileRel, nameToIds);
226
- tree.delete();
235
+ if (tree) tree.delete();
227
236
  }
228
237
 
229
238
  // ---- pass 2: scope-aware calls + inheritance (Go package = whole dir → same-dir symbols share scope;
@@ -243,6 +252,8 @@ export async function buildInternalGraph(repoDir, opts = {}) {
243
252
  links.push({ source: u.htmlFile, target: cssFile, relation: "references", confidence: "INFERRED" });
244
253
  }
245
254
  }
255
+ // code/SQL → schema-object edges (tables/views/functions declared in .sql files)
256
+ try { resolveSqlReferences({ sqlRefs, links, nodeById, perFileSymbols }); } catch { /* never sinks the build */ }
246
257
 
247
258
  // community = folder bucket (top 2 path parts) — deterministic, mirrors the folder-based module grouping the
248
259
  // app already uses (graph-builder-analysis.js). Populates Modules/community cards without a heavy clustering pass.
@@ -266,7 +277,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
266
277
  barrelResolutionV: 1,
267
278
  reExportOccurrencesV: 1,
268
279
  symbolSpacesV: 1,
269
- extractorSchemaV: 6,
280
+ extractorSchemaV: 7, // v7 = Solidity + SQL indexing (schema objects, embedded-SQL edges)
270
281
  reExportOccurrences,
271
282
  jsExportRecords: Object.fromEntries([...jsExports.entries()].sort(([a], [b]) => a.localeCompare(b))),
272
283
  fileHashes: snapshot.fileHashes,
@@ -16,6 +16,8 @@ import LANG_GO from "./builder/lang-go.js";
16
16
  import LANG_JAVA from "./builder/lang-java.js";
17
17
  import LANG_CSHARP from "./builder/lang-csharp.js";
18
18
  import LANG_RUST from "./builder/lang-rust.js";
19
+ import LANG_SOLIDITY from "./builder/lang-solidity.js";
20
+ import LANG_SQL from "./builder/lang-sql.js";
19
21
  import LANG_HTML from "./builder/lang-html.js";
20
22
  import LANG_CSS from "./builder/lang-css.js";
21
23
 
@@ -27,7 +29,7 @@ const NODE_MODULES = dirname(WTS_DIR);
27
29
  const DEFAULT_WASM_DIR = join(NODE_MODULES, "tree-sitter-wasms", "out");
28
30
 
29
31
  // ---- language registry (derived from the per-language modules) ----
30
- const LANG_MODULES = [LANG_JS, LANG_PY, LANG_GO, LANG_JAVA, LANG_CSHARP, LANG_RUST, LANG_HTML, LANG_CSS];
32
+ const LANG_MODULES = [LANG_JS, LANG_PY, LANG_GO, LANG_JAVA, LANG_CSHARP, LANG_RUST, LANG_SOLIDITY, LANG_SQL, LANG_HTML, LANG_CSS];
31
33
  const LANGS = {}; // family -> module
32
34
  const EXT_LANG = {}; // ext -> grammar
33
35
  const FAMILY = {}; // grammar -> family
@@ -207,6 +207,36 @@ export function buildResolvers(repoDir, fileSet) {
207
207
  if (seg[0] === "src" && seg.length > 2) pyTopDirs.add(seg[1]);
208
208
  }
209
209
 
210
+ // Solidity: relative imports, Foundry remappings (remappings.txt beats foundry.toml, longest prefix
211
+ // wins), then root-anchored specs (`src/X.sol` — forge resolves those against the project root).
212
+ // Nested remapping files are intentionally not scoped per-package: one remapping table per repo is the
213
+ // overwhelmingly common layout, and a wrong guess here would fabricate cross-package edges.
214
+ const solRemappings = [];
215
+ const addRemapping = (entry) => {
216
+ const m = /^\s*([^=\s]+)\s*=\s*(\S+)\s*$/.exec(String(entry || ""));
217
+ if (m && !solRemappings.some((r) => r.prefix === m[1])) solRemappings.push({ prefix: m[1], target: cleanRel(m[2]) });
218
+ };
219
+ // neither file has an indexed extension, so probe the filesystem directly rather than fileSet
220
+ try { for (const line of readLocal("remappings.txt").split(/\r?\n/)) addRemapping(line); } catch { /* absent/unreadable */ }
221
+ try {
222
+ const toml = readLocal("foundry.toml");
223
+ for (const m of toml.matchAll(/remappings\s*=\s*\[([^\]]*)\]/g)) for (const s of m[1].matchAll(/["']([^"']+)["']/g)) addRemapping(s[1]);
224
+ } catch { /* absent/unreadable */ }
225
+ solRemappings.sort((a, b) => b.prefix.length - a.prefix.length);
226
+ const resolveSolidityImport = (fromRel, spec) => {
227
+ if (!spec || !spec.endsWith(".sol")) return null;
228
+ if (spec.startsWith(".")) {
229
+ const candidate = cleanRel(join(dirname(fromRel), spec));
230
+ return fileSet.has(candidate) ? candidate : null;
231
+ }
232
+ for (const { prefix, target } of solRemappings) {
233
+ if (spec !== prefix && !spec.startsWith(prefix)) continue;
234
+ const candidate = cleanRel(target + "/" + spec.slice(prefix.length)).replace(/\/+/g, "/");
235
+ if (fileSet.has(candidate)) return candidate;
236
+ }
237
+ return fileSet.has(spec) ? spec : null;
238
+ };
239
+
210
240
  const selectorIndex = new Map();
211
241
  const htmlUsages = [];
212
242
  const resolveHref = (fromRel, href) => {
@@ -217,5 +247,5 @@ export function buildResolvers(repoDir, fileSet) {
217
247
  return fileSet.has(cand) ? cand : null;
218
248
  };
219
249
 
220
- return { resolveJsImport, resolveAlias, resolvePyPath, pyBaseDir, pyTopDirs, resolveGoImport, dirFiles, resolveRustMod, resolveRustPath, resolveJavaImport, resolveHref, selectorIndex, htmlUsages, goModule, goModules, goRequires };
250
+ return { resolveJsImport, resolveAlias, resolvePyPath, pyBaseDir, pyTopDirs, resolveGoImport, dirFiles, resolveRustMod, resolveRustPath, resolveJavaImport, resolveSolidityImport, resolveHref, selectorIndex, htmlUsages, goModule, goModules, goRequires };
221
251
  }
@@ -17,6 +17,7 @@ const GRAMMAR_SHA256 = Object.freeze({
17
17
  java: '637aac4415fb39a211a4f4292d63c66b5ce9c32fa2cd35464af4f681d91b9a1f',
18
18
  c_sharp: '6266a7e32d68a3459104d994dc848df15d5672b0ea8e86d327274b694f8e6991',
19
19
  rust: '4409921a70d0aa5bec7d1d7ce809a557a8ee1cf6ace901e3ac6a76e62cfea903',
20
+ solidity: '160745e470f234cae903a9ba445d19e758d0b02e1197401fc765976c6254d2b6',
20
21
  html: '11b3405c1543fb012f5ed7f8ee73125076dce8b168301e1e787e4c717da6b456',
21
22
  css: '5fc615467b1b98420ed7517e5bf9e1f88468132dd903d842dfb13714f6a1cb0c',
22
23
  })
@@ -76,7 +76,7 @@ export async function tOpenRepo(g, args, ctx) {
76
76
  savedPrecision = ['lsp', 'off'].includes(saved.graphPrecisionMode) ? saved.graphPrecisionMode : defaultPrecisionMode()
77
77
  schemaUpgrade = !Number.isInteger(saved.edgeTypesV) || saved.edgeTypesV < 2
78
78
  || !Number.isInteger(saved.edgeProvenanceV) || saved.edgeProvenanceV < 1
79
- || !Number.isInteger(saved.extractorSchemaV) || saved.extractorSchemaV < 6
79
+ || !Number.isInteger(saved.extractorSchemaV) || saved.extractorSchemaV < 7
80
80
  || !Number.isInteger(saved.reExportOccurrencesV) || saved.reExportOccurrencesV < 1
81
81
  || !Number.isInteger(saved.symbolSpacesV) || saved.symbolSpacesV < 1
82
82
  || !Number.isInteger(saved.physicalFileLocV) || saved.physicalFileLocV < 1
@@ -28,7 +28,7 @@ const BUDGET_PROPOSALS = Object.freeze(Object.entries(PROVISIONAL_BUDGETS)
28
28
  const SOURCE_ROOTS = new Set(['src', 'app', 'lib', 'packages', 'services'])
29
29
  const CODE_EXTENSIONS = new Set([
30
30
  '.cjs', '.cs', '.css', '.go', '.htm', '.html', '.java', '.js', '.jsx', '.less', '.mjs', '.py', '.pyi',
31
- '.rs', '.scss', '.ts', '.tsx',
31
+ '.rs', '.scss', '.sol', '.sql', '.ts', '.tsx',
32
32
  ])
33
33
  const CLASSIFIED = ['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
34
34
  const EDGE_RELATIONS = new Set(['imports', 're_exports', 'calls', 'references'])
@@ -17,11 +17,12 @@ const TOOL_EXECUTION_TERMS = QUERY_INTENTS.find(([id]) => id === 'tool-execution
17
17
  const TOOL_EXECUTION_TRIGGERS = new Set(['tool', 'tools', 'tooling', 'mcp'])
18
18
  const wordsOf = (value) => String(value ?? '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean)
19
19
  const normPath = (value) => String(value ?? '').replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
20
- const CODE_FILE_RE = /\.(?:[cm]?[jt]sx?|py|go|java|rs|kt|kts|cs|rb|php)$/i
20
+ const CODE_FILE_RE = /\.(?:[cm]?[jt]sx?|py|go|java|rs|kt|kts|cs|rb|php|sol|sql)$/i
21
21
  const DATA_OR_PROSE_RE = /\.(?:json|ya?ml|toml|ini|md|mdx|rst|adoc|html?|css|scss|less|svg)$/i
22
22
  const LANGUAGE_EXTENSIONS = Object.freeze({
23
23
  rust: ['rs'], python: ['py', 'pyi'], typescript: ['ts', 'tsx', 'mts', 'cts'],
24
24
  javascript: ['js', 'jsx', 'mjs', 'cjs'], go: ['go'], java: ['java'], csharp: ['cs'],
25
+ solidity: ['sol'], sql: ['sql'],
25
26
  })
26
27
 
27
28
  function requestedLanguages(query) {
@@ -33,6 +34,8 @@ function requestedLanguages(query) {
33
34
  if (words.has('golang') || /(?:^|[^A-Za-z])Go(?:[^A-Za-z]|$)/.test(raw)) requested.add('go')
34
35
  if (words.has('java')) requested.add('java')
35
36
  if (words.has('csharp') || words.has('dotnet') || /csharp|(?:^|[^A-Za-z])C#(?:[^A-Za-z]|$)/i.test(raw)) requested.add('csharp')
37
+ if (words.has('solidity') || words.has('contract') || words.has('contracts')) requested.add('solidity')
38
+ if (words.has('sql') || words.has('schema') || words.has('migration') || words.has('migrations')) requested.add('sql')
36
39
  return new Set([...requested].flatMap((language) => LANGUAGE_EXTENSIONS[language]))
37
40
  }
38
41
 
@@ -141,7 +144,7 @@ function conceptScore(g, node, concept, queryContext) {
141
144
  else if (term.length >= 4 && term !== 'tool' && term !== 'tools' && (id.includes(term) || label.includes(term))) match = Math.max(match, primary ? 12 : 7)
142
145
  })
143
146
  const extension = (/\.([^.\/]+)$/.exec(source) || [])[1] || ''
144
- const languageConcept = {rust: ['rs'], python: ['py', 'pyi'], py: ['py', 'pyi'], typescript: ['ts', 'tsx', 'mts', 'cts'], ts: ['ts', 'tsx', 'mts', 'cts'], javascript: ['js', 'jsx', 'mjs', 'cjs'], js: ['js', 'jsx', 'mjs', 'cjs'], nodejs: ['js', 'jsx', 'mjs', 'cjs'], golang: ['go'], go: ['go'], java: ['java'], csharp: ['cs'], dotnet: ['cs']}[concept.raw]
147
+ const languageConcept = {rust: ['rs'], python: ['py', 'pyi'], py: ['py', 'pyi'], typescript: ['ts', 'tsx', 'mts', 'cts'], ts: ['ts', 'tsx', 'mts', 'cts'], javascript: ['js', 'jsx', 'mjs', 'cjs'], js: ['js', 'jsx', 'mjs', 'cjs'], nodejs: ['js', 'jsx', 'mjs', 'cjs'], golang: ['go'], go: ['go'], java: ['java'], csharp: ['cs'], dotnet: ['cs'], solidity: ['sol'], sql: ['sql'], schema: ['sql']}[concept.raw]
145
148
  if (languageConcept?.includes(extension) && queryContext.languageExtensions.has(extension)) match = Math.max(match, 58)
146
149
  const fileNode = !isSymbol(node.id), depth = source ? source.split('/').length : 9
147
150
  if (concept.id === 'bootstrap') match = Math.max(match, entrypointSignal(g, node, source, stem))