@danielblomma/cortex-mcp 0.4.5 → 1.0.0

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