@danielblomma/cortex-mcp 2.2.2 → 2.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -413,12 +413,19 @@ Required npm configuration:
|
|
|
413
413
|
## Embedding performance
|
|
414
414
|
|
|
415
415
|
Embedding generation tunes itself to the machine: the number of parallel
|
|
416
|
-
workers, memory limits for long files, and skip-work caching are
|
|
417
|
-
from the available CPU cores and
|
|
418
|
-
included). No configuration is needed — on a laptop or
|
|
419
|
-
picks safe
|
|
420
|
-
|
|
421
|
-
|
|
416
|
+
workers, memory limits for long files, token budget, and skip-work caching are
|
|
417
|
+
all derived from the available CPU cores, RAM, and repository size at run time
|
|
418
|
+
(container memory limits included). No configuration is needed — on a laptop or
|
|
419
|
+
a CI runner, cortex picks safe settings by itself.
|
|
420
|
+
|
|
421
|
+
The embedding token budget defaults to `auto`, which preserves the embedding
|
|
422
|
+
model's own maximum context. Cortex does not lower the default token budget
|
|
423
|
+
just because a repository is large; lower caps can discard semantically useful
|
|
424
|
+
text and are reserved for explicit benchmark or quality experiments. To force a
|
|
425
|
+
specific capped run, set `CORTEX_EMBED_MAX_TOKENS` to a number such as `2048`;
|
|
426
|
+
set it to `model` or `full` to make the full-model baseline explicit. When
|
|
427
|
+
several cortex instances share one machine, set `CORTEX_EMBED_THREADS` to give
|
|
428
|
+
each its fair share of cores.
|
|
422
429
|
|
|
423
430
|
## Limitations
|
|
424
431
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danielblomma/cortex-mcp",
|
|
3
3
|
"mcpName": "io.github.DanielBlomma/cortex",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.4",
|
|
5
5
|
"description": "Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Daniel Blomma",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
],
|
|
65
65
|
"scripts": {
|
|
66
66
|
"pretest": "test -d scaffold/scripts/parsers/node_modules || npm --prefix scaffold/scripts/parsers install --no-fund --no-update-notifier --silent",
|
|
67
|
-
"test": "node tests/context-regressions.test.mjs && node --test tests/ingest-units.test.mjs tests/ingest-parallel.test.mjs tests/ingest-worker-crash.test.mjs tests/javascript-parser.test.mjs tests/markdown-parser.test.mjs tests/sql-parser.test.mjs tests/config-parser.test.mjs tests/resources-parser.test.mjs tests/vbnet-parser.test.mjs tests/cpp-parser.test.mjs tests/dashboard.test.mjs tests/init-config.test.mjs tests/init-agents.test.mjs tests/multi-level.test.mjs tests/no-legacy-paths.test.mjs tests/tree-sitter-error-reporting.test.mjs tests/tree-sitter-body-cap.test.mjs tests/tree-sitter-exported.test.mjs tests/tree-sitter-robustness.test.mjs tests/bootstrapbench-stats.test.mjs tests/bootstrapbench-aggregate.test.mjs tests/bootstrapbench-cleanup.test.mjs tests/query-cli-shim.test.mjs",
|
|
67
|
+
"test": "node tests/context-regressions.test.mjs && node --test tests/ingest-units.test.mjs tests/ingest-parallel.test.mjs tests/ingest-worker-crash.test.mjs tests/javascript-parser.test.mjs tests/markdown-parser.test.mjs tests/sql-parser.test.mjs tests/config-parser.test.mjs tests/resources-parser.test.mjs tests/vbnet-parser.test.mjs tests/cpp-parser.test.mjs tests/dashboard.test.mjs tests/init-config.test.mjs tests/init-agents.test.mjs tests/multi-level.test.mjs tests/no-legacy-paths.test.mjs tests/tree-sitter-error-reporting.test.mjs tests/tree-sitter-body-cap.test.mjs tests/tree-sitter-exported.test.mjs tests/tree-sitter-robustness.test.mjs tests/bootstrapbench-run.test.mjs tests/bootstrapbench-stats.test.mjs tests/bootstrapbench-aggregate.test.mjs tests/bootstrapbench-cleanup.test.mjs tests/query-cli-shim.test.mjs",
|
|
68
68
|
"release:sync-version": "node scripts/sync-release-version.mjs",
|
|
69
69
|
"release:check-version-sync": "node scripts/sync-release-version.mjs --check",
|
|
70
70
|
"prepublishOnly": "echo 'Ready to publish to npm'"
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
resolveMemoryHeadroom,
|
|
16
16
|
resolveModelMaxTokens,
|
|
17
17
|
resolvePoolConfig,
|
|
18
|
+
resolveTokenBudgetChoice,
|
|
18
19
|
runWorkUnits,
|
|
20
|
+
truncateTextToTokenBudget,
|
|
19
21
|
type EmbedExtractor,
|
|
20
22
|
type MeasuredText,
|
|
21
23
|
type PendingText
|
|
@@ -405,6 +407,14 @@ function roundVector(values: number[]): number[] {
|
|
|
405
407
|
return values.map((value) => Number(value.toFixed(6)));
|
|
406
408
|
}
|
|
407
409
|
|
|
410
|
+
function resolveSignatureProfile(maxTokenCap: number | null): string {
|
|
411
|
+
return maxTokenCap ? `embed|max_tokens=${maxTokenCap}` : "";
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function embeddingSignature(entitySignature: string, profile: string): string {
|
|
415
|
+
return profile ? hashText(`${profile}|${entitySignature}`) : entitySignature;
|
|
416
|
+
}
|
|
417
|
+
|
|
408
418
|
function* presentEmbeddingRecords(
|
|
409
419
|
slots: Array<EmbeddingRecord | null>
|
|
410
420
|
): Generator<EmbeddingRecord> {
|
|
@@ -438,6 +448,14 @@ async function main(): Promise<void> {
|
|
|
438
448
|
const chunks = parseChunkEntities(readJsonl(path.join(CACHE_DIR, "entities.chunk.jsonl")), filePathById);
|
|
439
449
|
|
|
440
450
|
const entities: SearchEntity[] = [...documents, ...rules, ...adrs, ...modules, ...projects, ...chunks].sort((a, b) => a.id.localeCompare(b.id));
|
|
451
|
+
const uniqueSignatures = new Set<string>();
|
|
452
|
+
for (const entity of entities) {
|
|
453
|
+
uniqueSignatures.add(entity.signature);
|
|
454
|
+
}
|
|
455
|
+
const uniqueTextCount = uniqueSignatures.size;
|
|
456
|
+
const tokenBudget = resolveTokenBudgetChoice(process.env.CORTEX_EMBED_MAX_TOKENS, uniqueTextCount);
|
|
457
|
+
uniqueSignatures.clear();
|
|
458
|
+
const signatureProfile = resolveSignatureProfile(tokenBudget.cap);
|
|
441
459
|
|
|
442
460
|
const existing = parseExistingEmbeddings(readJsonlRecords(EMBEDDINGS_PATH), modelId);
|
|
443
461
|
|
|
@@ -456,8 +474,9 @@ async function main(): Promise<void> {
|
|
|
456
474
|
let dimensions = 0;
|
|
457
475
|
|
|
458
476
|
entities.forEach((entity, index) => {
|
|
477
|
+
const signature = embeddingSignature(entity.signature, signatureProfile);
|
|
459
478
|
const previous = existing.get(entity.id);
|
|
460
|
-
if (previous && previous.signature ===
|
|
479
|
+
if (previous && previous.signature === signature && previous.vector.length > 0) {
|
|
461
480
|
reused += 1;
|
|
462
481
|
dimensions = dimensions || previous.vector.length;
|
|
463
482
|
slots[index] = {
|
|
@@ -470,7 +489,7 @@ async function main(): Promise<void> {
|
|
|
470
489
|
source_of_truth: entity.source_of_truth,
|
|
471
490
|
trust_level: entity.trust_level,
|
|
472
491
|
updated_at: entity.updated_at,
|
|
473
|
-
signature
|
|
492
|
+
signature,
|
|
474
493
|
model: modelId,
|
|
475
494
|
dimensions: previous.vector.length
|
|
476
495
|
};
|
|
@@ -522,6 +541,7 @@ async function main(): Promise<void> {
|
|
|
522
541
|
// Fully warm cache: nothing to embed, so skip model loading entirely —
|
|
523
542
|
// this is the common repeat-bootstrap / small-update path.
|
|
524
543
|
let embedded = 0;
|
|
544
|
+
let modelMaxTokensUsed = 0;
|
|
525
545
|
let result: { vectors: Map<number, number[]>; failures: Array<{ index: number; message: string }> } = {
|
|
526
546
|
vectors: new Map(),
|
|
527
547
|
failures: []
|
|
@@ -569,11 +589,22 @@ async function main(): Promise<void> {
|
|
|
569
589
|
|
|
570
590
|
// Inference truncates at the model max; token counts must too, or one
|
|
571
591
|
// giant file inflates scheduling cost and gate mass far beyond reality.
|
|
572
|
-
const modelMaxTokens = resolveModelMaxTokens(
|
|
592
|
+
const modelMaxTokens = resolveModelMaxTokens(
|
|
593
|
+
first.tokenizer?.model_max_length,
|
|
594
|
+
tokenBudget.cap ?? undefined
|
|
595
|
+
);
|
|
596
|
+
modelMaxTokensUsed = modelMaxTokens;
|
|
573
597
|
const countTokens = createTokenCounter(first.tokenizer, modelMaxTokens);
|
|
598
|
+
const countRawTokens = createTokenCounter(first.tokenizer, 131072);
|
|
574
599
|
|
|
575
|
-
const measured: MeasuredText[] = unique.map((item) =>
|
|
600
|
+
const measured: MeasuredText[] = unique.map((item) => {
|
|
601
|
+
const text = truncateTextToTokenBudget(item.text, countRawTokens, modelMaxTokens);
|
|
602
|
+
return { ...item, text, tokens: countTokens(text) };
|
|
603
|
+
});
|
|
576
604
|
const units = packWorkUnits(measured, schedulerOptions);
|
|
605
|
+
console.log(
|
|
606
|
+
`[embed] scheduler unique=${unique.length} units=${units.length} max_tokens<=${modelMaxTokens} token_budget=${tokenBudget.mode} reason=${tokenBudget.reason}`
|
|
607
|
+
);
|
|
577
608
|
// Recompute headroom after the model copies are resident so the gate
|
|
578
609
|
// reflects what is actually left for inference activations.
|
|
579
610
|
const inFlightRaw = Number(process.env.CORTEX_EMBED_INFLIGHT_TOKENS);
|
|
@@ -598,7 +629,7 @@ async function main(): Promise<void> {
|
|
|
598
629
|
source_of_truth: entity.source_of_truth,
|
|
599
630
|
trust_level: entity.trust_level,
|
|
600
631
|
updated_at: entity.updated_at,
|
|
601
|
-
signature: entity.signature,
|
|
632
|
+
signature: embeddingSignature(entity.signature, signatureProfile),
|
|
602
633
|
model: modelId,
|
|
603
634
|
dimensions: vector.length,
|
|
604
635
|
vector
|
|
@@ -632,7 +663,7 @@ async function main(): Promise<void> {
|
|
|
632
663
|
fs.writeFileSync(EMBEDDINGS_MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
633
664
|
|
|
634
665
|
console.log(
|
|
635
|
-
`[embed] mode=${mode} model=${modelId} dim=${dimensions} pool=${poolConfig.sessions}x${poolConfig.threadsPerSession} batch<=${schedulerOptions.batchMaxItems}`
|
|
666
|
+
`[embed] mode=${mode} model=${modelId} dim=${dimensions} pool=${poolConfig.sessions}x${poolConfig.threadsPerSession} batch<=${schedulerOptions.batchMaxItems} max_tokens<=${modelMaxTokensUsed || tokenBudget.cap || "model"} token_budget=${tokenBudget.mode}`
|
|
636
667
|
);
|
|
637
668
|
console.log(
|
|
638
669
|
`[embed] entities=${entities.length} embedded=${embedded} reused=${reused} failed=${failed}`
|
|
@@ -183,9 +183,46 @@ export function resolveMemoryHeadroom({
|
|
|
183
183
|
/** Sanitizes a tokenizer-reported maximum sequence length. Sentinel and
|
|
184
184
|
* absurd values (some configs report 1e30) fall back to 8192; anything
|
|
185
185
|
* beyond 128k tokens is treated as absurd. */
|
|
186
|
-
export function resolveModelMaxTokens(raw: unknown): number {
|
|
186
|
+
export function resolveModelMaxTokens(raw: unknown, overrideRaw?: unknown): number {
|
|
187
187
|
const value = Number(raw);
|
|
188
|
-
|
|
188
|
+
const modelMax = Number.isFinite(value) && value >= 1 && value <= 131072 ? Math.floor(value) : 8192;
|
|
189
|
+
const override = Number(overrideRaw);
|
|
190
|
+
if (Number.isFinite(override) && override >= 16 && override <= 131072) {
|
|
191
|
+
return Math.min(modelMax, Math.floor(override));
|
|
192
|
+
}
|
|
193
|
+
return modelMax;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export type TokenBudgetChoice = {
|
|
197
|
+
cap: number | null;
|
|
198
|
+
mode: "auto" | "explicit" | "model";
|
|
199
|
+
reason: string;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Chooses the requested embedding token budget before model load, so cache
|
|
204
|
+
* signatures can include the same cap that inference will later enforce.
|
|
205
|
+
* Auto mode is quality-preserving: it uses the model's own maximum by default
|
|
206
|
+
* and only truncates when the user explicitly requests a numeric cap.
|
|
207
|
+
*/
|
|
208
|
+
export function resolveTokenBudgetChoice(overrideRaw: unknown, uniqueCount: number): TokenBudgetChoice {
|
|
209
|
+
const raw = String(overrideRaw ?? "").trim().toLowerCase();
|
|
210
|
+
const override = Number(raw);
|
|
211
|
+
if (Number.isFinite(override) && override >= 16 && override <= 131072) {
|
|
212
|
+
const cap = Math.floor(override);
|
|
213
|
+
return { cap, mode: "explicit", reason: "env_override" };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (raw === "model" || raw === "none" || raw === "off" || raw === "full") {
|
|
217
|
+
return { cap: null, mode: "model", reason: "env_full_model" };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const count = Number.isFinite(uniqueCount) && uniqueCount > 0 ? Math.floor(uniqueCount) : 0;
|
|
221
|
+
return {
|
|
222
|
+
cap: null,
|
|
223
|
+
mode: "auto",
|
|
224
|
+
reason: `quality_preserving_model_max:unique=${count}`
|
|
225
|
+
};
|
|
189
226
|
}
|
|
190
227
|
|
|
191
228
|
/**
|
|
@@ -216,6 +253,29 @@ export function createTokenCounter(
|
|
|
216
253
|
};
|
|
217
254
|
}
|
|
218
255
|
|
|
256
|
+
export function truncateTextToTokenBudget(
|
|
257
|
+
text: string,
|
|
258
|
+
countTokens: (text: string) => number,
|
|
259
|
+
maxTokens: number
|
|
260
|
+
): string {
|
|
261
|
+
if (!Number.isFinite(maxTokens) || maxTokens < 1 || countTokens(text) <= maxTokens) {
|
|
262
|
+
return text;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let low = 0;
|
|
266
|
+
let high = text.length;
|
|
267
|
+
while (low < high) {
|
|
268
|
+
const mid = Math.ceil((low + high) / 2);
|
|
269
|
+
if (countTokens(text.slice(0, mid)) <= maxTokens) {
|
|
270
|
+
low = mid;
|
|
271
|
+
} else {
|
|
272
|
+
high = mid - 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return text.slice(0, low).trimEnd();
|
|
277
|
+
}
|
|
278
|
+
|
|
219
279
|
/** Estimated working memory one full pool session needs (model copy plus
|
|
220
280
|
* inference activations for a base-size model). Used only as a derating
|
|
221
281
|
* heuristic; deliberately conservative. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import ryugraph, { type Connection, type PreparedStatement, type QueryResult, type RyuValue } from "ryugraph";
|
|
4
|
-
import { readJsonl, asString, asNumber, asBoolean } from "./jsonl.js";
|
|
4
|
+
import { readJsonl, readJsonlRecords, asString, asNumber, asBoolean } from "./jsonl.js";
|
|
5
5
|
import { CACHE_DIR, CONTEXT_DIR, DB_PATH } from "./paths.js";
|
|
6
6
|
import { CSV_COPY_OPTIONS, writeCsv, toCopyPathLiteral, type CsvValue } from "./graphCsv.js";
|
|
7
7
|
import type { JsonObject } from "./types.js";
|
|
@@ -127,232 +127,244 @@ function readEntityFile(fileName: string): JsonObject[] {
|
|
|
127
127
|
return readJsonl(path.join(CACHE_DIR, fileName));
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
function
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
function readEntityRecords(fileName: string): Iterable<JsonObject> {
|
|
131
|
+
return readJsonlRecords(path.join(CACHE_DIR, fileName));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function* parseFiles(raw: Iterable<JsonObject>): Generator<FileEntity> {
|
|
135
|
+
for (const item of raw) {
|
|
136
|
+
const id = asString(item.id);
|
|
137
|
+
const filePath = asString(item.path);
|
|
138
|
+
if (!id || !filePath) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
138
141
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.filter((value): value is FileEntity => value !== null);
|
|
142
|
+
yield {
|
|
143
|
+
id,
|
|
144
|
+
path: filePath,
|
|
145
|
+
kind: asString(item.kind, "DOC"),
|
|
146
|
+
excerpt: asString(item.excerpt),
|
|
147
|
+
checksum: asString(item.checksum),
|
|
148
|
+
updated_at: asString(item.updated_at),
|
|
149
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
150
|
+
trust_level: asNumber(item.trust_level, 50),
|
|
151
|
+
status: asString(item.status, "active")
|
|
152
|
+
};
|
|
153
|
+
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
function parseRules(raw: JsonObject
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
156
|
+
function* parseRules(raw: Iterable<JsonObject>): Generator<RuleEntity> {
|
|
157
|
+
for (const item of raw) {
|
|
158
|
+
const id = asString(item.id);
|
|
159
|
+
if (!id) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.filter((value): value is RuleEntity => value !== null);
|
|
163
|
+
yield {
|
|
164
|
+
id,
|
|
165
|
+
title: asString(item.title, id),
|
|
166
|
+
body: asString(item.body),
|
|
167
|
+
scope: asString(item.scope, "global"),
|
|
168
|
+
updated_at: asString(item.updated_at),
|
|
169
|
+
source_of_truth: asBoolean(item.source_of_truth, true),
|
|
170
|
+
trust_level: asNumber(item.trust_level, 95),
|
|
171
|
+
status: asString(item.status, "active"),
|
|
172
|
+
priority: asNumber(item.priority, 0)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
function parseAdrs(raw: JsonObject
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
177
|
+
function* parseAdrs(raw: Iterable<JsonObject>): Generator<AdrEntity> {
|
|
178
|
+
for (const item of raw) {
|
|
179
|
+
const id = asString(item.id);
|
|
180
|
+
if (!id) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
184
183
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
.filter((value): value is AdrEntity => value !== null);
|
|
184
|
+
yield {
|
|
185
|
+
id,
|
|
186
|
+
path: asString(item.path),
|
|
187
|
+
title: asString(item.title, id),
|
|
188
|
+
body: asString(item.body),
|
|
189
|
+
decision_date: asString(item.decision_date),
|
|
190
|
+
supersedes_id: asString(item.supersedes_id),
|
|
191
|
+
source_of_truth: asBoolean(item.source_of_truth, true),
|
|
192
|
+
trust_level: asNumber(item.trust_level, 95),
|
|
193
|
+
status: asString(item.status, "active")
|
|
194
|
+
};
|
|
195
|
+
}
|
|
198
196
|
}
|
|
199
197
|
|
|
200
|
-
function parseChunks(raw: JsonObject
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
198
|
+
function* parseChunks(raw: Iterable<JsonObject>): Generator<ChunkEntity> {
|
|
199
|
+
for (const item of raw) {
|
|
200
|
+
const id = asString(item.id);
|
|
201
|
+
const file_id = asString(item.file_id);
|
|
202
|
+
const name = asString(item.name);
|
|
203
|
+
if (!id || !file_id || !name) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
209
206
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
.filter((value): value is ChunkEntity => value !== null);
|
|
207
|
+
yield {
|
|
208
|
+
id,
|
|
209
|
+
file_id,
|
|
210
|
+
name,
|
|
211
|
+
kind: asString(item.kind, "function"),
|
|
212
|
+
signature: asString(item.signature),
|
|
213
|
+
body: asString(item.body),
|
|
214
|
+
description: asString(item.description),
|
|
215
|
+
start_line: asNumber(item.start_line, 0),
|
|
216
|
+
end_line: asNumber(item.end_line, 0),
|
|
217
|
+
language: asString(item.language, "javascript"),
|
|
218
|
+
exported: asBoolean(item.exported, false),
|
|
219
|
+
checksum: asString(item.checksum),
|
|
220
|
+
updated_at: asString(item.updated_at),
|
|
221
|
+
source_of_truth: asBoolean(item.source_of_truth, true),
|
|
222
|
+
trust_level: asNumber(item.trust_level, 80),
|
|
223
|
+
status: asString(item.status, "active")
|
|
224
|
+
};
|
|
225
|
+
}
|
|
230
226
|
}
|
|
231
227
|
|
|
232
|
-
function parseModules(raw: JsonObject
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
228
|
+
function* parseModules(raw: Iterable<JsonObject>): Generator<ModuleEntity> {
|
|
229
|
+
for (const item of raw) {
|
|
230
|
+
const id = asString(item.id);
|
|
231
|
+
if (!id) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
239
234
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
.filter((value): value is ModuleEntity => value !== null);
|
|
235
|
+
yield {
|
|
236
|
+
id,
|
|
237
|
+
path: asString(item.path),
|
|
238
|
+
name: asString(item.name),
|
|
239
|
+
summary: asString(item.summary),
|
|
240
|
+
file_count: asNumber(item.file_count, 0),
|
|
241
|
+
exported_symbols: asString(item.exported_symbols),
|
|
242
|
+
updated_at: asString(item.updated_at),
|
|
243
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
244
|
+
trust_level: asNumber(item.trust_level, 75),
|
|
245
|
+
status: asString(item.status, "active")
|
|
246
|
+
};
|
|
247
|
+
}
|
|
254
248
|
}
|
|
255
249
|
|
|
256
|
-
function parseProjects(raw: JsonObject
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
250
|
+
function* parseProjects(raw: Iterable<JsonObject>): Generator<ProjectEntity> {
|
|
251
|
+
for (const item of raw) {
|
|
252
|
+
const id = asString(item.id);
|
|
253
|
+
if (!id) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
263
256
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
257
|
+
yield {
|
|
258
|
+
id,
|
|
259
|
+
path: asString(item.path),
|
|
260
|
+
name: asString(item.name),
|
|
261
|
+
kind: asString(item.kind, "project"),
|
|
262
|
+
language: asString(item.language, "dotnet"),
|
|
263
|
+
target_framework: asString(item.target_framework),
|
|
264
|
+
summary: asString(item.summary),
|
|
265
|
+
file_count: asNumber(item.file_count, 0),
|
|
266
|
+
updated_at: asString(item.updated_at),
|
|
267
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
268
|
+
trust_level: asNumber(item.trust_level, 80),
|
|
269
|
+
status: asString(item.status, "active")
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function* parseRelationRecords(raw: Iterable<JsonObject>, noteField: string): Generator<Relation> {
|
|
275
|
+
for (const item of raw) {
|
|
276
|
+
const from = asString(item.from);
|
|
277
|
+
const to = asString(item.to);
|
|
278
|
+
if (!from || !to) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
yield {
|
|
283
|
+
from,
|
|
284
|
+
to,
|
|
285
|
+
note: asString(item[noteField as keyof JsonObject])
|
|
286
|
+
};
|
|
287
|
+
}
|
|
280
288
|
}
|
|
281
289
|
|
|
282
290
|
function parseRelations(fileName: string, noteField: string): Relation[] {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
+
return Array.from(parseRelationRecords(readEntityRecords(fileName), noteField));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function* relationRecords(fileName: string, noteField: string): Generator<Relation> {
|
|
295
|
+
yield* parseRelationRecords(readEntityRecords(fileName), noteField);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function* parseCallRelationRecords(raw: Iterable<JsonObject>): Generator<CallRelation> {
|
|
299
|
+
for (const item of raw) {
|
|
300
|
+
const from = asString(item.from);
|
|
301
|
+
const to = asString(item.to);
|
|
302
|
+
if (!from || !to) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
291
305
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
.filter((value): value is Relation => value !== null);
|
|
306
|
+
yield {
|
|
307
|
+
from,
|
|
308
|
+
to,
|
|
309
|
+
call_type: asString(item.call_type, "direct")
|
|
310
|
+
};
|
|
311
|
+
}
|
|
299
312
|
}
|
|
300
313
|
|
|
301
314
|
function parseCallRelations(fileName: string): CallRelation[] {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
315
|
+
return Array.from(parseCallRelationRecords(readEntityRecords(fileName)));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function* callRelationRecords(fileName: string): Generator<CallRelation> {
|
|
319
|
+
yield* parseCallRelationRecords(readEntityRecords(fileName));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function* parseImportRelationRecords(raw: Iterable<JsonObject>): Generator<ImportRelation> {
|
|
323
|
+
for (const item of raw) {
|
|
324
|
+
const from = asString(item.from);
|
|
325
|
+
const to = asString(item.to);
|
|
326
|
+
if (!from || !to) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
310
329
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
.filter((value): value is CallRelation => value !== null);
|
|
330
|
+
yield {
|
|
331
|
+
from,
|
|
332
|
+
to,
|
|
333
|
+
import_name: asString(item.import_name, "")
|
|
334
|
+
};
|
|
335
|
+
}
|
|
318
336
|
}
|
|
319
337
|
|
|
320
338
|
function parseImportRelations(fileName: string): ImportRelation[] {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
339
|
+
return Array.from(parseImportRelationRecords(readEntityRecords(fileName)));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function* importRelationRecords(fileName: string): Generator<ImportRelation> {
|
|
343
|
+
yield* parseImportRelationRecords(readEntityRecords(fileName));
|
|
344
|
+
}
|
|
329
345
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
346
|
+
function* parseSimpleRelationRecords(raw: Iterable<JsonObject>): Generator<Relation> {
|
|
347
|
+
for (const item of raw) {
|
|
348
|
+
const from = asString(item.from);
|
|
349
|
+
const to = asString(item.to);
|
|
350
|
+
if (!from || !to) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
yield {
|
|
355
|
+
from,
|
|
356
|
+
to,
|
|
357
|
+
note: ""
|
|
358
|
+
};
|
|
359
|
+
}
|
|
337
360
|
}
|
|
338
361
|
|
|
339
362
|
function parseSimpleRelations(fileName: string): Relation[] {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
.map((item) => {
|
|
343
|
-
const from = asString(item.from);
|
|
344
|
-
const to = asString(item.to);
|
|
345
|
-
if (!from || !to) {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
363
|
+
return Array.from(parseSimpleRelationRecords(readEntityRecords(fileName)));
|
|
364
|
+
}
|
|
348
365
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
to,
|
|
352
|
-
note: ""
|
|
353
|
-
};
|
|
354
|
-
})
|
|
355
|
-
.filter((value): value is Relation => value !== null);
|
|
366
|
+
function* simpleRelationRecords(fileName: string): Generator<Relation> {
|
|
367
|
+
yield* parseSimpleRelationRecords(readEntityRecords(fileName));
|
|
356
368
|
}
|
|
357
369
|
|
|
358
370
|
async function rows(result: QueryResult | QueryResult[]): Promise<Record<string, unknown>[]> {
|
|
@@ -460,12 +472,12 @@ type GraphData = {
|
|
|
460
472
|
|
|
461
473
|
function parseGraphData(): GraphData {
|
|
462
474
|
return {
|
|
463
|
-
fileEntities: parseFiles(readEntityFile("entities.file.jsonl")),
|
|
464
|
-
ruleEntities: parseRules(readEntityFile("entities.rule.jsonl")),
|
|
465
|
-
adrEntities: parseAdrs(readEntityFile("entities.adr.jsonl")),
|
|
466
|
-
chunkEntities: parseChunks(readEntityFile("entities.chunk.jsonl")),
|
|
467
|
-
moduleEntities: parseModules(readEntityFile("entities.module.jsonl")),
|
|
468
|
-
projectEntities: parseProjects(readEntityFile("entities.project.jsonl")),
|
|
475
|
+
fileEntities: Array.from(parseFiles(readEntityFile("entities.file.jsonl"))),
|
|
476
|
+
ruleEntities: Array.from(parseRules(readEntityFile("entities.rule.jsonl"))),
|
|
477
|
+
adrEntities: Array.from(parseAdrs(readEntityFile("entities.adr.jsonl"))),
|
|
478
|
+
chunkEntities: Array.from(parseChunks(readEntityFile("entities.chunk.jsonl"))),
|
|
479
|
+
moduleEntities: Array.from(parseModules(readEntityFile("entities.module.jsonl"))),
|
|
480
|
+
projectEntities: Array.from(parseProjects(readEntityFile("entities.project.jsonl"))),
|
|
469
481
|
constrains: parseRelations("relations.constrains.jsonl", "note"),
|
|
470
482
|
implementsEdges: parseRelations("relations.implements.jsonl", "note"),
|
|
471
483
|
supersedes: parseRelations("relations.supersedes.jsonl", "reason"),
|
|
@@ -506,9 +518,10 @@ function filterEdges<T extends { from: string; to: string }>(
|
|
|
506
518
|
})();
|
|
507
519
|
}
|
|
508
520
|
|
|
509
|
-
function*
|
|
521
|
+
function* collectIds<T extends { id: string }>(items: Iterable<T>, collected: Set<string>): Iterable<T> {
|
|
510
522
|
for (const item of items) {
|
|
511
|
-
|
|
523
|
+
collected.add(item.id);
|
|
524
|
+
yield item;
|
|
512
525
|
}
|
|
513
526
|
}
|
|
514
527
|
|
|
@@ -537,28 +550,28 @@ async function copyTable(
|
|
|
537
550
|
await conn.query(`COPY ${table} FROM "${csvForCopy}" ${CSV_COPY_OPTIONS};`);
|
|
538
551
|
}
|
|
539
552
|
|
|
540
|
-
// Bulk path: one COPY per table instead of a prepared statement per row.
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
async function bulkLoad(conn: Connection
|
|
553
|
+
// Bulk path: one COPY per table instead of a prepared statement per row. Only
|
|
554
|
+
// used on the reset path, where the freshly created tables are empty. It reads
|
|
555
|
+
// node and relation JSONL streams directly into CSVs so the hot path retains
|
|
556
|
+
// only endpoint id sets rather than a full parsed GraphData object.
|
|
557
|
+
async function bulkLoad(conn: Connection): Promise<void> {
|
|
545
558
|
fs.rmSync(GRAPH_IMPORT_DIR, { recursive: true, force: true });
|
|
546
559
|
fs.mkdirSync(GRAPH_IMPORT_DIR, { recursive: true });
|
|
547
560
|
|
|
548
561
|
try {
|
|
549
|
-
const fileIds = new Set(
|
|
550
|
-
const ruleIds = new Set(
|
|
551
|
-
const adrIds = new Set(
|
|
552
|
-
const chunkIds = new Set(
|
|
553
|
-
const moduleIds = new Set(
|
|
554
|
-
const projectIds = new Set(
|
|
562
|
+
const fileIds = new Set<string>();
|
|
563
|
+
const ruleIds = new Set<string>();
|
|
564
|
+
const adrIds = new Set<string>();
|
|
565
|
+
const chunkIds = new Set<string>();
|
|
566
|
+
const moduleIds = new Set<string>();
|
|
567
|
+
const projectIds = new Set<string>();
|
|
555
568
|
|
|
556
569
|
// Nodes first — edges reference them by primary key.
|
|
557
570
|
await copyTable(
|
|
558
571
|
conn,
|
|
559
572
|
"File",
|
|
560
573
|
["id", "path", "kind", "excerpt", "checksum", "updated_at", "source_of_truth", "trust_level", "status"],
|
|
561
|
-
csvRows(
|
|
574
|
+
csvRows(collectIds(parseFiles(readEntityRecords("entities.file.jsonl")), fileIds), (e) => [
|
|
562
575
|
e.id, e.path, e.kind, e.excerpt, e.checksum, e.updated_at, e.source_of_truth, e.trust_level, e.status
|
|
563
576
|
])
|
|
564
577
|
);
|
|
@@ -566,7 +579,7 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
|
|
|
566
579
|
conn,
|
|
567
580
|
"Rule",
|
|
568
581
|
["id", "title", "body", "scope", "priority", "updated_at", "source_of_truth", "trust_level", "status"],
|
|
569
|
-
csvRows(
|
|
582
|
+
csvRows(collectIds(parseRules(readEntityRecords("entities.rule.jsonl")), ruleIds), (e) => [
|
|
570
583
|
e.id, e.title, e.body, e.scope, e.priority, e.updated_at, e.source_of_truth, e.trust_level, e.status
|
|
571
584
|
])
|
|
572
585
|
);
|
|
@@ -574,7 +587,7 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
|
|
|
574
587
|
conn,
|
|
575
588
|
"ADR",
|
|
576
589
|
["id", "path", "title", "body", "decision_date", "supersedes_id", "source_of_truth", "trust_level", "status"],
|
|
577
|
-
csvRows(
|
|
590
|
+
csvRows(collectIds(parseAdrs(readEntityRecords("entities.adr.jsonl")), adrIds), (e) => [
|
|
578
591
|
e.id, e.path, e.title, e.body, e.decision_date, e.supersedes_id, e.source_of_truth, e.trust_level, e.status
|
|
579
592
|
])
|
|
580
593
|
);
|
|
@@ -585,7 +598,7 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
|
|
|
585
598
|
"id", "file_id", "name", "kind", "signature", "body", "description", "start_line", "end_line",
|
|
586
599
|
"language", "exported", "checksum", "updated_at", "source_of_truth", "trust_level", "status"
|
|
587
600
|
],
|
|
588
|
-
csvRows(
|
|
601
|
+
csvRows(collectIds(parseChunks(readEntityRecords("entities.chunk.jsonl")), chunkIds), (e) => [
|
|
589
602
|
e.id, e.file_id, e.name, e.kind, e.signature, e.body, e.description, e.start_line, e.end_line,
|
|
590
603
|
e.language, e.exported, e.checksum, e.updated_at, e.source_of_truth, e.trust_level, e.status
|
|
591
604
|
])
|
|
@@ -594,7 +607,7 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
|
|
|
594
607
|
conn,
|
|
595
608
|
"Module",
|
|
596
609
|
["id", "path", "name", "summary", "file_count", "exported_symbols", "updated_at", "source_of_truth", "trust_level", "status"],
|
|
597
|
-
csvRows(
|
|
610
|
+
csvRows(collectIds(parseModules(readEntityRecords("entities.module.jsonl")), moduleIds), (e) => [
|
|
598
611
|
e.id, e.path, e.name, e.summary, e.file_count, e.exported_symbols, e.updated_at, e.source_of_truth, e.trust_level, e.status
|
|
599
612
|
])
|
|
600
613
|
);
|
|
@@ -605,7 +618,7 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
|
|
|
605
618
|
"id", "path", "name", "kind", "language", "target_framework", "summary", "file_count",
|
|
606
619
|
"updated_at", "source_of_truth", "trust_level", "status"
|
|
607
620
|
],
|
|
608
|
-
csvRows(
|
|
621
|
+
csvRows(collectIds(parseProjects(readEntityRecords("entities.project.jsonl")), projectIds), (e) => [
|
|
609
622
|
e.id, e.path, e.name, e.kind, e.language, e.target_framework, e.summary, e.file_count,
|
|
610
623
|
e.updated_at, e.source_of_truth, e.trust_level, e.status
|
|
611
624
|
])
|
|
@@ -615,43 +628,43 @@ async function bulkLoad(conn: Connection, data: GraphData): Promise<void> {
|
|
|
615
628
|
// (COPY into a rel table reads src key, dst key, then properties in schema
|
|
616
629
|
// order), but emitting real names keeps the CSVs self-describing.
|
|
617
630
|
await copyTable(conn, "CONSTRAINS", ["from", "to", "note"],
|
|
618
|
-
csvRows(filterEdges(
|
|
631
|
+
csvRows(filterEdges(relationRecords("relations.constrains.jsonl", "note"), ruleIds, fileIds), (e) => [e.from, e.to, e.note]));
|
|
619
632
|
await copyTable(conn, "IMPLEMENTS", ["from", "to", "note"],
|
|
620
|
-
csvRows(filterEdges(
|
|
633
|
+
csvRows(filterEdges(relationRecords("relations.implements.jsonl", "note"), fileIds, ruleIds), (e) => [e.from, e.to, e.note]));
|
|
621
634
|
await copyTable(conn, "SUPERSEDES", ["from", "to", "reason"],
|
|
622
|
-
csvRows(filterEdges(
|
|
635
|
+
csvRows(filterEdges(relationRecords("relations.supersedes.jsonl", "reason"), adrIds, adrIds), (e) => [e.from, e.to, e.note]));
|
|
623
636
|
await copyTable(conn, "DEFINES", ["from", "to"],
|
|
624
|
-
csvRows(filterEdges(
|
|
637
|
+
csvRows(filterEdges(simpleRelationRecords("relations.defines.jsonl"), fileIds, chunkIds), (e) => [e.from, e.to]));
|
|
625
638
|
await copyTable(conn, "CALLS", ["from", "to", "call_type"],
|
|
626
|
-
csvRows(filterEdges(
|
|
639
|
+
csvRows(filterEdges(callRelationRecords("relations.calls.jsonl"), chunkIds, chunkIds), (e) => [e.from, e.to, e.call_type]));
|
|
627
640
|
await copyTable(conn, "IMPORTS", ["from", "to", "import_name"],
|
|
628
|
-
csvRows(filterEdges(
|
|
641
|
+
csvRows(filterEdges(importRelationRecords("relations.imports.jsonl"), chunkIds, fileIds), (e) => [e.from, e.to, e.import_name]));
|
|
629
642
|
await copyTable(conn, "CALLS_SQL", ["from", "to", "note"],
|
|
630
|
-
csvRows(filterEdges(
|
|
643
|
+
csvRows(filterEdges(relationRecords("relations.calls_sql.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
|
|
631
644
|
await copyTable(conn, "USES_CONFIG_KEY", ["from", "to", "note"],
|
|
632
|
-
csvRows(filterEdges(
|
|
645
|
+
csvRows(filterEdges(relationRecords("relations.uses_config_key.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
|
|
633
646
|
await copyTable(conn, "USES_RESOURCE_KEY", ["from", "to", "note"],
|
|
634
|
-
csvRows(filterEdges(
|
|
647
|
+
csvRows(filterEdges(relationRecords("relations.uses_resource_key.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
|
|
635
648
|
await copyTable(conn, "USES_SETTING_KEY", ["from", "to", "note"],
|
|
636
|
-
csvRows(filterEdges(
|
|
649
|
+
csvRows(filterEdges(relationRecords("relations.uses_setting_key.jsonl", "note"), fileIds, chunkIds), (e) => [e.from, e.to, e.note]));
|
|
637
650
|
await copyTable(conn, "CONTAINS", ["from", "to"],
|
|
638
|
-
csvRows(filterEdges(
|
|
651
|
+
csvRows(filterEdges(simpleRelationRecords("relations.contains.jsonl"), moduleIds, fileIds), (e) => [e.from, e.to]));
|
|
639
652
|
await copyTable(conn, "CONTAINS_MODULE", ["from", "to"],
|
|
640
|
-
csvRows(filterEdges(
|
|
653
|
+
csvRows(filterEdges(simpleRelationRecords("relations.contains_module.jsonl"), moduleIds, moduleIds), (e) => [e.from, e.to]));
|
|
641
654
|
await copyTable(conn, "EXPORTS", ["from", "to"],
|
|
642
|
-
csvRows(filterEdges(
|
|
655
|
+
csvRows(filterEdges(simpleRelationRecords("relations.exports.jsonl"), moduleIds, chunkIds), (e) => [e.from, e.to]));
|
|
643
656
|
await copyTable(conn, "INCLUDES_FILE", ["from", "to"],
|
|
644
|
-
csvRows(filterEdges(
|
|
657
|
+
csvRows(filterEdges(simpleRelationRecords("relations.includes_file.jsonl"), projectIds, fileIds), (e) => [e.from, e.to]));
|
|
645
658
|
await copyTable(conn, "REFERENCES_PROJECT", ["from", "to", "note"],
|
|
646
|
-
csvRows(filterEdges(
|
|
659
|
+
csvRows(filterEdges(relationRecords("relations.references_project.jsonl", "note"), projectIds, projectIds), (e) => [e.from, e.to, e.note]));
|
|
647
660
|
await copyTable(conn, "USES_RESOURCE", ["from", "to", "note"],
|
|
648
|
-
csvRows(filterEdges(
|
|
661
|
+
csvRows(filterEdges(relationRecords("relations.uses_resource.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
|
|
649
662
|
await copyTable(conn, "USES_SETTING", ["from", "to", "note"],
|
|
650
|
-
csvRows(filterEdges(
|
|
663
|
+
csvRows(filterEdges(relationRecords("relations.uses_setting.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
|
|
651
664
|
await copyTable(conn, "USES_CONFIG", ["from", "to", "note"],
|
|
652
|
-
csvRows(filterEdges(
|
|
665
|
+
csvRows(filterEdges(relationRecords("relations.uses_config.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
|
|
653
666
|
await copyTable(conn, "TRANSFORMS_CONFIG", ["from", "to", "note"],
|
|
654
|
-
csvRows(filterEdges(
|
|
667
|
+
csvRows(filterEdges(relationRecords("relations.transforms_config.jsonl", "note"), fileIds, fileIds), (e) => [e.from, e.to, e.note]));
|
|
655
668
|
} finally {
|
|
656
669
|
fs.rmSync(GRAPH_IMPORT_DIR, { recursive: true, force: true });
|
|
657
670
|
}
|
|
@@ -942,13 +955,11 @@ async function main(): Promise<void> {
|
|
|
942
955
|
const ontologyStatements = parseOntologyStatements(fs.readFileSync(ONTOLOGY_PATH, "utf8"));
|
|
943
956
|
await executeStatements(conn, ontologyStatements);
|
|
944
957
|
|
|
945
|
-
const data = parseGraphData();
|
|
946
|
-
|
|
947
958
|
const bulkEnabled = reset && process.env.CORTEX_GRAPH_BULK_LOAD !== "never";
|
|
948
959
|
let usedBulk = false;
|
|
949
960
|
if (bulkEnabled) {
|
|
950
961
|
try {
|
|
951
|
-
await bulkLoad(conn
|
|
962
|
+
await bulkLoad(conn);
|
|
952
963
|
usedBulk = true;
|
|
953
964
|
console.log("[graph-load] loaded via COPY bulk import");
|
|
954
965
|
} catch (error) {
|
|
@@ -959,6 +970,7 @@ async function main(): Promise<void> {
|
|
|
959
970
|
}
|
|
960
971
|
}
|
|
961
972
|
if (!usedBulk) {
|
|
973
|
+
const data = parseGraphData();
|
|
962
974
|
await rowByRowLoad(conn, data);
|
|
963
975
|
}
|
|
964
976
|
|
|
@@ -448,6 +448,48 @@ test("resolveModelMaxTokens: sane values pass, sentinels and absurdities fall ba
|
|
|
448
448
|
assert.equal(resolveModelMaxTokens(0), 8192);
|
|
449
449
|
});
|
|
450
450
|
|
|
451
|
+
test("resolveModelMaxTokens: explicit override caps but never raises model max", async () => {
|
|
452
|
+
const { resolveModelMaxTokens } = await import("../dist/embedScheduler.js");
|
|
453
|
+
assert.equal(resolveModelMaxTokens(8192, 2048), 2048);
|
|
454
|
+
assert.equal(resolveModelMaxTokens(512, 2048), 512);
|
|
455
|
+
assert.equal(resolveModelMaxTokens(8192, "1024"), 1024);
|
|
456
|
+
assert.equal(resolveModelMaxTokens(8192, 4), 8192);
|
|
457
|
+
assert.equal(resolveModelMaxTokens(8192, "nope"), 8192);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("resolveTokenBudgetChoice: auto preserves model max regardless of repo size", async () => {
|
|
461
|
+
const { resolveTokenBudgetChoice } = await import("../dist/embedScheduler.js");
|
|
462
|
+
assert.deepEqual(resolveTokenBudgetChoice(undefined, 180), {
|
|
463
|
+
cap: null,
|
|
464
|
+
mode: "auto",
|
|
465
|
+
reason: "quality_preserving_model_max:unique=180"
|
|
466
|
+
});
|
|
467
|
+
assert.deepEqual(resolveTokenBudgetChoice("auto", 20000), {
|
|
468
|
+
cap: null,
|
|
469
|
+
mode: "auto",
|
|
470
|
+
reason: "quality_preserving_model_max:unique=20000"
|
|
471
|
+
});
|
|
472
|
+
assert.deepEqual(resolveTokenBudgetChoice(undefined, 48533), {
|
|
473
|
+
cap: null,
|
|
474
|
+
mode: "auto",
|
|
475
|
+
reason: "quality_preserving_model_max:unique=48533"
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("resolveTokenBudgetChoice: explicit caps and full-model opt-out win over auto", async () => {
|
|
480
|
+
const { resolveTokenBudgetChoice } = await import("../dist/embedScheduler.js");
|
|
481
|
+
assert.deepEqual(resolveTokenBudgetChoice("4096", 48533), {
|
|
482
|
+
cap: 4096,
|
|
483
|
+
mode: "explicit",
|
|
484
|
+
reason: "env_override"
|
|
485
|
+
});
|
|
486
|
+
assert.deepEqual(resolveTokenBudgetChoice("model", 48533), {
|
|
487
|
+
cap: null,
|
|
488
|
+
mode: "model",
|
|
489
|
+
reason: "env_full_model"
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
451
493
|
test("createTokenCounter: uses the tokenizer, clamps at model max, survives failures", async () => {
|
|
452
494
|
const { createTokenCounter } = await import("../dist/embedScheduler.js");
|
|
453
495
|
const tokenizer = (text) => ({ input_ids: { dims: [1, text.length * 2] } });
|
|
@@ -462,6 +504,13 @@ test("createTokenCounter: uses the tokenizer, clamps at model max, survives fail
|
|
|
462
504
|
assert.equal(missing("x".repeat(40)), 10);
|
|
463
505
|
});
|
|
464
506
|
|
|
507
|
+
test("truncateTextToTokenBudget: trims a prefix to the requested token cap", async () => {
|
|
508
|
+
const { truncateTextToTokenBudget } = await import("../dist/embedScheduler.js");
|
|
509
|
+
const countTokens = (text) => text.length;
|
|
510
|
+
assert.equal(truncateTextToTokenBudget("abcdef", countTokens, 3), "abc");
|
|
511
|
+
assert.equal(truncateTextToTokenBudget("abc", countTokens, 3), "abc");
|
|
512
|
+
});
|
|
513
|
+
|
|
465
514
|
test("resolveMemoryHeadroom: constrainedMemory 0 means unconstrained (Node sentinel)", async () => {
|
|
466
515
|
const { resolveMemoryHeadroom } = await import("../dist/embedScheduler.js");
|
|
467
516
|
const unconstrained = resolveMemoryHeadroom({ freeMemory: 50e9, totalMemory: 64e9 });
|