@danielblomma/cortex-mcp 2.2.1 → 2.2.3

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.
@@ -2,7 +2,7 @@ import { embedQuery, getEmbeddingRuntimeWarning, loadEmbeddingIndex } from "./em
2
2
  import {
3
3
  buildChunkPartOfRelations,
4
4
  buildEntitySearchMap,
5
- buildSearchEntities,
5
+ iterateSearchEntities,
6
6
  entityCatalog
7
7
  } from "./contextEntities.js";
8
8
  import { relationDegree } from "./graphMetrics.js";
@@ -19,7 +19,7 @@ import {
19
19
  semanticScore,
20
20
  tokenize
21
21
  } from "./searchCore.js";
22
- import { buildSearchResults } from "./searchResults.js";
22
+ import { buildSearchResultsWithStats } from "./searchResults.js";
23
23
  import { traverseImpactGraph } from "./impactTraversal.js";
24
24
  import { buildEmptyRelatedResponse, buildRelatedResponseMeta } from "./relatedResponse.js";
25
25
  import { traverseRelatedGraph } from "./relatedTraversal.js";
@@ -39,6 +39,7 @@ import type {
39
39
  ImpactParams,
40
40
  RelatedParams,
41
41
  RelationType,
42
+ SearchEntity,
42
43
  SearchParams,
43
44
  ToolPayload
44
45
  } from "./types.js";
@@ -57,6 +58,17 @@ const IMPACT_RELATION_TYPES = new Set([
57
58
  "PART_OF"
58
59
  ]);
59
60
 
61
+ function* filterSearchCandidates(
62
+ candidates: Iterable<SearchEntity>,
63
+ includeDeprecated: boolean
64
+ ): Generator<SearchEntity> {
65
+ for (const entity of candidates) {
66
+ if (includeDeprecated || entity.status.toLowerCase() !== "deprecated") {
67
+ yield entity;
68
+ }
69
+ }
70
+ }
71
+
60
72
  export async function runContextSearch(parsed: SearchParams): Promise<ToolPayload> {
61
73
  const searchPresetConfig = resolveSearchResponsePreset(parsed);
62
74
  const responsePreset = searchPresetConfig.responsePreset;
@@ -68,16 +80,15 @@ export async function runContextSearch(parsed: SearchParams): Promise<ToolPayloa
68
80
  const degreeByEntity = relationDegree(allRelations);
69
81
  const queryTokens = expandQueryTokens(Array.from(new Set(tokenize(parsed.query))));
70
82
  const queryPhrase = normalizeText(parsed.query).trim();
71
- const candidates = buildSearchEntities(data, includeContent).filter(
72
- (entity) => parsed.include_deprecated || entity.status.toLowerCase() !== "deprecated"
73
- );
83
+ const candidates = () =>
84
+ filterSearchCandidates(iterateSearchEntities(data, includeContent), parsed.include_deprecated);
74
85
  const embeddings = loadEmbeddingIndex();
75
86
  const queryVector =
76
87
  embeddings.model && embeddings.vectors.size > 0
77
88
  ? await embedQuery(parsed.query, embeddings.model)
78
89
  : null;
79
90
 
80
- const results = buildSearchResults({
91
+ const { results, totalCandidates } = buildSearchResultsWithStats({
81
92
  candidates,
82
93
  degreeByEntity,
83
94
  queryTokens,
@@ -107,7 +118,7 @@ export async function runContextSearch(parsed: SearchParams): Promise<ToolPayloa
107
118
  include_matched_rules: includeMatchedRules,
108
119
  include_content: includeContent,
109
120
  ranking: data.ranking,
110
- total_candidates: candidates.length,
121
+ total_candidates: totalCandidates,
111
122
  context_source: data.source,
112
123
  warning: warningMessages.length > 0 ? warningMessages.join(" | ") : undefined,
113
124
  semantic_engine:
@@ -1,5 +1,19 @@
1
1
  import type { RankingWeights, SearchEntity } from "./types.js";
2
2
 
3
+ type SearchResult = Record<string, unknown>;
4
+
5
+ type RankedSearchResult = {
6
+ result: SearchResult;
7
+ rankScore: number;
8
+ };
9
+
10
+ type SearchCandidateSource = Iterable<SearchEntity> | (() => Iterable<SearchEntity>);
11
+
12
+ type BaseChunkMetadata = {
13
+ label: string;
14
+ path: string;
15
+ };
16
+
3
17
  function isWindowChunkId(id: string): boolean {
4
18
  return id.includes(":window:");
5
19
  }
@@ -14,8 +28,26 @@ function baseChunkLabel(label: string): string {
14
28
  return markerIndex === -1 ? label : label.slice(0, markerIndex);
15
29
  }
16
30
 
17
- export function buildSearchResults(params: {
18
- candidates: SearchEntity[];
31
+ function pruneRankedResults(bestById: Map<string, RankedSearchResult>, limit: number): void {
32
+ if (bestById.size <= limit) {
33
+ return;
34
+ }
35
+
36
+ const retained = [...bestById.entries()]
37
+ .sort(([, a], [, b]) => b.rankScore - a.rankScore)
38
+ .slice(0, limit);
39
+ bestById.clear();
40
+ for (const [id, result] of retained) {
41
+ bestById.set(id, result);
42
+ }
43
+ }
44
+
45
+ function candidatesFrom(source: SearchCandidateSource): Iterable<SearchEntity> {
46
+ return typeof source === "function" ? source() : source;
47
+ }
48
+
49
+ export function buildSearchResultsWithStats(params: {
50
+ candidates: SearchCandidateSource;
19
51
  degreeByEntity: Map<string, number>;
20
52
  queryTokens: string[];
21
53
  queryPhrase: string;
@@ -32,7 +64,11 @@ export function buildSearchResults(params: {
32
64
  vectorScorer: (a: Float32Array, b: Float32Array) => number;
33
65
  recencyScorer: (isoDate: string) => number;
34
66
  legacyDataAccessBooster: (entity: SearchEntity, queryTokens: string[], queryPhrase: string) => number;
35
- }): Record<string, unknown>[] {
67
+ }): { results: Record<string, unknown>[]; totalCandidates: number } {
68
+ if (params.topK <= 0) {
69
+ return { results: [], totalCandidates: 0 };
70
+ }
71
+
36
72
  // Graph score = midrank percentile of relation degree within the entity's
37
73
  // own type. The previous min(1, degree/4) saturated at degree >= 4, which
38
74
  // nearly every entity exceeds, making the graph weight a constant. Per-type
@@ -40,7 +76,10 @@ export function buildSearchResults(params: {
40
76
  // (every type averages ~0.5), so hub-heavy types like rules cannot drown
41
77
  // out leaf code files or doc sections.
42
78
  const sortedDegreesByType = new Map<string, number[]>();
43
- for (const entity of params.candidates) {
79
+ const chunkCandidatesById = new Map<string, BaseChunkMetadata>();
80
+ let totalCandidates = 0;
81
+ for (const entity of candidatesFrom(params.candidates)) {
82
+ totalCandidates += 1;
44
83
  const degree = params.degreeByEntity.get(entity.id) ?? 0;
45
84
  const list = sortedDegreesByType.get(entity.entity_type);
46
85
  if (list) {
@@ -48,6 +87,13 @@ export function buildSearchResults(params: {
48
87
  } else {
49
88
  sortedDegreesByType.set(entity.entity_type, [degree]);
50
89
  }
90
+
91
+ if (entity.entity_type === "Chunk" && !isWindowChunkId(entity.id)) {
92
+ chunkCandidatesById.set(entity.id, {
93
+ label: entity.label,
94
+ path: entity.path
95
+ });
96
+ }
51
97
  }
52
98
  for (const list of sortedDegreesByType.values()) {
53
99
  list.sort((a, b) => a - b);
@@ -65,102 +111,100 @@ export function buildSearchResults(params: {
65
111
  return sorted.length > 0 ? (lo + upper) / (2 * sorted.length) : 0;
66
112
  };
67
113
 
68
- const rawResults = params.candidates
69
- .map((entity) => {
70
- const lexicalSemantic = params.semanticScorer(params.queryTokens, params.queryPhrase, entity.text);
71
- const entityVector = params.embeddingVectors.get(entity.id);
72
- const vectorSemantic =
73
- params.queryVector && entityVector
74
- ? Math.max(0, Math.min(1, params.vectorScorer(params.queryVector, entityVector)))
75
- : 0;
76
- const hasRelevanceSignal =
77
- lexicalSemantic >= params.minLexicalRelevance || vectorSemantic >= params.minVectorRelevance;
78
- if (!hasRelevanceSignal) {
79
- return null;
80
- }
114
+ const resultLimit = Math.max(1, params.topK);
115
+ const pruneThreshold = Math.max(resultLimit * 4, 64);
116
+ const bestById = new Map<string, RankedSearchResult>();
81
117
 
82
- const semantic =
83
- vectorSemantic > 0 ? vectorSemantic * 0.75 + lexicalSemantic * 0.25 : lexicalSemantic;
84
- const degree = params.degreeByEntity.get(entity.id) ?? 0;
85
- const graphScore = midrankPercentile(sortedDegreesByType.get(entity.entity_type) ?? [], degree);
86
- const trustScore = Math.max(0, Math.min(1, entity.trust_level / 100));
87
- const dateScore = params.recencyScorer(entity.updated_at);
88
-
89
- let score = 0;
90
- score += params.ranking.semantic * semantic;
91
- score += params.ranking.graph * graphScore;
92
- score += params.ranking.trust * trustScore;
93
- score += params.ranking.recency * dateScore;
94
- score += params.legacyDataAccessBooster(entity, params.queryTokens, params.queryPhrase);
95
-
96
- if (entity.source_of_truth) {
97
- score += 0.1 * semantic;
98
- }
118
+ for (const entity of candidatesFrom(params.candidates)) {
119
+ const lexicalSemantic = params.semanticScorer(params.queryTokens, params.queryPhrase, entity.text);
120
+ const entityVector = params.embeddingVectors.get(entity.id);
121
+ const vectorSemantic =
122
+ params.queryVector && entityVector
123
+ ? Math.max(0, Math.min(1, params.vectorScorer(params.queryVector, entityVector)))
124
+ : 0;
125
+ const hasRelevanceSignal =
126
+ lexicalSemantic >= params.minLexicalRelevance || vectorSemantic >= params.minVectorRelevance;
127
+ if (!hasRelevanceSignal) {
128
+ continue;
129
+ }
99
130
 
100
- return {
101
- id: entity.id,
102
- entity_type: entity.entity_type,
103
- kind: entity.kind,
104
- title: entity.label,
105
- path: entity.path || undefined,
106
- source_of_truth: entity.source_of_truth,
107
- status: entity.status,
108
- updated_at: entity.updated_at,
109
- excerpt: entity.snippet,
110
- ...(params.includeScores
111
- ? {
112
- score: Number(score.toFixed(4)),
113
- semantic_score: Number(semantic.toFixed(4)),
114
- embedding_score: Number(vectorSemantic.toFixed(4)),
115
- lexical_score: Number(lexicalSemantic.toFixed(4)),
116
- graph_score: Number(graphScore.toFixed(4))
117
- }
118
- : {}),
119
- ...(params.includeMatchedRules
120
- ? {
121
- matched_rules: entity.matched_rules
122
- }
123
- : {}),
124
- ...(params.includeContent
125
- ? {
126
- content: entity.content
127
- }
128
- : {})
129
- };
130
- })
131
- .filter((result): result is NonNullable<typeof result> => result !== null)
132
- .sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0));
133
-
134
- const chunkCandidatesById = new Map(
135
- params.candidates.filter((entity) => entity.entity_type === "Chunk").map((entity) => [entity.id, entity])
136
- );
137
- const normalizedById = new Map<string, (typeof rawResults)[number]>();
138
-
139
- for (const result of rawResults) {
140
- if (result.entity_type !== "Chunk" || !isWindowChunkId(String(result.id))) {
141
- if (!normalizedById.has(String(result.id))) {
142
- normalizedById.set(String(result.id), result);
131
+ const semantic =
132
+ vectorSemantic > 0 ? vectorSemantic * 0.75 + lexicalSemantic * 0.25 : lexicalSemantic;
133
+ const degree = params.degreeByEntity.get(entity.id) ?? 0;
134
+ const graphScore = midrankPercentile(sortedDegreesByType.get(entity.entity_type) ?? [], degree);
135
+ const trustScore = Math.max(0, Math.min(1, entity.trust_level / 100));
136
+ const dateScore = params.recencyScorer(entity.updated_at);
137
+
138
+ let score = 0;
139
+ score += params.ranking.semantic * semantic;
140
+ score += params.ranking.graph * graphScore;
141
+ score += params.ranking.trust * trustScore;
142
+ score += params.ranking.recency * dateScore;
143
+ score += params.legacyDataAccessBooster(entity, params.queryTokens, params.queryPhrase);
144
+
145
+ if (entity.source_of_truth) {
146
+ score += 0.1 * semantic;
147
+ }
148
+
149
+ const result: SearchResult = {
150
+ id: entity.id,
151
+ entity_type: entity.entity_type,
152
+ kind: entity.kind,
153
+ title: entity.label,
154
+ path: entity.path || undefined,
155
+ source_of_truth: entity.source_of_truth,
156
+ status: entity.status,
157
+ updated_at: entity.updated_at,
158
+ excerpt: entity.snippet,
159
+ ...(params.includeScores
160
+ ? {
161
+ score: Number(score.toFixed(4)),
162
+ semantic_score: Number(semantic.toFixed(4)),
163
+ embedding_score: Number(vectorSemantic.toFixed(4)),
164
+ lexical_score: Number(lexicalSemantic.toFixed(4)),
165
+ graph_score: Number(graphScore.toFixed(4))
166
+ }
167
+ : {}),
168
+ ...(params.includeMatchedRules
169
+ ? {
170
+ matched_rules: entity.matched_rules
171
+ }
172
+ : {}),
173
+ ...(params.includeContent
174
+ ? {
175
+ content: entity.content
176
+ }
177
+ : {})
178
+ };
179
+
180
+ if (entity.entity_type === "Chunk" && isWindowChunkId(entity.id)) {
181
+ const canonicalId = baseChunkId(entity.id);
182
+ const baseChunk = chunkCandidatesById.get(canonicalId);
183
+ if (baseChunk) {
184
+ result.id = canonicalId;
185
+ result.title = baseChunkLabel(baseChunk.label);
186
+ result.path = baseChunk.path || undefined;
143
187
  }
144
- continue;
145
188
  }
146
189
 
147
- const canonicalId = baseChunkId(String(result.id));
148
- const baseChunk = chunkCandidatesById.get(canonicalId);
149
- const normalizedResult = baseChunk
150
- ? {
151
- ...result,
152
- id: canonicalId,
153
- title: baseChunkLabel(baseChunk.label),
154
- path: baseChunk.path || undefined
155
- }
156
- : result;
157
- const existing = normalizedById.get(String(normalizedResult.id));
158
- if (!existing || Number(normalizedResult.score ?? 0) > Number(existing.score ?? 0)) {
159
- normalizedById.set(String(normalizedResult.id), normalizedResult);
190
+ const resultId = String(result.id);
191
+ const existing = bestById.get(resultId);
192
+ if (!existing || score > existing.rankScore) {
193
+ bestById.set(resultId, { result, rankScore: score });
194
+ }
195
+
196
+ if (bestById.size > pruneThreshold) {
197
+ pruneRankedResults(bestById, resultLimit);
160
198
  }
161
199
  }
162
200
 
163
- return [...normalizedById.values()]
164
- .sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0))
165
- .slice(0, params.topK);
201
+ const results = [...bestById.values()]
202
+ .sort((a, b) => b.rankScore - a.rankScore)
203
+ .slice(0, params.topK)
204
+ .map((ranked) => ranked.result);
205
+ return { results, totalCandidates };
206
+ }
207
+
208
+ export function buildSearchResults(params: Parameters<typeof buildSearchResultsWithStats>[0]): Record<string, unknown>[] {
209
+ return buildSearchResultsWithStats(params).results;
166
210
  }
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { buildSearchResults } from "../dist/searchResults.js";
3
+ import { buildSearchResults, buildSearchResultsWithStats } from "../dist/searchResults.js";
4
4
 
5
5
  function makeEntity(id, entityType, overrides = {}) {
6
6
  return {
@@ -130,3 +130,76 @@ test("graph score never saturates to a shared constant for common degrees", () =
130
130
 
131
131
  assert.equal(unique.size, 3);
132
132
  });
133
+
134
+ test("ranking is preserved when score fields are omitted from the response", () => {
135
+ const candidates = [
136
+ makeEntity("weak", "Chunk", { text: "weak query" }),
137
+ makeEntity("strong", "Chunk", { text: "strong query" }),
138
+ makeEntity("middle", "Chunk", { text: "middle query" })
139
+ ];
140
+
141
+ const results = buildSearchResults({
142
+ candidates,
143
+ degreeByEntity: new Map(),
144
+ queryTokens: ["query"],
145
+ queryPhrase: "query",
146
+ ranking: { semantic: 1, graph: 0, trust: 0, recency: 0 },
147
+ includeScores: false,
148
+ includeMatchedRules: false,
149
+ includeContent: false,
150
+ queryVector: null,
151
+ embeddingVectors: new Map(),
152
+ topK: 2,
153
+ minLexicalRelevance: 0,
154
+ minVectorRelevance: 0,
155
+ semanticScorer: (_tokens, _phrase, text) => {
156
+ if (text.includes("strong")) return 0.9;
157
+ if (text.includes("middle")) return 0.5;
158
+ return 0.1;
159
+ },
160
+ vectorScorer: () => 0,
161
+ recencyScorer: () => 0,
162
+ legacyDataAccessBooster: () => 0
163
+ });
164
+
165
+ assert.deepEqual(results.map((result) => result.id), ["strong", "middle"]);
166
+ assert.equal("score" in results[0], false);
167
+ });
168
+
169
+ test("search results can consume a repeatable candidate generator with stats", () => {
170
+ let passes = 0;
171
+ const candidateSource = function* () {
172
+ passes += 1;
173
+ yield makeEntity("weak", "Chunk", { text: "weak query" });
174
+ yield makeEntity("strong", "Chunk", { text: "strong query" });
175
+ yield makeEntity("middle", "Chunk", { text: "middle query" });
176
+ };
177
+
178
+ const { results, totalCandidates } = buildSearchResultsWithStats({
179
+ candidates: candidateSource,
180
+ degreeByEntity: new Map(),
181
+ queryTokens: ["query"],
182
+ queryPhrase: "query",
183
+ ranking: { semantic: 1, graph: 0, trust: 0, recency: 0 },
184
+ includeScores: false,
185
+ includeMatchedRules: false,
186
+ includeContent: false,
187
+ queryVector: null,
188
+ embeddingVectors: new Map(),
189
+ topK: 2,
190
+ minLexicalRelevance: 0,
191
+ minVectorRelevance: 0,
192
+ semanticScorer: (_tokens, _phrase, text) => {
193
+ if (text.includes("strong")) return 0.9;
194
+ if (text.includes("middle")) return 0.5;
195
+ return 0.1;
196
+ },
197
+ vectorScorer: () => 0,
198
+ recencyScorer: () => 0,
199
+ legacyDataAccessBooster: () => 0
200
+ });
201
+
202
+ assert.equal(passes, 2);
203
+ assert.equal(totalCandidates, 3);
204
+ assert.deepEqual(results.map((result) => result.id), ["strong", "middle"]);
205
+ });
@@ -758,6 +758,29 @@ function writeJsonl(filePath, records) {
758
758
  }
759
759
  }
760
760
 
761
+ function stageJsonl(filePath, records) {
762
+ const stagedPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
763
+ writeJsonl(stagedPath, records);
764
+ return {
765
+ commit() {
766
+ fs.renameSync(stagedPath, filePath);
767
+ },
768
+ discard() {
769
+ fs.rmSync(stagedPath, { force: true });
770
+ }
771
+ };
772
+ }
773
+
774
+ function countFileContentRecords(fileRecords) {
775
+ let count = 0;
776
+ for (const fileRecord of fileRecords) {
777
+ if (Object.prototype.hasOwnProperty.call(fileRecord, "content")) {
778
+ count += 1;
779
+ }
780
+ }
781
+ return count;
782
+ }
783
+
761
784
  function sanitizeTsvCell(value) {
762
785
  if (value === null || value === undefined) return "";
763
786
  return String(value).replace(/\t/g, " ").replace(/\r?\n/g, " ");
@@ -2288,7 +2311,8 @@ function resolveIngestWorkerCount(taskCount) {
2288
2311
  const raw = process.env.CORTEX_INGEST_WORKERS;
2289
2312
  const configured = raw !== undefined ? Number.parseInt(raw, 10) : Number.NaN;
2290
2313
  const cpuBudget = Math.max(1, (os.availableParallelism?.() ?? os.cpus().length) - 1);
2291
- const desired = Number.isFinite(configured) && configured >= 0 ? configured : Math.min(cpuBudget, 8);
2314
+ const defaultWorkerLimit = taskCount >= 1000 ? 4 : 8;
2315
+ const desired = Number.isFinite(configured) && configured >= 0 ? configured : Math.min(cpuBudget, defaultWorkerLimit);
2292
2316
  if (desired <= 1) return 1;
2293
2317
  // Worker spin-up plus per-worker WASM grammar init dominates on small or
2294
2318
  // incremental runs; stay sequential until there is enough work to amortize.
@@ -3352,6 +3376,12 @@ async function main() {
3352
3376
 
3353
3377
  const constrainsRelations = [];
3354
3378
  const implementsRelations = [];
3379
+ const stagedDocumentCache = stageJsonl(path.join(CACHE_DIR, "documents.jsonl"), fileRecords);
3380
+ const stagedFileEntityCache = stageJsonl(path.join(CACHE_DIR, "entities.file.jsonl"), fileRecords);
3381
+ memoryTrace.checkpoint("writes:file_cache_staged", {
3382
+ files: fileRecords.length,
3383
+ file_content_records: countFileContentRecords(fileRecords)
3384
+ });
3355
3385
  memoryTrace.checkpoint("tokens:rule_matching_start", {
3356
3386
  files: fileRecords.length,
3357
3387
  rules: ruleRecords.length
@@ -3365,9 +3395,10 @@ async function main() {
3365
3395
  const constrainsByKey = new Map();
3366
3396
  const implementsByKey = new Map();
3367
3397
  let fileTokenSetsProcessed = 0;
3398
+ let fileContentRecordsReleased = 0;
3368
3399
 
3369
3400
  for (const fileRecord of fileRecords) {
3370
- const lower = fileRecord.content.toLowerCase();
3401
+ const lower = String(fileRecord.content ?? "").toLowerCase();
3371
3402
  const tokens = fileTokenSet(fileRecord);
3372
3403
  fileTokenSetsProcessed += 1;
3373
3404
 
@@ -3406,6 +3437,11 @@ async function main() {
3406
3437
  }
3407
3438
  }
3408
3439
  }
3440
+
3441
+ if (Object.prototype.hasOwnProperty.call(fileRecord, "content")) {
3442
+ delete fileRecord.content;
3443
+ fileContentRecordsReleased += 1;
3444
+ }
3409
3445
  }
3410
3446
 
3411
3447
  for (const { ruleRecord } of ruleMatchers) {
@@ -3430,6 +3466,8 @@ async function main() {
3430
3466
  memoryTrace.checkpoint("tokens:rule_matching_complete", {
3431
3467
  file_token_sets: fileTokenSetsProcessed,
3432
3468
  file_token_sets_retained: 0,
3469
+ file_content_records_released: fileContentRecordsReleased,
3470
+ file_content_records_retained: countFileContentRecords(fileRecords),
3433
3471
  rules: ruleRecords.length,
3434
3472
  relations_constrains: constrainsRelations.length,
3435
3473
  relations_implements: implementsRelations.length,
@@ -3442,8 +3480,8 @@ async function main() {
3442
3480
  rules: ruleRecords.length,
3443
3481
  chunks: chunkRecords.length
3444
3482
  });
3445
- writeJsonl(path.join(CACHE_DIR, "documents.jsonl"), fileRecords);
3446
- writeJsonl(path.join(CACHE_DIR, "entities.file.jsonl"), fileRecords);
3483
+ stagedDocumentCache.commit();
3484
+ stagedFileEntityCache.commit();
3447
3485
  writeJsonl(path.join(CACHE_DIR, "entities.adr.jsonl"), adrRecords);
3448
3486
  writeJsonl(path.join(CACHE_DIR, "entities.rule.jsonl"), ruleRecords);
3449
3487
  writeJsonl(path.join(CACHE_DIR, "entities.chunk.jsonl"), chunkRecords);
@@ -3834,5 +3872,6 @@ export {
3834
3872
  generateSectionHandlerRelations,
3835
3873
  getChunkParserForExtension,
3836
3874
  parseFilesInWorkers,
3875
+ resolveIngestWorkerCount,
3837
3876
  resolveRelativeImportTargetId
3838
3877
  };