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.
Files changed (40) hide show
  1. package/README.md +29 -4
  2. package/docs/releases/v0.3.10.md +76 -0
  3. package/docs/releases/v0.3.11.md +55 -0
  4. package/package.json +4 -2
  5. package/skill/SKILL.md +13 -6
  6. package/src/analysis/dead-check.js +10 -8
  7. package/src/analysis/dead-code-review.js +3 -0
  8. package/src/analysis/dep-rules.js +1 -1
  9. package/src/analysis/hot-path-review.js +1 -1
  10. package/src/analysis/http-contracts/analysis.js +9 -1
  11. package/src/analysis/structure/findings.js +21 -1
  12. package/src/analysis/task-retrieval.js +2 -0
  13. package/src/build-graph.js +9 -1
  14. package/src/graph/builder/lang-rust.js +36 -0
  15. package/src/graph/builder/lang-solidity.js +126 -0
  16. package/src/graph/builder/lang-sql.js +263 -0
  17. package/src/graph/builder/pass2-resolution.js +3 -1
  18. package/src/graph/freshness-probe.js +1 -1
  19. package/src/graph/graph-filter.js +5 -3
  20. package/src/graph/incremental-refresh.js +1 -1
  21. package/src/graph/internal-builder.build.js +18 -6
  22. package/src/graph/internal-builder.langs.js +3 -1
  23. package/src/graph/internal-builder.resolvers.js +31 -1
  24. package/src/graph/parser-artifact-boundary.js +1 -0
  25. package/src/mcp/actions/graph-lifecycle.mjs +3 -3
  26. package/src/mcp/architecture-starter.mjs +1 -1
  27. package/src/mcp/catalog.mjs +2 -2
  28. package/src/mcp/evidence-snapshot.structure.mjs +5 -2
  29. package/src/mcp/graph/context-seeds.mjs +6 -2
  30. package/src/mcp/graph/tools-query.mjs +28 -2
  31. package/src/mcp/health/endpoints.mjs +34 -5
  32. package/src/mcp/sync/payload-v2.mjs +1 -0
  33. package/src/mcp/tools-company.mjs +9 -0
  34. package/src/mcp/tools-endpoints.mjs +54 -12
  35. package/src/mcp/tools-graph-hubs.mjs +1 -0
  36. package/src/mcp/tools-impact.mjs +9 -1
  37. package/src/precision/lsp-overlay/build.js +9 -4
  38. package/src/precision/lsp-overlay/contract.js +1 -1
  39. package/src/precision/lsp-overlay/target-index.js +8 -3
  40. package/src/precision/symbol-query.js +2 -1
@@ -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: 5,
30
+ extractorSchemaV: 7,
31
31
  })
32
32
  const CONTROL_FILES = ['.gitignore', '.weavatrixignore', '.weavatrix.json']
33
33
  const MAX_CONTROL_BYTES = 1_000_000
@@ -22,12 +22,14 @@ export function filterGraphForMode(graph, mode, { repoRoot = null } = {}) {
22
22
  const links = graph.links || [];
23
23
  const endpoint = (value) => (value && typeof value === "object" ? value.id : value);
24
24
  const classifier = createPathClassifier(repoRoot);
25
- const isTest = (path) => hasPathClass(classifier.explain(path), "test", "e2e");
25
+ // A node is a test surface when its file is test-classified OR the extractor proved the symbol
26
+ // itself is compiled only under test (Rust #[cfg(test)] inline modules live in production files).
27
+ const isTest = (node) => node.test_surface === true || hasPathClass(classifier.explain(node.source_file), "test", "e2e");
26
28
  let keep;
27
29
  if (mode === "no-tests") {
28
- keep = new Set(nodes.filter((node) => !isTest(node.source_file)).map((node) => node.id));
30
+ keep = new Set(nodes.filter((node) => !isTest(node)).map((node) => node.id));
29
31
  } else {
30
- const testIds = new Set(nodes.filter((node) => isTest(node.source_file)).map((node) => node.id));
32
+ const testIds = new Set(nodes.filter((node) => isTest(node)).map((node) => node.id));
31
33
  keep = new Set(testIds);
32
34
  for (const link of links) {
33
35
  if (testIds.has(endpoint(link.source))) keep.add(endpoint(link.target)); // a test's dependency
@@ -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) < 5 || 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) => {
@@ -148,6 +155,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
148
155
  } : {}),
149
156
  ...(complexity ? { complexity } : {}),
150
157
  ...(extra && extra.exported ? { exported: true } : {}),
158
+ ...(extra && extra.testSurface ? { test_surface: true } : {}),
151
159
  ...(extra && extra.decorated ? { decorated: true } : {}),
152
160
  ...(extra && extra.symbolKind ? { symbol_kind: extra.symbolKind } : {}),
153
161
  ...(extra && extra.symbolSpace ? { symbol_space: extra.symbolSpace } : {}),
@@ -213,8 +221,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
213
221
  // Post-hoc export flag (export {a}, export default X, CJS module.exports) — declarations are flagged at addSym time.
214
222
  const markExported = (name) => { const id = moduleNameToId.get(name); const n = id && nodeById.get(id); if (n) n.exported = true; };
215
223
 
216
- 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 }); }
217
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 */ }
218
228
 
219
229
  syms.sort((a, b) => a.start - b.start);
220
230
  const eof = code.split("\n").length;
@@ -222,7 +232,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
222
232
  if (!syms[i].end || syms[i].end < syms[i].start) syms[i].end = i + 1 < syms.length ? syms[i + 1].start - 1 : eof;
223
233
  }
224
234
  perFileSymbols.set(fileRel, syms); symByFileName.set(fileRel, nameToId); symIdsByFileName.set(fileRel, nameToIds);
225
- tree.delete();
235
+ if (tree) tree.delete();
226
236
  }
227
237
 
228
238
  // ---- pass 2: scope-aware calls + inheritance (Go package = whole dir → same-dir symbols share scope;
@@ -242,6 +252,8 @@ export async function buildInternalGraph(repoDir, opts = {}) {
242
252
  links.push({ source: u.htmlFile, target: cssFile, relation: "references", confidence: "INFERRED" });
243
253
  }
244
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 */ }
245
257
 
246
258
  // community = folder bucket (top 2 path parts) — deterministic, mirrors the folder-based module grouping the
247
259
  // app already uses (graph-builder-analysis.js). Populates Modules/community cards without a heavy clustering pass.
@@ -265,7 +277,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
265
277
  barrelResolutionV: 1,
266
278
  reExportOccurrencesV: 1,
267
279
  symbolSpacesV: 1,
268
- extractorSchemaV: 5,
280
+ extractorSchemaV: 7, // v7 = Solidity + SQL indexing (schema objects, embedded-SQL edges)
269
281
  reExportOccurrences,
270
282
  jsExportRecords: Object.fromEntries([...jsExports.entries()].sort(([a], [b]) => a.localeCompare(b))),
271
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
  })
@@ -1,7 +1,7 @@
1
1
  import {existsSync, readFileSync, realpathSync, statSync, writeFileSync} from 'node:fs'
2
2
  import {dirname, isAbsolute, join} from 'node:path'
3
3
  import {diffGraphs, formatGraphDiff, prevGraphPathFor} from '../graph-context.mjs'
4
- import {buildGraphForRepo, defaultPrecisionMode} from '../../build-graph.js'
4
+ import {buildGraphForRepo, defaultPrecisionMode, precisionStatusLine} from '../../build-graph.js'
5
5
  import {graphHomeDir, graphOutDirForModule, graphOutDirForRepo} from '../../graph/layout.js'
6
6
  import {liveRepositoryRecords, registerRepository} from '../../graph/repo-registry.js'
7
7
  import {precisionSemanticInputsMatch, readPrecisionOverlay} from '../../precision/lsp-overlay.js'
@@ -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 < 5
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
@@ -128,7 +128,7 @@ export async function tOpenRepo(g, args, ctx) {
128
128
  `Opened ${repoPath}${buildNote}: ${loaded.nodes.length} nodes / ${loaded.links.length} edges. All tools now target this repo.`,
129
129
  `Graph: ${graphPath}`,
130
130
  `Build mode: ${loaded.graphBuildMode || 'full'}`,
131
- `Semantic precision: ${loaded.precision?.state || 'UNAVAILABLE'} (${loaded.precision?.verifiedEdges || 0} EXACT_LSP edge(s))`,
131
+ precisionStatusLine(loaded.precision),
132
132
  ].join('\n')
133
133
  }
134
134
 
@@ -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'])
@@ -128,8 +128,8 @@ function buildTools({tg, ti, th, ts, tb, te, ta, tar, thi, tc, tv, caps}) {
128
128
  {cap: 'health', name: 'hot_path_review', description: 'Rank a focused production-symbol hot-path queue from parser-derived local complexity, inside-loop allocations/copies/scans/sorts/recursion, graph fan-in/fan-out, and measured coverage or clearly labelled static test reachability. The default score gate is 85 with a narrow strong-local fallback; set min_score=0 for the full diagnostic queue. This is not profiler data or interprocedural Big-O.', inputSchema: {type: 'object', properties: {path: {type: 'string', maxLength: 1024, description: 'Optional repository-relative path prefix'}, top_n: {type: 'integer', minimum: 1, maximum: 100, default: 20}, min_score: {type: 'integer', minimum: 0, maximum: 100, default: 85, description: 'Focused default is 85; lower explicitly to broaden, or use 0 for every threshold-matching diagnostic candidate'}, cyclomatic_threshold: {type: 'integer', minimum: 2, maximum: 1000, default: 8}, call_threshold: {type: 'integer', minimum: 1, maximum: 10000, default: 12}, loop_depth_threshold: {type: 'integer', minimum: 1, maximum: 10, default: 2}, time_rank_threshold: {type: 'integer', minimum: 0, maximum: 5, default: 2}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/mock/story/docs/benchmark/temp and explicitly excluded paths; tests still require include_tests'}}}, run: (g, a, ctx) => th.tHotPathReview(g, a, ctx)},
129
129
  {cap: 'graph', name: 'list_communities', description: 'List graph communities named by their dominant folder (largest first) with sample files — a readable module overview; feed the list position into get_community.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', description: 'Max communities to list, default 20'}}}, run: (g, a, ctx) => th.tListCommunities(g, a, ctx)},
130
130
  {cap: 'graph', name: 'module_map', description: 'First orientation view for understanding an unfamiliar application with little context: a production-first folder architecture map with file/symbol counts and strongest module dependencies, separating runtime, TypeScript type-only and language compile-only coupling.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', description: 'Max modules to list, default 25'}, include_non_product: {type: 'boolean', default: false, description: 'Include tests, fixtures, benchmarks, generated output, docs and other classified non-product files; false by default'}}}, run: (g, a, ctx) => th.tModuleMap(g, a, ctx)},
131
- {cap: 'source', refreshGraph: true, name: 'list_endpoints', description: 'Inventory of HTTP endpoints defined in the repo (Express/Fastify/Nest/Flask/FastAPI/Go mux/Rust axum and actix-web/Spring MVC and WebFlux): declared and reachable composed paths, static mount provenance, confidence, handler, file:line, and Spring conditional/default-active state.', inputSchema: {type: 'object', properties: {method: {type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT', 'ALL', 'ANY']}, path: {type: 'string', maxLength: 2048, description: 'Optional exact composed path or segment-aligned suffix'}, max_results: {type: 'integer', description: 'Max endpoints to list, default 100'}}}, run: (g, a, ctx) => th.tListEndpoints(g, a, ctx)},
132
- {cap: 'source', refreshGraph: true, name: 'trace_endpoint', description: 'Resolve one exact reachable HTTP endpoint, prove its router mount chain, bind its handler symbol, and return a bounded production-only multi-hop call graph with call-site excerpts. This is a focused projection of the repository graph, not text-search inference.', inputSchema: {type: 'object', properties: {path: {type: 'string', minLength: 1, maxLength: 2048, description: 'Exact composed path; a suffix is accepted only when unambiguous'}, method: {type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT', 'ALL', 'ANY']}, max_depth: {type: 'integer', minimum: 1, maximum: 4, default: 3}, max_nodes: {type: 'integer', minimum: 2, maximum: 40, default: 20}, max_excerpts: {type: 'integer', minimum: 0, maximum: 12, default: 6}, context_lines: {type: 'integer', minimum: 0, maximum: 6, default: 2}, include_classified: {type: 'boolean', default: false, description: 'Include test/e2e/generated/mock/story/docs/benchmark/temp and explicitly excluded targets'}}, required: ['path']}, run: (g, a, ctx) => te.tTraceEndpoint(g, a, ctx)},
131
+ {cap: 'source', refreshGraph: true, name: 'list_endpoints', description: 'Inventory of HTTP endpoints defined in the repo (Express/Fastify/Nest/Flask/FastAPI/Go mux/Rust axum and actix-web/Spring MVC and WebFlux): declared and reachable composed paths, static mount provenance, confidence, handler, file:line, and Spring conditional/default-active state.', inputSchema: {type: 'object', properties: {method: {type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT', 'ALL', 'ANY']}, path: {type: 'string', maxLength: 2048, description: 'Optional exact composed path or segment-aligned suffix'}, max_results: {type: 'integer', description: 'Max endpoints to list, default 100'}, include_classified: {type: 'boolean', default: false, description: 'Include test/e2e/generated/mock/story/docs/benchmark/temp and explicitly excluded targets'}}}, run: (g, a, ctx) => th.tListEndpoints(g, a, ctx)},
132
+ {cap: 'source', refreshGraph: true, name: 'trace_endpoint', description: 'Resolve one exact reachable HTTP endpoint, prove its router mount chain, bind its handler symbol, and return a bounded production-only multi-hop call graph with call-site excerpts. This is a focused projection of the repository graph, not text-search inference.', inputSchema: {type: 'object', properties: {path: {type: 'string', minLength: 1, maxLength: 2048, description: 'Exact composed path; a suffix is accepted only when unambiguous'}, method: {type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT', 'ALL', 'ANY']}, handler_file: {type: 'string', maxLength: 1024, description: 'Repo-relative file path (or unambiguous path suffix) declaring the handler; use after an AMBIGUOUS_HANDLER result.'}, max_depth: {type: 'integer', minimum: 1, maximum: 4, default: 3}, max_nodes: {type: 'integer', minimum: 2, maximum: 40, default: 20}, max_excerpts: {type: 'integer', minimum: 0, maximum: 12, default: 6}, context_lines: {type: 'integer', minimum: 0, maximum: 6, default: 2}, include_classified: {type: 'boolean', default: false, description: 'Include test/e2e/generated/mock/story/docs/benchmark/temp and explicitly excluded targets'}}, required: ['path']}, run: (g, a, ctx) => te.tTraceEndpoint(g, a, ctx)},
133
133
  {cap: 'build', name: 'rebuild_graph', description: "Rebuild the active full-repository graph and report a structural delta. Omitted mode/precision preserve the active graph; a first build uses full and the startup precision setting (lsp unless WEAVATRIX_PRECISION=off). The local TypeScript/JavaScript LSP overlay validates bounded ambiguous edges; precision:off is an explicit fallback. With scope, build an isolated diagnostic graph without replacing or diffing the full graph.", inputSchema: {type: 'object', properties: {mode: {type: 'string', enum: ['full', 'no-tests', 'tests-only'], description: 'Build mode; omit to preserve the active mode (or use full for a first build)'}, precision: {type: 'string', enum: ['lsp', 'off'], description: 'Semantic precision; omit to preserve the active mode (or use the startup setting for a first build)'}, scope: {type: 'string', description: 'Optional isolated diagnostic path prefix; never replaces the active full graph'}}}, run: (g, a, ctx) => ta.tRebuildGraph(g, a, ctx)},
134
134
  {cap: 'graph', name: 'graph_diff', description: 'Structural graph diff: compare the current graph with an immutable Git-ref baseline (base_ref such as HEAD~1 or main), or with graph.prev.json from the last rebuild when base_ref is omitted. Reports architecture drift, cycle changes and symbols that lost their last caller.', inputSchema: {type: 'object', properties: {base_ref: {type: 'string', maxLength: 256, description: 'Optional immutable Git baseline to build in isolation, e.g. HEAD~1, main or origin/main; never checks out or mutates the working tree'}, path: {type: 'string', description: 'Optional node-id/path prefix to scope the diff, e.g. src/query'}}}, run: (g, a, ctx) => ti.tGraphDiff(g, a, ctx)},
135
135
  {cap: 'graph', name: 'get_architecture_contract', description: 'Read the owner-approved architecture target or safely bootstrap one. With action=preview, returns an adaptive candidate, observed-but-not-enforced dependency directions, verification, exact file content/hash and a short-lived confirmation token. action=approve creates the local contract only after explicit token confirmation and never overwrites an active target.', inputSchema: {type: 'object', properties: {action: {type: 'string', enum: ['preview', 'approve'], description: 'Omit to read; preview is dry-run only; approve requires the preview token'}, candidate_contract: {type: 'object', description: 'Optional reviewed candidate to normalize and verify during preview'}, baseline_mode: {type: 'string', enum: ['none', 'accept-current'], default: 'none', description: 'Whether preview should materialize current violations as an explicit ratchet baseline'}, confirm_token: {type: 'string', description: 'One-time token returned by preview; required for approve'}}}, run: (g, a, ctx) => tar.tGetArchitectureContract(g, a, ctx)},
@@ -1,6 +1,6 @@
1
1
  import {createHash} from 'node:crypto'
2
2
  import {readFileSync} from 'node:fs'
3
- import {buildFileImportGraph, checkBoundaries, findSccs, representativeCycle} from '../analysis/dep-rules.js'
3
+ import {buildFileImportGraph, checkBoundaries, findSccs, isRustModuleTreeComponent, representativeCycle} from '../analysis/dep-rules.js'
4
4
  import {createRepoBoundary} from '../repo-path.js'
5
5
  import {CAPS, bounded, compareText, repoRelativePath, safeToken} from './evidence-snapshot.common.mjs'
6
6
 
@@ -59,7 +59,10 @@ export function buildStructureEvidence(graph, repoRoot) {
59
59
  const imports = buildFileImportGraph(graph)
60
60
  const runtimeSccs = sortedSccs(imports.runtimeAdj)
61
61
  const runtimeKeys = new Set(runtimeSccs.map((members) => members.join('\0')))
62
- const compileSccs = sortedSccs(imports.allAdj).filter((members) => !runtimeKeys.has(members.join('\0')))
62
+ // Same suppression as computeStructureFindings: a synced snapshot must not contradict the
63
+ // local structure findings on idiomatic Rust module trees.
64
+ const compileSccs = sortedSccs(imports.allAdj)
65
+ .filter((members) => !runtimeKeys.has(members.join('\0')) && !isRustModuleTreeComponent(members))
63
66
  const allCycles = [
64
67
  ...runtimeSccs.map((members) => cycleFact('runtime', imports.runtimeAdj, members)),
65
68
  ...compileSccs.map((members) => cycleFact('compile-time', imports.allAdj, members)),
@@ -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
 
@@ -52,6 +55,7 @@ export function requestedPathClasses(query) {
52
55
  }
53
56
 
54
57
  function isQueryEligible(node, requestedClasses, classificationCache, classifier) {
58
+ if (node?.test_surface === true && !requestedClasses.has('test')) return false
55
59
  const source = sourceFileOf(node)
56
60
  if (!source) return true
57
61
  if (!classificationCache.has(source)) classificationCache.set(source, classifier.explain(source, {content: ''}))
@@ -140,7 +144,7 @@ function conceptScore(g, node, concept, queryContext) {
140
144
  else if (term.length >= 4 && term !== 'tool' && term !== 'tools' && (id.includes(term) || label.includes(term))) match = Math.max(match, primary ? 12 : 7)
141
145
  })
142
146
  const extension = (/\.([^.\/]+)$/.exec(source) || [])[1] || ''
143
- 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]
144
148
  if (languageConcept?.includes(extension) && queryContext.languageExtensions.has(extension)) match = Math.max(match, 58)
145
149
  const fileNode = !isSymbol(node.id), depth = source ? source.split('/').length : 9
146
150
  if (concept.id === 'bootstrap') match = Math.max(match, entrypointSignal(g, node, source, stem))