@danielblomma/cortex-mcp 0.4.5 → 0.6.5
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 +38 -42
- package/bin/cortex.mjs +32 -60
- package/package.json +15 -3
- package/scaffold/.context/ontology.cypher +47 -0
- package/scaffold/.githooks/post-commit +14 -0
- package/scaffold/.githooks/post-rewrite +23 -0
- package/scaffold/mcp/package-lock.json +16 -16
- package/scaffold/mcp/package.json +4 -1
- package/scaffold/mcp/src/contextEntities.ts +311 -0
- package/scaffold/mcp/src/defaults.ts +6 -0
- package/scaffold/mcp/src/embed.ts +163 -37
- package/scaffold/mcp/src/frontmatter.ts +39 -0
- package/scaffold/mcp/src/graph.ts +253 -130
- package/scaffold/mcp/src/graphMetrics.ts +12 -0
- package/scaffold/mcp/src/impactPresentation.ts +202 -0
- package/scaffold/mcp/src/impactRanking.ts +237 -0
- package/scaffold/mcp/src/impactResponse.ts +47 -0
- package/scaffold/mcp/src/impactResults.ts +173 -0
- package/scaffold/mcp/src/impactSeed.ts +33 -0
- package/scaffold/mcp/src/impactTraversal.ts +83 -0
- package/scaffold/mcp/src/jsonl.ts +34 -0
- package/scaffold/mcp/src/loadGraph.ts +345 -86
- package/scaffold/mcp/src/paths.ts +17 -1
- package/scaffold/mcp/src/presets.ts +137 -0
- package/scaffold/mcp/src/relatedResponse.ts +30 -0
- package/scaffold/mcp/src/relatedTraversal.ts +101 -0
- package/scaffold/mcp/src/rules.ts +27 -0
- package/scaffold/mcp/src/search.ts +186 -455
- package/scaffold/mcp/src/searchCore.ts +274 -0
- package/scaffold/mcp/src/searchResults.ts +133 -0
- package/scaffold/mcp/src/server.ts +95 -3
- package/scaffold/mcp/src/types.ts +82 -3
- package/scaffold/scripts/context.sh +12 -46
- package/scaffold/scripts/dashboard.mjs +797 -0
- package/scaffold/scripts/dashboard.sh +13 -0
- package/scaffold/scripts/ingest.mjs +2227 -59
- package/scaffold/scripts/install-git-hooks.sh +3 -1
- package/scaffold/scripts/memory-compile.mjs +232 -0
- package/scaffold/scripts/memory-compile.sh +20 -0
- package/scaffold/scripts/memory-lint.mjs +375 -0
- package/scaffold/scripts/memory-lint.sh +20 -0
- package/scaffold/scripts/parsers/config.mjs +178 -0
- package/scaffold/scripts/parsers/cpp.mjs +316 -0
- package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +374 -0
- package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +13 -0
- package/scaffold/scripts/parsers/javascript/ast.mjs +61 -0
- package/scaffold/scripts/parsers/javascript/calls.mjs +53 -0
- package/scaffold/scripts/parsers/javascript/chunks.mjs +388 -0
- package/scaffold/scripts/parsers/javascript/imports.mjs +162 -0
- package/scaffold/scripts/parsers/javascript/patterns.mjs +82 -0
- package/scaffold/scripts/parsers/javascript/scope-analysis.mjs +3 -0
- package/scaffold/scripts/parsers/javascript/scope-builder.mjs +305 -0
- package/scaffold/scripts/parsers/javascript/scope-resolver.mjs +82 -0
- package/scaffold/scripts/parsers/javascript.mjs +27 -350
- package/scaffold/scripts/parsers/resources.mjs +166 -0
- package/scaffold/scripts/parsers/rust.mjs +515 -0
- package/scaffold/scripts/parsers/sql.mjs +137 -0
- package/scaffold/scripts/parsers/vbnet.mjs +143 -0
- package/scaffold/scripts/status.sh +0 -7
- package/scaffold/scripts/capture-note.sh +0 -55
- package/scaffold/scripts/plan-state-engine.cjs +0 -310
- package/scaffold/scripts/plan-state.sh +0 -71
|
@@ -4,7 +4,19 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
|
-
import { parseCode } from "./parsers/javascript.mjs";
|
|
7
|
+
import { parseCode as parseJavaScriptCode } from "./parsers/javascript.mjs";
|
|
8
|
+
import {
|
|
9
|
+
isVbNetParserAvailable,
|
|
10
|
+
parseCode as parseVbNetCode
|
|
11
|
+
} from "./parsers/vbnet.mjs";
|
|
12
|
+
import {
|
|
13
|
+
isCppParserAvailable,
|
|
14
|
+
parseCode as parseCppCode
|
|
15
|
+
} from "./parsers/cpp.mjs";
|
|
16
|
+
import { parseCode as parseConfigCode } from "./parsers/config.mjs";
|
|
17
|
+
import { parseCode as parseResourcesCode } from "./parsers/resources.mjs";
|
|
18
|
+
import { parseCode as parseSqlCode } from "./parsers/sql.mjs";
|
|
19
|
+
import { parseCode as parseRustCode } from "./parsers/rust.mjs";
|
|
8
20
|
|
|
9
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
22
|
const __dirname = path.dirname(__filename);
|
|
@@ -34,6 +46,16 @@ const SUPPORTED_TEXT_EXTENSIONS = new Set([
|
|
|
34
46
|
".go",
|
|
35
47
|
".java",
|
|
36
48
|
".cs",
|
|
49
|
+
".vb",
|
|
50
|
+
".sln",
|
|
51
|
+
".vbproj",
|
|
52
|
+
".csproj",
|
|
53
|
+
".fsproj",
|
|
54
|
+
".props",
|
|
55
|
+
".targets",
|
|
56
|
+
".config",
|
|
57
|
+
".resx",
|
|
58
|
+
".settings",
|
|
37
59
|
".rb",
|
|
38
60
|
".rs",
|
|
39
61
|
".php",
|
|
@@ -52,6 +74,216 @@ const SUPPORTED_TEXT_EXTENSIONS = new Set([
|
|
|
52
74
|
".hh"
|
|
53
75
|
]);
|
|
54
76
|
|
|
77
|
+
const LEGACY_DOTNET_METADATA_EXTENSIONS = new Set([
|
|
78
|
+
".sln",
|
|
79
|
+
".vbproj",
|
|
80
|
+
".csproj",
|
|
81
|
+
".fsproj",
|
|
82
|
+
".props",
|
|
83
|
+
".targets",
|
|
84
|
+
".config",
|
|
85
|
+
".resx",
|
|
86
|
+
".settings"
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const PROJECT_DEFINITION_EXTENSIONS = new Set([".sln", ".vbproj", ".csproj", ".fsproj"]);
|
|
90
|
+
const STRUCTURED_NON_CODE_CHUNK_EXTENSIONS = new Set([".config", ".resx", ".settings"]);
|
|
91
|
+
|
|
92
|
+
const CODE_FILE_EXTENSIONS = new Set([
|
|
93
|
+
".ts",
|
|
94
|
+
".tsx",
|
|
95
|
+
".js",
|
|
96
|
+
".jsx",
|
|
97
|
+
".mjs",
|
|
98
|
+
".cjs",
|
|
99
|
+
".py",
|
|
100
|
+
".go",
|
|
101
|
+
".java",
|
|
102
|
+
".cs",
|
|
103
|
+
".vb",
|
|
104
|
+
".rb",
|
|
105
|
+
".rs",
|
|
106
|
+
".php",
|
|
107
|
+
".swift",
|
|
108
|
+
".kt",
|
|
109
|
+
".sql",
|
|
110
|
+
".sh",
|
|
111
|
+
".bash",
|
|
112
|
+
".zsh",
|
|
113
|
+
".ps1",
|
|
114
|
+
".c",
|
|
115
|
+
".h",
|
|
116
|
+
".cpp",
|
|
117
|
+
".hpp",
|
|
118
|
+
".cc",
|
|
119
|
+
".hh"
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const SQL_REFERENCE_SOURCE_EXTENSIONS = new Set([
|
|
123
|
+
".vb",
|
|
124
|
+
".cs",
|
|
125
|
+
".config",
|
|
126
|
+
".resx",
|
|
127
|
+
".settings"
|
|
128
|
+
]);
|
|
129
|
+
const NAMED_RESOURCE_REFERENCE_SOURCE_EXTENSIONS = new Set([".vb", ".cs"]);
|
|
130
|
+
|
|
131
|
+
const SQL_OBJECT_REFERENCE_PATTERNS = [
|
|
132
|
+
/\b(?:SqlCommand|OleDbCommand|OdbcCommand)\s*\(\s*"([^"\r\n]{2,200})"/gi,
|
|
133
|
+
/\bCommandText\s*=\s*"([^"\r\n]{2,500})"/gi,
|
|
134
|
+
/\bCommandType\s*=\s*(?:CommandType\.)?StoredProcedure[\s\S]{0,240}?"([^"\r\n]{2,200})"/gi,
|
|
135
|
+
/"([^"\r\n]{2,200})"[\s\S]{0,240}?\bCommandType\s*=\s*(?:CommandType\.)?StoredProcedure/gi
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const SQL_STRING_REFERENCE_PATTERNS = [
|
|
139
|
+
/\bexec(?:ute)?\s+([#@]?[A-Za-z0-9_[\].]+)/gi,
|
|
140
|
+
/\bfrom\s+([#@]?[A-Za-z0-9_[\].]+)/gi,
|
|
141
|
+
/\bjoin\s+([#@]?[A-Za-z0-9_[\].]+)/gi,
|
|
142
|
+
/\bupdate\s+([#@]?[A-Za-z0-9_[\].]+)/gi,
|
|
143
|
+
/\binsert\s+into\s+([#@]?[A-Za-z0-9_[\].]+)/gi,
|
|
144
|
+
/\bdelete\s+from\s+([#@]?[A-Za-z0-9_[\].]+)/gi,
|
|
145
|
+
/\bmerge\s+into\s+([#@]?[A-Za-z0-9_[\].]+)/gi
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const SQL_RESOURCE_KEY_PATTERNS = [
|
|
149
|
+
/\bMy\.Resources\.([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
150
|
+
/\bResources\.([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
151
|
+
/\bMy\.Settings\.([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
152
|
+
/\b(?:[A-Za-z_][A-Za-z0-9_]*\.)?Settings\.Default\.([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
153
|
+
/\bGetString\(\s*"([^"\r\n]+)"/g,
|
|
154
|
+
/\bGetObject\(\s*"([^"\r\n]+)"/g
|
|
155
|
+
];
|
|
156
|
+
const CONFIG_KEY_REFERENCE_PATTERNS = [
|
|
157
|
+
/\bConfigurationManager\.ConnectionStrings\s*\[\s*"([^"\r\n]+)"\s*\]/g,
|
|
158
|
+
/\bConfigurationManager\.ConnectionStrings\s*\(\s*"([^"\r\n]+)"\s*\)/g,
|
|
159
|
+
/\bConfigurationManager\.AppSettings\s*\[\s*"([^"\r\n]+)"\s*\]/g,
|
|
160
|
+
/\bConfigurationManager\.AppSettings\s*\(\s*"([^"\r\n]+)"\s*\)/g,
|
|
161
|
+
/\bGetConnectionString\(\s*"([^"\r\n]+)"\s*\)/g,
|
|
162
|
+
/\bGetAppSetting\(\s*"([^"\r\n]+)"\s*\)/g
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const CHUNK_PARSERS = new Map([
|
|
166
|
+
[
|
|
167
|
+
".js",
|
|
168
|
+
{
|
|
169
|
+
language: "javascript",
|
|
170
|
+
parse: parseJavaScriptCode
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
[
|
|
174
|
+
".mjs",
|
|
175
|
+
{
|
|
176
|
+
language: "javascript",
|
|
177
|
+
parse: parseJavaScriptCode
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
[
|
|
181
|
+
".cjs",
|
|
182
|
+
{
|
|
183
|
+
language: "javascript",
|
|
184
|
+
parse: parseJavaScriptCode
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
".ts",
|
|
189
|
+
{
|
|
190
|
+
language: "typescript",
|
|
191
|
+
parse: parseJavaScriptCode
|
|
192
|
+
}
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
".vb",
|
|
196
|
+
{
|
|
197
|
+
language: "vbnet",
|
|
198
|
+
parse: parseVbNetCode,
|
|
199
|
+
isAvailable: isVbNetParserAvailable
|
|
200
|
+
}
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
".sql",
|
|
204
|
+
{
|
|
205
|
+
language: "sql",
|
|
206
|
+
parse: parseSqlCode
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
[
|
|
210
|
+
".config",
|
|
211
|
+
{
|
|
212
|
+
language: "config",
|
|
213
|
+
parse: parseConfigCode
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
".resx",
|
|
218
|
+
{
|
|
219
|
+
language: "resource",
|
|
220
|
+
parse: parseResourcesCode
|
|
221
|
+
}
|
|
222
|
+
],
|
|
223
|
+
[
|
|
224
|
+
".settings",
|
|
225
|
+
{
|
|
226
|
+
language: "settings",
|
|
227
|
+
parse: parseResourcesCode
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
".c",
|
|
232
|
+
{
|
|
233
|
+
language: "c",
|
|
234
|
+
parse: parseCppCode,
|
|
235
|
+
isAvailable: isCppParserAvailable
|
|
236
|
+
}
|
|
237
|
+
],
|
|
238
|
+
[
|
|
239
|
+
".h",
|
|
240
|
+
{
|
|
241
|
+
language: "c",
|
|
242
|
+
parse: parseCppCode,
|
|
243
|
+
isAvailable: isCppParserAvailable
|
|
244
|
+
}
|
|
245
|
+
],
|
|
246
|
+
[
|
|
247
|
+
".cpp",
|
|
248
|
+
{
|
|
249
|
+
language: "cpp",
|
|
250
|
+
parse: parseCppCode,
|
|
251
|
+
isAvailable: isCppParserAvailable
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
[
|
|
255
|
+
".cc",
|
|
256
|
+
{
|
|
257
|
+
language: "cpp",
|
|
258
|
+
parse: parseCppCode,
|
|
259
|
+
isAvailable: isCppParserAvailable
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
".hpp",
|
|
264
|
+
{
|
|
265
|
+
language: "cpp",
|
|
266
|
+
parse: parseCppCode,
|
|
267
|
+
isAvailable: isCppParserAvailable
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
[
|
|
271
|
+
".hh",
|
|
272
|
+
{
|
|
273
|
+
language: "cpp",
|
|
274
|
+
parse: parseCppCode,
|
|
275
|
+
isAvailable: isCppParserAvailable
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
[
|
|
279
|
+
".rs",
|
|
280
|
+
{
|
|
281
|
+
language: "rust",
|
|
282
|
+
parse: parseRustCode
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
]);
|
|
286
|
+
|
|
55
287
|
const SKIP_DIRECTORIES = new Set([
|
|
56
288
|
".git",
|
|
57
289
|
".idea",
|
|
@@ -69,6 +301,14 @@ const MAX_FILE_BYTES = 1024 * 1024;
|
|
|
69
301
|
const MAX_CONTENT_CHARS = 60000;
|
|
70
302
|
const MAX_BODY_CHARS = 12000;
|
|
71
303
|
const RULE_KEYWORD_LIMIT = 20;
|
|
304
|
+
const DEFAULT_CHUNK_WINDOW_LINES = 80;
|
|
305
|
+
const DEFAULT_CHUNK_OVERLAP_LINES = 16;
|
|
306
|
+
const DEFAULT_CHUNK_SPLIT_MIN_LINES = 120;
|
|
307
|
+
const DEFAULT_CHUNK_MAX_WINDOWS = 8;
|
|
308
|
+
const IMPORT_RESOLUTION_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
|
|
309
|
+
const IMPORT_RUNTIME_JS_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".cjs"]);
|
|
310
|
+
const IMPORT_RUNTIME_JS_RESOLUTION_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
311
|
+
const CPP_IMPORT_RESOLUTION_EXTENSIONS = [".h", ".hh", ".hpp", ".c", ".cc", ".cpp"];
|
|
72
312
|
|
|
73
313
|
const STOP_WORDS = new Set([
|
|
74
314
|
"the",
|
|
@@ -183,6 +423,30 @@ function uniqueSorted(values) {
|
|
|
183
423
|
return [...new Set(values)].sort();
|
|
184
424
|
}
|
|
185
425
|
|
|
426
|
+
function parsePositiveIntegerEnv(name, fallback) {
|
|
427
|
+
const rawValue = process.env[name];
|
|
428
|
+
if (!rawValue) {
|
|
429
|
+
return fallback;
|
|
430
|
+
}
|
|
431
|
+
const parsed = Number(rawValue);
|
|
432
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
433
|
+
return fallback;
|
|
434
|
+
}
|
|
435
|
+
return Math.floor(parsed);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function parseNonNegativeIntegerEnv(name, fallback) {
|
|
439
|
+
const rawValue = process.env[name];
|
|
440
|
+
if (!rawValue) {
|
|
441
|
+
return fallback;
|
|
442
|
+
}
|
|
443
|
+
const parsed = Number(rawValue);
|
|
444
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
445
|
+
return fallback;
|
|
446
|
+
}
|
|
447
|
+
return Math.floor(parsed);
|
|
448
|
+
}
|
|
449
|
+
|
|
186
450
|
function parseSourcePaths(configText) {
|
|
187
451
|
const sourcePaths = [];
|
|
188
452
|
const lines = configText.split(/\r?\n/);
|
|
@@ -290,6 +554,60 @@ function hasSourcePrefix(relPath, sourcePaths) {
|
|
|
290
554
|
});
|
|
291
555
|
}
|
|
292
556
|
|
|
557
|
+
function pushImportResolutionCandidate(candidates, seenCandidates, candidatePath) {
|
|
558
|
+
if (!seenCandidates.has(candidatePath)) {
|
|
559
|
+
seenCandidates.add(candidatePath);
|
|
560
|
+
candidates.push(candidatePath);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function isCppLikeFilePath(filePath) {
|
|
565
|
+
return [".c", ".h", ".cc", ".cpp", ".hh", ".hpp"].includes(path.posix.extname(filePath).toLowerCase());
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function resolveRelativeImportTargetId(filePath, importPath, indexedFileIds) {
|
|
569
|
+
const isCppLike = isCppLikeFilePath(filePath);
|
|
570
|
+
const isRelativeImport = importPath.startsWith(".");
|
|
571
|
+
const isLocalCppInclude =
|
|
572
|
+
isCppLike && !path.posix.isAbsolute(importPath) && !/^[A-Za-z]:[\\/]/.test(importPath);
|
|
573
|
+
|
|
574
|
+
if (!isRelativeImport && !isLocalCppInclude) {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const basePath = path.posix.normalize(path.posix.join(path.posix.dirname(filePath), importPath));
|
|
579
|
+
const candidates = [];
|
|
580
|
+
const seenCandidates = new Set();
|
|
581
|
+
pushImportResolutionCandidate(candidates, seenCandidates, basePath);
|
|
582
|
+
|
|
583
|
+
if (path.posix.extname(basePath) === "") {
|
|
584
|
+
const extensions = isCppLike ? CPP_IMPORT_RESOLUTION_EXTENSIONS : IMPORT_RESOLUTION_EXTENSIONS;
|
|
585
|
+
for (const extension of extensions) {
|
|
586
|
+
pushImportResolutionCandidate(candidates, seenCandidates, `${basePath}${extension}`);
|
|
587
|
+
}
|
|
588
|
+
if (!isCppLike) {
|
|
589
|
+
for (const extension of IMPORT_RESOLUTION_EXTENSIONS) {
|
|
590
|
+
pushImportResolutionCandidate(candidates, seenCandidates, path.posix.join(basePath, `index${extension}`));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
} else if (IMPORT_RUNTIME_JS_EXTENSIONS.has(path.posix.extname(basePath))) {
|
|
594
|
+
const extension = path.posix.extname(basePath);
|
|
595
|
+
const stemPath = basePath.slice(0, -extension.length);
|
|
596
|
+
for (const candidateExtension of IMPORT_RUNTIME_JS_RESOLUTION_EXTENSIONS) {
|
|
597
|
+
pushImportResolutionCandidate(candidates, seenCandidates, `${stemPath}${candidateExtension}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
for (const candidate of candidates) {
|
|
602
|
+
const targetFileId = `file:${candidate}`;
|
|
603
|
+
if (indexedFileIds.has(targetFileId)) {
|
|
604
|
+
return targetFileId;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
293
611
|
function getGitChanges() {
|
|
294
612
|
try {
|
|
295
613
|
const output = execSync("git status --porcelain", {
|
|
@@ -428,9 +746,17 @@ function detectKind(relPath) {
|
|
|
428
746
|
return "DOC";
|
|
429
747
|
}
|
|
430
748
|
|
|
749
|
+
if (LEGACY_DOTNET_METADATA_EXTENSIONS.has(ext) || !CODE_FILE_EXTENSIONS.has(ext)) {
|
|
750
|
+
return "DOC";
|
|
751
|
+
}
|
|
752
|
+
|
|
431
753
|
return "CODE";
|
|
432
754
|
}
|
|
433
755
|
|
|
756
|
+
function getChunkParserForExtension(ext) {
|
|
757
|
+
return CHUNK_PARSERS.get(ext) ?? null;
|
|
758
|
+
}
|
|
759
|
+
|
|
434
760
|
function trustLevelForKind(kind) {
|
|
435
761
|
if (kind === "ADR") return 95;
|
|
436
762
|
if (kind === "CODE") return 80;
|
|
@@ -509,32 +835,1217 @@ function sanitizeTsvCell(value) {
|
|
|
509
835
|
return String(value).replace(/\t/g, " ").replace(/\r?\n/g, " ");
|
|
510
836
|
}
|
|
511
837
|
|
|
512
|
-
function writeTsv(filePath, headers, rows) {
|
|
513
|
-
const lines = [headers.join("\t")];
|
|
514
|
-
for (const row of rows) {
|
|
515
|
-
lines.push(row.map((value) => sanitizeTsvCell(value)).join("\t"));
|
|
838
|
+
function writeTsv(filePath, headers, rows) {
|
|
839
|
+
const lines = [headers.join("\t")];
|
|
840
|
+
for (const row of rows) {
|
|
841
|
+
lines.push(row.map((value) => sanitizeTsvCell(value)).join("\t"));
|
|
842
|
+
}
|
|
843
|
+
fs.writeFileSync(filePath, `${lines.join("\n")}\n`, "utf8");
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function readJsonlSafe(filePath) {
|
|
847
|
+
if (!fs.existsSync(filePath)) {
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return fs
|
|
852
|
+
.readFileSync(filePath, "utf8")
|
|
853
|
+
.split(/\r?\n/)
|
|
854
|
+
.map((line) => line.trim())
|
|
855
|
+
.filter(Boolean)
|
|
856
|
+
.map((line) => {
|
|
857
|
+
try {
|
|
858
|
+
return JSON.parse(line);
|
|
859
|
+
} catch {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
})
|
|
863
|
+
.filter((record) => record !== null);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function relationKey(...parts) {
|
|
867
|
+
return parts.map((part) => String(part ?? "")).join("|");
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function uniqueRelations(relations) {
|
|
871
|
+
const deduped = new Map();
|
|
872
|
+
for (const relation of relations) {
|
|
873
|
+
const key = relationKey(relation.from, relation.to, relation.note);
|
|
874
|
+
if (!deduped.has(key)) {
|
|
875
|
+
deduped.set(key, relation);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return [...deduped.values()].sort((a, b) =>
|
|
879
|
+
relationKey(a.from, a.to, a.note).localeCompare(relationKey(b.from, b.to, b.note))
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function normalizeSqlName(value) {
|
|
884
|
+
if (!value) {
|
|
885
|
+
return "";
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return String(value)
|
|
889
|
+
.trim()
|
|
890
|
+
.replace(/[;"`]/g, "")
|
|
891
|
+
.replace(/\[(.+?)\]/g, "$1")
|
|
892
|
+
.replace(/\s+/g, "")
|
|
893
|
+
.replace(/^\.+|\.+$/g, "")
|
|
894
|
+
.replace(/\.\.+/g, ".")
|
|
895
|
+
.toLowerCase();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function sqlChunkAliases(name) {
|
|
899
|
+
const normalized = normalizeSqlName(name);
|
|
900
|
+
if (!normalized) {
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const aliases = new Set([normalized]);
|
|
905
|
+
const parts = normalized.split(".").filter(Boolean);
|
|
906
|
+
if (parts.length > 1) {
|
|
907
|
+
aliases.add(parts[parts.length - 1]);
|
|
908
|
+
}
|
|
909
|
+
return [...aliases];
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function configChunkAliases(chunk) {
|
|
913
|
+
const aliases = new Set();
|
|
914
|
+
const rawKey = String(chunk?.configKey ?? chunk?.name ?? "");
|
|
915
|
+
const normalizedKey = normalizeToken(rawKey);
|
|
916
|
+
if (normalizedKey) {
|
|
917
|
+
aliases.add(normalizedKey);
|
|
918
|
+
}
|
|
919
|
+
const chunkName = String(chunk?.name ?? "");
|
|
920
|
+
const tail = chunkName.split(".").pop() ?? "";
|
|
921
|
+
const normalizedTail = normalizeToken(tail);
|
|
922
|
+
if (normalizedTail) {
|
|
923
|
+
aliases.add(normalizedTail);
|
|
924
|
+
}
|
|
925
|
+
return [...aliases];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function namedEntryChunkAliases(chunk) {
|
|
929
|
+
const aliases = new Set();
|
|
930
|
+
const rawKey = String(chunk?.resourceKey ?? chunk?.configKey ?? chunk?.name ?? "");
|
|
931
|
+
const normalizedKey = normalizeToken(rawKey);
|
|
932
|
+
if (normalizedKey) {
|
|
933
|
+
aliases.add(normalizedKey);
|
|
934
|
+
}
|
|
935
|
+
const chunkName = String(chunk?.name ?? "");
|
|
936
|
+
const tail = chunkName.split(".").pop() ?? "";
|
|
937
|
+
const normalizedTail = normalizeToken(tail);
|
|
938
|
+
if (normalizedTail) {
|
|
939
|
+
aliases.add(normalizedTail);
|
|
940
|
+
}
|
|
941
|
+
return [...aliases];
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function extractSqlReferenceNamesFromString(text) {
|
|
945
|
+
const refs = new Set();
|
|
946
|
+
|
|
947
|
+
const normalizedName = normalizeSqlName(text);
|
|
948
|
+
if (/^[a-z0-9_.]+$/i.test(normalizedName) && normalizedName.includes(".")) {
|
|
949
|
+
refs.add(normalizedName);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
for (const pattern of SQL_STRING_REFERENCE_PATTERNS) {
|
|
953
|
+
let match;
|
|
954
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
955
|
+
const name = normalizeSqlName(match[1]);
|
|
956
|
+
if (!name || name.startsWith("@") || name.startsWith("#")) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
refs.add(name);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return [...refs];
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function parseResxSqlReferenceMap(content) {
|
|
967
|
+
const refsByKey = new Map();
|
|
968
|
+
const dataPattern = /<data\b[^>]*\bname="([^"]+)"[^>]*>([\s\S]*?)<\/data>/gi;
|
|
969
|
+
let match;
|
|
970
|
+
|
|
971
|
+
while ((match = dataPattern.exec(content)) !== null) {
|
|
972
|
+
const key = normalizeToken(decodeXmlEntities(match[1]));
|
|
973
|
+
if (!key) {
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const valueMatch = match[2].match(/<value>([\s\S]*?)<\/value>/i);
|
|
978
|
+
if (!valueMatch) {
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const value = decodeXmlEntities(valueMatch[1]).trim();
|
|
983
|
+
const refs = extractSqlReferenceNamesFromString(value);
|
|
984
|
+
if (refs.length === 0) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const existing = refsByKey.get(key) ?? [];
|
|
989
|
+
refsByKey.set(key, uniqueSorted([...existing, ...refs]));
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return refsByKey;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function parseResxKeyMap(content) {
|
|
996
|
+
const fileKeys = new Map();
|
|
997
|
+
const dataPattern = /<data\b[^>]*\bname="([^"]+)"[^>]*>/gi;
|
|
998
|
+
let match;
|
|
999
|
+
|
|
1000
|
+
while ((match = dataPattern.exec(content)) !== null) {
|
|
1001
|
+
const key = normalizeToken(decodeXmlEntities(match[1]));
|
|
1002
|
+
if (!key) {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
fileKeys.set(key, true);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return fileKeys;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function parseSettingsSqlReferenceMap(content) {
|
|
1012
|
+
const refsByKey = new Map();
|
|
1013
|
+
const settingPattern = /<Setting\b[^>]*\bName="([^"]+)"[^>]*>([\s\S]*?)<\/Setting>/gi;
|
|
1014
|
+
let match;
|
|
1015
|
+
|
|
1016
|
+
while ((match = settingPattern.exec(content)) !== null) {
|
|
1017
|
+
const key = normalizeToken(decodeXmlEntities(match[1]));
|
|
1018
|
+
if (!key) {
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const valueMatch = match[2].match(/<Value(?:\s[^>]*)?>([\s\S]*?)<\/Value>/i);
|
|
1023
|
+
if (!valueMatch) {
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const value = decodeXmlEntities(valueMatch[1]).trim();
|
|
1028
|
+
const refs = extractSqlReferenceNamesFromString(value);
|
|
1029
|
+
if (refs.length === 0) {
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const existing = refsByKey.get(key) ?? [];
|
|
1034
|
+
refsByKey.set(key, uniqueSorted([...existing, ...refs]));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return refsByKey;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function parseSettingsKeyMap(content) {
|
|
1041
|
+
const fileKeys = new Map();
|
|
1042
|
+
const settingPattern = /<Setting\b[^>]*\bName="([^"]+)"[^>]*>/gi;
|
|
1043
|
+
let match;
|
|
1044
|
+
|
|
1045
|
+
while ((match = settingPattern.exec(content)) !== null) {
|
|
1046
|
+
const key = normalizeToken(decodeXmlEntities(match[1]));
|
|
1047
|
+
if (!key) {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
fileKeys.set(key, true);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return fileKeys;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function parseConfigKeyMap(content) {
|
|
1057
|
+
const fileKeys = new Map();
|
|
1058
|
+
const addPattern = /<add\b([^>]+?)\/?>/gi;
|
|
1059
|
+
let match;
|
|
1060
|
+
|
|
1061
|
+
while ((match = addPattern.exec(content)) !== null) {
|
|
1062
|
+
const attributes = match[1];
|
|
1063
|
+
const nameMatch = attributes.match(/\bname="([^"]+)"/i);
|
|
1064
|
+
const keyMatch = attributes.match(/\bkey="([^"]+)"/i);
|
|
1065
|
+
const normalized = normalizeToken(decodeXmlEntities(nameMatch?.[1] ?? keyMatch?.[1] ?? ""));
|
|
1066
|
+
if (!normalized) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
fileKeys.set(normalized, true);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return fileKeys;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function buildSqlResourceReferenceMap(fileRecords) {
|
|
1076
|
+
const refsByKey = new Map();
|
|
1077
|
+
|
|
1078
|
+
for (const fileRecord of fileRecords) {
|
|
1079
|
+
const ext = path.extname(fileRecord.path).toLowerCase();
|
|
1080
|
+
let fileRefs = null;
|
|
1081
|
+
if (ext === ".resx") {
|
|
1082
|
+
fileRefs = parseResxSqlReferenceMap(fileRecord.content);
|
|
1083
|
+
} else if (ext === ".settings") {
|
|
1084
|
+
fileRefs = parseSettingsSqlReferenceMap(fileRecord.content);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (!fileRefs) {
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
for (const [key, refs] of fileRefs.entries()) {
|
|
1092
|
+
const existing = refsByKey.get(key) ?? [];
|
|
1093
|
+
refsByKey.set(key, uniqueSorted([...existing, ...refs]));
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return refsByKey;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function buildNamedResourceFileMaps(fileRecords) {
|
|
1101
|
+
const resourceFilesByKey = new Map();
|
|
1102
|
+
const settingFilesByKey = new Map();
|
|
1103
|
+
const configFilesByKey = new Map();
|
|
1104
|
+
|
|
1105
|
+
for (const fileRecord of fileRecords) {
|
|
1106
|
+
const ext = path.extname(fileRecord.path).toLowerCase();
|
|
1107
|
+
const keyMap =
|
|
1108
|
+
ext === ".resx"
|
|
1109
|
+
? parseResxKeyMap(fileRecord.content)
|
|
1110
|
+
: ext === ".settings"
|
|
1111
|
+
? parseSettingsKeyMap(fileRecord.content)
|
|
1112
|
+
: ext === ".config"
|
|
1113
|
+
? parseConfigKeyMap(fileRecord.content)
|
|
1114
|
+
: null;
|
|
1115
|
+
|
|
1116
|
+
if (!keyMap) {
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
for (const key of keyMap.keys()) {
|
|
1121
|
+
const targetMap =
|
|
1122
|
+
ext === ".resx" ? resourceFilesByKey : ext === ".settings" ? settingFilesByKey : configFilesByKey;
|
|
1123
|
+
const list = targetMap.get(key) ?? [];
|
|
1124
|
+
list.push(fileRecord.id);
|
|
1125
|
+
targetMap.set(key, uniqueSorted(list));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return { resourceFilesByKey, settingFilesByKey, configFilesByKey };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function extractSqlResourceKeyReferences(content) {
|
|
1133
|
+
const keys = new Set();
|
|
1134
|
+
|
|
1135
|
+
for (const pattern of SQL_RESOURCE_KEY_PATTERNS) {
|
|
1136
|
+
let match;
|
|
1137
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
1138
|
+
const key = normalizeToken(match[1]);
|
|
1139
|
+
if (key) {
|
|
1140
|
+
keys.add(key);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
return [...keys];
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function extractConfigKeyReferences(content) {
|
|
1149
|
+
const keys = new Set();
|
|
1150
|
+
|
|
1151
|
+
for (const pattern of CONFIG_KEY_REFERENCE_PATTERNS) {
|
|
1152
|
+
let match;
|
|
1153
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
1154
|
+
const key = normalizeToken(match[1]);
|
|
1155
|
+
if (key) {
|
|
1156
|
+
keys.add(key);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
return [...keys];
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function shouldExtractNamedResourceReferences(filePath) {
|
|
1165
|
+
return NAMED_RESOURCE_REFERENCE_SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function generateNamedResourceRelations(fileRecords) {
|
|
1169
|
+
const { resourceFilesByKey, settingFilesByKey, configFilesByKey } = buildNamedResourceFileMaps(fileRecords);
|
|
1170
|
+
const usesResourceRelations = [];
|
|
1171
|
+
const usesSettingRelations = [];
|
|
1172
|
+
const usesConfigRelations = [];
|
|
1173
|
+
const resourceSeen = new Set();
|
|
1174
|
+
const settingSeen = new Set();
|
|
1175
|
+
const configSeen = new Set();
|
|
1176
|
+
|
|
1177
|
+
for (const fileRecord of fileRecords) {
|
|
1178
|
+
if (!shouldExtractNamedResourceReferences(fileRecord.path)) {
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
for (const key of extractSqlResourceKeyReferences(fileRecord.content)) {
|
|
1183
|
+
for (const targetFileId of resourceFilesByKey.get(key) ?? []) {
|
|
1184
|
+
const relKey = relationKey(fileRecord.id, targetFileId, key);
|
|
1185
|
+
if (!resourceSeen.has(relKey) && fileRecord.id !== targetFileId) {
|
|
1186
|
+
resourceSeen.add(relKey);
|
|
1187
|
+
usesResourceRelations.push({
|
|
1188
|
+
from: fileRecord.id,
|
|
1189
|
+
to: targetFileId,
|
|
1190
|
+
note: key
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
for (const targetFileId of settingFilesByKey.get(key) ?? []) {
|
|
1196
|
+
const relKey = relationKey(fileRecord.id, targetFileId, key);
|
|
1197
|
+
if (!settingSeen.has(relKey) && fileRecord.id !== targetFileId) {
|
|
1198
|
+
settingSeen.add(relKey);
|
|
1199
|
+
usesSettingRelations.push({
|
|
1200
|
+
from: fileRecord.id,
|
|
1201
|
+
to: targetFileId,
|
|
1202
|
+
note: key
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
for (const key of extractConfigKeyReferences(fileRecord.content)) {
|
|
1209
|
+
for (const targetFileId of configFilesByKey.get(key) ?? []) {
|
|
1210
|
+
const relKey = relationKey(fileRecord.id, targetFileId, key);
|
|
1211
|
+
if (!configSeen.has(relKey) && fileRecord.id !== targetFileId) {
|
|
1212
|
+
configSeen.add(relKey);
|
|
1213
|
+
usesConfigRelations.push({
|
|
1214
|
+
from: fileRecord.id,
|
|
1215
|
+
to: targetFileId,
|
|
1216
|
+
note: key
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
usesResourceRelations.sort((a, b) =>
|
|
1224
|
+
relationKey(a.from, a.to, a.note).localeCompare(relationKey(b.from, b.to, b.note))
|
|
1225
|
+
);
|
|
1226
|
+
usesSettingRelations.sort((a, b) =>
|
|
1227
|
+
relationKey(a.from, a.to, a.note).localeCompare(relationKey(b.from, b.to, b.note))
|
|
1228
|
+
);
|
|
1229
|
+
usesConfigRelations.sort((a, b) =>
|
|
1230
|
+
relationKey(a.from, a.to, a.note).localeCompare(relationKey(b.from, b.to, b.note))
|
|
1231
|
+
);
|
|
1232
|
+
|
|
1233
|
+
return { usesResourceRelations, usesSettingRelations, usesConfigRelations };
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function parseConfigIncludeTargets(fileRecord) {
|
|
1237
|
+
const relPath = toPosixPath(String(fileRecord?.path ?? "").trim());
|
|
1238
|
+
const lowerPath = relPath.toLowerCase();
|
|
1239
|
+
if (!lowerPath.endsWith(".config")) {
|
|
1240
|
+
return [];
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const content = String(fileRecord?.content ?? "");
|
|
1244
|
+
const dir = path.posix.dirname(relPath);
|
|
1245
|
+
const includes = [];
|
|
1246
|
+
const sectionPattern =
|
|
1247
|
+
/<([A-Za-z_][A-Za-z0-9_.:-]*)\b([^>]*?)\b(configSource|file)="([^"]+)"([^>]*)>/gi;
|
|
1248
|
+
let match;
|
|
1249
|
+
|
|
1250
|
+
while ((match = sectionPattern.exec(content)) !== null) {
|
|
1251
|
+
const sectionName = String(match[1] ?? "").trim().toLowerCase();
|
|
1252
|
+
const attributeName = String(match[3] ?? "").trim().toLowerCase();
|
|
1253
|
+
const includePath = decodeXmlEntities(match[4] ?? "").trim().replace(/\\/g, "/");
|
|
1254
|
+
if (!sectionName || !attributeName || !includePath) {
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (includePath.startsWith("/") || includePath.startsWith("~")) {
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const resolvedPath = path.posix.normalize(dir === "." ? includePath : `${dir}/${includePath}`);
|
|
1262
|
+
if (!resolvedPath || resolvedPath.startsWith("../")) {
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
includes.push({
|
|
1267
|
+
targetPath: resolvedPath,
|
|
1268
|
+
note: `${sectionName}:${attributeName}`
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
return includes;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function generateConfigIncludeRelations(fileRecords) {
|
|
1276
|
+
const fileIdByPath = new Map(fileRecords.map((record) => [toPosixPath(record.path), record.id]));
|
|
1277
|
+
const relations = [];
|
|
1278
|
+
const seen = new Set();
|
|
1279
|
+
|
|
1280
|
+
for (const fileRecord of fileRecords) {
|
|
1281
|
+
for (const include of parseConfigIncludeTargets(fileRecord)) {
|
|
1282
|
+
const targetFileId = fileIdByPath.get(include.targetPath);
|
|
1283
|
+
if (!targetFileId || targetFileId === fileRecord.id) {
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
const key = relationKey(fileRecord.id, targetFileId, include.note);
|
|
1287
|
+
if (seen.has(key)) {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
seen.add(key);
|
|
1291
|
+
relations.push({
|
|
1292
|
+
from: fileRecord.id,
|
|
1293
|
+
to: targetFileId,
|
|
1294
|
+
note: include.note
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
relations.sort((a, b) => relationKey(a.from, a.to, a.note).localeCompare(relationKey(b.from, b.to, b.note)));
|
|
1300
|
+
return relations;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function parseSectionHandlerDeclarations(content) {
|
|
1304
|
+
const declarations = [];
|
|
1305
|
+
const sectionPattern = /<section\b([^>]*?)\/?>/gi;
|
|
1306
|
+
let match;
|
|
1307
|
+
|
|
1308
|
+
while ((match = sectionPattern.exec(String(content ?? ""))) !== null) {
|
|
1309
|
+
const attrs = match[1] ?? "";
|
|
1310
|
+
const nameMatch = attrs.match(/\bname="([^"]+)"/i);
|
|
1311
|
+
const typeMatch = attrs.match(/\btype="([^"]+)"/i);
|
|
1312
|
+
const sectionName = normalizeToken(decodeXmlEntities(nameMatch?.[1] ?? ""));
|
|
1313
|
+
const typeValue = decodeXmlEntities(typeMatch?.[1] ?? "").trim();
|
|
1314
|
+
if (!sectionName || !typeValue) {
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const typeParts = typeValue.split(",").map((part) => part.trim()).filter(Boolean);
|
|
1319
|
+
const fullTypeName = typeParts[0] ?? "";
|
|
1320
|
+
const assemblyName = typeParts[1] ?? "";
|
|
1321
|
+
const shortTypeName = fullTypeName.split(".").pop()?.split("+").pop() ?? "";
|
|
1322
|
+
const normalizedTypeName = normalizeToken(shortTypeName);
|
|
1323
|
+
const normalizedAssemblyName = normalizeToken(assemblyName);
|
|
1324
|
+
if (!normalizedTypeName && !normalizedAssemblyName) {
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
declarations.push({
|
|
1329
|
+
sectionName,
|
|
1330
|
+
normalizedTypeName,
|
|
1331
|
+
normalizedAssemblyName
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return declarations;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function buildProjectAssemblyFileMap(fileRecords) {
|
|
1339
|
+
const aliasMap = new Map();
|
|
1340
|
+
|
|
1341
|
+
for (const fileRecord of fileRecords) {
|
|
1342
|
+
const ext = path.extname(fileRecord.path).toLowerCase();
|
|
1343
|
+
if (!PROJECT_DEFINITION_EXTENSIONS.has(ext) || ext === ".sln") {
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const aliases = uniqueSorted([
|
|
1348
|
+
normalizeToken(extractXmlTagValue(fileRecord.content, "AssemblyName")),
|
|
1349
|
+
normalizeToken(extractXmlTagValue(fileRecord.content, "RootNamespace")),
|
|
1350
|
+
normalizeToken(path.basename(fileRecord.path, ext))
|
|
1351
|
+
].filter(Boolean));
|
|
1352
|
+
|
|
1353
|
+
for (const alias of aliases) {
|
|
1354
|
+
const existing = aliasMap.get(alias) ?? [];
|
|
1355
|
+
aliasMap.set(alias, uniqueSorted([...existing, fileRecord.id]));
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return aliasMap;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function extractDeclaredTypeNames(fileRecord) {
|
|
1363
|
+
const ext = path.extname(fileRecord.path).toLowerCase();
|
|
1364
|
+
const pattern =
|
|
1365
|
+
ext === ".vb"
|
|
1366
|
+
? /\b(?:Public|Friend|Private|Protected|Partial|MustInherit|NotInheritable|Shadows|Default|Overridable|Overrides|Shared|\s)*(?:Class|Module|Structure|Interface|Enum)\s+([A-Za-z_][A-Za-z0-9_]*)/gi
|
|
1367
|
+
: ext === ".cs"
|
|
1368
|
+
? /\b(?:public|internal|private|protected|abstract|sealed|static|partial|\s)*(?:class|struct|interface|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/gi
|
|
1369
|
+
: null;
|
|
1370
|
+
|
|
1371
|
+
if (!pattern) {
|
|
1372
|
+
return [];
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const typeNames = new Set();
|
|
1376
|
+
let match;
|
|
1377
|
+
while ((match = pattern.exec(String(fileRecord.content ?? ""))) !== null) {
|
|
1378
|
+
const normalized = normalizeToken(match[1] ?? "");
|
|
1379
|
+
if (normalized) {
|
|
1380
|
+
typeNames.add(normalized);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return [...typeNames];
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function buildCodeTypeFileMap(fileRecords) {
|
|
1388
|
+
const typeMap = new Map();
|
|
1389
|
+
|
|
1390
|
+
for (const fileRecord of fileRecords) {
|
|
1391
|
+
if (fileRecord.kind !== "CODE") {
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
for (const typeName of extractDeclaredTypeNames(fileRecord)) {
|
|
1395
|
+
const existing = typeMap.get(typeName) ?? [];
|
|
1396
|
+
typeMap.set(typeName, uniqueSorted([...existing, fileRecord.id]));
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
return typeMap;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function longestCommonPathPrefixLength(pathA, pathB) {
|
|
1404
|
+
const partsA = toPosixPath(pathA).split("/").filter(Boolean);
|
|
1405
|
+
const partsB = toPosixPath(pathB).split("/").filter(Boolean);
|
|
1406
|
+
const limit = Math.min(partsA.length, partsB.length);
|
|
1407
|
+
let count = 0;
|
|
1408
|
+
while (count < limit && partsA[count] === partsB[count]) {
|
|
1409
|
+
count += 1;
|
|
1410
|
+
}
|
|
1411
|
+
return count;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function generateMachineConfigRelations(fileRecords) {
|
|
1415
|
+
const machineConfigs = fileRecords.filter(
|
|
1416
|
+
(record) => path.basename(record.path).toLowerCase() === "machine.config"
|
|
1417
|
+
);
|
|
1418
|
+
if (machineConfigs.length === 0) {
|
|
1419
|
+
return [];
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const relations = [];
|
|
1423
|
+
const seen = new Set();
|
|
1424
|
+
|
|
1425
|
+
for (const fileRecord of fileRecords) {
|
|
1426
|
+
const lowerPath = fileRecord.path.toLowerCase();
|
|
1427
|
+
if (
|
|
1428
|
+
!lowerPath.endsWith(".config") ||
|
|
1429
|
+
path.basename(lowerPath) === "machine.config" ||
|
|
1430
|
+
!/<configuration\b/i.test(String(fileRecord.content ?? "")) ||
|
|
1431
|
+
parseConfigTransformTarget(fileRecord)
|
|
1432
|
+
) {
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const rankedTargets = machineConfigs
|
|
1437
|
+
.filter((candidate) => candidate.id !== fileRecord.id)
|
|
1438
|
+
.map((candidate) => ({
|
|
1439
|
+
id: candidate.id,
|
|
1440
|
+
score: longestCommonPathPrefixLength(fileRecord.path, candidate.path)
|
|
1441
|
+
}))
|
|
1442
|
+
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
1443
|
+
|
|
1444
|
+
const target = rankedTargets[0];
|
|
1445
|
+
if (!target) {
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const key = relationKey(fileRecord.id, target.id, "inherits:machine");
|
|
1450
|
+
if (seen.has(key)) {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
seen.add(key);
|
|
1454
|
+
relations.push({
|
|
1455
|
+
from: fileRecord.id,
|
|
1456
|
+
to: target.id,
|
|
1457
|
+
note: "inherits:machine"
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
return uniqueRelations(relations);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function generateSectionHandlerRelations(fileRecords) {
|
|
1465
|
+
const projectAssemblyFileMap = buildProjectAssemblyFileMap(fileRecords);
|
|
1466
|
+
const codeTypeFileMap = buildCodeTypeFileMap(fileRecords);
|
|
1467
|
+
const relations = [];
|
|
1468
|
+
|
|
1469
|
+
for (const fileRecord of fileRecords) {
|
|
1470
|
+
if (!fileRecord.path.toLowerCase().endsWith(".config")) {
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
for (const declaration of parseSectionHandlerDeclarations(fileRecord.content)) {
|
|
1475
|
+
const note = `section_handler:${declaration.sectionName}`;
|
|
1476
|
+
|
|
1477
|
+
for (const targetFileId of projectAssemblyFileMap.get(declaration.normalizedAssemblyName) ?? []) {
|
|
1478
|
+
relations.push({
|
|
1479
|
+
from: fileRecord.id,
|
|
1480
|
+
to: targetFileId,
|
|
1481
|
+
note
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
for (const targetFileId of codeTypeFileMap.get(declaration.normalizedTypeName) ?? []) {
|
|
1486
|
+
relations.push({
|
|
1487
|
+
from: fileRecord.id,
|
|
1488
|
+
to: targetFileId,
|
|
1489
|
+
note
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
return uniqueRelations(relations.filter((relation) => relation.from !== relation.to));
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function generateConfigTransformKeyRelations(fileRecords, chunkRecords) {
|
|
1499
|
+
const fileIdByPath = new Map(fileRecords.map((record) => [toPosixPath(record.path), record.id]));
|
|
1500
|
+
const chunkFileIdById = new Map(chunkRecords.map((chunk) => [chunk.id, chunk.file_id]));
|
|
1501
|
+
const configChunkIdsByAlias = new Map();
|
|
1502
|
+
|
|
1503
|
+
for (const chunk of chunkRecords) {
|
|
1504
|
+
if (isWindowChunkId(chunk.id) || String(chunk.language ?? "").toLowerCase() !== "config") {
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
for (const alias of configChunkAliases(chunk)) {
|
|
1508
|
+
const existing = configChunkIdsByAlias.get(alias) ?? [];
|
|
1509
|
+
configChunkIdsByAlias.set(alias, [...existing, chunk.id]);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const relations = [];
|
|
1514
|
+
for (const fileRecord of fileRecords) {
|
|
1515
|
+
const transform = parseConfigTransformTarget(fileRecord);
|
|
1516
|
+
if (!transform) {
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const targetFileId = fileIdByPath.get(transform.targetPath);
|
|
1521
|
+
if (!targetFileId) {
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
for (const key of parseConfigKeyMap(fileRecord.content).keys()) {
|
|
1526
|
+
for (const targetChunkId of configChunkIdsByAlias.get(key) ?? []) {
|
|
1527
|
+
if (chunkFileIdById.get(targetChunkId) !== targetFileId) {
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
relations.push({
|
|
1531
|
+
from: fileRecord.id,
|
|
1532
|
+
to: targetChunkId,
|
|
1533
|
+
note: `${key}:${transform.environment}`
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
return uniqueRelations(relations);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function parseConfigTransformTarget(fileRecord) {
|
|
1543
|
+
const relPath = toPosixPath(String(fileRecord?.path ?? "").trim());
|
|
1544
|
+
const lowerPath = relPath.toLowerCase();
|
|
1545
|
+
if (!lowerPath.endsWith(".config")) {
|
|
1546
|
+
return null;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const content = String(fileRecord?.content ?? "");
|
|
1550
|
+
if (!/\bxdt:(?:transform|locator)\b/i.test(content) && !/\bxmlns:xdt=/i.test(content)) {
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const dir = path.posix.dirname(relPath);
|
|
1555
|
+
const baseName = path.posix.basename(relPath, ".config");
|
|
1556
|
+
const match = baseName.match(/^(.+)\.([^.]+)$/);
|
|
1557
|
+
if (!match) {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
const baseStem = match[1]?.trim();
|
|
1562
|
+
const environment = match[2]?.trim();
|
|
1563
|
+
if (!baseStem || !environment) {
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const targetPath = dir === "." ? `${baseStem}.config` : `${dir}/${baseStem}.config`;
|
|
1568
|
+
return {
|
|
1569
|
+
targetPath,
|
|
1570
|
+
environment: normalizeToken(environment)
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function generateConfigTransformRelations(fileRecords) {
|
|
1575
|
+
const fileIdByPath = new Map(fileRecords.map((record) => [toPosixPath(record.path), record.id]));
|
|
1576
|
+
const relations = [];
|
|
1577
|
+
const seen = new Set();
|
|
1578
|
+
|
|
1579
|
+
for (const fileRecord of fileRecords) {
|
|
1580
|
+
const transform = parseConfigTransformTarget(fileRecord);
|
|
1581
|
+
if (!transform) {
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
const targetFileId = fileIdByPath.get(transform.targetPath);
|
|
1586
|
+
if (!targetFileId || targetFileId === fileRecord.id) {
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const key = relationKey(fileRecord.id, targetFileId, transform.environment);
|
|
1591
|
+
if (seen.has(key)) {
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
seen.add(key);
|
|
1595
|
+
relations.push({
|
|
1596
|
+
from: fileRecord.id,
|
|
1597
|
+
to: targetFileId,
|
|
1598
|
+
note: transform.environment
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
relations.sort((a, b) => relationKey(a.from, a.to, a.note).localeCompare(relationKey(b.from, b.to, b.note)));
|
|
1603
|
+
return relations;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function shouldExtractSqlReferences(filePath) {
|
|
1607
|
+
return SQL_REFERENCE_SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function extractSqlObjectReferencesFromContent(content, filePath = "", sqlResourceReferenceMap = new Map()) {
|
|
1611
|
+
const refs = new Set();
|
|
1612
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1613
|
+
|
|
1614
|
+
if (ext === ".resx") {
|
|
1615
|
+
for (const values of parseResxSqlReferenceMap(content).values()) {
|
|
1616
|
+
for (const ref of values) {
|
|
1617
|
+
refs.add(ref);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
} else if (ext === ".settings") {
|
|
1621
|
+
for (const values of parseSettingsSqlReferenceMap(content).values()) {
|
|
1622
|
+
for (const ref of values) {
|
|
1623
|
+
refs.add(ref);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
for (const pattern of SQL_OBJECT_REFERENCE_PATTERNS) {
|
|
1629
|
+
let match;
|
|
1630
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
1631
|
+
for (const ref of extractSqlReferenceNamesFromString(match[1])) {
|
|
1632
|
+
refs.add(ref);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if (sqlResourceReferenceMap.size > 0) {
|
|
1638
|
+
for (const key of extractSqlResourceKeyReferences(content)) {
|
|
1639
|
+
for (const ref of sqlResourceReferenceMap.get(key) ?? []) {
|
|
1640
|
+
refs.add(ref);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
return uniqueSorted([...refs]);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function decodeXmlEntities(value) {
|
|
1649
|
+
return String(value)
|
|
1650
|
+
.replace(/"/g, '"')
|
|
1651
|
+
.replace(/'/g, "'")
|
|
1652
|
+
.replace(/</g, "<")
|
|
1653
|
+
.replace(/>/g, ">")
|
|
1654
|
+
.replace(/&/g, "&");
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function projectIdFor(filePath) {
|
|
1658
|
+
return `project:${filePath}`;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function isProjectDefinitionFile(filePath) {
|
|
1662
|
+
return PROJECT_DEFINITION_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function resolveProjectRelativePath(baseFilePath, includePath) {
|
|
1666
|
+
if (!includePath) {
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const normalizedInclude = toPosixPath(decodeXmlEntities(includePath).trim().replace(/\\/g, "/"));
|
|
1671
|
+
if (!normalizedInclude) {
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
const resolved = path.resolve(REPO_ROOT, path.dirname(baseFilePath), normalizedInclude);
|
|
1676
|
+
const relPath = toPosixPath(path.relative(REPO_ROOT, resolved));
|
|
1677
|
+
if (!relPath || relPath.startsWith("../")) {
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
return relPath;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function projectLanguageForExtension(ext) {
|
|
1685
|
+
switch (ext) {
|
|
1686
|
+
case ".vbproj":
|
|
1687
|
+
return "vbnet";
|
|
1688
|
+
case ".csproj":
|
|
1689
|
+
return "csharp";
|
|
1690
|
+
case ".fsproj":
|
|
1691
|
+
return "fsharp";
|
|
1692
|
+
case ".sln":
|
|
1693
|
+
return "solution";
|
|
1694
|
+
default:
|
|
1695
|
+
return "dotnet";
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function extractXmlTagValue(content, tagName) {
|
|
1700
|
+
const match = content.match(new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`, "i"));
|
|
1701
|
+
return match ? decodeXmlEntities(match[1]).trim() : "";
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function collectXmlIncludeValues(content, elementNames) {
|
|
1705
|
+
const values = [];
|
|
1706
|
+
const pattern = new RegExp(
|
|
1707
|
+
`<(?:${elementNames.join("|")})\\b[^>]*\\bInclude="([^"]+)"[^>]*\\/?>`,
|
|
1708
|
+
"gi"
|
|
1709
|
+
);
|
|
1710
|
+
let match;
|
|
1711
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
1712
|
+
values.push(decodeXmlEntities(match[1]).trim());
|
|
1713
|
+
}
|
|
1714
|
+
return values;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function parseSolutionProject(fileRecord, indexedFileIds) {
|
|
1718
|
+
const declaredMembers = [];
|
|
1719
|
+
const referencesProjectRelations = [];
|
|
1720
|
+
const includesFileRelations = [];
|
|
1721
|
+
const fileRelationKeys = new Set();
|
|
1722
|
+
const ext = path.extname(fileRecord.path).toLowerCase();
|
|
1723
|
+
const fallbackName = path.basename(fileRecord.path, ext);
|
|
1724
|
+
const projectPattern =
|
|
1725
|
+
/^Project\([^)]*\)\s*=\s*"([^"]+)",\s*"([^"]+\.(?:vbproj|csproj|fsproj))",\s*"\{[^"]+\}"$/gim;
|
|
1726
|
+
|
|
1727
|
+
let match;
|
|
1728
|
+
while ((match = projectPattern.exec(fileRecord.content)) !== null) {
|
|
1729
|
+
const memberName = match[1].trim();
|
|
1730
|
+
const memberPath = resolveProjectRelativePath(fileRecord.path, match[2]);
|
|
1731
|
+
if (!memberPath) {
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
declaredMembers.push({ name: memberName, path: memberPath });
|
|
1735
|
+
const targetId = projectIdFor(memberPath);
|
|
1736
|
+
if (indexedFileIds.has(`file:${memberPath}`)) {
|
|
1737
|
+
referencesProjectRelations.push({
|
|
1738
|
+
from: projectIdFor(fileRecord.path),
|
|
1739
|
+
to: targetId,
|
|
1740
|
+
note: `solution_member:${memberName}`
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
for (const fileId of [`file:${fileRecord.path}`]) {
|
|
1746
|
+
if (indexedFileIds.has(fileId) && !fileRelationKeys.has(fileId)) {
|
|
1747
|
+
fileRelationKeys.add(fileId);
|
|
1748
|
+
includesFileRelations.push({ from: projectIdFor(fileRecord.path), to: fileId });
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
const summaryParts = [`Solution ${fallbackName}`];
|
|
1753
|
+
if (declaredMembers.length > 0) {
|
|
1754
|
+
summaryParts.push(`Contains ${declaredMembers.length} project references`);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
return {
|
|
1758
|
+
project: {
|
|
1759
|
+
id: projectIdFor(fileRecord.path),
|
|
1760
|
+
path: fileRecord.path,
|
|
1761
|
+
name: fallbackName,
|
|
1762
|
+
kind: "solution",
|
|
1763
|
+
language: projectLanguageForExtension(ext),
|
|
1764
|
+
target_framework: "",
|
|
1765
|
+
summary: `${summaryParts.join(". ")}.`,
|
|
1766
|
+
file_count: includesFileRelations.length,
|
|
1767
|
+
updated_at: fileRecord.updated_at,
|
|
1768
|
+
source_of_truth: false,
|
|
1769
|
+
trust_level: 78,
|
|
1770
|
+
status: "active"
|
|
1771
|
+
},
|
|
1772
|
+
includesFileRelations,
|
|
1773
|
+
referencesProjectRelations
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function parseDotNetProject(fileRecord, indexedFileIds) {
|
|
1778
|
+
const ext = path.extname(fileRecord.path).toLowerCase();
|
|
1779
|
+
const fallbackName = path.basename(fileRecord.path, ext);
|
|
1780
|
+
const assemblyName = extractXmlTagValue(fileRecord.content, "AssemblyName");
|
|
1781
|
+
const rootNamespace = extractXmlTagValue(fileRecord.content, "RootNamespace");
|
|
1782
|
+
const targetFrameworkRaw =
|
|
1783
|
+
extractXmlTagValue(fileRecord.content, "TargetFramework") ||
|
|
1784
|
+
extractXmlTagValue(fileRecord.content, "TargetFrameworkVersion") ||
|
|
1785
|
+
extractXmlTagValue(fileRecord.content, "TargetFrameworks");
|
|
1786
|
+
const targetFramework = targetFrameworkRaw.split(";")[0].trim();
|
|
1787
|
+
const includeCandidates = collectXmlIncludeValues(fileRecord.content, [
|
|
1788
|
+
"Compile",
|
|
1789
|
+
"Content",
|
|
1790
|
+
"EmbeddedResource",
|
|
1791
|
+
"None",
|
|
1792
|
+
"Page",
|
|
1793
|
+
"ApplicationDefinition"
|
|
1794
|
+
]);
|
|
1795
|
+
const projectReferenceCandidates = collectXmlIncludeValues(fileRecord.content, ["ProjectReference"]);
|
|
1796
|
+
const includesFileRelations = [];
|
|
1797
|
+
const referencesProjectRelations = [];
|
|
1798
|
+
const fileRelationKeys = new Set();
|
|
1799
|
+
|
|
1800
|
+
const addFileRelation = (relPath) => {
|
|
1801
|
+
const fileId = `file:${relPath}`;
|
|
1802
|
+
if (!indexedFileIds.has(fileId) || fileRelationKeys.has(fileId)) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
fileRelationKeys.add(fileId);
|
|
1806
|
+
includesFileRelations.push({
|
|
1807
|
+
from: projectIdFor(fileRecord.path),
|
|
1808
|
+
to: fileId
|
|
1809
|
+
});
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1812
|
+
addFileRelation(fileRecord.path);
|
|
1813
|
+
|
|
1814
|
+
for (const includePath of includeCandidates) {
|
|
1815
|
+
const relPath = resolveProjectRelativePath(fileRecord.path, includePath);
|
|
1816
|
+
if (!relPath) {
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
addFileRelation(relPath);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
for (const includePath of projectReferenceCandidates) {
|
|
1823
|
+
const relPath = resolveProjectRelativePath(fileRecord.path, includePath);
|
|
1824
|
+
if (!relPath) {
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
const targetFileId = `file:${relPath}`;
|
|
1828
|
+
if (!indexedFileIds.has(targetFileId)) {
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
referencesProjectRelations.push({
|
|
1832
|
+
from: projectIdFor(fileRecord.path),
|
|
1833
|
+
to: projectIdFor(relPath),
|
|
1834
|
+
note: includePath
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
const summaryParts = [
|
|
1839
|
+
`${projectLanguageForExtension(ext).toUpperCase()} project ${assemblyName || rootNamespace || fallbackName}`
|
|
1840
|
+
];
|
|
1841
|
+
if (targetFramework) {
|
|
1842
|
+
summaryParts.push(`Target framework ${targetFramework}`);
|
|
1843
|
+
}
|
|
1844
|
+
if (includesFileRelations.length > 1) {
|
|
1845
|
+
summaryParts.push(`Includes ${includesFileRelations.length - 1} indexed project files`);
|
|
1846
|
+
}
|
|
1847
|
+
if (referencesProjectRelations.length > 0) {
|
|
1848
|
+
summaryParts.push(`References ${referencesProjectRelations.length} projects`);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
return {
|
|
1852
|
+
project: {
|
|
1853
|
+
id: projectIdFor(fileRecord.path),
|
|
1854
|
+
path: fileRecord.path,
|
|
1855
|
+
name: assemblyName || rootNamespace || fallbackName,
|
|
1856
|
+
kind: "project",
|
|
1857
|
+
language: projectLanguageForExtension(ext),
|
|
1858
|
+
target_framework: targetFramework,
|
|
1859
|
+
summary: `${summaryParts.join(". ")}.`,
|
|
1860
|
+
file_count: includesFileRelations.length,
|
|
1861
|
+
updated_at: fileRecord.updated_at,
|
|
1862
|
+
source_of_truth: false,
|
|
1863
|
+
trust_level: 80,
|
|
1864
|
+
status: "active"
|
|
1865
|
+
},
|
|
1866
|
+
includesFileRelations,
|
|
1867
|
+
referencesProjectRelations
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
function generateProjects(fileRecords) {
|
|
1872
|
+
const indexedFileIds = new Set(fileRecords.map((record) => record.id));
|
|
1873
|
+
const projectRecords = [];
|
|
1874
|
+
const includesFileRelations = [];
|
|
1875
|
+
const referencesProjectRelations = [];
|
|
1876
|
+
const includeKeys = new Set();
|
|
1877
|
+
const referenceKeys = new Set();
|
|
1878
|
+
|
|
1879
|
+
for (const fileRecord of fileRecords) {
|
|
1880
|
+
if (!isProjectDefinitionFile(fileRecord.path)) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
const ext = path.extname(fileRecord.path).toLowerCase();
|
|
1885
|
+
const parsed =
|
|
1886
|
+
ext === ".sln"
|
|
1887
|
+
? parseSolutionProject(fileRecord, indexedFileIds)
|
|
1888
|
+
: parseDotNetProject(fileRecord, indexedFileIds);
|
|
1889
|
+
|
|
1890
|
+
projectRecords.push(parsed.project);
|
|
1891
|
+
|
|
1892
|
+
for (const relation of parsed.includesFileRelations) {
|
|
1893
|
+
const key = relationKey(relation.from, relation.to);
|
|
1894
|
+
if (includeKeys.has(key)) {
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
includeKeys.add(key);
|
|
1898
|
+
includesFileRelations.push(relation);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
for (const relation of parsed.referencesProjectRelations) {
|
|
1902
|
+
const key = relationKey(relation.from, relation.to, relation.note);
|
|
1903
|
+
if (referenceKeys.has(key)) {
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
referenceKeys.add(key);
|
|
1907
|
+
referencesProjectRelations.push(relation);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
projectRecords.sort((a, b) => a.path.localeCompare(b.path));
|
|
1912
|
+
includesFileRelations.sort((a, b) => relationKey(a.from, a.to).localeCompare(relationKey(b.from, b.to)));
|
|
1913
|
+
referencesProjectRelations.sort((a, b) =>
|
|
1914
|
+
relationKey(a.from, a.to, a.note).localeCompare(relationKey(b.from, b.to, b.note))
|
|
1915
|
+
);
|
|
1916
|
+
|
|
1917
|
+
return {
|
|
1918
|
+
projects: projectRecords,
|
|
1919
|
+
includesFileRelations,
|
|
1920
|
+
referencesProjectRelations
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function removeChunkStateForFile(fileId, chunkRecordMap, definesRelationMap, callsRelationMap, importsRelationMap, callsSqlRelationMap) {
|
|
1925
|
+
const removedChunkIds = new Set();
|
|
1926
|
+
|
|
1927
|
+
for (const [chunkId, chunkRecord] of chunkRecordMap.entries()) {
|
|
1928
|
+
if (chunkRecord.file_id === fileId) {
|
|
1929
|
+
removedChunkIds.add(chunkId);
|
|
1930
|
+
chunkRecordMap.delete(chunkId);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
if (removedChunkIds.size === 0) {
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
for (const [key, relation] of definesRelationMap.entries()) {
|
|
1939
|
+
if (relation.from === fileId || removedChunkIds.has(relation.to)) {
|
|
1940
|
+
definesRelationMap.delete(key);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
for (const [key, relation] of callsRelationMap.entries()) {
|
|
1945
|
+
if (removedChunkIds.has(relation.from) || removedChunkIds.has(relation.to)) {
|
|
1946
|
+
callsRelationMap.delete(key);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
for (const [key, relation] of importsRelationMap.entries()) {
|
|
1951
|
+
if (removedChunkIds.has(relation.from)) {
|
|
1952
|
+
importsRelationMap.delete(key);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
for (const [key, relation] of callsSqlRelationMap.entries()) {
|
|
1957
|
+
if (relation.from === fileId || removedChunkIds.has(relation.to)) {
|
|
1958
|
+
callsSqlRelationMap.delete(key);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
function hydrateIncrementalChunkState(fileRecords) {
|
|
1964
|
+
const fileIdSet = new Set(fileRecords.map((record) => record.id));
|
|
1965
|
+
const chunkRecordMap = new Map();
|
|
1966
|
+
const definesRelationMap = new Map();
|
|
1967
|
+
const callsRelationMap = new Map();
|
|
1968
|
+
const importsRelationMap = new Map();
|
|
1969
|
+
const callsSqlRelationMap = new Map();
|
|
1970
|
+
|
|
1971
|
+
for (const record of readJsonlSafe(path.join(CACHE_DIR, "entities.chunk.jsonl"))) {
|
|
1972
|
+
if (!record || typeof record !== "object") continue;
|
|
1973
|
+
const chunkId = String(record.id ?? "");
|
|
1974
|
+
const fileId = String(record.file_id ?? "");
|
|
1975
|
+
if (!chunkId || !fileIdSet.has(fileId)) {
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
chunkRecordMap.set(chunkId, {
|
|
1979
|
+
...record,
|
|
1980
|
+
id: chunkId,
|
|
1981
|
+
file_id: fileId
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
const chunkIdSet = new Set(chunkRecordMap.keys());
|
|
1986
|
+
|
|
1987
|
+
for (const record of readJsonlSafe(path.join(CACHE_DIR, "relations.defines.jsonl"))) {
|
|
1988
|
+
if (!record || typeof record !== "object") continue;
|
|
1989
|
+
const from = String(record.from ?? "");
|
|
1990
|
+
const to = String(record.to ?? "");
|
|
1991
|
+
if (!fileIdSet.has(from) || !chunkIdSet.has(to)) {
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
definesRelationMap.set(relationKey(from, to), { from, to });
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
for (const record of readJsonlSafe(path.join(CACHE_DIR, "relations.calls.jsonl"))) {
|
|
1998
|
+
if (!record || typeof record !== "object") continue;
|
|
1999
|
+
const from = String(record.from ?? "");
|
|
2000
|
+
const to = String(record.to ?? "");
|
|
2001
|
+
const callType = String(record.call_type ?? "direct");
|
|
2002
|
+
if (!chunkIdSet.has(from) || !chunkIdSet.has(to)) {
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
callsRelationMap.set(relationKey(from, to, callType), {
|
|
2006
|
+
from,
|
|
2007
|
+
to,
|
|
2008
|
+
call_type: callType
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
for (const record of readJsonlSafe(path.join(CACHE_DIR, "relations.imports.jsonl"))) {
|
|
2013
|
+
if (!record || typeof record !== "object") continue;
|
|
2014
|
+
const from = String(record.from ?? "");
|
|
2015
|
+
const to = String(record.to ?? "");
|
|
2016
|
+
const importName = String(record.import_name ?? "");
|
|
2017
|
+
if (!chunkIdSet.has(from) || !fileIdSet.has(to)) {
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
importsRelationMap.set(relationKey(from, to, importName), {
|
|
2021
|
+
from,
|
|
2022
|
+
to,
|
|
2023
|
+
import_name: importName
|
|
2024
|
+
});
|
|
516
2025
|
}
|
|
517
|
-
fs.writeFileSync(filePath, `${lines.join("\n")}\n`, "utf8");
|
|
518
|
-
}
|
|
519
2026
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
2027
|
+
for (const record of readJsonlSafe(path.join(CACHE_DIR, "relations.calls_sql.jsonl"))) {
|
|
2028
|
+
if (!record || typeof record !== "object") continue;
|
|
2029
|
+
const from = String(record.from ?? "");
|
|
2030
|
+
const to = String(record.to ?? "");
|
|
2031
|
+
const note = String(record.note ?? "");
|
|
2032
|
+
if (!fileIdSet.has(from) || !chunkIdSet.has(to)) {
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
callsSqlRelationMap.set(relationKey(from, to, note), {
|
|
2036
|
+
from,
|
|
2037
|
+
to,
|
|
2038
|
+
note
|
|
2039
|
+
});
|
|
523
2040
|
}
|
|
524
2041
|
|
|
525
|
-
return
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
return JSON.parse(line);
|
|
533
|
-
} catch {
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
})
|
|
537
|
-
.filter((record) => record !== null);
|
|
2042
|
+
return {
|
|
2043
|
+
chunkRecordMap,
|
|
2044
|
+
definesRelationMap,
|
|
2045
|
+
callsRelationMap,
|
|
2046
|
+
importsRelationMap,
|
|
2047
|
+
callsSqlRelationMap
|
|
2048
|
+
};
|
|
538
2049
|
}
|
|
539
2050
|
|
|
540
2051
|
function normalizeRuleTokens(ruleRecord) {
|
|
@@ -559,6 +2070,198 @@ function chunkIdFor(filePath, chunk) {
|
|
|
559
2070
|
return `chunk:${filePath}:${chunk.name}:${startLine}-${endLine}`;
|
|
560
2071
|
}
|
|
561
2072
|
|
|
2073
|
+
function generateChunkDescription(chunk) {
|
|
2074
|
+
const parts = [chunk.kind];
|
|
2075
|
+
if (chunk.exported) parts.push("exported");
|
|
2076
|
+
if (chunk.async) parts.push("async");
|
|
2077
|
+
parts.push(chunk.signature);
|
|
2078
|
+
|
|
2079
|
+
if (typeof chunk.description === "string" && chunk.description.trim().length > 10) {
|
|
2080
|
+
parts.push(normalizeWhitespace(chunk.description).slice(0, 200));
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// Extract leading JSDoc/comment from body
|
|
2084
|
+
// Match leading JSDoc (/** */), block (/* */) and line (//) comments
|
|
2085
|
+
const commentMatch = chunk.body.match(/^(?:\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*)[\s\n]*)+/);
|
|
2086
|
+
if (commentMatch) {
|
|
2087
|
+
const cleaned = commentMatch[0]
|
|
2088
|
+
.replace(/\/\*\*|\*\/|\*|\/\//g, "")
|
|
2089
|
+
.replace(/\s+/g, " ").trim()
|
|
2090
|
+
.slice(0, 200);
|
|
2091
|
+
if (cleaned.length > 10) parts.push(cleaned);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
return parts.join(". ") + ".";
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function generateModuleSummary(dir, files, exportNames, repoRoot = REPO_ROOT) {
|
|
2098
|
+
// Check for README.md in directory
|
|
2099
|
+
const readmePath = path.join(repoRoot, dir, "README.md");
|
|
2100
|
+
if (fs.existsSync(readmePath)) {
|
|
2101
|
+
try {
|
|
2102
|
+
const content = fs.readFileSync(readmePath, "utf8");
|
|
2103
|
+
// Skip first heading line, take first 300 chars
|
|
2104
|
+
const lines = content.split(/\r?\n/);
|
|
2105
|
+
const startIdx = lines.findIndex(l => !l.startsWith("#") && l.trim().length > 0);
|
|
2106
|
+
if (startIdx >= 0) {
|
|
2107
|
+
const excerpt = lines.slice(startIdx).join(" ").trim().slice(0, 300);
|
|
2108
|
+
if (excerpt.length > 20) return excerpt;
|
|
2109
|
+
}
|
|
2110
|
+
} catch {
|
|
2111
|
+
// fall through to auto-generated summary
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
const name = path.basename(dir);
|
|
2116
|
+
const codeFiles = files.filter(f => f.kind === "CODE");
|
|
2117
|
+
const docFiles = files.filter(f => f.kind !== "CODE");
|
|
2118
|
+
|
|
2119
|
+
const parts = [`Module ${name}`];
|
|
2120
|
+
parts.push(`Contains ${files.length} files (${codeFiles.length} code, ${docFiles.length} docs)`);
|
|
2121
|
+
|
|
2122
|
+
// Detect common file extension pattern
|
|
2123
|
+
const exts = new Set(codeFiles.map(f => path.extname(f.path).toLowerCase()));
|
|
2124
|
+
if (exts.size === 1) {
|
|
2125
|
+
const ext = [...exts][0];
|
|
2126
|
+
const extNames = { ".ts": "TypeScript", ".js": "JavaScript", ".mjs": "JavaScript (ESM)", ".tsx": "TypeScript React" };
|
|
2127
|
+
if (extNames[ext]) parts.push(`${extNames[ext]} source files`);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
if (exportNames.length > 0) {
|
|
2131
|
+
parts.push(`Key exports: ${exportNames.slice(0, 5).join(", ")}`);
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
return parts.join(". ") + ".";
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function generateModules(fileRecords, chunkRecords) {
|
|
2138
|
+
const dirFiles = new Map();
|
|
2139
|
+
const dirChunks = new Map();
|
|
2140
|
+
const fileById = new Map(fileRecords.map(f => [f.id, f]));
|
|
2141
|
+
|
|
2142
|
+
for (const file of fileRecords) {
|
|
2143
|
+
const dir = path.dirname(file.path);
|
|
2144
|
+
if (!dirFiles.has(dir)) dirFiles.set(dir, []);
|
|
2145
|
+
dirFiles.get(dir).push(file);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
for (const chunk of chunkRecords) {
|
|
2149
|
+
if (!chunk.exported || isWindowChunkId(chunk.id)) continue;
|
|
2150
|
+
const file = fileById.get(chunk.file_id);
|
|
2151
|
+
if (!file) continue;
|
|
2152
|
+
const dir = path.dirname(file.path);
|
|
2153
|
+
if (!dirChunks.has(dir)) dirChunks.set(dir, []);
|
|
2154
|
+
dirChunks.get(dir).push(chunk);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
const modules = [];
|
|
2158
|
+
const containsRelations = [];
|
|
2159
|
+
const containsModuleRelations = [];
|
|
2160
|
+
const exportsRelations = [];
|
|
2161
|
+
|
|
2162
|
+
const MIN_MODULE_FILES = 2;
|
|
2163
|
+
|
|
2164
|
+
for (const [dir, files] of dirFiles) {
|
|
2165
|
+
if (files.length < MIN_MODULE_FILES) continue;
|
|
2166
|
+
|
|
2167
|
+
const exports = dirChunks.get(dir) || [];
|
|
2168
|
+
const exportNames = [...new Set(exports.slice(0, 20).map(c => c.name))];
|
|
2169
|
+
const moduleId = `module:${dir}`;
|
|
2170
|
+
|
|
2171
|
+
modules.push({
|
|
2172
|
+
id: moduleId,
|
|
2173
|
+
path: dir,
|
|
2174
|
+
name: path.basename(dir),
|
|
2175
|
+
summary: generateModuleSummary(dir, files, exportNames),
|
|
2176
|
+
file_count: files.length,
|
|
2177
|
+
exported_symbols: exportNames.join(", "),
|
|
2178
|
+
updated_at: files.reduce((latest, f) => f.updated_at > latest ? f.updated_at : latest, ""),
|
|
2179
|
+
source_of_truth: false,
|
|
2180
|
+
trust_level: 75,
|
|
2181
|
+
status: "active"
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
// CONTAINS: Module -> File
|
|
2185
|
+
for (const file of files) {
|
|
2186
|
+
containsRelations.push({ from: moduleId, to: file.id });
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// EXPORTS: Module -> Chunk
|
|
2190
|
+
for (const chunk of exports) {
|
|
2191
|
+
exportsRelations.push({ from: moduleId, to: chunk.id });
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// CONTAINS_MODULE: parent Module -> child Module
|
|
2196
|
+
const moduleDirs = new Set(modules.map(m => m.path));
|
|
2197
|
+
for (const dir of moduleDirs) {
|
|
2198
|
+
const parent = path.dirname(dir);
|
|
2199
|
+
if (parent !== dir && moduleDirs.has(parent)) {
|
|
2200
|
+
containsModuleRelations.push({
|
|
2201
|
+
from: `module:${parent}`,
|
|
2202
|
+
to: `module:${dir}`
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
return { modules, containsRelations, containsModuleRelations, exportsRelations };
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
function isWindowChunkId(chunkId) {
|
|
2211
|
+
return typeof chunkId === "string" && chunkId.includes(":window:");
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
function splitChunkIntoWindows(chunkRecord, options) {
|
|
2215
|
+
const { windowLines, overlapLines, splitMinLines, maxWindows, chunkBody } = options;
|
|
2216
|
+
const sourceBody = typeof chunkBody === "string" ? chunkBody : chunkRecord.body;
|
|
2217
|
+
const lines = sourceBody.split(/\r?\n/);
|
|
2218
|
+
const totalLines = lines.length;
|
|
2219
|
+
if (totalLines < splitMinLines || totalLines <= windowLines) {
|
|
2220
|
+
return [];
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
const windows = [];
|
|
2224
|
+
const safeOverlap = Math.max(0, Math.min(overlapLines, windowLines - 1));
|
|
2225
|
+
let start = 0;
|
|
2226
|
+
let windowIndex = 1;
|
|
2227
|
+
|
|
2228
|
+
while (start < totalLines && windows.length < maxWindows) {
|
|
2229
|
+
const isLastAllowedWindow = windows.length + 1 >= maxWindows;
|
|
2230
|
+
const end = isLastAllowedWindow ? totalLines : Math.min(totalLines, start + windowLines);
|
|
2231
|
+
const windowStartLine = chunkRecord.start_line + start;
|
|
2232
|
+
const windowEndLine = chunkRecord.start_line + Math.max(0, end - 1);
|
|
2233
|
+
const windowBody = lines.slice(start, end).join("\n");
|
|
2234
|
+
const persistedBody = isLastAllowedWindow ? windowBody : windowBody.slice(0, MAX_BODY_CHARS);
|
|
2235
|
+
windows.push({
|
|
2236
|
+
id: `${chunkRecord.id}:window:${windowIndex}:${windowStartLine}-${windowEndLine}`,
|
|
2237
|
+
file_id: chunkRecord.file_id,
|
|
2238
|
+
name: `${chunkRecord.name}#window${windowIndex}`,
|
|
2239
|
+
kind: chunkRecord.kind,
|
|
2240
|
+
signature: `${chunkRecord.signature} [window ${windowIndex}]`,
|
|
2241
|
+
body: persistedBody,
|
|
2242
|
+
description: chunkRecord.description || "",
|
|
2243
|
+
start_line: windowStartLine,
|
|
2244
|
+
end_line: windowEndLine,
|
|
2245
|
+
language: chunkRecord.language,
|
|
2246
|
+
exported: chunkRecord.exported || false,
|
|
2247
|
+
checksum: checksum(Buffer.from(windowBody)),
|
|
2248
|
+
updated_at: chunkRecord.updated_at,
|
|
2249
|
+
trust_level: chunkRecord.trust_level,
|
|
2250
|
+
status: chunkRecord.status,
|
|
2251
|
+
source_of_truth: chunkRecord.source_of_truth
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2254
|
+
if (end >= totalLines) {
|
|
2255
|
+
break;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
start = end - safeOverlap;
|
|
2259
|
+
windowIndex += 1;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
return windows;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
562
2265
|
function main() {
|
|
563
2266
|
const { mode, verbose } = parseArgs(process.argv);
|
|
564
2267
|
const configPath = path.join(CONTEXT_DIR, "config.yaml");
|
|
@@ -582,6 +2285,25 @@ function main() {
|
|
|
582
2285
|
|
|
583
2286
|
const rules = parseRules(fs.readFileSync(rulesPath, "utf8"));
|
|
584
2287
|
const { candidates, incrementalMode, deletedRelPaths } = collectCandidateFiles(sourcePaths, mode);
|
|
2288
|
+
const chunkWindowLines = parsePositiveIntegerEnv(
|
|
2289
|
+
"CORTEX_CHUNK_WINDOW_LINES",
|
|
2290
|
+
DEFAULT_CHUNK_WINDOW_LINES
|
|
2291
|
+
);
|
|
2292
|
+
const chunkOverlapLines = Math.max(
|
|
2293
|
+
0,
|
|
2294
|
+
Math.min(
|
|
2295
|
+
chunkWindowLines - 1,
|
|
2296
|
+
parseNonNegativeIntegerEnv("CORTEX_CHUNK_OVERLAP_LINES", DEFAULT_CHUNK_OVERLAP_LINES)
|
|
2297
|
+
)
|
|
2298
|
+
);
|
|
2299
|
+
const chunkSplitMinLines = Math.max(
|
|
2300
|
+
chunkWindowLines + 1,
|
|
2301
|
+
parsePositiveIntegerEnv("CORTEX_CHUNK_SPLIT_MIN_LINES", DEFAULT_CHUNK_SPLIT_MIN_LINES)
|
|
2302
|
+
);
|
|
2303
|
+
const chunkMaxWindows = parsePositiveIntegerEnv(
|
|
2304
|
+
"CORTEX_CHUNK_MAX_WINDOWS",
|
|
2305
|
+
DEFAULT_CHUNK_MAX_WINDOWS
|
|
2306
|
+
);
|
|
585
2307
|
|
|
586
2308
|
const fileRecordMap = new Map();
|
|
587
2309
|
const adrRecordMap = new Map();
|
|
@@ -715,23 +2437,70 @@ function main() {
|
|
|
715
2437
|
|
|
716
2438
|
const fileRecords = [...fileRecordMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
717
2439
|
const adrRecords = [...adrRecordMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
2440
|
+
const indexedFileIds = new Set(fileRecords.map((record) => record.id));
|
|
2441
|
+
const changedFileIds = new Set(
|
|
2442
|
+
[...candidates].map((absolutePath) => `file:${toPosixPath(path.relative(REPO_ROOT, absolutePath))}`)
|
|
2443
|
+
);
|
|
2444
|
+
|
|
2445
|
+
const {
|
|
2446
|
+
chunkRecordMap,
|
|
2447
|
+
definesRelationMap,
|
|
2448
|
+
callsRelationMap,
|
|
2449
|
+
importsRelationMap,
|
|
2450
|
+
callsSqlRelationMap
|
|
2451
|
+
} = incrementalMode
|
|
2452
|
+
? hydrateIncrementalChunkState(fileRecords)
|
|
2453
|
+
: {
|
|
2454
|
+
chunkRecordMap: new Map(),
|
|
2455
|
+
definesRelationMap: new Map(),
|
|
2456
|
+
callsRelationMap: new Map(),
|
|
2457
|
+
importsRelationMap: new Map(),
|
|
2458
|
+
callsSqlRelationMap: new Map()
|
|
2459
|
+
};
|
|
718
2460
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
|
|
2461
|
+
const cachedChunkFileIds = new Set(
|
|
2462
|
+
[...chunkRecordMap.values()].map((record) => String(record.file_id ?? "")).filter(Boolean)
|
|
2463
|
+
);
|
|
2464
|
+
const cachedSqlReferenceFileIds = new Set(
|
|
2465
|
+
[...callsSqlRelationMap.values()].map((record) => String(record.from ?? "")).filter(Boolean)
|
|
2466
|
+
);
|
|
2467
|
+
const usesConfigKeyRelationMap = new Map();
|
|
2468
|
+
const usesResourceKeyRelationMap = new Map();
|
|
2469
|
+
const usesSettingKeyRelationMap = new Map();
|
|
2470
|
+
|
|
2471
|
+
// Extract chunks from changed or uncached code files
|
|
2472
|
+
let windowedChunkCount = 0;
|
|
2473
|
+
const sqlChunkIdsByAlias = new Map();
|
|
2474
|
+
const configChunkIdsByAlias = new Map();
|
|
2475
|
+
const resourceChunkIdsByAlias = new Map();
|
|
2476
|
+
const settingChunkIdsByAlias = new Map();
|
|
2477
|
+
const deferredSqlCallEdges = [];
|
|
724
2478
|
|
|
725
2479
|
for (const fileRecord of fileRecords) {
|
|
726
|
-
if (fileRecord.kind !== "CODE") continue;
|
|
727
|
-
|
|
728
2480
|
const ext = path.extname(fileRecord.path).toLowerCase();
|
|
729
|
-
const
|
|
730
|
-
|
|
2481
|
+
const parser = getChunkParserForExtension(ext);
|
|
2482
|
+
const isStructuredNonCodeChunk = STRUCTURED_NON_CODE_CHUNK_EXTENSIONS.has(ext);
|
|
2483
|
+
if (fileRecord.kind !== "CODE" && !isStructuredNonCodeChunk) continue;
|
|
2484
|
+
if (!parser) continue;
|
|
2485
|
+
if (typeof parser.isAvailable === "function" && !parser.isAvailable()) continue;
|
|
2486
|
+
|
|
2487
|
+
const shouldParseFile =
|
|
2488
|
+
!incrementalMode || changedFileIds.has(fileRecord.id) || !cachedChunkFileIds.has(fileRecord.id);
|
|
2489
|
+
if (!shouldParseFile) {
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
removeChunkStateForFile(
|
|
2494
|
+
fileRecord.id,
|
|
2495
|
+
chunkRecordMap,
|
|
2496
|
+
definesRelationMap,
|
|
2497
|
+
callsRelationMap,
|
|
2498
|
+
importsRelationMap,
|
|
2499
|
+
callsSqlRelationMap
|
|
2500
|
+
);
|
|
731
2501
|
|
|
732
2502
|
try {
|
|
733
|
-
const
|
|
734
|
-
const parseResult = parseCode(fileRecord.content, fileRecord.path, language);
|
|
2503
|
+
const parseResult = parser.parse(fileRecord.content, fileRecord.path, parser.language);
|
|
735
2504
|
|
|
736
2505
|
if (parseResult.errors.length > 0 && verbose) {
|
|
737
2506
|
console.log(`[ingest] parse errors in ${fileRecord.path}:`, parseResult.errors[0].message);
|
|
@@ -747,6 +2516,51 @@ function main() {
|
|
|
747
2516
|
chunkIdsByName.set(chunk.name, []);
|
|
748
2517
|
}
|
|
749
2518
|
chunkIdsByName.get(chunk.name).push(chunkId);
|
|
2519
|
+
if (parser.language === "sql") {
|
|
2520
|
+
for (const alias of sqlChunkAliases(chunk.name)) {
|
|
2521
|
+
if (!sqlChunkIdsByAlias.has(alias)) {
|
|
2522
|
+
sqlChunkIdsByAlias.set(alias, []);
|
|
2523
|
+
}
|
|
2524
|
+
sqlChunkIdsByAlias.get(alias).push(chunkId);
|
|
2525
|
+
}
|
|
2526
|
+
deferredSqlCallEdges.push({
|
|
2527
|
+
chunkId,
|
|
2528
|
+
calls: Array.isArray(chunk.calls) ? chunk.calls : []
|
|
2529
|
+
});
|
|
2530
|
+
} else if (parser.language === "config") {
|
|
2531
|
+
for (const alias of configChunkAliases(chunk)) {
|
|
2532
|
+
if (!configChunkIdsByAlias.has(alias)) {
|
|
2533
|
+
configChunkIdsByAlias.set(alias, []);
|
|
2534
|
+
}
|
|
2535
|
+
configChunkIdsByAlias.get(alias).push(chunkId);
|
|
2536
|
+
}
|
|
2537
|
+
deferredSqlCallEdges.push({
|
|
2538
|
+
chunkId,
|
|
2539
|
+
calls: Array.isArray(chunk.calls) ? chunk.calls : []
|
|
2540
|
+
});
|
|
2541
|
+
} else if (parser.language === "resource") {
|
|
2542
|
+
for (const alias of namedEntryChunkAliases(chunk)) {
|
|
2543
|
+
if (!resourceChunkIdsByAlias.has(alias)) {
|
|
2544
|
+
resourceChunkIdsByAlias.set(alias, []);
|
|
2545
|
+
}
|
|
2546
|
+
resourceChunkIdsByAlias.get(alias).push(chunkId);
|
|
2547
|
+
}
|
|
2548
|
+
deferredSqlCallEdges.push({
|
|
2549
|
+
chunkId,
|
|
2550
|
+
calls: Array.isArray(chunk.calls) ? chunk.calls : []
|
|
2551
|
+
});
|
|
2552
|
+
} else if (parser.language === "settings") {
|
|
2553
|
+
for (const alias of namedEntryChunkAliases(chunk)) {
|
|
2554
|
+
if (!settingChunkIdsByAlias.has(alias)) {
|
|
2555
|
+
settingChunkIdsByAlias.set(alias, []);
|
|
2556
|
+
}
|
|
2557
|
+
settingChunkIdsByAlias.get(alias).push(chunkId);
|
|
2558
|
+
}
|
|
2559
|
+
deferredSqlCallEdges.push({
|
|
2560
|
+
chunkId,
|
|
2561
|
+
calls: Array.isArray(chunk.calls) ? chunk.calls : []
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
750
2564
|
|
|
751
2565
|
const chunkRecord = {
|
|
752
2566
|
id: chunkId,
|
|
@@ -754,35 +2568,59 @@ function main() {
|
|
|
754
2568
|
name: chunk.name,
|
|
755
2569
|
kind: chunk.kind,
|
|
756
2570
|
signature: chunk.signature,
|
|
757
|
-
body: chunk.body.slice(0,
|
|
2571
|
+
body: chunk.body.slice(0, MAX_BODY_CHARS), // Limit chunk body size
|
|
2572
|
+
description: generateChunkDescription(chunk),
|
|
758
2573
|
start_line: chunk.startLine,
|
|
759
2574
|
end_line: chunk.endLine,
|
|
760
2575
|
language: chunk.language,
|
|
2576
|
+
exported: Boolean(chunk.exported),
|
|
761
2577
|
checksum: checksum(Buffer.from(chunk.body)),
|
|
762
2578
|
updated_at: fileRecord.updated_at,
|
|
763
|
-
trust_level: fileRecord.trust_level
|
|
2579
|
+
trust_level: fileRecord.trust_level,
|
|
2580
|
+
status:
|
|
2581
|
+
typeof fileRecord.status === "string" && fileRecord.status.trim().length > 0
|
|
2582
|
+
? fileRecord.status
|
|
2583
|
+
: "active",
|
|
2584
|
+
source_of_truth: Boolean(fileRecord.source_of_truth)
|
|
764
2585
|
};
|
|
765
|
-
|
|
2586
|
+
chunkRecordMap.set(chunkId, chunkRecord);
|
|
766
2587
|
|
|
767
2588
|
// DEFINES relation: File -> Chunk
|
|
768
|
-
|
|
2589
|
+
definesRelationMap.set(relationKey(fileRecord.id, chunkId), {
|
|
769
2590
|
from: fileRecord.id,
|
|
770
2591
|
to: chunkId
|
|
771
2592
|
});
|
|
772
2593
|
|
|
2594
|
+
const windows = splitChunkIntoWindows(chunkRecord, {
|
|
2595
|
+
windowLines: chunkWindowLines,
|
|
2596
|
+
overlapLines: chunkOverlapLines,
|
|
2597
|
+
splitMinLines: chunkSplitMinLines,
|
|
2598
|
+
maxWindows: chunkMaxWindows,
|
|
2599
|
+
chunkBody: chunk.body
|
|
2600
|
+
});
|
|
2601
|
+
if (windows.length > 0) {
|
|
2602
|
+
windowedChunkCount += windows.length;
|
|
2603
|
+
for (const windowChunk of windows) {
|
|
2604
|
+
chunkRecordMap.set(windowChunk.id, windowChunk);
|
|
2605
|
+
definesRelationMap.set(relationKey(fileRecord.id, windowChunk.id), {
|
|
2606
|
+
from: fileRecord.id,
|
|
2607
|
+
to: windowChunk.id
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
|
|
773
2612
|
// IMPORTS relations: Chunk -> File
|
|
774
2613
|
for (const importPath of chunk.imports || []) {
|
|
775
|
-
|
|
776
|
-
if (
|
|
777
|
-
|
|
778
|
-
const resolvedImport = path.posix.normalize(path.posix.join(dirName, importPath));
|
|
779
|
-
const targetFileId = `file:${resolvedImport}`;
|
|
780
|
-
importsRelations.push({
|
|
781
|
-
from: chunkId,
|
|
782
|
-
to: targetFileId,
|
|
783
|
-
import_name: importPath
|
|
784
|
-
});
|
|
2614
|
+
const targetFileId = resolveRelativeImportTargetId(fileRecord.path, importPath, indexedFileIds);
|
|
2615
|
+
if (!targetFileId) {
|
|
2616
|
+
continue;
|
|
785
2617
|
}
|
|
2618
|
+
|
|
2619
|
+
importsRelationMap.set(relationKey(chunkId, targetFileId, importPath), {
|
|
2620
|
+
from: chunkId,
|
|
2621
|
+
to: targetFileId,
|
|
2622
|
+
import_name: importPath
|
|
2623
|
+
});
|
|
786
2624
|
}
|
|
787
2625
|
}
|
|
788
2626
|
|
|
@@ -797,7 +2635,7 @@ function main() {
|
|
|
797
2635
|
continue;
|
|
798
2636
|
}
|
|
799
2637
|
seenCallEdges.add(callKey);
|
|
800
|
-
|
|
2638
|
+
callsRelationMap.set(relationKey(chunkId, targetChunkId, "direct"), {
|
|
801
2639
|
from: chunkId,
|
|
802
2640
|
to: targetChunkId,
|
|
803
2641
|
call_type: "direct"
|
|
@@ -812,13 +2650,194 @@ function main() {
|
|
|
812
2650
|
}
|
|
813
2651
|
}
|
|
814
2652
|
|
|
2653
|
+
const chunkRecords = [...chunkRecordMap.values()].sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
2654
|
+
|
|
815
2655
|
// Filter CALLS relations to only valid targets (chunks that actually exist)
|
|
816
2656
|
const chunkIdSet = new Set(chunkRecords.map(c => c.id));
|
|
817
|
-
const
|
|
2657
|
+
const validDefinesRelations = [...definesRelationMap.values()].filter(
|
|
2658
|
+
(rel) => indexedFileIds.has(rel.from) && chunkIdSet.has(rel.to)
|
|
2659
|
+
);
|
|
2660
|
+
const totalCallsRelations = callsRelationMap.size;
|
|
2661
|
+
for (const edge of deferredSqlCallEdges) {
|
|
2662
|
+
for (const calledName of edge.calls) {
|
|
2663
|
+
for (const alias of sqlChunkAliases(calledName)) {
|
|
2664
|
+
const targetChunkIds = sqlChunkIdsByAlias.get(alias) || [];
|
|
2665
|
+
for (const targetChunkId of targetChunkIds) {
|
|
2666
|
+
if (targetChunkId === edge.chunkId) {
|
|
2667
|
+
continue;
|
|
2668
|
+
}
|
|
2669
|
+
callsRelationMap.set(relationKey(edge.chunkId, targetChunkId, "sql_reference"), {
|
|
2670
|
+
from: edge.chunkId,
|
|
2671
|
+
to: targetChunkId,
|
|
2672
|
+
call_type: "sql_reference"
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
const validCallsRelations = [...callsRelationMap.values()].filter(
|
|
2679
|
+
(rel) => chunkIdSet.has(rel.from) && chunkIdSet.has(rel.to)
|
|
2680
|
+
);
|
|
2681
|
+
const validImportsRelations = [...importsRelationMap.values()].filter(
|
|
2682
|
+
(rel) => chunkIdSet.has(rel.from) && indexedFileIds.has(rel.to)
|
|
2683
|
+
);
|
|
2684
|
+
const sqlDefinitionsChanged =
|
|
2685
|
+
incrementalMode &&
|
|
2686
|
+
fileRecords.some(
|
|
2687
|
+
(fileRecord) =>
|
|
2688
|
+
changedFileIds.has(fileRecord.id) && path.extname(fileRecord.path).toLowerCase() === ".sql"
|
|
2689
|
+
);
|
|
2690
|
+
const sqlResourceReferenceMap = buildSqlResourceReferenceMap(fileRecords);
|
|
2691
|
+
for (const fileRecord of fileRecords) {
|
|
2692
|
+
if (!shouldExtractSqlReferences(fileRecord.path)) {
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
const shouldAnalyzeFile =
|
|
2697
|
+
!incrementalMode ||
|
|
2698
|
+
sqlDefinitionsChanged ||
|
|
2699
|
+
changedFileIds.has(fileRecord.id) ||
|
|
2700
|
+
!cachedSqlReferenceFileIds.has(fileRecord.id);
|
|
2701
|
+
if (!shouldAnalyzeFile) {
|
|
2702
|
+
continue;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
for (const [key, relation] of callsSqlRelationMap.entries()) {
|
|
2706
|
+
if (relation.from === fileRecord.id) {
|
|
2707
|
+
callsSqlRelationMap.delete(key);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
for (const refName of extractSqlObjectReferencesFromContent(
|
|
2712
|
+
fileRecord.content,
|
|
2713
|
+
fileRecord.path,
|
|
2714
|
+
sqlResourceReferenceMap
|
|
2715
|
+
)) {
|
|
2716
|
+
for (const alias of sqlChunkAliases(refName)) {
|
|
2717
|
+
const targetChunkIds = sqlChunkIdsByAlias.get(alias) || [];
|
|
2718
|
+
for (const targetChunkId of targetChunkIds) {
|
|
2719
|
+
callsSqlRelationMap.set(relationKey(fileRecord.id, targetChunkId, refName), {
|
|
2720
|
+
from: fileRecord.id,
|
|
2721
|
+
to: targetChunkId,
|
|
2722
|
+
note: refName
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
const validCallsSqlRelations = [...callsSqlRelationMap.values()].filter(
|
|
2729
|
+
(rel) => indexedFileIds.has(rel.from) && chunkIdSet.has(rel.to)
|
|
2730
|
+
);
|
|
2731
|
+
for (const fileRecord of fileRecords) {
|
|
2732
|
+
if (!shouldExtractNamedResourceReferences(fileRecord.path)) {
|
|
2733
|
+
continue;
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
for (const key of extractSqlResourceKeyReferences(fileRecord.content)) {
|
|
2737
|
+
for (const targetChunkId of resourceChunkIdsByAlias.get(key) ?? []) {
|
|
2738
|
+
usesResourceKeyRelationMap.set(relationKey(fileRecord.id, targetChunkId, key), {
|
|
2739
|
+
from: fileRecord.id,
|
|
2740
|
+
to: targetChunkId,
|
|
2741
|
+
note: key
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
for (const targetChunkId of settingChunkIdsByAlias.get(key) ?? []) {
|
|
2745
|
+
usesSettingKeyRelationMap.set(relationKey(fileRecord.id, targetChunkId, key), {
|
|
2746
|
+
from: fileRecord.id,
|
|
2747
|
+
to: targetChunkId,
|
|
2748
|
+
note: key
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
for (const key of extractConfigKeyReferences(fileRecord.content)) {
|
|
2754
|
+
for (const targetChunkId of configChunkIdsByAlias.get(key) ?? []) {
|
|
2755
|
+
usesConfigKeyRelationMap.set(relationKey(fileRecord.id, targetChunkId, key), {
|
|
2756
|
+
from: fileRecord.id,
|
|
2757
|
+
to: targetChunkId,
|
|
2758
|
+
note: key
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
for (const relation of generateConfigTransformKeyRelations(fileRecords, chunkRecords)) {
|
|
2764
|
+
usesConfigKeyRelationMap.set(relationKey(relation.from, relation.to, relation.note), relation);
|
|
2765
|
+
}
|
|
2766
|
+
const validUsesConfigKeyRelations = [...usesConfigKeyRelationMap.values()].filter(
|
|
2767
|
+
(rel) => indexedFileIds.has(rel.from) && chunkIdSet.has(rel.to)
|
|
2768
|
+
);
|
|
2769
|
+
const validUsesResourceKeyRelations = [...usesResourceKeyRelationMap.values()].filter(
|
|
2770
|
+
(rel) => indexedFileIds.has(rel.from) && chunkIdSet.has(rel.to)
|
|
2771
|
+
);
|
|
2772
|
+
const validUsesSettingKeyRelations = [...usesSettingKeyRelationMap.values()].filter(
|
|
2773
|
+
(rel) => indexedFileIds.has(rel.from) && chunkIdSet.has(rel.to)
|
|
2774
|
+
);
|
|
818
2775
|
|
|
819
2776
|
if (verbose && chunkRecords.length > 0) {
|
|
820
2777
|
console.log(`[ingest] extracted ${chunkRecords.length} chunks from ${fileRecords.filter(f => f.kind === "CODE").length} code files`);
|
|
821
|
-
|
|
2778
|
+
if (windowedChunkCount > 0) {
|
|
2779
|
+
console.log(
|
|
2780
|
+
`[ingest] overlap windows added=${windowedChunkCount} (window_lines=${chunkWindowLines}, overlap_lines=${chunkOverlapLines}, max_windows=${chunkMaxWindows})`
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
console.log(`[ingest] ${validCallsRelations.length} call relations (${totalCallsRelations - validCallsRelations.length} filtered)`);
|
|
2784
|
+
if (validCallsSqlRelations.length > 0) {
|
|
2785
|
+
console.log(`[ingest] sql call links=${validCallsSqlRelations.length}`);
|
|
2786
|
+
}
|
|
2787
|
+
if (validUsesConfigKeyRelations.length > 0) {
|
|
2788
|
+
console.log(`[ingest] uses_config_key=${validUsesConfigKeyRelations.length}`);
|
|
2789
|
+
}
|
|
2790
|
+
if (validUsesResourceKeyRelations.length > 0 || validUsesSettingKeyRelations.length > 0) {
|
|
2791
|
+
console.log(
|
|
2792
|
+
`[ingest] uses_resource_key=${validUsesResourceKeyRelations.length} uses_setting_key=${validUsesSettingKeyRelations.length}`
|
|
2793
|
+
);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
// Generate Module entities and relations
|
|
2798
|
+
const moduleResult = generateModules(fileRecords, chunkRecords);
|
|
2799
|
+
const moduleRecords = moduleResult.modules;
|
|
2800
|
+
const moduleContainsRelations = moduleResult.containsRelations;
|
|
2801
|
+
const moduleContainsModuleRelations = moduleResult.containsModuleRelations;
|
|
2802
|
+
const moduleExportsRelations = moduleResult.exportsRelations;
|
|
2803
|
+
const projectResult = generateProjects(fileRecords);
|
|
2804
|
+
const projectRecords = projectResult.projects;
|
|
2805
|
+
const projectIncludesFileRelations = projectResult.includesFileRelations;
|
|
2806
|
+
const projectReferencesProjectRelations = projectResult.referencesProjectRelations;
|
|
2807
|
+
const namedResourceRelationResult = generateNamedResourceRelations(fileRecords);
|
|
2808
|
+
const usesResourceRelations = namedResourceRelationResult.usesResourceRelations;
|
|
2809
|
+
const usesSettingRelations = namedResourceRelationResult.usesSettingRelations;
|
|
2810
|
+
const configIncludeRelations = generateConfigIncludeRelations(fileRecords);
|
|
2811
|
+
const machineConfigRelations = generateMachineConfigRelations(fileRecords);
|
|
2812
|
+
const sectionHandlerRelations = generateSectionHandlerRelations(fileRecords);
|
|
2813
|
+
const usesConfigRelations = uniqueRelations([
|
|
2814
|
+
...namedResourceRelationResult.usesConfigRelations,
|
|
2815
|
+
...configIncludeRelations,
|
|
2816
|
+
...machineConfigRelations,
|
|
2817
|
+
...sectionHandlerRelations
|
|
2818
|
+
]);
|
|
2819
|
+
const configTransformRelations = generateConfigTransformRelations(fileRecords);
|
|
2820
|
+
|
|
2821
|
+
if (verbose && moduleRecords.length > 0) {
|
|
2822
|
+
console.log(`[ingest] modules=${moduleRecords.length} contains=${moduleContainsRelations.length} contains_module=${moduleContainsModuleRelations.length} exports=${moduleExportsRelations.length}`);
|
|
2823
|
+
}
|
|
2824
|
+
if (verbose && projectRecords.length > 0) {
|
|
2825
|
+
console.log(
|
|
2826
|
+
`[ingest] projects=${projectRecords.length} includes_file=${projectIncludesFileRelations.length} references_project=${projectReferencesProjectRelations.length}`
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
if (
|
|
2830
|
+
verbose &&
|
|
2831
|
+
(
|
|
2832
|
+
usesResourceRelations.length > 0 ||
|
|
2833
|
+
usesSettingRelations.length > 0 ||
|
|
2834
|
+
usesConfigRelations.length > 0 ||
|
|
2835
|
+
configTransformRelations.length > 0
|
|
2836
|
+
)
|
|
2837
|
+
) {
|
|
2838
|
+
console.log(
|
|
2839
|
+
`[ingest] uses_resource=${usesResourceRelations.length} uses_setting=${usesSettingRelations.length} uses_config=${usesConfigRelations.length} transforms_config=${configTransformRelations.length}`
|
|
2840
|
+
);
|
|
822
2841
|
}
|
|
823
2842
|
|
|
824
2843
|
const ruleRecords = rules.map((rule) => ({
|
|
@@ -920,9 +2939,27 @@ function main() {
|
|
|
920
2939
|
writeJsonl(path.join(CACHE_DIR, "relations.supersedes.jsonl"), supersedesRelations);
|
|
921
2940
|
writeJsonl(path.join(CACHE_DIR, "relations.constrains.jsonl"), constrainsRelations);
|
|
922
2941
|
writeJsonl(path.join(CACHE_DIR, "relations.implements.jsonl"), implementsRelations);
|
|
923
|
-
writeJsonl(path.join(CACHE_DIR, "relations.defines.jsonl"),
|
|
2942
|
+
writeJsonl(path.join(CACHE_DIR, "relations.defines.jsonl"), validDefinesRelations);
|
|
924
2943
|
writeJsonl(path.join(CACHE_DIR, "relations.calls.jsonl"), validCallsRelations);
|
|
925
|
-
writeJsonl(path.join(CACHE_DIR, "relations.imports.jsonl"),
|
|
2944
|
+
writeJsonl(path.join(CACHE_DIR, "relations.imports.jsonl"), validImportsRelations);
|
|
2945
|
+
writeJsonl(path.join(CACHE_DIR, "relations.calls_sql.jsonl"), validCallsSqlRelations);
|
|
2946
|
+
writeJsonl(path.join(CACHE_DIR, "relations.uses_config_key.jsonl"), validUsesConfigKeyRelations);
|
|
2947
|
+
writeJsonl(path.join(CACHE_DIR, "relations.uses_resource_key.jsonl"), validUsesResourceKeyRelations);
|
|
2948
|
+
writeJsonl(path.join(CACHE_DIR, "relations.uses_setting_key.jsonl"), validUsesSettingKeyRelations);
|
|
2949
|
+
writeJsonl(path.join(CACHE_DIR, "entities.module.jsonl"), moduleRecords);
|
|
2950
|
+
writeJsonl(path.join(CACHE_DIR, "relations.contains.jsonl"), moduleContainsRelations);
|
|
2951
|
+
writeJsonl(path.join(CACHE_DIR, "relations.contains_module.jsonl"), moduleContainsModuleRelations);
|
|
2952
|
+
writeJsonl(path.join(CACHE_DIR, "relations.exports.jsonl"), moduleExportsRelations);
|
|
2953
|
+
writeJsonl(path.join(CACHE_DIR, "entities.project.jsonl"), projectRecords);
|
|
2954
|
+
writeJsonl(path.join(CACHE_DIR, "relations.includes_file.jsonl"), projectIncludesFileRelations);
|
|
2955
|
+
writeJsonl(path.join(CACHE_DIR, "relations.uses_resource.jsonl"), usesResourceRelations);
|
|
2956
|
+
writeJsonl(path.join(CACHE_DIR, "relations.uses_setting.jsonl"), usesSettingRelations);
|
|
2957
|
+
writeJsonl(path.join(CACHE_DIR, "relations.uses_config.jsonl"), usesConfigRelations);
|
|
2958
|
+
writeJsonl(path.join(CACHE_DIR, "relations.transforms_config.jsonl"), configTransformRelations);
|
|
2959
|
+
writeJsonl(
|
|
2960
|
+
path.join(CACHE_DIR, "relations.references_project.jsonl"),
|
|
2961
|
+
projectReferencesProjectRelations
|
|
2962
|
+
);
|
|
926
2963
|
|
|
927
2964
|
writeTsv(
|
|
928
2965
|
path.join(DB_IMPORT_DIR, "file_nodes.tsv"),
|
|
@@ -1055,7 +3092,7 @@ function main() {
|
|
|
1055
3092
|
writeTsv(
|
|
1056
3093
|
path.join(DB_IMPORT_DIR, "defines_rel.tsv"),
|
|
1057
3094
|
["from", "to"],
|
|
1058
|
-
|
|
3095
|
+
validDefinesRelations.map((record) => [record.from, record.to])
|
|
1059
3096
|
);
|
|
1060
3097
|
|
|
1061
3098
|
writeTsv(
|
|
@@ -1067,7 +3104,99 @@ function main() {
|
|
|
1067
3104
|
writeTsv(
|
|
1068
3105
|
path.join(DB_IMPORT_DIR, "imports_rel.tsv"),
|
|
1069
3106
|
["from", "to", "import_name"],
|
|
1070
|
-
|
|
3107
|
+
validImportsRelations.map((record) => [record.from, record.to, record.import_name])
|
|
3108
|
+
);
|
|
3109
|
+
|
|
3110
|
+
writeTsv(
|
|
3111
|
+
path.join(DB_IMPORT_DIR, "calls_sql_rel.tsv"),
|
|
3112
|
+
["from", "to", "note"],
|
|
3113
|
+
validCallsSqlRelations.map((record) => [record.from, record.to, record.note])
|
|
3114
|
+
);
|
|
3115
|
+
|
|
3116
|
+
writeTsv(
|
|
3117
|
+
path.join(DB_IMPORT_DIR, "uses_config_key_rel.tsv"),
|
|
3118
|
+
["from", "to", "note"],
|
|
3119
|
+
validUsesConfigKeyRelations.map((record) => [record.from, record.to, record.note])
|
|
3120
|
+
);
|
|
3121
|
+
|
|
3122
|
+
writeTsv(
|
|
3123
|
+
path.join(DB_IMPORT_DIR, "uses_resource_key_rel.tsv"),
|
|
3124
|
+
["from", "to", "note"],
|
|
3125
|
+
validUsesResourceKeyRelations.map((record) => [record.from, record.to, record.note])
|
|
3126
|
+
);
|
|
3127
|
+
|
|
3128
|
+
writeTsv(
|
|
3129
|
+
path.join(DB_IMPORT_DIR, "uses_setting_key_rel.tsv"),
|
|
3130
|
+
["from", "to", "note"],
|
|
3131
|
+
validUsesSettingKeyRelations.map((record) => [record.from, record.to, record.note])
|
|
3132
|
+
);
|
|
3133
|
+
|
|
3134
|
+
writeTsv(
|
|
3135
|
+
path.join(DB_IMPORT_DIR, "project_nodes.tsv"),
|
|
3136
|
+
[
|
|
3137
|
+
"id",
|
|
3138
|
+
"path",
|
|
3139
|
+
"name",
|
|
3140
|
+
"kind",
|
|
3141
|
+
"language",
|
|
3142
|
+
"target_framework",
|
|
3143
|
+
"summary",
|
|
3144
|
+
"file_count",
|
|
3145
|
+
"updated_at",
|
|
3146
|
+
"source_of_truth",
|
|
3147
|
+
"trust_level",
|
|
3148
|
+
"status"
|
|
3149
|
+
],
|
|
3150
|
+
projectRecords.map((record) => [
|
|
3151
|
+
record.id,
|
|
3152
|
+
record.path,
|
|
3153
|
+
record.name,
|
|
3154
|
+
record.kind,
|
|
3155
|
+
record.language,
|
|
3156
|
+
record.target_framework,
|
|
3157
|
+
record.summary,
|
|
3158
|
+
record.file_count,
|
|
3159
|
+
record.updated_at,
|
|
3160
|
+
record.source_of_truth,
|
|
3161
|
+
record.trust_level,
|
|
3162
|
+
record.status
|
|
3163
|
+
])
|
|
3164
|
+
);
|
|
3165
|
+
|
|
3166
|
+
writeTsv(
|
|
3167
|
+
path.join(DB_IMPORT_DIR, "includes_file_rel.tsv"),
|
|
3168
|
+
["from", "to"],
|
|
3169
|
+
projectIncludesFileRelations.map((record) => [record.from, record.to])
|
|
3170
|
+
);
|
|
3171
|
+
|
|
3172
|
+
writeTsv(
|
|
3173
|
+
path.join(DB_IMPORT_DIR, "references_project_rel.tsv"),
|
|
3174
|
+
["from", "to", "note"],
|
|
3175
|
+
projectReferencesProjectRelations.map((record) => [record.from, record.to, record.note])
|
|
3176
|
+
);
|
|
3177
|
+
|
|
3178
|
+
writeTsv(
|
|
3179
|
+
path.join(DB_IMPORT_DIR, "uses_resource_rel.tsv"),
|
|
3180
|
+
["from", "to", "note"],
|
|
3181
|
+
usesResourceRelations.map((record) => [record.from, record.to, record.note])
|
|
3182
|
+
);
|
|
3183
|
+
|
|
3184
|
+
writeTsv(
|
|
3185
|
+
path.join(DB_IMPORT_DIR, "uses_setting_rel.tsv"),
|
|
3186
|
+
["from", "to", "note"],
|
|
3187
|
+
usesSettingRelations.map((record) => [record.from, record.to, record.note])
|
|
3188
|
+
);
|
|
3189
|
+
|
|
3190
|
+
writeTsv(
|
|
3191
|
+
path.join(DB_IMPORT_DIR, "uses_config_rel.tsv"),
|
|
3192
|
+
["from", "to", "note"],
|
|
3193
|
+
usesConfigRelations.map((record) => [record.from, record.to, record.note])
|
|
3194
|
+
);
|
|
3195
|
+
|
|
3196
|
+
writeTsv(
|
|
3197
|
+
path.join(DB_IMPORT_DIR, "transforms_config_rel.tsv"),
|
|
3198
|
+
["from", "to", "note"],
|
|
3199
|
+
configTransformRelations.map((record) => [record.from, record.to, record.note])
|
|
1071
3200
|
);
|
|
1072
3201
|
|
|
1073
3202
|
const manifest = {
|
|
@@ -1082,9 +3211,24 @@ function main() {
|
|
|
1082
3211
|
relations_constrains: constrainsRelations.length,
|
|
1083
3212
|
relations_implements: implementsRelations.length,
|
|
1084
3213
|
relations_supersedes: supersedesRelations.length,
|
|
1085
|
-
relations_defines:
|
|
3214
|
+
relations_defines: validDefinesRelations.length,
|
|
1086
3215
|
relations_calls: validCallsRelations.length,
|
|
1087
|
-
relations_imports:
|
|
3216
|
+
relations_imports: validImportsRelations.length,
|
|
3217
|
+
relations_calls_sql: validCallsSqlRelations.length,
|
|
3218
|
+
relations_uses_config_key: validUsesConfigKeyRelations.length,
|
|
3219
|
+
relations_uses_resource_key: validUsesResourceKeyRelations.length,
|
|
3220
|
+
relations_uses_setting_key: validUsesSettingKeyRelations.length,
|
|
3221
|
+
modules: moduleRecords.length,
|
|
3222
|
+
relations_contains: moduleContainsRelations.length,
|
|
3223
|
+
relations_contains_module: moduleContainsModuleRelations.length,
|
|
3224
|
+
relations_exports: moduleExportsRelations.length,
|
|
3225
|
+
projects: projectRecords.length,
|
|
3226
|
+
relations_includes_file: projectIncludesFileRelations.length,
|
|
3227
|
+
relations_references_project: projectReferencesProjectRelations.length,
|
|
3228
|
+
relations_uses_resource: usesResourceRelations.length,
|
|
3229
|
+
relations_uses_setting: usesSettingRelations.length,
|
|
3230
|
+
relations_uses_config: usesConfigRelations.length,
|
|
3231
|
+
relations_transforms_config: configTransformRelations.length
|
|
1088
3232
|
},
|
|
1089
3233
|
skipped,
|
|
1090
3234
|
incremental_mode: incrementalMode,
|
|
@@ -1107,7 +3251,10 @@ function main() {
|
|
|
1107
3251
|
`[ingest] rels constrains=${manifest.counts.relations_constrains} implements=${manifest.counts.relations_implements} supersedes=${manifest.counts.relations_supersedes}`
|
|
1108
3252
|
);
|
|
1109
3253
|
console.log(
|
|
1110
|
-
`[ingest] rels defines=${manifest.counts.relations_defines} calls=${manifest.counts.relations_calls} imports=${manifest.counts.relations_imports}`
|
|
3254
|
+
`[ingest] rels defines=${manifest.counts.relations_defines} calls=${manifest.counts.relations_calls} imports=${manifest.counts.relations_imports} calls_sql=${manifest.counts.relations_calls_sql} uses_config_key=${manifest.counts.relations_uses_config_key} uses_resource_key=${manifest.counts.relations_uses_resource_key} uses_setting_key=${manifest.counts.relations_uses_setting_key}`
|
|
3255
|
+
);
|
|
3256
|
+
console.log(
|
|
3257
|
+
`[ingest] rels contains=${manifest.counts.relations_contains} contains_module=${manifest.counts.relations_contains_module} exports=${manifest.counts.relations_exports} includes_file=${manifest.counts.relations_includes_file} references_project=${manifest.counts.relations_references_project} uses_resource=${manifest.counts.relations_uses_resource} uses_setting=${manifest.counts.relations_uses_setting} uses_config=${manifest.counts.relations_uses_config} transforms_config=${manifest.counts.relations_transforms_config}`
|
|
1111
3258
|
);
|
|
1112
3259
|
console.log(
|
|
1113
3260
|
`[ingest] skipped unsupported=${skipped.unsupported} too_large=${skipped.tooLarge} binary=${skipped.binary}`
|
|
@@ -1115,4 +3262,25 @@ function main() {
|
|
|
1115
3262
|
console.log(`[ingest] wrote cache + db import files under .context/`);
|
|
1116
3263
|
}
|
|
1117
3264
|
|
|
1118
|
-
|
|
3265
|
+
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename);
|
|
3266
|
+
if (isMainModule) {
|
|
3267
|
+
main();
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
export {
|
|
3271
|
+
buildSqlResourceReferenceMap,
|
|
3272
|
+
detectKind,
|
|
3273
|
+
extractSqlObjectReferencesFromContent,
|
|
3274
|
+
generateChunkDescription,
|
|
3275
|
+
generateConfigIncludeRelations,
|
|
3276
|
+
generateConfigTransformKeyRelations,
|
|
3277
|
+
generateMachineConfigRelations,
|
|
3278
|
+
generateConfigTransformRelations,
|
|
3279
|
+
generateModuleSummary,
|
|
3280
|
+
generateModules,
|
|
3281
|
+
generateNamedResourceRelations,
|
|
3282
|
+
generateProjects,
|
|
3283
|
+
generateSectionHandlerRelations,
|
|
3284
|
+
getChunkParserForExtension,
|
|
3285
|
+
resolveRelativeImportTargetId
|
|
3286
|
+
};
|