@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,347 @@
1
+ import { loadContextData } from "./graph.js";
2
+ import { iterateSearchEntities } from "./contextEntities.js";
3
+ import { embedQuery, loadEmbeddingIndex } from "./embeddings.js";
4
+ import { runContextSearch } from "./search.js";
5
+ import type { ChunkRecord, ContextData, PatternEvidenceParams, ToolPayload } from "./types.js";
6
+
7
+ type SearchResult = Record<string, unknown>;
8
+
9
+ type PatternTarget = {
10
+ input: string;
11
+ entity_id: string;
12
+ entity_type: "File" | "Chunk" | "ADR";
13
+ path: string;
14
+ };
15
+
16
+ type PatternEvidence = {
17
+ id: string;
18
+ entity_type: string;
19
+ kind: string;
20
+ title: string;
21
+ path: string;
22
+ start_line?: number;
23
+ end_line?: number;
24
+ excerpt: string;
25
+ score?: number;
26
+ matched_rules?: unknown[];
27
+ };
28
+
29
+ export type PatternEvidenceTierName =
30
+ | "same_file"
31
+ | "same_module"
32
+ | "same_feature_area"
33
+ | "repo_wide";
34
+
35
+ type PatternEvidenceTier = {
36
+ name: PatternEvidenceTierName;
37
+ scope: string;
38
+ evidence: PatternEvidence[];
39
+ };
40
+
41
+ const EVIDENCE_TIERS: Array<{ name: PatternEvidenceTierName; scope: string }> = [
42
+ { name: "same_file", scope: "Same file as the review target." },
43
+ { name: "same_module", scope: "Same directory or module as the review target." },
44
+ { name: "same_feature_area", scope: "Same parent feature area as the review target." },
45
+ { name: "repo_wide", scope: "Repository-wide fallback evidence." },
46
+ ];
47
+
48
+ export function normalizeRepoPath(value: string): string {
49
+ const normalized = value.trim().replace(/\\/g, "/").replace(/^\.\//, "");
50
+ return normalized.replace(/\/{2,}/g, "/").replace(/\/$/, "");
51
+ }
52
+
53
+ function dirnameRepoPath(value: string): string {
54
+ const normalized = normalizeRepoPath(value);
55
+ const separator = normalized.lastIndexOf("/");
56
+ return separator === -1 ? "." : normalized.slice(0, separator) || ".";
57
+ }
58
+
59
+ function parentRepoPath(value: string): string | null {
60
+ const normalized = normalizeRepoPath(value);
61
+ if (!normalized || normalized === ".") {
62
+ return null;
63
+ }
64
+ const separator = normalized.lastIndexOf("/");
65
+ return separator === -1 ? "." : normalized.slice(0, separator) || ".";
66
+ }
67
+
68
+ function isWithinPath(candidate: string, directory: string | null): boolean {
69
+ if (!directory || directory === ".") {
70
+ return false;
71
+ }
72
+ return candidate === directory || candidate.startsWith(`${directory}/`);
73
+ }
74
+
75
+ function resolvePatternTarget(data: ContextData, input: string): PatternTarget {
76
+ const normalizedInput = normalizeRepoPath(input);
77
+ const document = data.documents.find(
78
+ (entry) => entry.id === input || normalizeRepoPath(entry.path) === normalizedInput,
79
+ );
80
+ if (document) {
81
+ return {
82
+ input,
83
+ entity_id: document.id,
84
+ entity_type: document.kind === "ADR" ? "ADR" : "File",
85
+ path: normalizeRepoPath(document.path),
86
+ };
87
+ }
88
+
89
+ const chunk = data.chunks.find((entry) => entry.id === input);
90
+ if (chunk) {
91
+ const owner = data.documents.find((entry) => entry.id === chunk.file_id);
92
+ if (!owner) {
93
+ throw new Error(`Pattern target chunk has no indexed owner file: ${input}`);
94
+ }
95
+ return {
96
+ input,
97
+ entity_id: chunk.id,
98
+ entity_type: "Chunk",
99
+ path: normalizeRepoPath(owner.path),
100
+ };
101
+ }
102
+
103
+ const adr = data.adrs.find((entry) => entry.id === input);
104
+ if (adr?.path) {
105
+ return {
106
+ input,
107
+ entity_id: adr.id,
108
+ entity_type: "ADR",
109
+ path: normalizeRepoPath(adr.path),
110
+ };
111
+ }
112
+
113
+ const knownEntity = [
114
+ ...data.rules.map((entry) => entry.id),
115
+ ...data.modules.map((entry) => entry.id),
116
+ ...data.projects.map((entry) => entry.id),
117
+ ].includes(input);
118
+ if (knownEntity) {
119
+ throw new Error(`Pattern target is not file-backed: ${input}`);
120
+ }
121
+ throw new Error(`Pattern target was not found in indexed context: ${input}`);
122
+ }
123
+
124
+ function derivePatternQuery(data: ContextData, target: PatternTarget): string {
125
+ if (target.entity_type === "Chunk") {
126
+ const chunk = data.chunks.find((entry) => entry.id === target.entity_id);
127
+ if (chunk) {
128
+ return [chunk.name, chunk.kind, chunk.signature].filter(Boolean).join(" ");
129
+ }
130
+ }
131
+
132
+ const document = data.documents.find((entry) => entry.id === target.entity_id);
133
+ const chunkSignals = data.chunks
134
+ .filter((entry) => entry.file_id === target.entity_id && !entry.id.includes(":window:"))
135
+ .slice(0, 12)
136
+ .flatMap((entry) => [entry.name, entry.kind])
137
+ .filter(Boolean);
138
+ const basename = target.path.split("/").at(-1)?.replace(/\.[^.]+$/, "") ?? target.path;
139
+ return [basename, ...chunkSignals, document?.excerpt ?? ""].filter(Boolean).join(" ").slice(0, 1000);
140
+ }
141
+
142
+ function tierForPath(targetPath: string, candidatePath: string): PatternEvidenceTierName {
143
+ const normalizedTarget = normalizeRepoPath(targetPath);
144
+ const normalizedCandidate = normalizeRepoPath(candidatePath);
145
+ if (normalizedCandidate === normalizedTarget) {
146
+ return "same_file";
147
+ }
148
+
149
+ const targetModule = dirnameRepoPath(normalizedTarget);
150
+ if (dirnameRepoPath(normalizedCandidate) === targetModule) {
151
+ return "same_module";
152
+ }
153
+
154
+ const featureArea = parentRepoPath(targetModule);
155
+ if (isWithinPath(normalizedCandidate, featureArea)) {
156
+ return "same_feature_area";
157
+ }
158
+ return "repo_wide";
159
+ }
160
+
161
+ function toPatternEvidence(result: SearchResult, chunksById: Map<string, ChunkRecord>): PatternEvidence | null {
162
+ const id = typeof result.id === "string" ? result.id : "";
163
+ const candidatePath = typeof result.path === "string" ? normalizeRepoPath(result.path) : "";
164
+ if (!id || !candidatePath) {
165
+ return null;
166
+ }
167
+
168
+ const chunk = chunksById.get(id);
169
+ const entityType = typeof result.entity_type === "string" ? result.entity_type : "";
170
+ if (entityType === "Chunk" && (!chunk || chunk.start_line <= 0 || chunk.end_line < chunk.start_line)) {
171
+ return null;
172
+ }
173
+ const evidence: PatternEvidence = {
174
+ id,
175
+ entity_type: entityType,
176
+ kind: typeof result.kind === "string" ? result.kind : "",
177
+ title: typeof result.title === "string" ? result.title : id,
178
+ path: candidatePath,
179
+ excerpt: typeof result.excerpt === "string" ? result.excerpt : "",
180
+ };
181
+ if (chunk) {
182
+ evidence.start_line = chunk.start_line;
183
+ evidence.end_line = chunk.end_line;
184
+ }
185
+ if (typeof result.score === "number") {
186
+ evidence.score = result.score;
187
+ }
188
+ if (Array.isArray(result.matched_rules)) {
189
+ evidence.matched_rules = [...new Set(result.matched_rules)];
190
+ }
191
+ return evidence;
192
+ }
193
+
194
+ const referenceTimeCache = new WeakMap<ContextData, number>();
195
+
196
+ export function contextReferenceTimeMs(data: ContextData): number {
197
+ const cached = referenceTimeCache.get(data);
198
+ if (cached !== undefined) {
199
+ return cached;
200
+ }
201
+ let latest = 0;
202
+ const consider = (value: string): void => {
203
+ const timestamp = Date.parse(value);
204
+ if (Number.isFinite(timestamp) && timestamp > latest) {
205
+ latest = timestamp;
206
+ }
207
+ };
208
+ for (const entry of data.documents) consider(entry.updated_at);
209
+ for (const entry of data.rules) consider(entry.updated_at);
210
+ for (const entry of data.adrs) consider(entry.decision_date);
211
+ for (const entry of data.chunks) consider(entry.updated_at);
212
+ for (const entry of data.modules) consider(entry.updated_at);
213
+ for (const entry of data.projects) consider(entry.updated_at);
214
+ referenceTimeCache.set(data, latest);
215
+ return latest;
216
+ }
217
+
218
+ export function classifyPatternEvidence(input: {
219
+ target: PatternTarget;
220
+ results: SearchResult[];
221
+ chunks: ChunkRecord[];
222
+ topK: number;
223
+ }): { tiers: PatternEvidenceTier[]; localPatternFound: boolean; fallbackUsed: boolean } {
224
+ const chunksById = new Map(input.chunks.map((chunk) => [chunk.id, chunk]));
225
+ const evidenceByTier = new Map<PatternEvidenceTierName, PatternEvidence[]>(
226
+ EVIDENCE_TIERS.map((tier) => [tier.name, []]),
227
+ );
228
+ const seen = new Set<string>();
229
+
230
+ for (const result of input.results) {
231
+ if (result.id === input.target.entity_id) {
232
+ continue;
233
+ }
234
+ const evidence = toPatternEvidence(result, chunksById);
235
+ if (!evidence || seen.has(evidence.id)) {
236
+ continue;
237
+ }
238
+ seen.add(evidence.id);
239
+ const tierName = tierForPath(input.target.path, evidence.path);
240
+ const tierEvidence = evidenceByTier.get(tierName);
241
+ if (tierEvidence && tierEvidence.length < input.topK) {
242
+ tierEvidence.push(evidence);
243
+ }
244
+ }
245
+
246
+ const tiers = EVIDENCE_TIERS.map((tier) => ({
247
+ ...tier,
248
+ evidence: evidenceByTier.get(tier.name) ?? [],
249
+ }));
250
+ const localPatternFound = tiers.slice(0, 3).some((tier) => tier.evidence.length > 0);
251
+ const fallbackUsed = !localPatternFound && tiers[3].evidence.length > 0;
252
+ return { tiers, localPatternFound, fallbackUsed };
253
+ }
254
+
255
+ export async function runPatternEvidence(
256
+ parsed: PatternEvidenceParams,
257
+ options: { data?: ContextData; use_embeddings?: boolean } = {}
258
+ ): Promise<ToolPayload> {
259
+ const data = options.data ?? await loadContextData();
260
+ const target = resolvePatternTarget(data, parsed.target);
261
+ const explicitQuery = parsed.query?.trim();
262
+ const query = explicitQuery || derivePatternQuery(data, target);
263
+ if (!query) {
264
+ throw new Error(`Could not derive a pattern query for target: ${parsed.target}`);
265
+ }
266
+
267
+ const referenceTimeMs = contextReferenceTimeMs(data);
268
+ const embeddingIndex = options.use_embeddings === false
269
+ ? { model: null, vectors: new Map<string, Float32Array>() }
270
+ : loadEmbeddingIndex();
271
+ const queryVector = embeddingIndex.model && embeddingIndex.vectors.size > 0
272
+ ? await embedQuery(query, embeddingIndex.model)
273
+ : null;
274
+ const tierByEntityId = new Map<string, PatternEvidenceTierName>();
275
+ for (const entity of iterateSearchEntities(data, false)) {
276
+ if (
277
+ entity.id !== target.entity_id &&
278
+ entity.path &&
279
+ (entity.entity_type === "File" || entity.entity_type === "Chunk" || entity.entity_type === "ADR")
280
+ ) {
281
+ tierByEntityId.set(entity.id, tierForPath(target.path, entity.path));
282
+ }
283
+ }
284
+
285
+ const searchResults: SearchResult[] = [];
286
+ const warningParts: string[] = [];
287
+ let contextSource: unknown = data.source;
288
+ let semanticEngine: unknown;
289
+ for (const tier of EVIDENCE_TIERS) {
290
+ const search = await runContextSearch(
291
+ {
292
+ query,
293
+ top_k: parsed.top_k,
294
+ include_deprecated: parsed.include_deprecated ?? false,
295
+ response_preset: "full",
296
+ include_scores: true,
297
+ include_matched_rules: true,
298
+ },
299
+ {
300
+ data,
301
+ reference_time_ms: referenceTimeMs,
302
+ embedding_index: embeddingIndex,
303
+ query_vector: queryVector,
304
+ candidate_filter: (entity) => tierByEntityId.get(entity.id) === tier.name,
305
+ },
306
+ );
307
+ if (Array.isArray(search.results)) {
308
+ searchResults.push(...search.results as SearchResult[]);
309
+ }
310
+ if (typeof search.warning === "string" && !warningParts.includes(search.warning)) {
311
+ warningParts.push(search.warning);
312
+ }
313
+ contextSource = search.context_source ?? contextSource;
314
+ semanticEngine = search.semantic_engine ?? semanticEngine;
315
+ }
316
+ const classified = classifyPatternEvidence({
317
+ target,
318
+ results: searchResults,
319
+ chunks: data.chunks,
320
+ topK: parsed.top_k,
321
+ });
322
+ if (!classified.localPatternFound) {
323
+ warningParts.push("No applicable file-local, module-local, or feature-local pattern evidence was found.");
324
+ }
325
+
326
+ return {
327
+ target,
328
+ query,
329
+ query_source: explicitQuery ? "explicit" : "derived_from_target",
330
+ evidence_order: EVIDENCE_TIERS.map((tier) => tier.name),
331
+ top_k_per_tier: parsed.top_k,
332
+ ranking_reference_time: referenceTimeMs > 0 ? new Date(referenceTimeMs).toISOString() : null,
333
+ local_pattern_found: classified.localPatternFound,
334
+ fallback_used: classified.fallbackUsed,
335
+ tiers: classified.tiers,
336
+ context_source: contextSource,
337
+ semantic_engine: semanticEngine,
338
+ warning: warningParts.length > 0 ? warningParts.join(" | ") : undefined,
339
+ };
340
+ }
341
+
342
+ export async function runLocalPatternEvidence(
343
+ parsed: PatternEvidenceParams,
344
+ options: { data?: ContextData } = {}
345
+ ): Promise<ToolPayload> {
346
+ return runPatternEvidence(parsed, { ...options, use_embeddings: false });
347
+ }
@@ -120,6 +120,26 @@ export async function loadPlugins(server: McpServer): Promise<void> {
120
120
  );
121
121
  return;
122
122
  }
123
+ const [
124
+ { matchesEnterpriseHostIdentity },
125
+ { enterpriseCredentialId },
126
+ ] = await Promise.all([
127
+ import("./core/enterprise-host-identity.js"),
128
+ import("./core/license.js"),
129
+ ]);
130
+ if (
131
+ !matchesEnterpriseHostIdentity(
132
+ enterpriseCredentialId(
133
+ config.enterprise.base_url || config.enterprise.endpoint,
134
+ config.enterprise.api_key,
135
+ ),
136
+ )
137
+ ) {
138
+ process.stderr.write(
139
+ "[cortex] Enterprise inactive: this user profile is enrolled to another endpoint or API key\n",
140
+ );
141
+ return;
142
+ }
123
143
 
124
144
  if (typeof enterprise.register === "function") {
125
145
  await enterprise.register(server);
@@ -17,6 +17,7 @@ import {
17
17
  normalizeText,
18
18
  recencyScore,
19
19
  semanticScore,
20
+ structuralSearchBoost,
20
21
  tokenize
21
22
  } from "./searchCore.js";
22
23
  import { buildSearchResultsWithStats } from "./searchResults.js";
@@ -36,6 +37,8 @@ import {
36
37
  resolveSearchResponsePreset
37
38
  } from "./presets.js";
38
39
  import type {
40
+ ContextData,
41
+ EmbeddingIndex,
39
42
  ImpactParams,
40
43
  RelatedParams,
41
44
  RelationType,
@@ -58,33 +61,53 @@ const IMPACT_RELATION_TYPES = new Set([
58
61
  "PART_OF"
59
62
  ]);
60
63
 
64
+ export type ContextSearchOptions = {
65
+ data?: ContextData;
66
+ candidate_filter?: (entity: SearchEntity) => boolean;
67
+ reference_time_ms?: number;
68
+ embedding_index?: EmbeddingIndex;
69
+ query_vector?: Float32Array | null;
70
+ };
71
+
61
72
  function* filterSearchCandidates(
62
73
  candidates: Iterable<SearchEntity>,
63
- includeDeprecated: boolean
74
+ includeDeprecated: boolean,
75
+ candidateFilter?: (entity: SearchEntity) => boolean
64
76
  ): Generator<SearchEntity> {
65
77
  for (const entity of candidates) {
66
- if (includeDeprecated || entity.status.toLowerCase() !== "deprecated") {
78
+ if (
79
+ (includeDeprecated || entity.status.toLowerCase() !== "deprecated") &&
80
+ (!candidateFilter || candidateFilter(entity))
81
+ ) {
67
82
  yield entity;
68
83
  }
69
84
  }
70
85
  }
71
86
 
72
- export async function runContextSearch(parsed: SearchParams): Promise<ToolPayload> {
87
+ export async function runContextSearch(
88
+ parsed: SearchParams,
89
+ options: ContextSearchOptions = {}
90
+ ): Promise<ToolPayload> {
73
91
  const searchPresetConfig = resolveSearchResponsePreset(parsed);
74
92
  const responsePreset = searchPresetConfig.responsePreset;
75
93
  const includeScores = searchPresetConfig.includeScores;
76
94
  const includeMatchedRules = searchPresetConfig.includeMatchedRules;
77
95
  const includeContent = searchPresetConfig.includeContent;
78
- const data = await loadContextData();
96
+ const data = options.data ?? await loadContextData();
79
97
  const allRelations = [...data.relations, ...buildChunkPartOfRelations(data)];
80
98
  const degreeByEntity = relationDegree(allRelations);
81
99
  const queryTokens = expandQueryTokens(Array.from(new Set(tokenize(parsed.query))));
82
100
  const queryPhrase = normalizeText(parsed.query).trim();
83
101
  const candidates = () =>
84
- filterSearchCandidates(iterateSearchEntities(data, includeContent), parsed.include_deprecated);
85
- const embeddings = loadEmbeddingIndex();
86
- const queryVector =
87
- embeddings.model && embeddings.vectors.size > 0
102
+ filterSearchCandidates(
103
+ iterateSearchEntities(data, includeContent),
104
+ parsed.include_deprecated,
105
+ options.candidate_filter
106
+ );
107
+ const embeddings = options.embedding_index ?? loadEmbeddingIndex();
108
+ const queryVector = Object.prototype.hasOwnProperty.call(options, "query_vector")
109
+ ? options.query_vector ?? null
110
+ : embeddings.model && embeddings.vectors.size > 0
88
111
  ? await embedQuery(parsed.query, embeddings.model)
89
112
  : null;
90
113
 
@@ -104,7 +127,10 @@ export async function runContextSearch(parsed: SearchParams): Promise<ToolPayloa
104
127
  minVectorRelevance: MIN_VECTOR_RELEVANCE,
105
128
  semanticScorer: semanticScore,
106
129
  vectorScorer: cosineSimilarity,
107
- recencyScorer: recencyScore,
130
+ recencyScorer: options.reference_time_ms === undefined
131
+ ? recencyScore
132
+ : (isoDate) => recencyScore(isoDate, options.reference_time_ms),
133
+ structuralSearchBooster: structuralSearchBoost,
108
134
  legacyDataAccessBooster: legacyDataAccessBoost
109
135
  });
110
136