@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.
- package/package.json +2 -2
- package/scaffold/mcp/src/contextEntities.ts +30 -19
- package/scaffold/mcp/src/loadGraph.ts +254 -242
- package/scaffold/mcp/src/search.ts +18 -7
- package/scaffold/mcp/src/searchResults.ts +138 -94
- package/scaffold/mcp/tests/search-graph-score.test.mjs +74 -1
- package/scaffold/scripts/ingest.mjs +43 -4
|
@@ -2,7 +2,7 @@ import { embedQuery, getEmbeddingRuntimeWarning, loadEmbeddingIndex } from "./em
|
|
|
2
2
|
import {
|
|
3
3
|
buildChunkPartOfRelations,
|
|
4
4
|
buildEntitySearchMap,
|
|
5
|
-
|
|
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 {
|
|
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 =
|
|
72
|
-
(
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
164
|
-
.sort((a, b) =>
|
|
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
|
|
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
|
-
|
|
3446
|
-
|
|
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
|
};
|