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