@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.
Files changed (61) hide show
  1. package/README.md +64 -16
  2. package/bin/cortex.mjs +32 -60
  3. package/package.json +17 -3
  4. package/scaffold/.context/ontology.cypher +47 -0
  5. package/scaffold/.githooks/post-commit +14 -0
  6. package/scaffold/.githooks/post-rewrite +23 -0
  7. package/scaffold/mcp/package-lock.json +19 -23
  8. package/scaffold/mcp/package.json +3 -1
  9. package/scaffold/mcp/src/contextEntities.ts +311 -0
  10. package/scaffold/mcp/src/defaults.ts +6 -0
  11. package/scaffold/mcp/src/embed.ts +163 -37
  12. package/scaffold/mcp/src/frontmatter.ts +39 -0
  13. package/scaffold/mcp/src/graph.ts +330 -109
  14. package/scaffold/mcp/src/graphMetrics.ts +12 -0
  15. package/scaffold/mcp/src/impactPresentation.ts +202 -0
  16. package/scaffold/mcp/src/impactRanking.ts +237 -0
  17. package/scaffold/mcp/src/impactResponse.ts +47 -0
  18. package/scaffold/mcp/src/impactResults.ts +173 -0
  19. package/scaffold/mcp/src/impactSeed.ts +33 -0
  20. package/scaffold/mcp/src/impactTraversal.ts +83 -0
  21. package/scaffold/mcp/src/jsonl.ts +34 -0
  22. package/scaffold/mcp/src/loadGraph.ts +345 -86
  23. package/scaffold/mcp/src/paths.ts +24 -2
  24. package/scaffold/mcp/src/presets.ts +137 -0
  25. package/scaffold/mcp/src/relatedResponse.ts +30 -0
  26. package/scaffold/mcp/src/relatedTraversal.ts +101 -0
  27. package/scaffold/mcp/src/rules.ts +27 -0
  28. package/scaffold/mcp/src/search.ts +191 -355
  29. package/scaffold/mcp/src/searchCore.ts +274 -0
  30. package/scaffold/mcp/src/searchResults.ts +133 -0
  31. package/scaffold/mcp/src/server.ts +95 -3
  32. package/scaffold/mcp/src/types.ts +99 -3
  33. package/scaffold/scripts/context.sh +12 -46
  34. package/scaffold/scripts/dashboard.mjs +797 -0
  35. package/scaffold/scripts/dashboard.sh +13 -0
  36. package/scaffold/scripts/ingest.mjs +2219 -59
  37. package/scaffold/scripts/install-git-hooks.sh +3 -1
  38. package/scaffold/scripts/memory-compile.mjs +232 -0
  39. package/scaffold/scripts/memory-compile.sh +20 -0
  40. package/scaffold/scripts/memory-lint.mjs +375 -0
  41. package/scaffold/scripts/memory-lint.sh +20 -0
  42. package/scaffold/scripts/parsers/config.mjs +178 -0
  43. package/scaffold/scripts/parsers/cpp.mjs +316 -0
  44. package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +374 -0
  45. package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +13 -0
  46. package/scaffold/scripts/parsers/javascript/ast.mjs +61 -0
  47. package/scaffold/scripts/parsers/javascript/calls.mjs +53 -0
  48. package/scaffold/scripts/parsers/javascript/chunks.mjs +388 -0
  49. package/scaffold/scripts/parsers/javascript/imports.mjs +162 -0
  50. package/scaffold/scripts/parsers/javascript/patterns.mjs +82 -0
  51. package/scaffold/scripts/parsers/javascript/scope-analysis.mjs +3 -0
  52. package/scaffold/scripts/parsers/javascript/scope-builder.mjs +305 -0
  53. package/scaffold/scripts/parsers/javascript/scope-resolver.mjs +82 -0
  54. package/scaffold/scripts/parsers/javascript.mjs +27 -350
  55. package/scaffold/scripts/parsers/resources.mjs +166 -0
  56. package/scaffold/scripts/parsers/sql.mjs +137 -0
  57. package/scaffold/scripts/parsers/vbnet.mjs +143 -0
  58. package/scaffold/scripts/status.sh +15 -8
  59. package/scaffold/scripts/capture-note.sh +0 -55
  60. package/scaffold/scripts/plan-state-engine.cjs +0 -310
  61. 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(/&quot;/g, '"')
1643
+ .replace(/&apos;/g, "'")
1644
+ .replace(/&lt;/g, "<")
1645
+ .replace(/&gt;/g, ">")
1646
+ .replace(/&amp;/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
- function readJsonlSafe(filePath) {
521
- if (!fs.existsSync(filePath)) {
522
- return [];
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 fs
526
- .readFileSync(filePath, "utf8")
527
- .split(/\r?\n/)
528
- .map((line) => line.trim())
529
- .filter(Boolean)
530
- .map((line) => {
531
- try {
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
- // Extract chunks from code files
720
- const chunkRecords = [];
721
- const definesRelations = [];
722
- const callsRelations = [];
723
- const importsRelations = [];
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 supportedForChunking = [".js", ".mjs", ".cjs", ".ts"].includes(ext);
730
- if (!supportedForChunking) continue;
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 language = ext === ".ts" ? "typescript" : "javascript";
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, 12000), // Limit chunk body size
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
- chunkRecords.push(chunkRecord);
2578
+ chunkRecordMap.set(chunkId, chunkRecord);
766
2579
 
767
2580
  // DEFINES relation: File -> Chunk
768
- definesRelations.push({
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
- // Normalize relative imports to absolute paths
776
- if (importPath.startsWith(".")) {
777
- const dirName = path.dirname(fileRecord.path);
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
- callsRelations.push({
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 validCallsRelations = callsRelations.filter(rel => chunkIdSet.has(rel.to));
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
- console.log(`[ingest] ${validCallsRelations.length} call relations (${callsRelations.length - validCallsRelations.length} filtered)`);
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"), definesRelations);
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"), importsRelations);
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
- definesRelations.map((record) => [record.from, record.to])
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
- importsRelations.map((record) => [record.from, record.to, record.import_name])
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: definesRelations.length,
3206
+ relations_defines: validDefinesRelations.length,
1086
3207
  relations_calls: validCallsRelations.length,
1087
- relations_imports: importsRelations.length
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
- main();
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
+ };