weavatrix 0.3.10 → 0.3.12
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 +12 -4
- package/docs/releases/v0.3.11.md +55 -0
- package/docs/releases/v0.3.12.md +44 -0
- package/package.json +4 -1
- package/src/analysis/cargo-manifests.js +7 -0
- package/src/analysis/dead-check.js +4 -4
- package/src/analysis/duplicates.compute.js +5 -1
- package/src/analysis/hot-path-review.js +25 -3
- package/src/analysis/internal-audit/supply-chain.js +1 -1
- package/src/analysis/manifests.js +4 -1
- package/src/analysis/static-test-reachability.js +19 -2
- package/src/analysis-kit.mjs +17 -0
- package/src/graph/builder/lang-rust-calls.js +63 -0
- package/src/graph/builder/lang-rust.js +31 -6
- 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 +15 -2
- package/src/graph/freshness-probe.js +1 -1
- package/src/graph/incremental-refresh.js +1 -1
- package/src/graph/internal-builder.build.js +18 -7
- package/src/graph/internal-builder.langs.js +3 -1
- package/src/graph/internal-builder.pass2.js +4 -2
- package/src/graph/internal-builder.resolvers.js +66 -1
- package/src/graph/parser-artifact-boundary.js +1 -0
- package/src/mcp/actions/graph-lifecycle.mjs +1 -1
- package/src/mcp/architecture-starter.mjs +1 -1
- package/src/mcp/graph/context-seeds.mjs +23 -7
- package/src/precision/typescript-provider/client.js +7 -0
- package/src/security/advisory-store.js +0 -1
- package/src/security/malware-heuristics.exclusions.js +1 -1
- package/src/security/registry-sig.rules.js +1 -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,11 @@
|
|
|
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
|
+
// Rust associated functions / methods indexed by their owning type, per directory: `Type::method`
|
|
4
|
+
// resolves to the exact impl member instead of any same-named function in the directory.
|
|
5
|
+
const rustMethods = new Map() // dir -> Map(typeName -> Map(methodName -> id))
|
|
6
|
+
// .sol shares dir scope because Solidity's project namespace is flat: `import "./Base.sol"` names no
|
|
7
|
+
// symbols yet pulls every declaration into scope, so same-dir name resolution is the honest static proxy.
|
|
8
|
+
const sharesDirScope = (file) => file.endsWith('.go') || file.endsWith('.cs') || file.endsWith('.rs') || file.endsWith('.sol')
|
|
4
9
|
for (const [file, names] of symIdsByFileName) {
|
|
5
10
|
if (!sharesDirScope(file)) continue
|
|
6
11
|
const dir = file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : ''
|
|
@@ -22,6 +27,13 @@ export function createPass2Resolution({symIdsByFileName, nodeById, importedLocal
|
|
|
22
27
|
continue
|
|
23
28
|
}
|
|
24
29
|
if (!symbols.has(name)) symbols.set(name, id)
|
|
30
|
+
if (file.endsWith('.rs') && node?.member_of) {
|
|
31
|
+
let byType = rustMethods.get(dir)
|
|
32
|
+
if (!byType) rustMethods.set(dir, (byType = new Map()))
|
|
33
|
+
let methods = byType.get(node.member_of)
|
|
34
|
+
if (!methods) byType.set(node.member_of, (methods = new Map()))
|
|
35
|
+
if (!methods.has(name)) methods.set(name, id)
|
|
36
|
+
}
|
|
25
37
|
if (file.endsWith('.go') && node?.symbol_space === 'type') {
|
|
26
38
|
let types = dirTypes.get(dir)
|
|
27
39
|
if (!types) dirTypes.set(dir, (types = new Map()))
|
|
@@ -53,6 +65,7 @@ export function createPass2Resolution({symIdsByFileName, nodeById, importedLocal
|
|
|
53
65
|
if (!imported?.targetFile) return null
|
|
54
66
|
return resolveNamedSymbol(imported.originFile || imported.targetFile, imported.originName || imported.imported, 'value')
|
|
55
67
|
}
|
|
68
|
+
const resolveRustMethod = (dir, typeName, methodName) => rustMethods.get(dir)?.get(typeName)?.get(methodName) || null
|
|
56
69
|
const javaTypeKinds = new Set(['class', 'interface', 'enum', 'record', 'annotation'])
|
|
57
70
|
const resolveJavaType = (name, file) => {
|
|
58
71
|
const imported = importedLocals.get(file)?.get(name)
|
|
@@ -64,5 +77,5 @@ export function createPass2Resolution({symIdsByFileName, nodeById, importedLocal
|
|
|
64
77
|
const target = symByFileName.get(file)?.get(name)
|
|
65
78
|
return target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind) ? target : null
|
|
66
79
|
}
|
|
67
|
-
return {dirSymbols, dirMethods, dirMethodsByName, dirTypes, resolveNamedSymbol, resolveCall, resolveJavaType}
|
|
80
|
+
return {dirSymbols, dirMethods, dirMethodsByName, dirTypes, resolveNamedSymbol, resolveCall, resolveJavaType, resolveRustMethod}
|
|
68
81
|
}
|
|
@@ -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
|
|
@@ -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) => {
|
|
@@ -214,8 +221,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
214
221
|
// Post-hoc export flag (export {a}, export default X, CJS module.exports) — declarations are flagged at addSym time.
|
|
215
222
|
const markExported = (name) => { const id = moduleNameToId.get(name); const n = id && nodeById.get(id); if (n) n.exported = true; };
|
|
216
223
|
|
|
217
|
-
try { lang.pass1({ grammar, tree, fileRel, code, caps, field, addSym, addNode, links, nodeIds, syms, nameToId, imports, addImportEdge, addExternalImport, markExported, recordJsExport, fileSet, ...resolvers }); }
|
|
224
|
+
try { lang.pass1({ grammar, tree, fileRel, code, caps, field, addSym, addNode, links, nodeIds, syms, nameToId, imports, addImportEdge, addExternalImport, markExported, recordJsExport, fileSet, sqlRefs, ...resolvers }); }
|
|
218
225
|
catch (e) { /* one bad file never sinks the whole build */ void e; }
|
|
226
|
+
// string-literal SQL in host-language code → schema reference candidates (resolved post-pass-2)
|
|
227
|
+
if (!lang.textOnly && !lang.isWeb) try { scanEmbeddedSql(code, fileRel, sqlRefs); } catch { /* never sinks the build */ }
|
|
219
228
|
|
|
220
229
|
syms.sort((a, b) => a.start - b.start);
|
|
221
230
|
const eof = code.split("\n").length;
|
|
@@ -223,7 +232,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
223
232
|
if (!syms[i].end || syms[i].end < syms[i].start) syms[i].end = i + 1 < syms.length ? syms[i + 1].start - 1 : eof;
|
|
224
233
|
}
|
|
225
234
|
perFileSymbols.set(fileRel, syms); symByFileName.set(fileRel, nameToId); symIdsByFileName.set(fileRel, nameToIds);
|
|
226
|
-
tree.delete();
|
|
235
|
+
if (tree) tree.delete();
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
// ---- pass 2: scope-aware calls + inheritance (Go package = whole dir → same-dir symbols share scope;
|
|
@@ -231,7 +240,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
231
240
|
// not files, so the folder map is the only reliable cross-file resolver) ----
|
|
232
241
|
const {reExportOccurrences} = runInternalGraphPass2({
|
|
233
242
|
files, rel, langs, caps, field, links, nodeById, perFileSymbols, symByFileName,
|
|
234
|
-
symIdsByFileName, importedLocals, jsExports,
|
|
243
|
+
symIdsByFileName, importedLocals, jsExports, resolvers,
|
|
235
244
|
});
|
|
236
245
|
// HTML class/id usage → the CSS file(s) defining that selector: file-level reference edges (deduped per pair).
|
|
237
246
|
const htmlRefSeen = new Set();
|
|
@@ -243,6 +252,8 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
243
252
|
links.push({ source: u.htmlFile, target: cssFile, relation: "references", confidence: "INFERRED" });
|
|
244
253
|
}
|
|
245
254
|
}
|
|
255
|
+
// code/SQL → schema-object edges (tables/views/functions declared in .sql files)
|
|
256
|
+
try { resolveSqlReferences({ sqlRefs, links, nodeById, perFileSymbols }); } catch { /* never sinks the build */ }
|
|
246
257
|
|
|
247
258
|
// community = folder bucket (top 2 path parts) — deterministic, mirrors the folder-based module grouping the
|
|
248
259
|
// app already uses (graph-builder-analysis.js). Populates Modules/community cards without a heavy clustering pass.
|
|
@@ -266,7 +277,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
266
277
|
barrelResolutionV: 1,
|
|
267
278
|
reExportOccurrencesV: 1,
|
|
268
279
|
symbolSpacesV: 1,
|
|
269
|
-
extractorSchemaV:
|
|
280
|
+
extractorSchemaV: 7, // v7 = Solidity + SQL indexing (schema objects, embedded-SQL edges)
|
|
270
281
|
reExportOccurrences,
|
|
271
282
|
jsExportRecords: Object.fromEntries([...jsExports.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
272
283
|
fileHashes: snapshot.fileHashes,
|
|
@@ -16,6 +16,8 @@ import LANG_GO from "./builder/lang-go.js";
|
|
|
16
16
|
import LANG_JAVA from "./builder/lang-java.js";
|
|
17
17
|
import LANG_CSHARP from "./builder/lang-csharp.js";
|
|
18
18
|
import LANG_RUST from "./builder/lang-rust.js";
|
|
19
|
+
import LANG_SOLIDITY from "./builder/lang-solidity.js";
|
|
20
|
+
import LANG_SQL from "./builder/lang-sql.js";
|
|
19
21
|
import LANG_HTML from "./builder/lang-html.js";
|
|
20
22
|
import LANG_CSS from "./builder/lang-css.js";
|
|
21
23
|
|
|
@@ -27,7 +29,7 @@ const NODE_MODULES = dirname(WTS_DIR);
|
|
|
27
29
|
const DEFAULT_WASM_DIR = join(NODE_MODULES, "tree-sitter-wasms", "out");
|
|
28
30
|
|
|
29
31
|
// ---- language registry (derived from the per-language modules) ----
|
|
30
|
-
const LANG_MODULES = [LANG_JS, LANG_PY, LANG_GO, LANG_JAVA, LANG_CSHARP, LANG_RUST, LANG_HTML, LANG_CSS];
|
|
32
|
+
const LANG_MODULES = [LANG_JS, LANG_PY, LANG_GO, LANG_JAVA, LANG_CSHARP, LANG_RUST, LANG_SOLIDITY, LANG_SQL, LANG_HTML, LANG_CSS];
|
|
31
33
|
const LANGS = {}; // family -> module
|
|
32
34
|
const EXT_LANG = {}; // ext -> grammar
|
|
33
35
|
const FAMILY = {}; // grammar -> family
|
|
@@ -8,10 +8,11 @@ import {createGoReceiverResolution} from './builder/go-receiver-resolution.js'
|
|
|
8
8
|
|
|
9
9
|
export function runInternalGraphPass2({
|
|
10
10
|
files, rel, langs, caps, field, links, nodeById, perFileSymbols, symByFileName,
|
|
11
|
-
symIdsByFileName, importedLocals, jsExports,
|
|
11
|
+
symIdsByFileName, importedLocals, jsExports, resolvers,
|
|
12
12
|
}) {
|
|
13
13
|
const resolution = createPass2Resolution({symIdsByFileName, nodeById, importedLocals, symByFileName})
|
|
14
|
-
const {dirSymbols, resolveNamedSymbol, resolveCall, resolveJavaType} = resolution
|
|
14
|
+
const {dirSymbols, resolveNamedSymbol, resolveCall, resolveJavaType, resolveRustMethod} = resolution
|
|
15
|
+
const {resolveRustPath, resolveRustCratePath} = resolvers || {}
|
|
15
16
|
const {resolveNamespaceMember, reExportOccurrences} = resolveJsBarrels({
|
|
16
17
|
jsExports, importedLocals, links,
|
|
17
18
|
resolveSymbol: (file, name, typeOnly) => resolveNamedSymbol(file, name, typeOnly ? 'type' : 'value'),
|
|
@@ -46,6 +47,7 @@ export function runInternalGraphPass2({
|
|
|
46
47
|
lang.pass2({
|
|
47
48
|
grammar, tree, fileRel: file, code, caps, field, enclosing, links, nodeById,
|
|
48
49
|
perFileSymbols, symByFileName, symIdsByFileName, importedLocals, resolveCall, resolveJavaType,
|
|
50
|
+
dirSymbols, resolveNamedSymbol, resolveRustMethod, resolveRustPath, resolveRustCratePath,
|
|
49
51
|
})
|
|
50
52
|
} catch { /* one language-specific resolver never sinks the graph */ }
|
|
51
53
|
if (lang.selectorCall) for (const capture of caps(grammar, lang.selectorCall, tree.rootNode)) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { join, dirname } from "node:path";
|
|
7
7
|
import { parseGoMod } from "../analysis/manifests.js";
|
|
8
|
+
import { parseCargoToml, cargoName } from "../analysis/cargo-manifests.js";
|
|
8
9
|
import { createRepoBoundary } from "../repo-path.js";
|
|
9
10
|
import { listRepoFiles } from "../analysis/internal-audit/repo-files.js";
|
|
10
11
|
import { createRustResolvers } from "./resolvers/rust.js";
|
|
@@ -61,6 +62,40 @@ export function buildResolvers(repoDir, fileSet) {
|
|
|
61
62
|
|
|
62
63
|
const { resolveRustMod, resolveRustPath } = createRustResolvers(fileSet);
|
|
63
64
|
|
|
65
|
+
// Workspace crate map: normalized package name → its crate source root file (lib.rs/main.rs). A path or
|
|
66
|
+
// `use` that starts with a sibling crate's name (`use radiochron::wlan`) then resolves INTO that crate's
|
|
67
|
+
// files instead of being logged as an external crates.io dependency — so cross-crate imports and calls
|
|
68
|
+
// become real graph edges rather than a false "unused dependency" plus missing coupling.
|
|
69
|
+
// Cargo.toml (like go.mod) is a manifest, not a parsed source file, so it is read via listRepoFiles
|
|
70
|
+
// rather than the source fileSet. The crate root .rs it points at IS in fileSet (it is parsed).
|
|
71
|
+
const rustCrates = new Map();
|
|
72
|
+
const ambiguousCrates = new Set();
|
|
73
|
+
for (const manifest of listRepoFiles(repoDir).filter((file) => /(^|\/)Cargo\.toml$/i.test(file))) {
|
|
74
|
+
let parsed;
|
|
75
|
+
try { parsed = parseCargoToml(readLocal(manifest)); } catch { continue; }
|
|
76
|
+
if (!parsed.packageName) continue;
|
|
77
|
+
const dir = manifest.includes("/") ? manifest.slice(0, manifest.lastIndexOf("/")) : "";
|
|
78
|
+
const rootFile = ["src/lib.rs", "src/main.rs", "lib.rs", "main.rs"]
|
|
79
|
+
.map((candidate) => [dir, candidate].filter(Boolean).join("/"))
|
|
80
|
+
.find((path) => fileSet.has(path));
|
|
81
|
+
if (!rootFile) continue;
|
|
82
|
+
const key = cargoName(parsed.packageName);
|
|
83
|
+
// A repo can hold two crates with the same package name (independent workspaces, vendored copies).
|
|
84
|
+
// Cross-crate resolution has no way to pick the right one from a bare `name::path`, so treat the name
|
|
85
|
+
// as ambiguous and resolve nothing for it — better a missing edge than a confident wrong one.
|
|
86
|
+
if (rustCrates.has(key) && rustCrates.get(key) !== rootFile) ambiguousCrates.add(key);
|
|
87
|
+
else rustCrates.set(key, rootFile);
|
|
88
|
+
}
|
|
89
|
+
// Resolve `<crate>::<segments...>` against the named sibling crate's root, reusing the crate-anchored
|
|
90
|
+
// module resolver. Returns {targetFile, remaining} or null when the head is not an unambiguous workspace crate.
|
|
91
|
+
const resolveRustCratePath = (crateName, segments = []) => {
|
|
92
|
+
const key = cargoName(crateName);
|
|
93
|
+
if (ambiguousCrates.has(key)) return null;
|
|
94
|
+
const rootFile = rustCrates.get(key);
|
|
95
|
+
if (!rootFile) return null;
|
|
96
|
+
return resolveRustPath(rootFile, ["crate", ...segments], { unqualified: false });
|
|
97
|
+
};
|
|
98
|
+
|
|
64
99
|
// Path aliases (tsconfig compilerOptions.paths + vite/webpack alias) are scoped to their config folder.
|
|
65
100
|
// Without nearest-config resolution, a monorepo's root `@/*` can hijack the same alias in web/.
|
|
66
101
|
const aliasContexts = new Map();
|
|
@@ -207,6 +242,36 @@ export function buildResolvers(repoDir, fileSet) {
|
|
|
207
242
|
if (seg[0] === "src" && seg.length > 2) pyTopDirs.add(seg[1]);
|
|
208
243
|
}
|
|
209
244
|
|
|
245
|
+
// Solidity: relative imports, Foundry remappings (remappings.txt beats foundry.toml, longest prefix
|
|
246
|
+
// wins), then root-anchored specs (`src/X.sol` — forge resolves those against the project root).
|
|
247
|
+
// Nested remapping files are intentionally not scoped per-package: one remapping table per repo is the
|
|
248
|
+
// overwhelmingly common layout, and a wrong guess here would fabricate cross-package edges.
|
|
249
|
+
const solRemappings = [];
|
|
250
|
+
const addRemapping = (entry) => {
|
|
251
|
+
const m = /^\s*([^=\s]+)\s*=\s*(\S+)\s*$/.exec(String(entry || ""));
|
|
252
|
+
if (m && !solRemappings.some((r) => r.prefix === m[1])) solRemappings.push({ prefix: m[1], target: cleanRel(m[2]) });
|
|
253
|
+
};
|
|
254
|
+
// neither file has an indexed extension, so probe the filesystem directly rather than fileSet
|
|
255
|
+
try { for (const line of readLocal("remappings.txt").split(/\r?\n/)) addRemapping(line); } catch { /* absent/unreadable */ }
|
|
256
|
+
try {
|
|
257
|
+
const toml = readLocal("foundry.toml");
|
|
258
|
+
for (const m of toml.matchAll(/remappings\s*=\s*\[([^\]]*)\]/g)) for (const s of m[1].matchAll(/["']([^"']+)["']/g)) addRemapping(s[1]);
|
|
259
|
+
} catch { /* absent/unreadable */ }
|
|
260
|
+
solRemappings.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
261
|
+
const resolveSolidityImport = (fromRel, spec) => {
|
|
262
|
+
if (!spec || !spec.endsWith(".sol")) return null;
|
|
263
|
+
if (spec.startsWith(".")) {
|
|
264
|
+
const candidate = cleanRel(join(dirname(fromRel), spec));
|
|
265
|
+
return fileSet.has(candidate) ? candidate : null;
|
|
266
|
+
}
|
|
267
|
+
for (const { prefix, target } of solRemappings) {
|
|
268
|
+
if (spec !== prefix && !spec.startsWith(prefix)) continue;
|
|
269
|
+
const candidate = cleanRel(target + "/" + spec.slice(prefix.length)).replace(/\/+/g, "/");
|
|
270
|
+
if (fileSet.has(candidate)) return candidate;
|
|
271
|
+
}
|
|
272
|
+
return fileSet.has(spec) ? spec : null;
|
|
273
|
+
};
|
|
274
|
+
|
|
210
275
|
const selectorIndex = new Map();
|
|
211
276
|
const htmlUsages = [];
|
|
212
277
|
const resolveHref = (fromRel, href) => {
|
|
@@ -217,5 +282,5 @@ export function buildResolvers(repoDir, fileSet) {
|
|
|
217
282
|
return fileSet.has(cand) ? cand : null;
|
|
218
283
|
};
|
|
219
284
|
|
|
220
|
-
return { resolveJsImport, resolveAlias, resolvePyPath, pyBaseDir, pyTopDirs, resolveGoImport, dirFiles, resolveRustMod, resolveRustPath, resolveJavaImport, resolveHref, selectorIndex, htmlUsages, goModule, goModules, goRequires };
|
|
285
|
+
return { resolveJsImport, resolveAlias, resolvePyPath, pyBaseDir, pyTopDirs, resolveGoImport, dirFiles, resolveRustMod, resolveRustPath, resolveRustCratePath, resolveJavaImport, resolveSolidityImport, resolveHref, selectorIndex, htmlUsages, goModule, goModules, goRequires };
|
|
221
286
|
}
|
|
@@ -17,6 +17,7 @@ const GRAMMAR_SHA256 = Object.freeze({
|
|
|
17
17
|
java: '637aac4415fb39a211a4f4292d63c66b5ce9c32fa2cd35464af4f681d91b9a1f',
|
|
18
18
|
c_sharp: '6266a7e32d68a3459104d994dc848df15d5672b0ea8e86d327274b694f8e6991',
|
|
19
19
|
rust: '4409921a70d0aa5bec7d1d7ce809a557a8ee1cf6ace901e3ac6a76e62cfea903',
|
|
20
|
+
solidity: '160745e470f234cae903a9ba445d19e758d0b02e1197401fc765976c6254d2b6',
|
|
20
21
|
html: '11b3405c1543fb012f5ed7f8ee73125076dce8b168301e1e787e4c717da6b456',
|
|
21
22
|
css: '5fc615467b1b98420ed7517e5bf9e1f88468132dd903d842dfb13714f6a1cb0c',
|
|
22
23
|
})
|
|
@@ -76,7 +76,7 @@ export async function tOpenRepo(g, args, ctx) {
|
|
|
76
76
|
savedPrecision = ['lsp', 'off'].includes(saved.graphPrecisionMode) ? saved.graphPrecisionMode : defaultPrecisionMode()
|
|
77
77
|
schemaUpgrade = !Number.isInteger(saved.edgeTypesV) || saved.edgeTypesV < 2
|
|
78
78
|
|| !Number.isInteger(saved.edgeProvenanceV) || saved.edgeProvenanceV < 1
|
|
79
|
-
|| !Number.isInteger(saved.extractorSchemaV) || saved.extractorSchemaV <
|
|
79
|
+
|| !Number.isInteger(saved.extractorSchemaV) || saved.extractorSchemaV < 7
|
|
80
80
|
|| !Number.isInteger(saved.reExportOccurrencesV) || saved.reExportOccurrencesV < 1
|
|
81
81
|
|| !Number.isInteger(saved.symbolSpacesV) || saved.symbolSpacesV < 1
|
|
82
82
|
|| !Number.isInteger(saved.physicalFileLocV) || saved.physicalFileLocV < 1
|
|
@@ -28,7 +28,7 @@ const BUDGET_PROPOSALS = Object.freeze(Object.entries(PROVISIONAL_BUDGETS)
|
|
|
28
28
|
const SOURCE_ROOTS = new Set(['src', 'app', 'lib', 'packages', 'services'])
|
|
29
29
|
const CODE_EXTENSIONS = new Set([
|
|
30
30
|
'.cjs', '.cs', '.css', '.go', '.htm', '.html', '.java', '.js', '.jsx', '.less', '.mjs', '.py', '.pyi',
|
|
31
|
-
'.rs', '.scss', '.ts', '.tsx',
|
|
31
|
+
'.rs', '.scss', '.sol', '.sql', '.ts', '.tsx',
|
|
32
32
|
])
|
|
33
33
|
const CLASSIFIED = ['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
34
34
|
const EDGE_RELATIONS = new Set(['imports', 're_exports', 'calls', 'references'])
|