weavatrix 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -4
- package/docs/releases/v0.3.10.md +76 -0
- package/docs/releases/v0.3.11.md +55 -0
- package/package.json +4 -2
- package/skill/SKILL.md +13 -6
- package/src/analysis/dead-check.js +10 -8
- package/src/analysis/dead-code-review.js +3 -0
- package/src/analysis/dep-rules.js +1 -1
- package/src/analysis/hot-path-review.js +1 -1
- package/src/analysis/http-contracts/analysis.js +9 -1
- package/src/analysis/structure/findings.js +21 -1
- package/src/analysis/task-retrieval.js +2 -0
- package/src/build-graph.js +9 -1
- package/src/graph/builder/lang-rust.js +36 -0
- package/src/graph/builder/lang-solidity.js +126 -0
- package/src/graph/builder/lang-sql.js +263 -0
- package/src/graph/builder/pass2-resolution.js +3 -1
- package/src/graph/freshness-probe.js +1 -1
- package/src/graph/graph-filter.js +5 -3
- package/src/graph/incremental-refresh.js +1 -1
- package/src/graph/internal-builder.build.js +18 -6
- package/src/graph/internal-builder.langs.js +3 -1
- package/src/graph/internal-builder.resolvers.js +31 -1
- package/src/graph/parser-artifact-boundary.js +1 -0
- package/src/mcp/actions/graph-lifecycle.mjs +3 -3
- package/src/mcp/architecture-starter.mjs +1 -1
- package/src/mcp/catalog.mjs +2 -2
- package/src/mcp/evidence-snapshot.structure.mjs +5 -2
- package/src/mcp/graph/context-seeds.mjs +6 -2
- package/src/mcp/graph/tools-query.mjs +28 -2
- package/src/mcp/health/endpoints.mjs +34 -5
- package/src/mcp/sync/payload-v2.mjs +1 -0
- package/src/mcp/tools-company.mjs +9 -0
- package/src/mcp/tools-endpoints.mjs +54 -12
- package/src/mcp/tools-graph-hubs.mjs +1 -0
- package/src/mcp/tools-impact.mjs +9 -1
- package/src/precision/lsp-overlay/build.js +9 -4
- package/src/precision/lsp-overlay/contract.js +1 -1
- package/src/precision/lsp-overlay/target-index.js +8 -3
- package/src/precision/symbol-query.js +2 -1
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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) <
|
|
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
|
-
|
|
99
|
-
|
|
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:
|
|
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 <
|
|
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
|
-
|
|
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'])
|
package/src/mcp/catalog.mjs
CHANGED
|
@@ -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
|
-
|
|
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))
|