@danielblomma/cortex-mcp 2.4.1 → 2.4.2

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 (45) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +5 -0
  3. package/bin/cli/arguments.mjs +60 -0
  4. package/bin/cli/context-passthrough.mjs +129 -0
  5. package/bin/cli/daemon.mjs +157 -0
  6. package/bin/cli/enterprise.mjs +516 -0
  7. package/bin/cli/help.mjs +88 -0
  8. package/bin/cli/hooks.mjs +199 -0
  9. package/bin/cli/mcp-command.mjs +87 -0
  10. package/bin/cli/paths.mjs +14 -0
  11. package/bin/cli/process.mjs +49 -0
  12. package/bin/cli/project-commands.mjs +237 -0
  13. package/bin/cli/project-runtime.mjs +34 -0
  14. package/bin/cli/query-command.mjs +18 -0
  15. package/bin/cli/router.mjs +66 -0
  16. package/bin/cli/run-command.mjs +44 -0
  17. package/bin/cli/scaffold-ownership.mjs +936 -0
  18. package/bin/cli/scaffold.mjs +687 -0
  19. package/bin/cli/stage-command.mjs +9 -0
  20. package/bin/cli/telemetry-command.mjs +18 -0
  21. package/bin/cli/trusted-runtime.mjs +18 -0
  22. package/bin/cortex.mjs +6 -1834
  23. package/mcp-registry-submission.json +1 -1
  24. package/package.json +3 -2
  25. package/scaffold/ownership/baseline-v2.4.1.json +22 -0
  26. package/scaffold/ownership/current.json +4 -0
  27. package/scaffold/ownership/v1.json +456 -0
  28. package/scaffold/scripts/ingest-parsers.mjs +1 -387
  29. package/scaffold/scripts/ingest-worker.mjs +4 -1
  30. package/scaffold/scripts/ingest.mjs +5 -3899
  31. package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
  32. package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
  33. package/scaffold/scripts/lib/ingest/config.mjs +120 -0
  34. package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
  35. package/scaffold/scripts/lib/ingest/files.mjs +387 -0
  36. package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
  37. package/scaffold/scripts/lib/ingest/io.mjs +78 -0
  38. package/scaffold/scripts/lib/ingest/main.mjs +76 -0
  39. package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
  40. package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
  41. package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
  42. package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
  43. package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
  44. package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
  45. package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
@@ -0,0 +1,1625 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ collectParseEligibleFiles,
5
+ createCSharpBatchCache,
6
+ createWorkerTasks,
7
+ inspectCSharpParser
8
+ } from "./parser-composition.mjs";
9
+ import {
10
+ createIngestMemoryTrace,
11
+ parseArgs,
12
+ parseNonNegativeIntegerEnv,
13
+ parsePositiveIntegerEnv
14
+ } from "./arguments.mjs";
15
+ import {
16
+ DEFAULT_CHUNK_MAX_WINDOWS,
17
+ DEFAULT_CHUNK_OVERLAP_LINES,
18
+ DEFAULT_CHUNK_SPLIT_MIN_LINES,
19
+ DEFAULT_CHUNK_WINDOW_LINES,
20
+ MAX_BODY_CHARS,
21
+ MAX_CONTENT_CHARS,
22
+ MAX_FILE_BYTES
23
+ } from "./constants.mjs";
24
+ import {
25
+ collectRuleKeywordMatch,
26
+ fileTokenSet,
27
+ normalizeRuleTokens,
28
+ parseRules,
29
+ parseSourcePaths
30
+ } from "./config.mjs";
31
+ import {
32
+ chunkIdFor,
33
+ generateChunkDescription,
34
+ generateModules,
35
+ isWindowChunkId,
36
+ splitChunkIntoWindows
37
+ } from "./chunks.mjs";
38
+ import {
39
+ adrTokens,
40
+ checksum,
41
+ collectCandidateFiles,
42
+ detectKind,
43
+ ensureDirectory,
44
+ extractTitle,
45
+ findSupersedesReferences,
46
+ hasSourcePrefix,
47
+ isBinaryBuffer,
48
+ isTextFile,
49
+ normalizeToken,
50
+ normalizeWhitespace,
51
+ parseDecisionDate,
52
+ resolveRelativeImportTargetId,
53
+ toPosixPath,
54
+ trustLevelForKind
55
+ } from "./files.mjs";
56
+ import {
57
+ countFileContentRecords,
58
+ mapRows,
59
+ readJsonlSafe,
60
+ stageJsonl,
61
+ writeJsonl,
62
+ writeTsv
63
+ } from "./io.mjs";
64
+ import {
65
+ hydrateIncrementalChunkState,
66
+ removeChunkStateForFile
67
+ } from "./incremental-state.mjs";
68
+ import { generateProjects } from "./projects.mjs";
69
+ import {
70
+ buildChunkAliasIndexes,
71
+ buildSqlResourceReferenceMap,
72
+ configChunkAliases,
73
+ extractConfigKeyReferences,
74
+ extractSqlObjectReferencesFromContent,
75
+ extractSqlResourceKeyReferences,
76
+ generateConfigIncludeRelations,
77
+ generateConfigTransformKeyRelations,
78
+ generateConfigTransformRelations,
79
+ generateMachineConfigRelations,
80
+ generateNamedResourceRelations,
81
+ generateSectionHandlerRelations,
82
+ namedEntryChunkAliases,
83
+ relationKey,
84
+ shouldExtractNamedResourceReferences,
85
+ shouldExtractSqlReferences,
86
+ sqlChunkAliases,
87
+ uniqueRelations
88
+ } from "./relations.mjs";
89
+ import {
90
+ CACHE_DIR,
91
+ CONTEXT_DIR,
92
+ DB_IMPORT_DIR,
93
+ REPO_ROOT
94
+ } from "./runtime-paths.mjs";
95
+ import {
96
+ resolveIngestWorkerCount,
97
+ startWorkerParseStream
98
+ } from "./workers.mjs";
99
+
100
+ function createIngestPipelineState() {
101
+ const { mode, verbose } = parseArgs(process.argv);
102
+ const memoryTrace = createIngestMemoryTrace();
103
+ const configPath = path.join(CONTEXT_DIR, "config.yaml");
104
+ const rulesPath = path.join(CONTEXT_DIR, "rules.yaml");
105
+
106
+ if (!fs.existsSync(configPath)) {
107
+ throw new Error(`Missing config: ${configPath}`);
108
+ }
109
+ if (!fs.existsSync(rulesPath)) {
110
+ throw new Error(`Missing rules: ${rulesPath}`);
111
+ }
112
+
113
+ ensureDirectory(CACHE_DIR);
114
+ ensureDirectory(DB_IMPORT_DIR);
115
+
116
+ const configText = fs.readFileSync(configPath, "utf8");
117
+ const sourcePaths = parseSourcePaths(configText);
118
+ if (sourcePaths.length === 0) {
119
+ throw new Error("No source_paths found in .context/config.yaml");
120
+ }
121
+
122
+ const rules = parseRules(fs.readFileSync(rulesPath, "utf8"));
123
+ memoryTrace.checkpoint("scan:start", {
124
+ mode,
125
+ source_paths: sourcePaths.length,
126
+ rules: rules.length
127
+ });
128
+ const { candidates, incrementalMode, deletedRelPaths } = collectCandidateFiles(sourcePaths, mode);
129
+ const chunkWindowLines = parsePositiveIntegerEnv(
130
+ "CORTEX_CHUNK_WINDOW_LINES",
131
+ DEFAULT_CHUNK_WINDOW_LINES
132
+ );
133
+ const chunkOverlapLines = Math.max(
134
+ 0,
135
+ Math.min(
136
+ chunkWindowLines - 1,
137
+ parseNonNegativeIntegerEnv("CORTEX_CHUNK_OVERLAP_LINES", DEFAULT_CHUNK_OVERLAP_LINES)
138
+ )
139
+ );
140
+ const chunkSplitMinLines = Math.max(
141
+ chunkWindowLines + 1,
142
+ parsePositiveIntegerEnv("CORTEX_CHUNK_SPLIT_MIN_LINES", DEFAULT_CHUNK_SPLIT_MIN_LINES)
143
+ );
144
+ const chunkMaxWindows = parsePositiveIntegerEnv(
145
+ "CORTEX_CHUNK_MAX_WINDOWS",
146
+ DEFAULT_CHUNK_MAX_WINDOWS
147
+ );
148
+
149
+ return {
150
+ mode,
151
+ verbose,
152
+ memoryTrace,
153
+ sourcePaths,
154
+ rules,
155
+ candidates,
156
+ incrementalMode,
157
+ deletedRelPaths,
158
+ chunkWindowLines,
159
+ chunkOverlapLines,
160
+ chunkSplitMinLines,
161
+ chunkMaxWindows
162
+ };
163
+ }
164
+
165
+ function runScanHydrationStage(state) {
166
+ const {
167
+ verbose,
168
+ memoryTrace,
169
+ sourcePaths,
170
+ candidates,
171
+ incrementalMode,
172
+ deletedRelPaths
173
+ } = state;
174
+ const fileRecordMap = new Map();
175
+ const adrRecordMap = new Map();
176
+ const skipped = {
177
+ unsupported: 0,
178
+ tooLarge: 0,
179
+ binary: 0
180
+ };
181
+
182
+ if (incrementalMode) {
183
+ const existingFiles = readJsonlSafe(path.join(CACHE_DIR, "entities.file.jsonl"));
184
+ for (const record of existingFiles) {
185
+ if (!record || typeof record !== "object") continue;
186
+ const filePath = toPosixPath(String(record.path ?? ""));
187
+ if (!filePath || !hasSourcePrefix(filePath, sourcePaths)) {
188
+ continue;
189
+ }
190
+ const absolutePath = path.resolve(REPO_ROOT, filePath);
191
+ if (!fs.existsSync(absolutePath)) {
192
+ continue;
193
+ }
194
+ fileRecordMap.set(String(record.id ?? `file:${filePath}`), {
195
+ ...record,
196
+ id: String(record.id ?? `file:${filePath}`),
197
+ path: filePath,
198
+ kind: String(record.kind ?? detectKind(filePath)),
199
+ content: String(record.content ?? "")
200
+ });
201
+ }
202
+
203
+ const existingAdrs = readJsonlSafe(path.join(CACHE_DIR, "entities.adr.jsonl"));
204
+ for (const adr of existingAdrs) {
205
+ if (!adr || typeof adr !== "object") continue;
206
+ const adrPath = toPosixPath(String(adr.path ?? ""));
207
+ if (!adrPath || !hasSourcePrefix(adrPath, sourcePaths)) {
208
+ continue;
209
+ }
210
+ if (!fs.existsSync(path.resolve(REPO_ROOT, adrPath))) {
211
+ continue;
212
+ }
213
+ adrRecordMap.set(String(adr.id ?? ""), {
214
+ ...adr,
215
+ id: String(adr.id ?? ""),
216
+ path: adrPath
217
+ });
218
+ }
219
+ }
220
+
221
+ for (const relPath of deletedRelPaths) {
222
+ fileRecordMap.delete(`file:${relPath}`);
223
+ const relPrefix = relPath.endsWith("/") ? relPath : `${relPath}/`;
224
+ for (const [fileId, fileRecord] of fileRecordMap.entries()) {
225
+ if (String(fileRecord.path ?? "").startsWith(relPrefix)) {
226
+ fileRecordMap.delete(fileId);
227
+ }
228
+ }
229
+
230
+ for (const [adrId, adrRecord] of adrRecordMap.entries()) {
231
+ if (adrRecord.path === relPath || String(adrRecord.path ?? "").startsWith(relPrefix)) {
232
+ adrRecordMap.delete(adrId);
233
+ }
234
+ }
235
+ }
236
+
237
+ for (const absolutePath of [...candidates].sort()) {
238
+ const relPath = toPosixPath(path.relative(REPO_ROOT, absolutePath));
239
+ if (!isTextFile(relPath)) {
240
+ skipped.unsupported += 1;
241
+ if (verbose) console.log(`[ingest] skip unsupported: ${relPath}`);
242
+ continue;
243
+ }
244
+
245
+ const stats = fs.statSync(absolutePath);
246
+ if (stats.size > MAX_FILE_BYTES) {
247
+ skipped.tooLarge += 1;
248
+ if (verbose) console.log(`[ingest] skip large: ${relPath}`);
249
+ continue;
250
+ }
251
+
252
+ const buffer = fs.readFileSync(absolutePath);
253
+ if (isBinaryBuffer(buffer)) {
254
+ skipped.binary += 1;
255
+ if (verbose) console.log(`[ingest] skip binary: ${relPath}`);
256
+ continue;
257
+ }
258
+
259
+ const content = buffer.toString("utf8");
260
+ const kind = detectKind(relPath);
261
+ const id = `file:${relPath}`;
262
+ const updatedAt = stats.mtime.toISOString();
263
+ const sourceOfTruth = kind === "ADR";
264
+ const trustLevel = trustLevelForKind(kind);
265
+
266
+ const fileRecord = {
267
+ id,
268
+ path: relPath,
269
+ kind,
270
+ checksum: checksum(buffer),
271
+ updated_at: updatedAt,
272
+ source_of_truth: sourceOfTruth,
273
+ trust_level: trustLevel,
274
+ status: "active",
275
+ size_bytes: stats.size,
276
+ excerpt: normalizeWhitespace(content).slice(0, 500),
277
+ content: content.slice(0, MAX_CONTENT_CHARS)
278
+ };
279
+ fileRecordMap.set(fileRecord.id, fileRecord);
280
+
281
+ if (kind === "ADR") {
282
+ const title = extractTitle(content, path.basename(relPath, path.extname(relPath)));
283
+ const adrRecord = {
284
+ id: `adr:${path.basename(relPath, path.extname(relPath)).toLowerCase()}`,
285
+ path: relPath,
286
+ title,
287
+ body: content.slice(0, MAX_BODY_CHARS),
288
+ decision_date: parseDecisionDate(content, updatedAt),
289
+ supersedes_id: "",
290
+ source_of_truth: true,
291
+ trust_level: 95,
292
+ status: "active"
293
+ };
294
+ adrRecordMap.set(adrRecord.id, adrRecord);
295
+ } else {
296
+ for (const [adrId, adrRecord] of adrRecordMap.entries()) {
297
+ if (adrRecord.path === relPath) {
298
+ adrRecordMap.delete(adrId);
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ const fileRecords = [...fileRecordMap.values()].sort((a, b) => a.path.localeCompare(b.path));
305
+ const adrRecords = [...adrRecordMap.values()].sort((a, b) => a.path.localeCompare(b.path));
306
+ memoryTrace.checkpoint("scan:file_records", {
307
+ candidates: candidates.size,
308
+ incremental_mode: incrementalMode,
309
+ deleted_paths: deletedRelPaths.length,
310
+ files: fileRecords.length,
311
+ adrs: adrRecords.length,
312
+ skipped_unsupported: skipped.unsupported,
313
+ skipped_too_large: skipped.tooLarge,
314
+ skipped_binary: skipped.binary
315
+ });
316
+ const {
317
+ fileCount: csharpFileCount,
318
+ runtime: csharpRuntime
319
+ } = inspectCSharpParser(fileRecords);
320
+ const indexedFileIds = new Set(fileRecords.map((record) => record.id));
321
+ const changedFileIds = new Set(
322
+ [...candidates].map((absolutePath) => `file:${toPosixPath(path.relative(REPO_ROOT, absolutePath))}`)
323
+ );
324
+
325
+ const {
326
+ chunkRecordMap,
327
+ definesRelationMap,
328
+ callsRelationMap,
329
+ importsRelationMap,
330
+ callsSqlRelationMap
331
+ } = incrementalMode
332
+ ? hydrateIncrementalChunkState(fileRecords)
333
+ : {
334
+ chunkRecordMap: new Map(),
335
+ definesRelationMap: new Map(),
336
+ callsRelationMap: new Map(),
337
+ importsRelationMap: new Map(),
338
+ callsSqlRelationMap: new Map()
339
+ };
340
+ memoryTrace.checkpoint("hydration:complete", {
341
+ incremental_mode: incrementalMode,
342
+ cached_chunks: chunkRecordMap.size,
343
+ cached_defines_relations: definesRelationMap.size,
344
+ cached_calls_relations: callsRelationMap.size,
345
+ cached_imports_relations: importsRelationMap.size,
346
+ cached_calls_sql_relations: callsSqlRelationMap.size
347
+ });
348
+
349
+ const cachedChunkFileIds = new Set(
350
+ [...chunkRecordMap.values()].map((record) => String(record.file_id ?? "")).filter(Boolean)
351
+ );
352
+ const cachedSqlReferenceFileIds = new Set(
353
+ [...callsSqlRelationMap.values()].map((record) => String(record.from ?? "")).filter(Boolean)
354
+ );
355
+ const usesConfigKeyRelationMap = new Map();
356
+ const usesResourceKeyRelationMap = new Map();
357
+ const usesSettingKeyRelationMap = new Map();
358
+
359
+ // Extract chunks from changed or uncached code files
360
+ let windowedChunkCount = 0;
361
+ let {
362
+ sqlChunkIdsByAlias,
363
+ configChunkIdsByAlias,
364
+ resourceChunkIdsByAlias,
365
+ settingChunkIdsByAlias
366
+ } = buildChunkAliasIndexes([...chunkRecordMap.values()]);
367
+ const deferredSqlCallEdges = [];
368
+
369
+ Object.assign(state, {
370
+ fileRecords,
371
+ adrRecords,
372
+ skipped,
373
+ csharpFileCount,
374
+ csharpRuntime,
375
+ indexedFileIds,
376
+ changedFileIds,
377
+ chunkRecordMap,
378
+ definesRelationMap,
379
+ callsRelationMap,
380
+ importsRelationMap,
381
+ callsSqlRelationMap,
382
+ cachedChunkFileIds,
383
+ cachedSqlReferenceFileIds,
384
+ usesConfigKeyRelationMap,
385
+ usesResourceKeyRelationMap,
386
+ usesSettingKeyRelationMap,
387
+ windowedChunkCount,
388
+ sqlChunkIdsByAlias,
389
+ configChunkIdsByAlias,
390
+ resourceChunkIdsByAlias,
391
+ settingChunkIdsByAlias,
392
+ deferredSqlCallEdges
393
+ });
394
+ }
395
+
396
+ async function runParseStage(state) {
397
+ const {
398
+ verbose,
399
+ memoryTrace,
400
+ fileRecords,
401
+ incrementalMode,
402
+ changedFileIds,
403
+ indexedFileIds,
404
+ cachedChunkFileIds,
405
+ chunkRecordMap,
406
+ definesRelationMap,
407
+ callsRelationMap,
408
+ importsRelationMap,
409
+ callsSqlRelationMap,
410
+ chunkWindowLines,
411
+ chunkOverlapLines,
412
+ chunkSplitMinLines,
413
+ chunkMaxWindows,
414
+ deferredSqlCallEdges
415
+ } = state;
416
+ let {
417
+ windowedChunkCount,
418
+ sqlChunkIdsByAlias,
419
+ configChunkIdsByAlias,
420
+ resourceChunkIdsByAlias,
421
+ settingChunkIdsByAlias
422
+ } = state;
423
+
424
+ // C# project-wide batch parse: when Roslyn is available and batching
425
+ // isn't disabled, compile all .cs files together via CSharpCompilation
426
+ // to enable SemanticModel-resolved calls (e.g. "System.IO.File.ReadAllText"
427
+ // instead of bare "ReadAllText"). Falls back silently to per-file parse
428
+ // if batch isn't usable.
429
+ const csharpBatchCache = createCSharpBatchCache({
430
+ fileRecords,
431
+ incrementalMode,
432
+ changedFileIds,
433
+ cachedChunkFileIds,
434
+ verbose
435
+ });
436
+
437
+ // Determine which files need parsing (single pass over the gates, shared by
438
+ // the worker dispatch and the merge loop below so the two cannot diverge).
439
+ const parseEligible = await collectParseEligibleFiles({
440
+ fileRecords,
441
+ incrementalMode,
442
+ changedFileIds,
443
+ cachedChunkFileIds
444
+ });
445
+ memoryTrace.checkpoint("parse:eligible", {
446
+ files: fileRecords.length,
447
+ parse_eligible: parseEligible.size,
448
+ csharp_batch_cached: csharpBatchCache.size
449
+ });
450
+
451
+ // Parse parallel-safe files (tree-sitter/JS, no subprocess, no cross-file
452
+ // state) in a worker pool. C# batch-cached files and any worker miss fall
453
+ // through to inline parsing in the loop, so the result is byte-identical to
454
+ // the sequential path regardless of where a file actually parsed.
455
+ const workerTasks = createWorkerTasks(
456
+ fileRecords,
457
+ parseEligible,
458
+ csharpBatchCache
459
+ );
460
+ const workerCount = resolveIngestWorkerCount(workerTasks.length);
461
+ memoryTrace.checkpoint("parse:workers_start", {
462
+ worker_tasks: workerTasks.length,
463
+ worker_count: workerCount
464
+ });
465
+ const workerStream =
466
+ workerCount > 1 ? startWorkerParseStream(workerTasks, { workerCount, verbose }) : null;
467
+ if (!workerStream) {
468
+ memoryTrace.checkpoint("parse:workers_complete", {
469
+ worker_tasks: workerTasks.length,
470
+ worker_count: workerCount,
471
+ worker_results: 0,
472
+ worker_results_consumed: 0,
473
+ worker_results_retained: 0,
474
+ worker_results_retained_peak: 0,
475
+ worker_results_pending: 0,
476
+ worker_results_missing: workerTasks.length
477
+ });
478
+ }
479
+
480
+ for (const fileRecord of fileRecords) {
481
+ const eligible = parseEligible.get(fileRecord.id);
482
+ if (!eligible) continue;
483
+ const { parser } = eligible;
484
+
485
+ removeChunkStateForFile(
486
+ fileRecord.id,
487
+ chunkRecordMap,
488
+ definesRelationMap,
489
+ callsRelationMap,
490
+ importsRelationMap,
491
+ callsSqlRelationMap
492
+ );
493
+
494
+ try {
495
+ let parseResult;
496
+ if (parser.language === "csharp" && csharpBatchCache.has(fileRecord.path)) {
497
+ parseResult = csharpBatchCache.get(fileRecord.path);
498
+ } else if (workerStream?.hasTask(fileRecord.id)) {
499
+ parseResult = await workerStream.take(fileRecord.id);
500
+ if (!parseResult) {
501
+ parseResult = await parser.parse(fileRecord.content, fileRecord.path, parser.language);
502
+ }
503
+ } else {
504
+ parseResult = await parser.parse(fileRecord.content, fileRecord.path, parser.language);
505
+ }
506
+
507
+ if (parseResult.errors.length > 0 && verbose) {
508
+ console.log(`[ingest] parse errors in ${fileRecord.path}:`, parseResult.errors[0].message);
509
+ }
510
+
511
+ const parsedChunks = [];
512
+ const chunkIdsByName = new Map();
513
+
514
+ for (const chunk of parseResult.chunks) {
515
+ const chunkId = chunkIdFor(fileRecord.path, chunk);
516
+ parsedChunks.push({ chunk, chunkId });
517
+ if (!chunkIdsByName.has(chunk.name)) {
518
+ chunkIdsByName.set(chunk.name, []);
519
+ }
520
+ chunkIdsByName.get(chunk.name).push(chunkId);
521
+ if (parser.language === "sql") {
522
+ for (const alias of sqlChunkAliases(chunk.name)) {
523
+ if (!sqlChunkIdsByAlias.has(alias)) {
524
+ sqlChunkIdsByAlias.set(alias, []);
525
+ }
526
+ sqlChunkIdsByAlias.get(alias).push(chunkId);
527
+ }
528
+ deferredSqlCallEdges.push({
529
+ chunkId,
530
+ calls: Array.isArray(chunk.calls) ? chunk.calls : []
531
+ });
532
+ } else if (parser.language === "config") {
533
+ for (const alias of configChunkAliases(chunk)) {
534
+ if (!configChunkIdsByAlias.has(alias)) {
535
+ configChunkIdsByAlias.set(alias, []);
536
+ }
537
+ configChunkIdsByAlias.get(alias).push(chunkId);
538
+ }
539
+ deferredSqlCallEdges.push({
540
+ chunkId,
541
+ calls: Array.isArray(chunk.calls) ? chunk.calls : []
542
+ });
543
+ } else if (parser.language === "resource") {
544
+ for (const alias of namedEntryChunkAliases(chunk)) {
545
+ if (!resourceChunkIdsByAlias.has(alias)) {
546
+ resourceChunkIdsByAlias.set(alias, []);
547
+ }
548
+ resourceChunkIdsByAlias.get(alias).push(chunkId);
549
+ }
550
+ deferredSqlCallEdges.push({
551
+ chunkId,
552
+ calls: Array.isArray(chunk.calls) ? chunk.calls : []
553
+ });
554
+ } else if (parser.language === "settings") {
555
+ for (const alias of namedEntryChunkAliases(chunk)) {
556
+ if (!settingChunkIdsByAlias.has(alias)) {
557
+ settingChunkIdsByAlias.set(alias, []);
558
+ }
559
+ settingChunkIdsByAlias.get(alias).push(chunkId);
560
+ }
561
+ deferredSqlCallEdges.push({
562
+ chunkId,
563
+ calls: Array.isArray(chunk.calls) ? chunk.calls : []
564
+ });
565
+ }
566
+
567
+ const chunkRecord = {
568
+ id: chunkId,
569
+ file_id: fileRecord.id,
570
+ name: chunk.name,
571
+ kind: chunk.kind,
572
+ signature: chunk.signature,
573
+ body: chunk.body.slice(0, MAX_BODY_CHARS), // Limit chunk body size
574
+ description: generateChunkDescription(chunk),
575
+ start_line: chunk.startLine,
576
+ end_line: chunk.endLine,
577
+ language: chunk.language,
578
+ exported: Boolean(chunk.exported),
579
+ checksum: checksum(Buffer.from(chunk.body)),
580
+ updated_at: fileRecord.updated_at,
581
+ trust_level: fileRecord.trust_level,
582
+ status:
583
+ typeof fileRecord.status === "string" && fileRecord.status.trim().length > 0
584
+ ? fileRecord.status
585
+ : "active",
586
+ source_of_truth: Boolean(fileRecord.source_of_truth)
587
+ };
588
+ chunkRecordMap.set(chunkId, chunkRecord);
589
+
590
+ // DEFINES relation: File -> Chunk
591
+ definesRelationMap.set(relationKey(fileRecord.id, chunkId), {
592
+ from: fileRecord.id,
593
+ to: chunkId
594
+ });
595
+
596
+ const windows = splitChunkIntoWindows(chunkRecord, {
597
+ windowLines: chunkWindowLines,
598
+ overlapLines: chunkOverlapLines,
599
+ splitMinLines: chunkSplitMinLines,
600
+ maxWindows: chunkMaxWindows,
601
+ chunkBody: chunk.body
602
+ });
603
+ if (windows.length > 0) {
604
+ windowedChunkCount += windows.length;
605
+ for (const windowChunk of windows) {
606
+ chunkRecordMap.set(windowChunk.id, windowChunk);
607
+ definesRelationMap.set(relationKey(fileRecord.id, windowChunk.id), {
608
+ from: fileRecord.id,
609
+ to: windowChunk.id
610
+ });
611
+ }
612
+ }
613
+
614
+ // IMPORTS relations: Chunk -> File
615
+ for (const importPath of chunk.imports || []) {
616
+ const targetFileId = resolveRelativeImportTargetId(fileRecord.path, importPath, indexedFileIds);
617
+ if (!targetFileId) {
618
+ continue;
619
+ }
620
+
621
+ importsRelationMap.set(relationKey(chunkId, targetFileId, importPath), {
622
+ from: chunkId,
623
+ to: targetFileId,
624
+ import_name: importPath
625
+ });
626
+ }
627
+ }
628
+
629
+ const seenCallEdges = new Set();
630
+ for (const { chunk, chunkId } of parsedChunks) {
631
+ // CALLS relations: Chunk -> Chunk (within same file)
632
+ for (const calledName of chunk.calls || []) {
633
+ const targetChunkIds = chunkIdsByName.get(calledName) || [];
634
+ for (const targetChunkId of targetChunkIds) {
635
+ const callKey = `${chunkId}|${targetChunkId}|direct`;
636
+ if (seenCallEdges.has(callKey)) {
637
+ continue;
638
+ }
639
+ seenCallEdges.add(callKey);
640
+ callsRelationMap.set(relationKey(chunkId, targetChunkId, "direct"), {
641
+ from: chunkId,
642
+ to: targetChunkId,
643
+ call_type: "direct"
644
+ });
645
+ }
646
+ }
647
+ }
648
+ } catch (error) {
649
+ if (verbose) {
650
+ console.log(`[ingest] failed to parse ${fileRecord.path}: ${error instanceof Error ? error.message : String(error)}`);
651
+ }
652
+ }
653
+ }
654
+ const finalWorkerStats = workerStream ? await workerStream.drain() : null;
655
+ if (finalWorkerStats) {
656
+ memoryTrace.checkpoint("parse:workers_complete", finalWorkerStats);
657
+ if (verbose) {
658
+ console.log(
659
+ `[ingest] parsed ${finalWorkerStats.worker_results}/${workerTasks.length} files across ${workerCount} workers`
660
+ );
661
+ }
662
+ }
663
+ memoryTrace.checkpoint("parse:merge_complete", {
664
+ chunk_map: chunkRecordMap.size,
665
+ defines_relations: definesRelationMap.size,
666
+ calls_relations: callsRelationMap.size,
667
+ imports_relations: importsRelationMap.size,
668
+ calls_sql_relations: callsSqlRelationMap.size,
669
+ deferred_sql_edges: deferredSqlCallEdges.length,
670
+ windowed_chunks: windowedChunkCount,
671
+ worker_results_retained: finalWorkerStats?.worker_results_retained ?? 0,
672
+ worker_results_retained_peak: finalWorkerStats?.worker_results_retained_peak ?? 0,
673
+ worker_results_pending: finalWorkerStats?.worker_results_pending ?? 0
674
+ });
675
+
676
+ Object.assign(state, {
677
+ windowedChunkCount,
678
+ sqlChunkIdsByAlias,
679
+ configChunkIdsByAlias,
680
+ resourceChunkIdsByAlias,
681
+ settingChunkIdsByAlias
682
+ });
683
+ }
684
+
685
+ function runMaterializationStage(state) {
686
+ const {
687
+ verbose,
688
+ memoryTrace,
689
+ rules,
690
+ fileRecords,
691
+ adrRecords,
692
+ incrementalMode,
693
+ changedFileIds,
694
+ indexedFileIds,
695
+ cachedSqlReferenceFileIds,
696
+ csharpFileCount,
697
+ csharpRuntime,
698
+ chunkWindowLines,
699
+ chunkOverlapLines,
700
+ chunkMaxWindows,
701
+ chunkRecordMap,
702
+ definesRelationMap,
703
+ callsRelationMap,
704
+ importsRelationMap,
705
+ callsSqlRelationMap,
706
+ usesConfigKeyRelationMap,
707
+ usesResourceKeyRelationMap,
708
+ usesSettingKeyRelationMap,
709
+ deferredSqlCallEdges,
710
+ windowedChunkCount
711
+ } = state;
712
+ let {
713
+ sqlChunkIdsByAlias,
714
+ configChunkIdsByAlias,
715
+ resourceChunkIdsByAlias,
716
+ settingChunkIdsByAlias
717
+ } = state;
718
+ const chunkRecords = [...chunkRecordMap.values()].sort((a, b) => String(a.id).localeCompare(String(b.id)));
719
+ ({
720
+ sqlChunkIdsByAlias,
721
+ configChunkIdsByAlias,
722
+ resourceChunkIdsByAlias,
723
+ settingChunkIdsByAlias
724
+ } = buildChunkAliasIndexes(chunkRecords));
725
+
726
+ // Filter CALLS relations to only valid targets (chunks that actually exist)
727
+ const chunkIdSet = new Set(chunkRecords.map(c => c.id));
728
+ const validDefinesRelations = [...definesRelationMap.values()].filter(
729
+ (rel) => indexedFileIds.has(rel.from) && chunkIdSet.has(rel.to)
730
+ );
731
+ const totalCallsRelations = callsRelationMap.size;
732
+ for (const edge of deferredSqlCallEdges) {
733
+ for (const calledName of edge.calls) {
734
+ for (const alias of sqlChunkAliases(calledName)) {
735
+ const targetChunkIds = sqlChunkIdsByAlias.get(alias) || [];
736
+ for (const targetChunkId of targetChunkIds) {
737
+ if (targetChunkId === edge.chunkId) {
738
+ continue;
739
+ }
740
+ callsRelationMap.set(relationKey(edge.chunkId, targetChunkId, "sql_reference"), {
741
+ from: edge.chunkId,
742
+ to: targetChunkId,
743
+ call_type: "sql_reference"
744
+ });
745
+ }
746
+ }
747
+ }
748
+ }
749
+ const validCallsRelations = [...callsRelationMap.values()].filter(
750
+ (rel) => chunkIdSet.has(rel.from) && chunkIdSet.has(rel.to)
751
+ );
752
+ const validImportsRelations = [...importsRelationMap.values()].filter(
753
+ (rel) => chunkIdSet.has(rel.from) && indexedFileIds.has(rel.to)
754
+ );
755
+ const sqlDefinitionsChanged =
756
+ incrementalMode &&
757
+ fileRecords.some(
758
+ (fileRecord) =>
759
+ changedFileIds.has(fileRecord.id) && path.extname(fileRecord.path).toLowerCase() === ".sql"
760
+ );
761
+ const sqlResourceReferenceMap = buildSqlResourceReferenceMap(fileRecords);
762
+ for (const fileRecord of fileRecords) {
763
+ if (!shouldExtractSqlReferences(fileRecord.path)) {
764
+ continue;
765
+ }
766
+
767
+ const shouldAnalyzeFile =
768
+ !incrementalMode ||
769
+ sqlDefinitionsChanged ||
770
+ changedFileIds.has(fileRecord.id) ||
771
+ !cachedSqlReferenceFileIds.has(fileRecord.id);
772
+ if (!shouldAnalyzeFile) {
773
+ continue;
774
+ }
775
+
776
+ for (const [key, relation] of callsSqlRelationMap.entries()) {
777
+ if (relation.from === fileRecord.id) {
778
+ callsSqlRelationMap.delete(key);
779
+ }
780
+ }
781
+
782
+ for (const refName of extractSqlObjectReferencesFromContent(
783
+ fileRecord.content,
784
+ fileRecord.path,
785
+ sqlResourceReferenceMap
786
+ )) {
787
+ for (const alias of sqlChunkAliases(refName)) {
788
+ const targetChunkIds = sqlChunkIdsByAlias.get(alias) || [];
789
+ for (const targetChunkId of targetChunkIds) {
790
+ callsSqlRelationMap.set(relationKey(fileRecord.id, targetChunkId, refName), {
791
+ from: fileRecord.id,
792
+ to: targetChunkId,
793
+ note: refName
794
+ });
795
+ }
796
+ }
797
+ }
798
+ }
799
+ const validCallsSqlRelations = [...callsSqlRelationMap.values()].filter(
800
+ (rel) => indexedFileIds.has(rel.from) && chunkIdSet.has(rel.to)
801
+ );
802
+ for (const fileRecord of fileRecords) {
803
+ if (!shouldExtractNamedResourceReferences(fileRecord.path)) {
804
+ continue;
805
+ }
806
+
807
+ for (const key of extractSqlResourceKeyReferences(fileRecord.content)) {
808
+ for (const targetChunkId of resourceChunkIdsByAlias.get(key) ?? []) {
809
+ usesResourceKeyRelationMap.set(relationKey(fileRecord.id, targetChunkId, key), {
810
+ from: fileRecord.id,
811
+ to: targetChunkId,
812
+ note: key
813
+ });
814
+ }
815
+ for (const targetChunkId of settingChunkIdsByAlias.get(key) ?? []) {
816
+ usesSettingKeyRelationMap.set(relationKey(fileRecord.id, targetChunkId, key), {
817
+ from: fileRecord.id,
818
+ to: targetChunkId,
819
+ note: key
820
+ });
821
+ }
822
+ }
823
+
824
+ for (const key of extractConfigKeyReferences(fileRecord.content)) {
825
+ for (const targetChunkId of configChunkIdsByAlias.get(key) ?? []) {
826
+ usesConfigKeyRelationMap.set(relationKey(fileRecord.id, targetChunkId, key), {
827
+ from: fileRecord.id,
828
+ to: targetChunkId,
829
+ note: key
830
+ });
831
+ }
832
+ }
833
+ }
834
+ for (const relation of generateConfigTransformKeyRelations(fileRecords, chunkRecords)) {
835
+ usesConfigKeyRelationMap.set(relationKey(relation.from, relation.to, relation.note), relation);
836
+ }
837
+ const validUsesConfigKeyRelations = [...usesConfigKeyRelationMap.values()].filter(
838
+ (rel) => indexedFileIds.has(rel.from) && chunkIdSet.has(rel.to)
839
+ );
840
+ const validUsesResourceKeyRelations = [...usesResourceKeyRelationMap.values()].filter(
841
+ (rel) => indexedFileIds.has(rel.from) && chunkIdSet.has(rel.to)
842
+ );
843
+ const validUsesSettingKeyRelations = [...usesSettingKeyRelationMap.values()].filter(
844
+ (rel) => indexedFileIds.has(rel.from) && chunkIdSet.has(rel.to)
845
+ );
846
+ memoryTrace.checkpoint("materialize:chunks_relations", {
847
+ chunks: chunkRecords.length,
848
+ chunk_ids: chunkIdSet.size,
849
+ relations_defines: validDefinesRelations.length,
850
+ relations_calls: validCallsRelations.length,
851
+ relations_imports: validImportsRelations.length,
852
+ relations_calls_sql: validCallsSqlRelations.length,
853
+ relations_uses_config_key: validUsesConfigKeyRelations.length,
854
+ relations_uses_resource_key: validUsesResourceKeyRelations.length,
855
+ relations_uses_setting_key: validUsesSettingKeyRelations.length
856
+ });
857
+
858
+ if (verbose && chunkRecords.length > 0) {
859
+ console.log(`[ingest] extracted ${chunkRecords.length} chunks from ${fileRecords.filter(f => f.kind === "CODE").length} code files`);
860
+ if (windowedChunkCount > 0) {
861
+ console.log(
862
+ `[ingest] overlap windows added=${windowedChunkCount} (window_lines=${chunkWindowLines}, overlap_lines=${chunkOverlapLines}, max_windows=${chunkMaxWindows})`
863
+ );
864
+ }
865
+ console.log(`[ingest] ${validCallsRelations.length} call relations (${totalCallsRelations - validCallsRelations.length} filtered)`);
866
+ if (validCallsSqlRelations.length > 0) {
867
+ console.log(`[ingest] sql call links=${validCallsSqlRelations.length}`);
868
+ }
869
+ if (validUsesConfigKeyRelations.length > 0) {
870
+ console.log(`[ingest] uses_config_key=${validUsesConfigKeyRelations.length}`);
871
+ }
872
+ if (validUsesResourceKeyRelations.length > 0 || validUsesSettingKeyRelations.length > 0) {
873
+ console.log(
874
+ `[ingest] uses_resource_key=${validUsesResourceKeyRelations.length} uses_setting_key=${validUsesSettingKeyRelations.length}`
875
+ );
876
+ }
877
+ }
878
+
879
+ const csharpChunkCount = chunkRecords.filter((record) => record.language === "csharp").length;
880
+ const parserHealth = {};
881
+ if (csharpFileCount > 0) {
882
+ parserHealth.csharp = {
883
+ files: csharpFileCount,
884
+ available: Boolean(csharpRuntime?.available),
885
+ reason: csharpRuntime?.available ? null : (csharpRuntime?.reason ?? "C# parser unavailable"),
886
+ chunks: csharpChunkCount,
887
+ };
888
+
889
+ if (!csharpRuntime?.available) {
890
+ console.log(`[ingest] warning csharp parser unavailable: ${parserHealth.csharp.reason}`);
891
+ } else if (csharpChunkCount === 0) {
892
+ console.log("[ingest] warning csharp parser produced 0 chunks across C# files");
893
+ }
894
+ }
895
+
896
+ // Generate Module entities and relations
897
+ const moduleResult = generateModules(fileRecords, chunkRecords);
898
+ const moduleRecords = moduleResult.modules;
899
+ const moduleContainsRelations = moduleResult.containsRelations;
900
+ const moduleContainsModuleRelations = moduleResult.containsModuleRelations;
901
+ const moduleExportsRelations = moduleResult.exportsRelations;
902
+ const projectResult = generateProjects(fileRecords);
903
+ const projectRecords = projectResult.projects;
904
+ const projectIncludesFileRelations = projectResult.includesFileRelations;
905
+ const projectReferencesProjectRelations = projectResult.referencesProjectRelations;
906
+ const namedResourceRelationResult = generateNamedResourceRelations(fileRecords);
907
+ const usesResourceRelations = namedResourceRelationResult.usesResourceRelations;
908
+ const usesSettingRelations = namedResourceRelationResult.usesSettingRelations;
909
+ const configIncludeRelations = generateConfigIncludeRelations(fileRecords);
910
+ const machineConfigRelations = generateMachineConfigRelations(fileRecords);
911
+ const sectionHandlerRelations = generateSectionHandlerRelations(fileRecords);
912
+ const usesConfigRelations = uniqueRelations([
913
+ ...namedResourceRelationResult.usesConfigRelations,
914
+ ...configIncludeRelations,
915
+ ...machineConfigRelations,
916
+ ...sectionHandlerRelations
917
+ ]);
918
+ const configTransformRelations = generateConfigTransformRelations(fileRecords);
919
+ memoryTrace.checkpoint("materialize:modules_projects_relations", {
920
+ modules: moduleRecords.length,
921
+ projects: projectRecords.length,
922
+ relations_contains: moduleContainsRelations.length,
923
+ relations_contains_module: moduleContainsModuleRelations.length,
924
+ relations_exports: moduleExportsRelations.length,
925
+ relations_includes_file: projectIncludesFileRelations.length,
926
+ relations_references_project: projectReferencesProjectRelations.length,
927
+ relations_uses_resource: usesResourceRelations.length,
928
+ relations_uses_setting: usesSettingRelations.length,
929
+ relations_uses_config: usesConfigRelations.length,
930
+ relations_transforms_config: configTransformRelations.length
931
+ });
932
+
933
+ if (verbose && moduleRecords.length > 0) {
934
+ console.log(`[ingest] modules=${moduleRecords.length} contains=${moduleContainsRelations.length} contains_module=${moduleContainsModuleRelations.length} exports=${moduleExportsRelations.length}`);
935
+ }
936
+ if (verbose && projectRecords.length > 0) {
937
+ console.log(
938
+ `[ingest] projects=${projectRecords.length} includes_file=${projectIncludesFileRelations.length} references_project=${projectReferencesProjectRelations.length}`
939
+ );
940
+ }
941
+ if (
942
+ verbose &&
943
+ (
944
+ usesResourceRelations.length > 0 ||
945
+ usesSettingRelations.length > 0 ||
946
+ usesConfigRelations.length > 0 ||
947
+ configTransformRelations.length > 0
948
+ )
949
+ ) {
950
+ console.log(
951
+ `[ingest] uses_resource=${usesResourceRelations.length} uses_setting=${usesSettingRelations.length} uses_config=${usesConfigRelations.length} transforms_config=${configTransformRelations.length}`
952
+ );
953
+ }
954
+
955
+ const ruleRecords = rules.map((rule) => ({
956
+ id: rule.id,
957
+ title: rule.id,
958
+ body: rule.description,
959
+ scope: "global",
960
+ updated_at: new Date().toISOString(),
961
+ source_of_truth: true,
962
+ trust_level: 95,
963
+ status: rule.enforce ? "active" : "draft",
964
+ priority: rule.priority
965
+ }));
966
+
967
+ const adrTokenIndex = new Map();
968
+ for (const adrRecord of adrRecords) {
969
+ for (const token of adrTokens(adrRecord)) {
970
+ if (!adrTokenIndex.has(token)) {
971
+ adrTokenIndex.set(token, adrRecord.id);
972
+ }
973
+ }
974
+ }
975
+
976
+ const supersedesRelations = [];
977
+ for (const adrRecord of adrRecords) {
978
+ const refs = findSupersedesReferences(adrRecord.body);
979
+ for (const ref of refs) {
980
+ const target = adrTokenIndex.get(normalizeToken(ref));
981
+ if (!target || target === adrRecord.id) {
982
+ continue;
983
+ }
984
+ adrRecord.supersedes_id = target;
985
+ supersedesRelations.push({
986
+ from: adrRecord.id,
987
+ to: target,
988
+ reason: `Supersedes ${ref}`
989
+ });
990
+ }
991
+ }
992
+
993
+ Object.assign(state, {
994
+ chunkRecords,
995
+ validDefinesRelations,
996
+ validCallsRelations,
997
+ validImportsRelations,
998
+ validCallsSqlRelations,
999
+ validUsesConfigKeyRelations,
1000
+ validUsesResourceKeyRelations,
1001
+ validUsesSettingKeyRelations,
1002
+ parserHealth,
1003
+ moduleRecords,
1004
+ moduleContainsRelations,
1005
+ moduleContainsModuleRelations,
1006
+ moduleExportsRelations,
1007
+ projectRecords,
1008
+ projectIncludesFileRelations,
1009
+ projectReferencesProjectRelations,
1010
+ usesResourceRelations,
1011
+ usesSettingRelations,
1012
+ usesConfigRelations,
1013
+ configTransformRelations,
1014
+ ruleRecords,
1015
+ supersedesRelations
1016
+ });
1017
+ }
1018
+
1019
+ function runFileCacheStagingStage(state) {
1020
+ const {
1021
+ memoryTrace,
1022
+ fileRecords
1023
+ } = state;
1024
+ const constrainsRelations = [];
1025
+ const implementsRelations = [];
1026
+ const stagedDocumentCache = stageJsonl(path.join(CACHE_DIR, "documents.jsonl"), fileRecords);
1027
+ const stagedFileEntityCache = stageJsonl(path.join(CACHE_DIR, "entities.file.jsonl"), fileRecords);
1028
+ memoryTrace.checkpoint("writes:file_cache_staged", {
1029
+ files: fileRecords.length,
1030
+ file_content_records: countFileContentRecords(fileRecords)
1031
+ });
1032
+
1033
+ Object.assign(state, {
1034
+ constrainsRelations,
1035
+ implementsRelations,
1036
+ stagedDocumentCache,
1037
+ stagedFileEntityCache
1038
+ });
1039
+ }
1040
+
1041
+ function runTokenMatchingStage(state) {
1042
+ const {
1043
+ memoryTrace,
1044
+ fileRecords,
1045
+ ruleRecords,
1046
+ constrainsRelations,
1047
+ implementsRelations,
1048
+ supersedesRelations
1049
+ } = state;
1050
+ memoryTrace.checkpoint("tokens:rule_matching_start", {
1051
+ files: fileRecords.length,
1052
+ rules: ruleRecords.length
1053
+ });
1054
+
1055
+ const ruleMatchers = ruleRecords.map((ruleRecord) => ({
1056
+ ruleRecord,
1057
+ needle: ruleRecord.id.toLowerCase(),
1058
+ keywords: normalizeRuleTokens(ruleRecord)
1059
+ }));
1060
+ const constrainsByKey = new Map();
1061
+ const implementsByKey = new Map();
1062
+ let fileTokenSetsProcessed = 0;
1063
+ let fileContentRecordsReleased = 0;
1064
+
1065
+ for (const fileRecord of fileRecords) {
1066
+ const lower = String(fileRecord.content ?? "").toLowerCase();
1067
+ const tokens = fileTokenSet(fileRecord);
1068
+ fileTokenSetsProcessed += 1;
1069
+
1070
+ for (const { ruleRecord, needle, keywords } of ruleMatchers) {
1071
+ const explicitMention = lower.includes(needle);
1072
+ const minimumMatches = fileRecord.kind === "CODE" ? 1 : 2;
1073
+ const keywordResult = explicitMention
1074
+ ? { matched: false, sample: [] }
1075
+ : collectRuleKeywordMatch(keywords, tokens, minimumMatches);
1076
+
1077
+ if (!explicitMention && !keywordResult.matched) {
1078
+ continue;
1079
+ }
1080
+
1081
+ const constrainsKey = `${ruleRecord.id}|${fileRecord.id}`;
1082
+ if (!constrainsByKey.has(constrainsKey)) {
1083
+ constrainsByKey.set(constrainsKey, {
1084
+ from: ruleRecord.id,
1085
+ to: fileRecord.id,
1086
+ note: explicitMention
1087
+ ? `Mentions ${ruleRecord.id}`
1088
+ : `Keyword match ${keywordResult.sample.join(", ")}`
1089
+ });
1090
+ }
1091
+
1092
+ if (fileRecord.kind === "CODE") {
1093
+ const implementsKey = `${fileRecord.id}|${ruleRecord.id}`;
1094
+ if (!implementsByKey.has(implementsKey)) {
1095
+ implementsByKey.set(implementsKey, {
1096
+ from: fileRecord.id,
1097
+ to: ruleRecord.id,
1098
+ note: explicitMention
1099
+ ? `Code references ${ruleRecord.id}`
1100
+ : `Code keywords ${keywordResult.sample.join(", ")}`
1101
+ });
1102
+ }
1103
+ }
1104
+ }
1105
+
1106
+ if (Object.prototype.hasOwnProperty.call(fileRecord, "content")) {
1107
+ delete fileRecord.content;
1108
+ fileContentRecordsReleased += 1;
1109
+ }
1110
+ }
1111
+
1112
+ for (const { ruleRecord } of ruleMatchers) {
1113
+ for (const fileRecord of fileRecords) {
1114
+ const constrainsKey = `${ruleRecord.id}|${fileRecord.id}`;
1115
+ const constrainsRelation = constrainsByKey.get(constrainsKey);
1116
+ if (constrainsRelation) {
1117
+ constrainsRelations.push(constrainsRelation);
1118
+ constrainsByKey.delete(constrainsKey);
1119
+ }
1120
+ if (fileRecord.kind === "CODE") {
1121
+ const implementsKey = `${fileRecord.id}|${ruleRecord.id}`;
1122
+ const implementsRelation = implementsByKey.get(implementsKey);
1123
+ if (implementsRelation) {
1124
+ implementsRelations.push(implementsRelation);
1125
+ implementsByKey.delete(implementsKey);
1126
+ }
1127
+ }
1128
+ }
1129
+ }
1130
+
1131
+ memoryTrace.checkpoint("tokens:rule_matching_complete", {
1132
+ file_token_sets: fileTokenSetsProcessed,
1133
+ file_token_sets_retained: 0,
1134
+ file_content_records_released: fileContentRecordsReleased,
1135
+ file_content_records_retained: countFileContentRecords(fileRecords),
1136
+ rules: ruleRecords.length,
1137
+ relations_constrains: constrainsRelations.length,
1138
+ relations_implements: implementsRelations.length,
1139
+ relations_supersedes: supersedesRelations.length
1140
+ });
1141
+ }
1142
+
1143
+ function runCacheWriteStage(state) {
1144
+ const {
1145
+ memoryTrace,
1146
+ fileRecords,
1147
+ adrRecords,
1148
+ ruleRecords,
1149
+ chunkRecords,
1150
+ supersedesRelations,
1151
+ constrainsRelations,
1152
+ implementsRelations,
1153
+ validDefinesRelations,
1154
+ validCallsRelations,
1155
+ validImportsRelations,
1156
+ validCallsSqlRelations,
1157
+ validUsesConfigKeyRelations,
1158
+ validUsesResourceKeyRelations,
1159
+ validUsesSettingKeyRelations,
1160
+ moduleRecords,
1161
+ moduleContainsRelations,
1162
+ moduleContainsModuleRelations,
1163
+ moduleExportsRelations,
1164
+ projectRecords,
1165
+ projectIncludesFileRelations,
1166
+ projectReferencesProjectRelations,
1167
+ usesResourceRelations,
1168
+ usesSettingRelations,
1169
+ usesConfigRelations,
1170
+ configTransformRelations,
1171
+ stagedDocumentCache,
1172
+ stagedFileEntityCache
1173
+ } = state;
1174
+ memoryTrace.checkpoint("writes:cache_start", {
1175
+ files: fileRecords.length,
1176
+ adrs: adrRecords.length,
1177
+ rules: ruleRecords.length,
1178
+ chunks: chunkRecords.length
1179
+ });
1180
+ stagedDocumentCache.commit();
1181
+ stagedFileEntityCache.commit();
1182
+ writeJsonl(path.join(CACHE_DIR, "entities.adr.jsonl"), adrRecords);
1183
+ writeJsonl(path.join(CACHE_DIR, "entities.rule.jsonl"), ruleRecords);
1184
+ writeJsonl(path.join(CACHE_DIR, "entities.chunk.jsonl"), chunkRecords);
1185
+ writeJsonl(path.join(CACHE_DIR, "relations.supersedes.jsonl"), supersedesRelations);
1186
+ writeJsonl(path.join(CACHE_DIR, "relations.constrains.jsonl"), constrainsRelations);
1187
+ writeJsonl(path.join(CACHE_DIR, "relations.implements.jsonl"), implementsRelations);
1188
+ writeJsonl(path.join(CACHE_DIR, "relations.defines.jsonl"), validDefinesRelations);
1189
+ writeJsonl(path.join(CACHE_DIR, "relations.calls.jsonl"), validCallsRelations);
1190
+ writeJsonl(path.join(CACHE_DIR, "relations.imports.jsonl"), validImportsRelations);
1191
+ writeJsonl(path.join(CACHE_DIR, "relations.calls_sql.jsonl"), validCallsSqlRelations);
1192
+ writeJsonl(path.join(CACHE_DIR, "relations.uses_config_key.jsonl"), validUsesConfigKeyRelations);
1193
+ writeJsonl(path.join(CACHE_DIR, "relations.uses_resource_key.jsonl"), validUsesResourceKeyRelations);
1194
+ writeJsonl(path.join(CACHE_DIR, "relations.uses_setting_key.jsonl"), validUsesSettingKeyRelations);
1195
+ writeJsonl(path.join(CACHE_DIR, "entities.module.jsonl"), moduleRecords);
1196
+ writeJsonl(path.join(CACHE_DIR, "relations.contains.jsonl"), moduleContainsRelations);
1197
+ writeJsonl(path.join(CACHE_DIR, "relations.contains_module.jsonl"), moduleContainsModuleRelations);
1198
+ writeJsonl(path.join(CACHE_DIR, "relations.exports.jsonl"), moduleExportsRelations);
1199
+ writeJsonl(path.join(CACHE_DIR, "entities.project.jsonl"), projectRecords);
1200
+ writeJsonl(path.join(CACHE_DIR, "relations.includes_file.jsonl"), projectIncludesFileRelations);
1201
+ writeJsonl(path.join(CACHE_DIR, "relations.uses_resource.jsonl"), usesResourceRelations);
1202
+ writeJsonl(path.join(CACHE_DIR, "relations.uses_setting.jsonl"), usesSettingRelations);
1203
+ writeJsonl(path.join(CACHE_DIR, "relations.uses_config.jsonl"), usesConfigRelations);
1204
+ writeJsonl(path.join(CACHE_DIR, "relations.transforms_config.jsonl"), configTransformRelations);
1205
+ writeJsonl(
1206
+ path.join(CACHE_DIR, "relations.references_project.jsonl"),
1207
+ projectReferencesProjectRelations
1208
+ );
1209
+ memoryTrace.checkpoint("writes:cache_complete", {
1210
+ jsonl_files: 26,
1211
+ files: fileRecords.length,
1212
+ chunks: chunkRecords.length
1213
+ });
1214
+ }
1215
+
1216
+ function runDatabaseWriteStage(state) {
1217
+ const {
1218
+ memoryTrace,
1219
+ fileRecords,
1220
+ adrRecords,
1221
+ ruleRecords,
1222
+ chunkRecords,
1223
+ supersedesRelations,
1224
+ constrainsRelations,
1225
+ implementsRelations,
1226
+ validDefinesRelations,
1227
+ validCallsRelations,
1228
+ validImportsRelations,
1229
+ validCallsSqlRelations,
1230
+ validUsesConfigKeyRelations,
1231
+ validUsesResourceKeyRelations,
1232
+ validUsesSettingKeyRelations,
1233
+ moduleRecords,
1234
+ moduleContainsRelations,
1235
+ moduleContainsModuleRelations,
1236
+ moduleExportsRelations,
1237
+ projectRecords,
1238
+ projectIncludesFileRelations,
1239
+ projectReferencesProjectRelations,
1240
+ usesResourceRelations,
1241
+ usesSettingRelations,
1242
+ usesConfigRelations,
1243
+ configTransformRelations
1244
+ } = state;
1245
+ memoryTrace.checkpoint("writes:db_start", {
1246
+ tsv_files: 21
1247
+ });
1248
+ writeTsv(
1249
+ path.join(DB_IMPORT_DIR, "file_nodes.tsv"),
1250
+ [
1251
+ "id",
1252
+ "path",
1253
+ "kind",
1254
+ "excerpt",
1255
+ "checksum",
1256
+ "updated_at",
1257
+ "source_of_truth",
1258
+ "trust_level",
1259
+ "status"
1260
+ ],
1261
+ mapRows(fileRecords, (record) => [
1262
+ record.id,
1263
+ record.path,
1264
+ record.kind,
1265
+ record.excerpt,
1266
+ record.checksum,
1267
+ record.updated_at,
1268
+ record.source_of_truth,
1269
+ record.trust_level,
1270
+ record.status
1271
+ ])
1272
+ );
1273
+
1274
+ writeTsv(
1275
+ path.join(DB_IMPORT_DIR, "rule_nodes.tsv"),
1276
+ [
1277
+ "id",
1278
+ "title",
1279
+ "body",
1280
+ "scope",
1281
+ "priority",
1282
+ "updated_at",
1283
+ "source_of_truth",
1284
+ "trust_level",
1285
+ "status"
1286
+ ],
1287
+ mapRows(ruleRecords, (record) => [
1288
+ record.id,
1289
+ record.title,
1290
+ record.body,
1291
+ record.scope,
1292
+ record.priority,
1293
+ record.updated_at,
1294
+ record.source_of_truth,
1295
+ record.trust_level,
1296
+ record.status
1297
+ ])
1298
+ );
1299
+
1300
+ writeTsv(
1301
+ path.join(DB_IMPORT_DIR, "adr_nodes.tsv"),
1302
+ [
1303
+ "id",
1304
+ "path",
1305
+ "title",
1306
+ "body",
1307
+ "decision_date",
1308
+ "supersedes_id",
1309
+ "source_of_truth",
1310
+ "trust_level",
1311
+ "status"
1312
+ ],
1313
+ mapRows(adrRecords, (record) => [
1314
+ record.id,
1315
+ record.path,
1316
+ record.title,
1317
+ record.body,
1318
+ record.decision_date,
1319
+ record.supersedes_id,
1320
+ record.source_of_truth,
1321
+ record.trust_level,
1322
+ record.status
1323
+ ])
1324
+ );
1325
+
1326
+ writeTsv(
1327
+ path.join(DB_IMPORT_DIR, "constrains_rel.tsv"),
1328
+ ["from", "to", "note"],
1329
+ mapRows(constrainsRelations, (record) => [record.from, record.to, record.note])
1330
+ );
1331
+
1332
+ writeTsv(
1333
+ path.join(DB_IMPORT_DIR, "implements_rel.tsv"),
1334
+ ["from", "to", "note"],
1335
+ mapRows(implementsRelations, (record) => [record.from, record.to, record.note])
1336
+ );
1337
+
1338
+ writeTsv(
1339
+ path.join(DB_IMPORT_DIR, "supersedes_rel.tsv"),
1340
+ ["from", "to", "reason"],
1341
+ mapRows(supersedesRelations, (record) => [record.from, record.to, record.reason])
1342
+ );
1343
+
1344
+ writeTsv(
1345
+ path.join(DB_IMPORT_DIR, "chunk_nodes.tsv"),
1346
+ [
1347
+ "id",
1348
+ "file_id",
1349
+ "name",
1350
+ "kind",
1351
+ "signature",
1352
+ "body",
1353
+ "start_line",
1354
+ "end_line",
1355
+ "language",
1356
+ "checksum",
1357
+ "updated_at",
1358
+ "trust_level"
1359
+ ],
1360
+ mapRows(chunkRecords, (record) => [
1361
+ record.id,
1362
+ record.file_id,
1363
+ record.name,
1364
+ record.kind,
1365
+ record.signature,
1366
+ record.body,
1367
+ record.start_line,
1368
+ record.end_line,
1369
+ record.language,
1370
+ record.checksum,
1371
+ record.updated_at,
1372
+ record.trust_level
1373
+ ])
1374
+ );
1375
+
1376
+ writeTsv(
1377
+ path.join(DB_IMPORT_DIR, "defines_rel.tsv"),
1378
+ ["from", "to"],
1379
+ mapRows(validDefinesRelations, (record) => [record.from, record.to])
1380
+ );
1381
+
1382
+ writeTsv(
1383
+ path.join(DB_IMPORT_DIR, "calls_rel.tsv"),
1384
+ ["from", "to", "call_type"],
1385
+ mapRows(validCallsRelations, (record) => [record.from, record.to, record.call_type])
1386
+ );
1387
+
1388
+ writeTsv(
1389
+ path.join(DB_IMPORT_DIR, "imports_rel.tsv"),
1390
+ ["from", "to", "import_name"],
1391
+ mapRows(validImportsRelations, (record) => [record.from, record.to, record.import_name])
1392
+ );
1393
+
1394
+ writeTsv(
1395
+ path.join(DB_IMPORT_DIR, "calls_sql_rel.tsv"),
1396
+ ["from", "to", "note"],
1397
+ mapRows(validCallsSqlRelations, (record) => [record.from, record.to, record.note])
1398
+ );
1399
+
1400
+ writeTsv(
1401
+ path.join(DB_IMPORT_DIR, "uses_config_key_rel.tsv"),
1402
+ ["from", "to", "note"],
1403
+ mapRows(validUsesConfigKeyRelations, (record) => [record.from, record.to, record.note])
1404
+ );
1405
+
1406
+ writeTsv(
1407
+ path.join(DB_IMPORT_DIR, "uses_resource_key_rel.tsv"),
1408
+ ["from", "to", "note"],
1409
+ mapRows(validUsesResourceKeyRelations, (record) => [record.from, record.to, record.note])
1410
+ );
1411
+
1412
+ writeTsv(
1413
+ path.join(DB_IMPORT_DIR, "uses_setting_key_rel.tsv"),
1414
+ ["from", "to", "note"],
1415
+ mapRows(validUsesSettingKeyRelations, (record) => [record.from, record.to, record.note])
1416
+ );
1417
+
1418
+ writeTsv(
1419
+ path.join(DB_IMPORT_DIR, "project_nodes.tsv"),
1420
+ [
1421
+ "id",
1422
+ "path",
1423
+ "name",
1424
+ "kind",
1425
+ "language",
1426
+ "target_framework",
1427
+ "summary",
1428
+ "file_count",
1429
+ "updated_at",
1430
+ "source_of_truth",
1431
+ "trust_level",
1432
+ "status"
1433
+ ],
1434
+ mapRows(projectRecords, (record) => [
1435
+ record.id,
1436
+ record.path,
1437
+ record.name,
1438
+ record.kind,
1439
+ record.language,
1440
+ record.target_framework,
1441
+ record.summary,
1442
+ record.file_count,
1443
+ record.updated_at,
1444
+ record.source_of_truth,
1445
+ record.trust_level,
1446
+ record.status
1447
+ ])
1448
+ );
1449
+
1450
+ writeTsv(
1451
+ path.join(DB_IMPORT_DIR, "includes_file_rel.tsv"),
1452
+ ["from", "to"],
1453
+ mapRows(projectIncludesFileRelations, (record) => [record.from, record.to])
1454
+ );
1455
+
1456
+ writeTsv(
1457
+ path.join(DB_IMPORT_DIR, "references_project_rel.tsv"),
1458
+ ["from", "to", "note"],
1459
+ mapRows(projectReferencesProjectRelations, (record) => [record.from, record.to, record.note])
1460
+ );
1461
+
1462
+ writeTsv(
1463
+ path.join(DB_IMPORT_DIR, "uses_resource_rel.tsv"),
1464
+ ["from", "to", "note"],
1465
+ mapRows(usesResourceRelations, (record) => [record.from, record.to, record.note])
1466
+ );
1467
+
1468
+ writeTsv(
1469
+ path.join(DB_IMPORT_DIR, "uses_setting_rel.tsv"),
1470
+ ["from", "to", "note"],
1471
+ mapRows(usesSettingRelations, (record) => [record.from, record.to, record.note])
1472
+ );
1473
+
1474
+ writeTsv(
1475
+ path.join(DB_IMPORT_DIR, "uses_config_rel.tsv"),
1476
+ ["from", "to", "note"],
1477
+ mapRows(usesConfigRelations, (record) => [record.from, record.to, record.note])
1478
+ );
1479
+
1480
+ writeTsv(
1481
+ path.join(DB_IMPORT_DIR, "transforms_config_rel.tsv"),
1482
+ ["from", "to", "note"],
1483
+ mapRows(configTransformRelations, (record) => [record.from, record.to, record.note])
1484
+ );
1485
+ memoryTrace.checkpoint("writes:db_complete", {
1486
+ tsv_files: 21
1487
+ });
1488
+ }
1489
+
1490
+ function runManifestCompletionStage(state) {
1491
+ const {
1492
+ mode,
1493
+ memoryTrace,
1494
+ sourcePaths,
1495
+ candidates,
1496
+ incrementalMode,
1497
+ deletedRelPaths,
1498
+ fileRecords,
1499
+ adrRecords,
1500
+ ruleRecords,
1501
+ chunkRecords,
1502
+ constrainsRelations,
1503
+ implementsRelations,
1504
+ supersedesRelations,
1505
+ validDefinesRelations,
1506
+ validCallsRelations,
1507
+ validImportsRelations,
1508
+ validCallsSqlRelations,
1509
+ validUsesConfigKeyRelations,
1510
+ validUsesResourceKeyRelations,
1511
+ validUsesSettingKeyRelations,
1512
+ moduleRecords,
1513
+ moduleContainsRelations,
1514
+ moduleContainsModuleRelations,
1515
+ moduleExportsRelations,
1516
+ projectRecords,
1517
+ projectIncludesFileRelations,
1518
+ projectReferencesProjectRelations,
1519
+ usesResourceRelations,
1520
+ usesSettingRelations,
1521
+ usesConfigRelations,
1522
+ configTransformRelations,
1523
+ skipped,
1524
+ parserHealth
1525
+ } = state;
1526
+ const manifest = {
1527
+ generated_at: new Date().toISOString(),
1528
+ mode,
1529
+ source_paths: sourcePaths,
1530
+ counts: {
1531
+ files: fileRecords.length,
1532
+ adrs: adrRecords.length,
1533
+ rules: ruleRecords.length,
1534
+ chunks: chunkRecords.length,
1535
+ relations_constrains: constrainsRelations.length,
1536
+ relations_implements: implementsRelations.length,
1537
+ relations_supersedes: supersedesRelations.length,
1538
+ relations_defines: validDefinesRelations.length,
1539
+ relations_calls: validCallsRelations.length,
1540
+ relations_imports: validImportsRelations.length,
1541
+ relations_calls_sql: validCallsSqlRelations.length,
1542
+ relations_uses_config_key: validUsesConfigKeyRelations.length,
1543
+ relations_uses_resource_key: validUsesResourceKeyRelations.length,
1544
+ relations_uses_setting_key: validUsesSettingKeyRelations.length,
1545
+ modules: moduleRecords.length,
1546
+ relations_contains: moduleContainsRelations.length,
1547
+ relations_contains_module: moduleContainsModuleRelations.length,
1548
+ relations_exports: moduleExportsRelations.length,
1549
+ projects: projectRecords.length,
1550
+ relations_includes_file: projectIncludesFileRelations.length,
1551
+ relations_references_project: projectReferencesProjectRelations.length,
1552
+ relations_uses_resource: usesResourceRelations.length,
1553
+ relations_uses_setting: usesSettingRelations.length,
1554
+ relations_uses_config: usesConfigRelations.length,
1555
+ relations_transforms_config: configTransformRelations.length
1556
+ },
1557
+ skipped,
1558
+ parser_health: parserHealth,
1559
+ incremental_mode: incrementalMode,
1560
+ changed_candidates: candidates.size,
1561
+ deleted_paths: deletedRelPaths.length
1562
+ };
1563
+
1564
+ fs.writeFileSync(path.join(CACHE_DIR, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
1565
+ memoryTrace.checkpoint("writes:manifest_complete", {
1566
+ files: manifest.counts.files,
1567
+ chunks: manifest.counts.chunks,
1568
+ total_relations:
1569
+ manifest.counts.relations_constrains +
1570
+ manifest.counts.relations_implements +
1571
+ manifest.counts.relations_supersedes +
1572
+ manifest.counts.relations_defines +
1573
+ manifest.counts.relations_calls +
1574
+ manifest.counts.relations_imports +
1575
+ manifest.counts.relations_calls_sql +
1576
+ manifest.counts.relations_uses_config_key +
1577
+ manifest.counts.relations_uses_resource_key +
1578
+ manifest.counts.relations_uses_setting_key +
1579
+ manifest.counts.relations_contains +
1580
+ manifest.counts.relations_contains_module +
1581
+ manifest.counts.relations_exports +
1582
+ manifest.counts.relations_includes_file +
1583
+ manifest.counts.relations_references_project +
1584
+ manifest.counts.relations_uses_resource +
1585
+ manifest.counts.relations_uses_setting +
1586
+ manifest.counts.relations_uses_config +
1587
+ manifest.counts.relations_transforms_config
1588
+ });
1589
+
1590
+ console.log(`[ingest] mode=${mode}`);
1591
+ if (incrementalMode) {
1592
+ console.log(
1593
+ `[ingest] incremental changed_candidates=${manifest.changed_candidates} deleted_paths=${manifest.deleted_paths}`
1594
+ );
1595
+ } else if (mode === "changed") {
1596
+ console.log("[ingest] incremental diff unavailable; processed full source set");
1597
+ }
1598
+ console.log(`[ingest] files=${manifest.counts.files} adrs=${manifest.counts.adrs} rules=${manifest.counts.rules} chunks=${manifest.counts.chunks}`);
1599
+ console.log(
1600
+ `[ingest] rels constrains=${manifest.counts.relations_constrains} implements=${manifest.counts.relations_implements} supersedes=${manifest.counts.relations_supersedes}`
1601
+ );
1602
+ console.log(
1603
+ `[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}`
1604
+ );
1605
+ console.log(
1606
+ `[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}`
1607
+ );
1608
+ console.log(
1609
+ `[ingest] skipped unsupported=${skipped.unsupported} too_large=${skipped.tooLarge} binary=${skipped.binary}`
1610
+ );
1611
+ console.log(`[ingest] wrote cache + db import files under .context/`);
1612
+ state.manifest = manifest;
1613
+ }
1614
+
1615
+ export {
1616
+ createIngestPipelineState,
1617
+ runCacheWriteStage,
1618
+ runDatabaseWriteStage,
1619
+ runFileCacheStagingStage,
1620
+ runManifestCompletionStage,
1621
+ runMaterializationStage,
1622
+ runParseStage,
1623
+ runScanHydrationStage,
1624
+ runTokenMatchingStage
1625
+ };