@danielblomma/cortex-mcp 2.4.0 → 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.
- package/CHANGELOG.md +226 -0
- package/README.md +24 -3
- package/bin/cli/arguments.mjs +60 -0
- package/bin/cli/context-passthrough.mjs +129 -0
- package/bin/cli/daemon.mjs +157 -0
- package/bin/cli/enterprise.mjs +516 -0
- package/bin/cli/help.mjs +88 -0
- package/bin/cli/hooks.mjs +199 -0
- package/bin/cli/mcp-command.mjs +87 -0
- package/bin/cli/paths.mjs +14 -0
- package/bin/cli/process.mjs +49 -0
- package/bin/cli/project-commands.mjs +237 -0
- package/bin/cli/project-runtime.mjs +34 -0
- package/bin/cli/query-command.mjs +18 -0
- package/bin/cli/router.mjs +66 -0
- package/bin/cli/run-command.mjs +44 -0
- package/bin/cli/scaffold-ownership.mjs +936 -0
- package/bin/cli/scaffold.mjs +687 -0
- package/bin/cli/stage-command.mjs +9 -0
- package/bin/cli/telemetry-command.mjs +18 -0
- package/bin/cli/trusted-runtime.mjs +18 -0
- package/bin/cortex.mjs +12 -1607
- package/bin/daemon-control.mjs +162 -0
- package/mcp-registry-submission.json +1 -1
- package/package.json +8 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +321 -200
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
- package/scaffold/ownership/baseline-v2.4.1.json +22 -0
- package/scaffold/ownership/current.json +4 -0
- package/scaffold/ownership/v1.json +456 -0
- package/scaffold/scripts/ingest-parsers.mjs +1 -387
- package/scaffold/scripts/ingest-worker.mjs +4 -1
- package/scaffold/scripts/ingest.mjs +5 -3899
- package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
- package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
- package/scaffold/scripts/lib/ingest/config.mjs +120 -0
- package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
- package/scaffold/scripts/lib/ingest/files.mjs +387 -0
- package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
- package/scaffold/scripts/lib/ingest/io.mjs +78 -0
- package/scaffold/scripts/lib/ingest/main.mjs +76 -0
- package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
- package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
- package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
- package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
- package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
- package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
- package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { env, pipeline } from "@huggingface/transformers";
|
|
7
|
+
import { readJsonl, readJsonlRecords, writeJsonlRecords, asString, asNumber, asBoolean } from "./jsonl.js";
|
|
8
|
+
import { CACHE_DIR, PATHS } from "./paths.js";
|
|
9
|
+
import { createTokenCounter, DEFAULT_SCHEDULER_OPTIONS, groupDuplicates, packWorkUnits, resolveEffectiveTokenBudget, resolveInFlightTokens, resolveMemoryHeadroom, resolveModelMaxTokens, resolvePoolConfig, resolveTokenBudgetChoice, runWorkUnits, truncateTextToTokenBudget } from "./embedScheduler.js";
|
|
10
|
+
const EMBEDDINGS_PATH = PATHS.embeddingsEntities;
|
|
11
|
+
const EMBEDDINGS_MANIFEST_PATH = PATHS.embeddingsManifest;
|
|
12
|
+
const MODEL_CACHE_DIR = PATHS.embeddingsModelCache;
|
|
13
|
+
const EMBEDDINGS_DIR = path.dirname(EMBEDDINGS_PATH);
|
|
14
|
+
export const DEFAULT_MODEL_ID = "jinaai/jina-embeddings-v2-base-code";
|
|
15
|
+
export const COMPACT_FILE_TEXT_STRATEGY = "compact_files_v1";
|
|
16
|
+
export const COMPACT_FILE_TEXT_THRESHOLD_CHARS = 32768;
|
|
17
|
+
export const COMPACT_FILE_TEXT_TARGET_CHARS = 16000;
|
|
18
|
+
const COMPACT_FILE_SIGNAL_BUDGET_CHARS = 4096;
|
|
19
|
+
const COMPACT_FILE_SIGNAL_MAX_LINE_CHARS = 512;
|
|
20
|
+
const COMPACT_FILE_MIN_HEAD_CHARS = 4096;
|
|
21
|
+
const COMPACT_FILE_MIN_TAIL_CHARS = 2048;
|
|
22
|
+
export function resolveModelId() {
|
|
23
|
+
return (process.env.CORTEX_EMBED_MODEL ?? DEFAULT_MODEL_ID).trim() || DEFAULT_MODEL_ID;
|
|
24
|
+
}
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const args = new Set(argv.slice(2));
|
|
27
|
+
return {
|
|
28
|
+
mode: args.has("--changed") ? "changed" : "full"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function hashText(value) {
|
|
32
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
33
|
+
}
|
|
34
|
+
function normalizeText(value) {
|
|
35
|
+
return value.replace(/\s+/g, " ").trim();
|
|
36
|
+
}
|
|
37
|
+
export function resolveEmbedTextProfile(raw = process.env.CORTEX_EMBED_TEXT_PROFILE) {
|
|
38
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
39
|
+
if (!value || value === "full") {
|
|
40
|
+
return "full";
|
|
41
|
+
}
|
|
42
|
+
if (value === "compact-files") {
|
|
43
|
+
return "compact-files";
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Unsupported CORTEX_EMBED_TEXT_PROFILE=${JSON.stringify(raw)}; expected "full" or "compact-files"`);
|
|
46
|
+
}
|
|
47
|
+
function isSignalLine(line) {
|
|
48
|
+
const trimmed = line.trim();
|
|
49
|
+
if (!trimmed) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return (/^(import|export)\b/.test(trimmed) ||
|
|
53
|
+
/^(abstract\s+|async\s+|public\s+|private\s+|protected\s+|static\s+|readonly\s+|override\s+)*(class|interface|type|enum|function)\b/.test(trimmed) ||
|
|
54
|
+
/^(const|let|var)\s+[$A-Z_a-z][$\w]*\s*=/.test(trimmed) ||
|
|
55
|
+
/^(describe|it|test)\s*\(/.test(trimmed) ||
|
|
56
|
+
/^(@[A-Z_a-z][$\w]*|#[#\s])/.test(trimmed) ||
|
|
57
|
+
/^```[A-Za-z0-9_-]+/.test(trimmed) ||
|
|
58
|
+
/\b(route|router|endpoint|controller|handler|middleware|permission|auth|token|secret|security|todo|fixme)\b/i.test(trimmed));
|
|
59
|
+
}
|
|
60
|
+
function collectSignalLines(content, budgetChars) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
let used = 0;
|
|
64
|
+
for (const line of content.split(/\r?\n/)) {
|
|
65
|
+
if (!isSignalLine(line)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const normalized = line.trimEnd();
|
|
69
|
+
const signalLine = normalized.length > COMPACT_FILE_SIGNAL_MAX_LINE_CHARS
|
|
70
|
+
? `${normalized.slice(0, COMPACT_FILE_SIGNAL_MAX_LINE_CHARS)} [cortex ${COMPACT_FILE_TEXT_STRATEGY} signal_line_truncated_chars=${normalized.length - COMPACT_FILE_SIGNAL_MAX_LINE_CHARS}]`
|
|
71
|
+
: normalized;
|
|
72
|
+
if (seen.has(signalLine)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const next = used + signalLine.length + 1;
|
|
76
|
+
if (next > budgetChars) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
lines.push(signalLine);
|
|
80
|
+
seen.add(signalLine);
|
|
81
|
+
used = next;
|
|
82
|
+
}
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
export function buildFileEmbeddingText(filePath, excerpt, content, profile = "full") {
|
|
86
|
+
const fullText = `${filePath}\n${excerpt}\n${content}`;
|
|
87
|
+
if (profile === "full" || fullText.length <= COMPACT_FILE_TEXT_THRESHOLD_CHARS) {
|
|
88
|
+
return {
|
|
89
|
+
text: fullText,
|
|
90
|
+
profile,
|
|
91
|
+
compacted: false,
|
|
92
|
+
original_chars: fullText.length,
|
|
93
|
+
text_chars: fullText.length,
|
|
94
|
+
omitted_chars: 0
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const signalText = collectSignalLines(content, COMPACT_FILE_SIGNAL_BUDGET_CHARS);
|
|
98
|
+
const markerPrefix = `[cortex ${COMPACT_FILE_TEXT_STRATEGY} omitted_chars=`;
|
|
99
|
+
const staticChars = filePath.length +
|
|
100
|
+
excerpt.length +
|
|
101
|
+
signalText.length +
|
|
102
|
+
markerPrefix.length +
|
|
103
|
+
64;
|
|
104
|
+
const available = Math.max(COMPACT_FILE_MIN_HEAD_CHARS + COMPACT_FILE_MIN_TAIL_CHARS, COMPACT_FILE_TEXT_TARGET_CHARS - staticChars);
|
|
105
|
+
const headChars = Math.max(COMPACT_FILE_MIN_HEAD_CHARS, Math.floor(available * 0.62));
|
|
106
|
+
const tailChars = Math.max(COMPACT_FILE_MIN_TAIL_CHARS, available - headChars);
|
|
107
|
+
const head = content.slice(0, headChars);
|
|
108
|
+
const tail = content.slice(-tailChars);
|
|
109
|
+
const omittedChars = Math.max(0, content.length - head.length - tail.length);
|
|
110
|
+
const marker = `${markerPrefix}${omittedChars}]`;
|
|
111
|
+
const compactText = [
|
|
112
|
+
filePath,
|
|
113
|
+
excerpt,
|
|
114
|
+
head,
|
|
115
|
+
marker,
|
|
116
|
+
signalText ? `[cortex ${COMPACT_FILE_TEXT_STRATEGY} signal_lines]\n${signalText}` : "",
|
|
117
|
+
tail
|
|
118
|
+
].filter((part) => part.length > 0).join("\n");
|
|
119
|
+
if (compactText.length >= fullText.length) {
|
|
120
|
+
return {
|
|
121
|
+
text: fullText,
|
|
122
|
+
profile,
|
|
123
|
+
compacted: false,
|
|
124
|
+
original_chars: fullText.length,
|
|
125
|
+
text_chars: fullText.length,
|
|
126
|
+
omitted_chars: 0
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
text: compactText,
|
|
131
|
+
profile,
|
|
132
|
+
compacted: true,
|
|
133
|
+
original_chars: fullText.length,
|
|
134
|
+
text_chars: compactText.length,
|
|
135
|
+
omitted_chars: fullText.length - compactText.length
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function ensureRequiredFiles() {
|
|
139
|
+
const required = [
|
|
140
|
+
path.join(CACHE_DIR, "documents.jsonl"),
|
|
141
|
+
path.join(CACHE_DIR, "entities.rule.jsonl"),
|
|
142
|
+
path.join(CACHE_DIR, "entities.adr.jsonl")
|
|
143
|
+
];
|
|
144
|
+
for (const filePath of required) {
|
|
145
|
+
if (!fs.existsSync(filePath)) {
|
|
146
|
+
throw new Error(`Missing required cache file: ${filePath}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function parseFileEntities(raw, options = {}) {
|
|
151
|
+
const textProfile = options.textProfile ?? "full";
|
|
152
|
+
return raw
|
|
153
|
+
.map((item) => {
|
|
154
|
+
const id = asString(item.id);
|
|
155
|
+
const filePath = asString(item.path);
|
|
156
|
+
if (!id || !filePath) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const content = asString(item.content);
|
|
160
|
+
const excerpt = asString(item.excerpt);
|
|
161
|
+
const updatedAt = asString(item.updated_at);
|
|
162
|
+
const checksum = asString(item.checksum, hashText(content));
|
|
163
|
+
const embeddingText = buildFileEmbeddingText(filePath, excerpt, content, textProfile);
|
|
164
|
+
return {
|
|
165
|
+
id,
|
|
166
|
+
type: "File",
|
|
167
|
+
kind: asString(item.kind, "DOC"),
|
|
168
|
+
label: filePath,
|
|
169
|
+
path: filePath,
|
|
170
|
+
status: asString(item.status, "active"),
|
|
171
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
172
|
+
trust_level: asNumber(item.trust_level, 50),
|
|
173
|
+
updated_at: updatedAt,
|
|
174
|
+
text: embeddingText.text,
|
|
175
|
+
signature: hashText(`file|${checksum}|${updatedAt}|${hashText(embeddingText.text)}`),
|
|
176
|
+
text_profile: embeddingText.profile,
|
|
177
|
+
text_compacted: embeddingText.compacted,
|
|
178
|
+
text_original_chars: embeddingText.original_chars,
|
|
179
|
+
text_chars: embeddingText.text_chars,
|
|
180
|
+
text_omitted_chars: embeddingText.omitted_chars
|
|
181
|
+
};
|
|
182
|
+
})
|
|
183
|
+
.filter((value) => value !== null);
|
|
184
|
+
}
|
|
185
|
+
function parseRuleEntities(raw) {
|
|
186
|
+
return raw
|
|
187
|
+
.map((item) => {
|
|
188
|
+
const id = asString(item.id);
|
|
189
|
+
if (!id) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const title = asString(item.title, id);
|
|
193
|
+
const body = asString(item.body);
|
|
194
|
+
const updatedAt = asString(item.updated_at, "");
|
|
195
|
+
const text = `${title}\n${body}`;
|
|
196
|
+
return {
|
|
197
|
+
id,
|
|
198
|
+
type: "Rule",
|
|
199
|
+
kind: "RULE",
|
|
200
|
+
label: title,
|
|
201
|
+
path: "",
|
|
202
|
+
status: asString(item.status, "active"),
|
|
203
|
+
source_of_truth: asBoolean(item.source_of_truth, true),
|
|
204
|
+
trust_level: asNumber(item.trust_level, 95),
|
|
205
|
+
updated_at: updatedAt,
|
|
206
|
+
text,
|
|
207
|
+
signature: hashText(`rule|${id}|${updatedAt}|${hashText(text)}`)
|
|
208
|
+
};
|
|
209
|
+
})
|
|
210
|
+
.filter((value) => value !== null);
|
|
211
|
+
}
|
|
212
|
+
function parseAdrEntities(raw) {
|
|
213
|
+
return raw
|
|
214
|
+
.map((item) => {
|
|
215
|
+
const id = asString(item.id);
|
|
216
|
+
if (!id) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const title = asString(item.title, id);
|
|
220
|
+
const body = asString(item.body);
|
|
221
|
+
const adrPath = asString(item.path);
|
|
222
|
+
const decisionDate = asString(item.decision_date, "");
|
|
223
|
+
const text = `${adrPath}\n${title}\n${body}`;
|
|
224
|
+
return {
|
|
225
|
+
id,
|
|
226
|
+
type: "ADR",
|
|
227
|
+
kind: "ADR",
|
|
228
|
+
label: title,
|
|
229
|
+
path: adrPath,
|
|
230
|
+
status: asString(item.status, "active"),
|
|
231
|
+
source_of_truth: asBoolean(item.source_of_truth, true),
|
|
232
|
+
trust_level: asNumber(item.trust_level, 95),
|
|
233
|
+
updated_at: decisionDate,
|
|
234
|
+
text,
|
|
235
|
+
signature: hashText(`adr|${id}|${decisionDate}|${hashText(text)}`)
|
|
236
|
+
};
|
|
237
|
+
})
|
|
238
|
+
.filter((value) => value !== null);
|
|
239
|
+
}
|
|
240
|
+
function parseModuleEntities(raw) {
|
|
241
|
+
return raw
|
|
242
|
+
.map((item) => {
|
|
243
|
+
const id = asString(item.id);
|
|
244
|
+
if (!id) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const modulePath = asString(item.path);
|
|
248
|
+
const name = asString(item.name);
|
|
249
|
+
const summary = asString(item.summary);
|
|
250
|
+
const exportedSymbols = asString(item.exported_symbols);
|
|
251
|
+
const updatedAt = asString(item.updated_at);
|
|
252
|
+
const text = `${modulePath}\n${name}\n${summary}\n${exportedSymbols}`;
|
|
253
|
+
return {
|
|
254
|
+
id,
|
|
255
|
+
type: "Module",
|
|
256
|
+
kind: "MODULE",
|
|
257
|
+
label: name || modulePath,
|
|
258
|
+
path: modulePath,
|
|
259
|
+
status: asString(item.status, "active"),
|
|
260
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
261
|
+
trust_level: asNumber(item.trust_level, 75),
|
|
262
|
+
updated_at: updatedAt,
|
|
263
|
+
text,
|
|
264
|
+
signature: hashText(`module|${id}|${updatedAt}|${hashText(text)}`)
|
|
265
|
+
};
|
|
266
|
+
})
|
|
267
|
+
.filter((value) => value !== null);
|
|
268
|
+
}
|
|
269
|
+
export function parseChunkEntities(raw, filePathById) {
|
|
270
|
+
return raw
|
|
271
|
+
.map((item) => {
|
|
272
|
+
const id = asString(item.id);
|
|
273
|
+
if (!id) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const fileId = asString(item.file_id);
|
|
277
|
+
const filePath = filePathById.get(fileId) ?? "";
|
|
278
|
+
const name = asString(item.name);
|
|
279
|
+
const sig = asString(item.signature);
|
|
280
|
+
const description = asString(item.description);
|
|
281
|
+
const body = asString(item.body);
|
|
282
|
+
const updatedAt = asString(item.updated_at);
|
|
283
|
+
const checksum = asString(item.checksum, hashText(body));
|
|
284
|
+
const text = `${filePath}\n${name}\n${sig}\n${description}\n${body}`;
|
|
285
|
+
return {
|
|
286
|
+
id,
|
|
287
|
+
type: "Chunk",
|
|
288
|
+
kind: asString(item.kind, "chunk"),
|
|
289
|
+
label: name || id,
|
|
290
|
+
path: filePath,
|
|
291
|
+
status: asString(item.status, "active"),
|
|
292
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
293
|
+
trust_level: asNumber(item.trust_level, 60),
|
|
294
|
+
updated_at: updatedAt,
|
|
295
|
+
text,
|
|
296
|
+
signature: hashText(`chunk|${checksum}|${updatedAt}|${hashText(text)}`)
|
|
297
|
+
};
|
|
298
|
+
})
|
|
299
|
+
.filter((value) => value !== null);
|
|
300
|
+
}
|
|
301
|
+
function parseProjectEntities(raw) {
|
|
302
|
+
return raw
|
|
303
|
+
.map((item) => {
|
|
304
|
+
const id = asString(item.id);
|
|
305
|
+
if (!id) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const projectPath = asString(item.path);
|
|
309
|
+
const name = asString(item.name);
|
|
310
|
+
const kind = asString(item.kind, "project");
|
|
311
|
+
const language = asString(item.language, "dotnet");
|
|
312
|
+
const targetFramework = asString(item.target_framework);
|
|
313
|
+
const summary = asString(item.summary);
|
|
314
|
+
const updatedAt = asString(item.updated_at);
|
|
315
|
+
const text = `${projectPath}\n${name}\n${kind}\n${language}\n${targetFramework}\n${summary}`;
|
|
316
|
+
return {
|
|
317
|
+
id,
|
|
318
|
+
type: "Project",
|
|
319
|
+
kind: kind.toUpperCase() || "PROJECT",
|
|
320
|
+
label: name || projectPath,
|
|
321
|
+
path: projectPath,
|
|
322
|
+
status: asString(item.status, "active"),
|
|
323
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
324
|
+
trust_level: asNumber(item.trust_level, 80),
|
|
325
|
+
updated_at: updatedAt,
|
|
326
|
+
text,
|
|
327
|
+
signature: hashText(`project|${id}|${updatedAt}|${hashText(text)}`)
|
|
328
|
+
};
|
|
329
|
+
})
|
|
330
|
+
.filter((value) => value !== null);
|
|
331
|
+
}
|
|
332
|
+
function parseExistingEmbeddings(raw, modelId) {
|
|
333
|
+
const index = new Map();
|
|
334
|
+
for (const item of raw) {
|
|
335
|
+
const id = asString(item.id);
|
|
336
|
+
if (!id)
|
|
337
|
+
continue;
|
|
338
|
+
const vectorRaw = item.vector;
|
|
339
|
+
if (!Array.isArray(vectorRaw))
|
|
340
|
+
continue;
|
|
341
|
+
const vector = [];
|
|
342
|
+
for (const value of vectorRaw) {
|
|
343
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
344
|
+
vector.push(value);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (vector.length === 0)
|
|
348
|
+
continue;
|
|
349
|
+
const model = asString(item.model);
|
|
350
|
+
if (model && model !== modelId)
|
|
351
|
+
continue;
|
|
352
|
+
index.set(id, {
|
|
353
|
+
id,
|
|
354
|
+
entity_type: asString(item.entity_type, "Unknown"),
|
|
355
|
+
kind: asString(item.kind, "DOC"),
|
|
356
|
+
label: asString(item.label, id),
|
|
357
|
+
path: asString(item.path),
|
|
358
|
+
status: asString(item.status, "active"),
|
|
359
|
+
source_of_truth: asBoolean(item.source_of_truth, false),
|
|
360
|
+
trust_level: asNumber(item.trust_level, 50),
|
|
361
|
+
updated_at: asString(item.updated_at),
|
|
362
|
+
signature: asString(item.signature),
|
|
363
|
+
model: modelId,
|
|
364
|
+
dimensions: asNumber(item.dimensions, vector.length),
|
|
365
|
+
vector
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return index;
|
|
369
|
+
}
|
|
370
|
+
function roundVector(values) {
|
|
371
|
+
return values.map((value) => Number(value.toFixed(6)));
|
|
372
|
+
}
|
|
373
|
+
export function resolveSignatureProfile(maxTokenCap, textProfile = "full", entityType) {
|
|
374
|
+
const parts = [];
|
|
375
|
+
if (maxTokenCap) {
|
|
376
|
+
parts.push(`max_tokens=${maxTokenCap}`);
|
|
377
|
+
}
|
|
378
|
+
if (textProfile === "compact-files" && (entityType === undefined || entityType === "File")) {
|
|
379
|
+
parts.push("text_profile=compact-files", COMPACT_FILE_TEXT_STRATEGY, `threshold_chars=${COMPACT_FILE_TEXT_THRESHOLD_CHARS}`, `target_chars=${COMPACT_FILE_TEXT_TARGET_CHARS}`);
|
|
380
|
+
}
|
|
381
|
+
return parts.length ? `embed|${parts.join("|")}` : "";
|
|
382
|
+
}
|
|
383
|
+
function embeddingSignature(entitySignature, profile) {
|
|
384
|
+
return profile ? hashText(`${profile}|${entitySignature}`) : entitySignature;
|
|
385
|
+
}
|
|
386
|
+
function* presentEmbeddingRecords(slots) {
|
|
387
|
+
for (const record of slots) {
|
|
388
|
+
if (record) {
|
|
389
|
+
yield record;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function main() {
|
|
394
|
+
const { mode } = parseArgs(process.argv);
|
|
395
|
+
ensureRequiredFiles();
|
|
396
|
+
fs.mkdirSync(EMBEDDINGS_DIR, { recursive: true });
|
|
397
|
+
fs.mkdirSync(MODEL_CACHE_DIR, { recursive: true });
|
|
398
|
+
const modelId = resolveModelId();
|
|
399
|
+
const textProfile = resolveEmbedTextProfile();
|
|
400
|
+
const documents = parseFileEntities(readJsonl(path.join(CACHE_DIR, "documents.jsonl")), { textProfile });
|
|
401
|
+
const textProfileStats = {
|
|
402
|
+
strategy: textProfile === "compact-files" ? COMPACT_FILE_TEXT_STRATEGY : null,
|
|
403
|
+
threshold_chars: textProfile === "compact-files" ? COMPACT_FILE_TEXT_THRESHOLD_CHARS : null,
|
|
404
|
+
target_chars: textProfile === "compact-files" ? COMPACT_FILE_TEXT_TARGET_CHARS : null,
|
|
405
|
+
file_entities: documents.length,
|
|
406
|
+
compacted_files: documents.filter((doc) => doc.text_compacted).length,
|
|
407
|
+
original_chars: documents.reduce((total, doc) => total + doc.text_original_chars, 0),
|
|
408
|
+
text_chars: documents.reduce((total, doc) => total + doc.text_chars, 0),
|
|
409
|
+
saved_chars: documents.reduce((total, doc) => total + doc.text_omitted_chars, 0)
|
|
410
|
+
};
|
|
411
|
+
const rules = parseRuleEntities(readJsonl(path.join(CACHE_DIR, "entities.rule.jsonl")));
|
|
412
|
+
const adrs = parseAdrEntities(readJsonl(path.join(CACHE_DIR, "entities.adr.jsonl")));
|
|
413
|
+
const modules = parseModuleEntities(readJsonl(path.join(CACHE_DIR, "entities.module.jsonl")));
|
|
414
|
+
const projects = parseProjectEntities(readJsonl(path.join(CACHE_DIR, "entities.project.jsonl")));
|
|
415
|
+
// Build file path lookup for chunk embedding text (reuse already-parsed documents)
|
|
416
|
+
const filePathById = new Map();
|
|
417
|
+
for (const doc of documents) {
|
|
418
|
+
filePathById.set(doc.id, doc.path);
|
|
419
|
+
}
|
|
420
|
+
const chunks = parseChunkEntities(readJsonl(path.join(CACHE_DIR, "entities.chunk.jsonl")), filePathById);
|
|
421
|
+
const entities = [...documents, ...rules, ...adrs, ...modules, ...projects, ...chunks].sort((a, b) => a.id.localeCompare(b.id));
|
|
422
|
+
const uniqueSignatures = new Set();
|
|
423
|
+
for (const entity of entities) {
|
|
424
|
+
uniqueSignatures.add(entity.signature);
|
|
425
|
+
}
|
|
426
|
+
const uniqueTextCount = uniqueSignatures.size;
|
|
427
|
+
uniqueSignatures.clear();
|
|
428
|
+
env.cacheDir = MODEL_CACHE_DIR;
|
|
429
|
+
// Total thread budget for embedding. CORTEX_EMBED_THREADS caps it so
|
|
430
|
+
// co-located embedders (parallel CI jobs, eval containers) do not
|
|
431
|
+
// oversubscribe shared cores; unset = all cores.
|
|
432
|
+
const threadsRaw = Number(process.env.CORTEX_EMBED_THREADS);
|
|
433
|
+
const threadBudget = Number.isFinite(threadsRaw) && threadsRaw >= 1 ? Math.floor(threadsRaw) : os.cpus().length;
|
|
434
|
+
const readHeadroom = () => resolveMemoryHeadroom({
|
|
435
|
+
freeMemory: os.freemem(),
|
|
436
|
+
totalMemory: os.totalmem(),
|
|
437
|
+
constrainedMemory: process.constrainedMemory?.() ?? null,
|
|
438
|
+
availableMemory: process.availableMemory?.() ?? null
|
|
439
|
+
});
|
|
440
|
+
const memoryHeadroom = readHeadroom();
|
|
441
|
+
const previewPoolConfig = resolvePoolConfig({
|
|
442
|
+
threadBudget,
|
|
443
|
+
uniqueCount: uniqueTextCount,
|
|
444
|
+
memoryBytes: memoryHeadroom
|
|
445
|
+
});
|
|
446
|
+
const requestedTokenBudget = resolveTokenBudgetChoice(process.env.CORTEX_EMBED_MAX_TOKENS, uniqueTextCount);
|
|
447
|
+
const tokenBudget = resolveEffectiveTokenBudget({
|
|
448
|
+
choice: requestedTokenBudget,
|
|
449
|
+
modelMaxTokens: resolveModelMaxTokens(undefined, requestedTokenBudget.cap ?? undefined),
|
|
450
|
+
memoryBytes: memoryHeadroom,
|
|
451
|
+
sessions: previewPoolConfig.sessions
|
|
452
|
+
});
|
|
453
|
+
const defaultSignatureProfile = resolveSignatureProfile(tokenBudget.cap, "full");
|
|
454
|
+
const fileSignatureProfile = resolveSignatureProfile(tokenBudget.cap, textProfile, "File");
|
|
455
|
+
const signatureProfileForEntity = (entity) => entity.type === "File" ? fileSignatureProfile : defaultSignatureProfile;
|
|
456
|
+
const existing = parseExistingEmbeddings(readJsonlRecords(EMBEDDINGS_PATH), modelId);
|
|
457
|
+
let reused = 0;
|
|
458
|
+
// Slot per entity keeps output in entity order; failed slots stay null.
|
|
459
|
+
const slots = entities.map(() => null);
|
|
460
|
+
const pending = [];
|
|
461
|
+
let dimensions = 0;
|
|
462
|
+
entities.forEach((entity, index) => {
|
|
463
|
+
const signatureProfile = signatureProfileForEntity(entity);
|
|
464
|
+
const signature = embeddingSignature(entity.signature, signatureProfile);
|
|
465
|
+
const previous = existing.get(entity.id);
|
|
466
|
+
if (previous && previous.signature === signature && previous.vector.length > 0) {
|
|
467
|
+
reused += 1;
|
|
468
|
+
dimensions = dimensions || previous.vector.length;
|
|
469
|
+
slots[index] = {
|
|
470
|
+
...previous,
|
|
471
|
+
entity_type: entity.type,
|
|
472
|
+
kind: entity.kind,
|
|
473
|
+
label: entity.label,
|
|
474
|
+
path: entity.path,
|
|
475
|
+
status: entity.status,
|
|
476
|
+
source_of_truth: entity.source_of_truth,
|
|
477
|
+
trust_level: entity.trust_level,
|
|
478
|
+
updated_at: entity.updated_at,
|
|
479
|
+
signature,
|
|
480
|
+
model: modelId,
|
|
481
|
+
dimensions: previous.vector.length
|
|
482
|
+
};
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
pending.push({ index, text: normalizeText(entity.text) });
|
|
486
|
+
});
|
|
487
|
+
// Deduplicate identical texts (lossless: identical input -> identical
|
|
488
|
+
// vector), then measure token lengths for routing and batch packing.
|
|
489
|
+
const unique = groupDuplicates(pending);
|
|
490
|
+
// Memory headroom drives pool size and the concurrency gate; everything
|
|
491
|
+
// adapts to the machine so no tuning is expected from users. Container
|
|
492
|
+
// limits (cgroups) and platforms that under-report free memory are both
|
|
493
|
+
// handled inside resolveMemoryHeadroom.
|
|
494
|
+
const poolConfig = resolvePoolConfig({
|
|
495
|
+
threadBudget,
|
|
496
|
+
poolOverride: Number(process.env.CORTEX_EMBED_POOL) || null,
|
|
497
|
+
uniqueCount: unique.length,
|
|
498
|
+
memoryBytes: memoryHeadroom
|
|
499
|
+
});
|
|
500
|
+
const batchSizeRaw = Number(process.env.CORTEX_EMBED_BATCH_SIZE);
|
|
501
|
+
const batchTokensRaw = Number(process.env.CORTEX_EMBED_BATCH_TOKENS);
|
|
502
|
+
const shortTokensRaw = Number(process.env.CORTEX_EMBED_SHORT_TOKENS);
|
|
503
|
+
const schedulerOptions = {
|
|
504
|
+
...DEFAULT_SCHEDULER_OPTIONS,
|
|
505
|
+
// 0 (or any value below 1) disables micro-batching rather than silently
|
|
506
|
+
// meaning "use the default".
|
|
507
|
+
...(Number.isFinite(batchSizeRaw) && batchSizeRaw >= 0
|
|
508
|
+
? { batchMaxItems: Math.max(1, Math.floor(batchSizeRaw)) }
|
|
509
|
+
: {}),
|
|
510
|
+
...(Number.isFinite(batchTokensRaw) && batchTokensRaw >= 16
|
|
511
|
+
? { batchTokenBudget: Math.floor(batchTokensRaw) }
|
|
512
|
+
: {}),
|
|
513
|
+
...(Number.isFinite(shortTokensRaw) && shortTokensRaw >= 1
|
|
514
|
+
? { shortMaxTokens: Math.floor(shortTokensRaw) }
|
|
515
|
+
: {})
|
|
516
|
+
};
|
|
517
|
+
// Fully warm cache: nothing to embed, so skip model loading entirely —
|
|
518
|
+
// this is the common repeat-bootstrap / small-update path.
|
|
519
|
+
let embedded = 0;
|
|
520
|
+
let modelMaxTokensUsed = 0;
|
|
521
|
+
let result = {
|
|
522
|
+
vectors: new Map(),
|
|
523
|
+
failures: []
|
|
524
|
+
};
|
|
525
|
+
if (unique.length > 0) {
|
|
526
|
+
const makeExtractor = async (threads) => (await pipeline("feature-extraction", modelId, {
|
|
527
|
+
session_options: { intraOpNumThreads: threads, interOpNumThreads: 1 }
|
|
528
|
+
}));
|
|
529
|
+
// First session loads (and caches) the model; the rest load in parallel.
|
|
530
|
+
// Extra sessions failing (e.g. memory pressure) degrades the pool instead
|
|
531
|
+
// of aborting the run — one session can always finish the work.
|
|
532
|
+
const first = await makeExtractor(poolConfig.threadsPerSession);
|
|
533
|
+
const extraSessions = await Promise.allSettled(Array.from({ length: poolConfig.sessions - 1 }, () => makeExtractor(poolConfig.threadsPerSession)));
|
|
534
|
+
const extractors = [
|
|
535
|
+
first,
|
|
536
|
+
...extraSessions
|
|
537
|
+
.filter((settled) => settled.status === "fulfilled")
|
|
538
|
+
.map((settled) => settled.value)
|
|
539
|
+
];
|
|
540
|
+
const failedSessions = extraSessions.length + 1 - extractors.length;
|
|
541
|
+
if (failedSessions > 0) {
|
|
542
|
+
const firstFailure = extraSessions.find((settled) => settled.status === "rejected");
|
|
543
|
+
const reason = firstFailure?.reason instanceof Error ? firstFailure.reason.message : String(firstFailure?.reason ?? "unknown");
|
|
544
|
+
console.warn(`[embed] ${failedSessions} pool session(s) failed to load (${reason}); continuing with ${extractors.length}`);
|
|
545
|
+
}
|
|
546
|
+
// Inference truncates at the model max; token counts must too, or one
|
|
547
|
+
// giant file inflates scheduling cost and gate mass far beyond reality.
|
|
548
|
+
const modelMaxTokens = resolveModelMaxTokens(first.tokenizer?.model_max_length, tokenBudget.cap ?? undefined);
|
|
549
|
+
modelMaxTokensUsed = modelMaxTokens;
|
|
550
|
+
const countTokens = createTokenCounter(first.tokenizer, modelMaxTokens);
|
|
551
|
+
const countRawTokens = createTokenCounter(first.tokenizer, 131072);
|
|
552
|
+
const measured = unique.map((item) => {
|
|
553
|
+
const text = truncateTextToTokenBudget(item.text, countRawTokens, modelMaxTokens);
|
|
554
|
+
return { ...item, text, tokens: countTokens(text) };
|
|
555
|
+
});
|
|
556
|
+
const units = packWorkUnits(measured, schedulerOptions);
|
|
557
|
+
console.log(`[embed] scheduler unique=${unique.length} units=${units.length} max_tokens<=${modelMaxTokens} token_budget=${tokenBudget.mode} reason=${tokenBudget.reason}`);
|
|
558
|
+
// Recompute headroom after the model copies are resident so the gate
|
|
559
|
+
// reflects what is actually left for inference activations.
|
|
560
|
+
const inFlightRaw = Number(process.env.CORTEX_EMBED_INFLIGHT_TOKENS);
|
|
561
|
+
const maxInFlightTokens = Number.isFinite(inFlightRaw) && inFlightRaw >= 1024
|
|
562
|
+
? Math.floor(inFlightRaw)
|
|
563
|
+
: resolveInFlightTokens({ memoryBytes: readHeadroom(), modelMaxTokens });
|
|
564
|
+
result = await runWorkUnits(units, extractors, {
|
|
565
|
+
maxInFlightTokens,
|
|
566
|
+
onVector(index, rawVector) {
|
|
567
|
+
const entity = entities[index];
|
|
568
|
+
const signatureProfile = signatureProfileForEntity(entity);
|
|
569
|
+
const vector = roundVector(rawVector);
|
|
570
|
+
embedded += 1;
|
|
571
|
+
dimensions = dimensions || vector.length;
|
|
572
|
+
slots[index] = {
|
|
573
|
+
id: entity.id,
|
|
574
|
+
entity_type: entity.type,
|
|
575
|
+
kind: entity.kind,
|
|
576
|
+
label: entity.label,
|
|
577
|
+
path: entity.path,
|
|
578
|
+
status: entity.status,
|
|
579
|
+
source_of_truth: entity.source_of_truth,
|
|
580
|
+
trust_level: entity.trust_level,
|
|
581
|
+
updated_at: entity.updated_at,
|
|
582
|
+
signature: embeddingSignature(entity.signature, signatureProfile),
|
|
583
|
+
model: modelId,
|
|
584
|
+
dimensions: vector.length,
|
|
585
|
+
vector
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
const failures = result.failures.map((failure) => `${entities[failure.index].id}: ${failure.message}`);
|
|
591
|
+
const failed = result.failures.length;
|
|
592
|
+
const outputCount = writeJsonlRecords(EMBEDDINGS_PATH, presentEmbeddingRecords(slots));
|
|
593
|
+
const manifest = {
|
|
594
|
+
generated_at: new Date().toISOString(),
|
|
595
|
+
mode,
|
|
596
|
+
model: modelId,
|
|
597
|
+
dimensions,
|
|
598
|
+
text_profile: textProfile,
|
|
599
|
+
signature_profile: fileSignatureProfile === defaultSignatureProfile ? defaultSignatureProfile : "per_entity",
|
|
600
|
+
signature_profiles: {
|
|
601
|
+
default: defaultSignatureProfile,
|
|
602
|
+
file: fileSignatureProfile
|
|
603
|
+
},
|
|
604
|
+
text_profile_stats: textProfileStats,
|
|
605
|
+
counts: {
|
|
606
|
+
entities: entities.length,
|
|
607
|
+
output: outputCount,
|
|
608
|
+
embedded,
|
|
609
|
+
reused,
|
|
610
|
+
failed
|
|
611
|
+
},
|
|
612
|
+
failures: failures.slice(0, 50)
|
|
613
|
+
};
|
|
614
|
+
fs.writeFileSync(EMBEDDINGS_MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
615
|
+
console.log(`[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} reason=${tokenBudget.reason} text_profile=${textProfile}`);
|
|
616
|
+
console.log(`[embed] text_profile=${textProfile} compacted_files=${textProfileStats.compacted_files}/${textProfileStats.file_entities} saved_chars=${textProfileStats.saved_chars}`);
|
|
617
|
+
console.log(`[embed] entities=${entities.length} embedded=${embedded} reused=${reused} failed=${failed}`);
|
|
618
|
+
console.log(`[embed] wrote ${EMBEDDINGS_PATH}`);
|
|
619
|
+
console.log(`[embed] manifest ${EMBEDDINGS_MANIFEST_PATH}`);
|
|
620
|
+
}
|
|
621
|
+
const isMain = process.argv[1] ? pathToFileURL(process.argv[1]).href === import.meta.url : false;
|
|
622
|
+
if (isMain) {
|
|
623
|
+
main().catch((error) => {
|
|
624
|
+
process.stderr.write(`${error instanceof Error ? error.message : "Embedding error"}\n`);
|
|
625
|
+
process.exit(1);
|
|
626
|
+
});
|
|
627
|
+
}
|