@danielblomma/cortex-mcp 2.2.5 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +73 -3
  3. package/bin/cortex.mjs +339 -242
  4. package/bin/daemon-control.mjs +162 -0
  5. package/package.json +7 -3
  6. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  7. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  8. package/scaffold/mcp/dist/cli/govern.js +937 -0
  9. package/scaffold/mcp/dist/cli/query.js +409 -0
  10. package/scaffold/mcp/dist/cli/run.js +295 -0
  11. package/scaffold/mcp/dist/cli/stage.js +308 -0
  12. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  13. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  14. package/scaffold/mcp/dist/contextEntities.js +282 -0
  15. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  16. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  17. package/scaffold/mcp/dist/core/config.js +235 -0
  18. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  19. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  20. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  21. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  22. package/scaffold/mcp/dist/core/index.js +14 -0
  23. package/scaffold/mcp/dist/core/license.js +278 -0
  24. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  25. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  26. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  27. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  28. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  29. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  30. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  31. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  32. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  33. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  34. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  35. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  36. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  37. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  38. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  39. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  40. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  41. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  42. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  43. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  44. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  45. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  46. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  47. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  48. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  49. package/scaffold/mcp/dist/daemon/client.js +130 -0
  50. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  51. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  52. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  53. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  54. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  55. package/scaffold/mcp/dist/daemon/main.js +449 -0
  56. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  57. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  58. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  59. package/scaffold/mcp/dist/daemon/server.js +180 -0
  60. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  61. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  62. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  63. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  64. package/scaffold/mcp/dist/defaults.js +6 -0
  65. package/scaffold/mcp/dist/embed.js +627 -0
  66. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  67. package/scaffold/mcp/dist/embeddings.js +167 -0
  68. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  69. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  70. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  71. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  72. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  73. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  74. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  75. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  76. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  77. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  78. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  79. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  80. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  81. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  82. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  83. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  84. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  85. package/scaffold/mcp/dist/frontmatter.js +33 -0
  86. package/scaffold/mcp/dist/graph.js +769 -0
  87. package/scaffold/mcp/dist/graphCsv.js +55 -0
  88. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  89. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  90. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  91. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  92. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  93. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  94. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  95. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  96. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  97. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  98. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  99. package/scaffold/mcp/dist/impactRanking.js +191 -0
  100. package/scaffold/mcp/dist/impactResponse.js +30 -0
  101. package/scaffold/mcp/dist/impactResults.js +105 -0
  102. package/scaffold/mcp/dist/impactSeed.js +20 -0
  103. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  104. package/scaffold/mcp/dist/jsonl.js +77 -0
  105. package/scaffold/mcp/dist/loadGraph.js +759 -0
  106. package/scaffold/mcp/dist/lruCache.js +38 -0
  107. package/scaffold/mcp/dist/paths.js +97 -0
  108. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  109. package/scaffold/mcp/dist/plugin.js +81 -0
  110. package/scaffold/mcp/dist/presets.js +78 -0
  111. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  112. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  113. package/scaffold/mcp/dist/rules.js +23 -0
  114. package/scaffold/mcp/dist/search.js +212 -0
  115. package/scaffold/mcp/dist/searchCore.js +457 -0
  116. package/scaffold/mcp/dist/searchResults.js +230 -0
  117. package/scaffold/mcp/dist/server.js +317 -0
  118. package/scaffold/mcp/dist/types.js +1 -0
  119. package/scaffold/mcp/package-lock.json +336 -212
  120. package/scaffold/mcp/package.json +15 -6
  121. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  122. package/scaffold/mcp/src/cli/govern.ts +137 -52
  123. package/scaffold/mcp/src/cli/query.ts +36 -2
  124. package/scaffold/mcp/src/cli/run.ts +53 -19
  125. package/scaffold/mcp/src/cli/stage.ts +8 -3
  126. package/scaffold/mcp/src/core/config.ts +9 -2
  127. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  128. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  129. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  130. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  131. package/scaffold/mcp/src/core/license.ts +186 -17
  132. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  133. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  134. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  135. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  136. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  137. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  138. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  139. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  140. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  142. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  143. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  144. package/scaffold/mcp/src/daemon/main.ts +99 -25
  145. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  146. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  147. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  148. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  149. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  150. package/scaffold/mcp/src/embed.ts +211 -10
  151. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  152. package/scaffold/mcp/src/enterprise/index.ts +1 -1
  153. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  154. package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
  155. package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
  156. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  157. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  158. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  159. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
  160. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  161. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  162. package/scaffold/mcp/src/paths.ts +3 -5
  163. package/scaffold/mcp/src/patternEvidence.ts +347 -0
  164. package/scaffold/mcp/src/plugin.ts +20 -0
  165. package/scaffold/mcp/src/search.ts +35 -9
  166. package/scaffold/mcp/src/searchCore.ts +254 -12
  167. package/scaffold/mcp/src/searchResults.ts +96 -9
  168. package/scaffold/mcp/src/types.ts +7 -0
  169. package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
  170. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  171. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  172. package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
  173. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  174. package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
  175. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  176. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  177. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  178. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  179. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  180. package/scaffold/mcp/tests/license.test.mjs +367 -0
  181. package/scaffold/mcp/tests/paths.test.mjs +11 -3
  182. package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
  183. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  184. package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
  185. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  186. package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
  187. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  188. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  189. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  190. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  191. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  192. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  193. package/scaffold/scripts/dashboard.mjs +29 -18
  194. package/scaffold/scripts/doctor.sh +26 -8
  195. package/scaffold/scripts/ingest.mjs +37 -4
  196. package/scaffold/scripts/status.sh +13 -6
@@ -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
+ }