@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
@@ -20,6 +20,19 @@ const CONFIG_ENVIRONMENT_TOKENS = [
20
20
  ];
21
21
 
22
22
  const QUERY_TOKEN_EXPANSIONS: Record<string, string[]> = {
23
+ dashboard: ["status"],
24
+ embed: ["embedding", "embeddings"],
25
+ embedding: ["embed", "embeddings"],
26
+ embeddings: ["embed", "embedding"],
27
+ git: ["githooks"],
28
+ hook: ["hooks", "githooks"],
29
+ hooks: ["hook", "githooks"],
30
+ import: ["imports"],
31
+ imports: ["import"],
32
+ javascript: ["js"],
33
+ parser: ["parsers"],
34
+ parsers: ["parser"],
35
+ status: ["dashboard"],
23
36
  semantisk: ["semantic"],
24
37
  sökning: ["search"],
25
38
  sokning: ["search"],
@@ -30,15 +43,105 @@ const QUERY_TOKEN_EXPANSIONS: Record<string, string[]> = {
30
43
  avvikelse: ["deviation"]
31
44
  };
32
45
 
46
+ const STRUCTURAL_QUERY_STOP_TOKENS = new Set([
47
+ "about",
48
+ "after",
49
+ "and",
50
+ "are",
51
+ "before",
52
+ "between",
53
+ "does",
54
+ "from",
55
+ "happen",
56
+ "happens",
57
+ "has",
58
+ "how",
59
+ "into",
60
+ "the",
61
+ "their",
62
+ "then",
63
+ "through",
64
+ "when",
65
+ "where",
66
+ "while",
67
+ "with",
68
+ "what",
69
+ "which",
70
+ "who",
71
+ "why"
72
+ ]);
73
+
74
+ const IDENTIFIER_COMPOUND_TOKENS = new Set(["csharp", "fsharp", "graphql", "javascript", "typescript", "vbnet"]);
75
+ const LANGUAGE_TOKEN_GROUPS = [
76
+ ["javascript", "typescript", "js", "ts"],
77
+ ["csharp", "cs"],
78
+ ["vbnet", "vb"],
79
+ ["fsharp", "fs"],
80
+ ["java"],
81
+ ["cpp"],
82
+ ["python", "py"],
83
+ ["rust", "rs"],
84
+ ["ruby", "rb"],
85
+ ["go"],
86
+ ["php"],
87
+ ["swift"],
88
+ ["kotlin", "kt"],
89
+ ["sql"]
90
+ ];
91
+
33
92
  export function normalizeText(value: string): string {
34
93
  return value.normalize("NFKC").toLowerCase();
35
94
  }
36
95
 
96
+ function normalizeLanguageAliases(value: string): string {
97
+ return value
98
+ .replace(/(^|[^\p{L}\p{N}])c#(?=$|[^\p{L}\p{N}])/giu, "$1 csharp ")
99
+ .replace(/(^|[^\p{L}\p{N}])f#(?=$|[^\p{L}\p{N}])/giu, "$1 fsharp ")
100
+ .replace(/(^|[^\p{L}\p{N}])vb\.net(?=$|[^\p{L}\p{N}])/giu, "$1 vbnet ");
101
+ }
102
+
103
+ function splitIdentifierPart(part: string): string[] {
104
+ const split = part
105
+ .replace(/([\p{Ll}\p{N}])([\p{Lu}])/gu, "$1 $2")
106
+ .replace(/([\p{Lu}]+)([\p{Lu}][\p{Ll}])/gu, "$1 $2")
107
+ .split(/\s+/u)
108
+ .filter(Boolean);
109
+ if (split.length <= 1) {
110
+ return [];
111
+ }
112
+
113
+ const merged: string[] = [];
114
+ for (let index = 0; index < split.length; index += 1) {
115
+ const current = normalizeText(split[index]);
116
+ const next = index + 1 < split.length ? normalizeText(split[index + 1]) : "";
117
+ const compound = `${current}${next}`;
118
+ if (next && IDENTIFIER_COMPOUND_TOKENS.has(compound)) {
119
+ merged.push(compound);
120
+ index += 1;
121
+ continue;
122
+ }
123
+ merged.push(current);
124
+ }
125
+ return merged;
126
+ }
127
+
37
128
  export function tokenize(value: string): string[] {
38
- return normalizeText(value)
39
- .split(/[^\p{L}\p{N}]+/gu)
40
- .map((part) => part.trim())
41
- .filter((part) => part.length >= 2);
129
+ const tokens: string[] = [];
130
+ for (const rawPart of normalizeLanguageAliases(value).normalize("NFKC").split(/[^\p{L}\p{N}]+/gu)) {
131
+ const part = rawPart.trim();
132
+ if (part.length < 2) {
133
+ continue;
134
+ }
135
+
136
+ const variants = [part, ...splitIdentifierPart(part)];
137
+ for (const variant of variants) {
138
+ const token = normalizeText(variant).trim();
139
+ if (token.length >= 2) {
140
+ tokens.push(token);
141
+ }
142
+ }
143
+ }
144
+ return tokens;
42
145
  }
43
146
 
44
147
  export function expandQueryTokens(tokens: string[]): string[] {
@@ -55,18 +158,69 @@ export function expandQueryTokens(tokens: string[]): string[] {
55
158
  return Array.from(expanded);
56
159
  }
57
160
 
58
- function daysSince(isoDate: string): number {
161
+ function relatedQueryTokens(token: string): string[] {
162
+ const related = new Set<string>(QUERY_TOKEN_EXPANSIONS[token] ?? []);
163
+ for (const [source, aliases] of Object.entries(QUERY_TOKEN_EXPANSIONS)) {
164
+ if (!aliases.includes(token)) {
165
+ continue;
166
+ }
167
+ related.add(source);
168
+ for (const alias of aliases) {
169
+ related.add(alias);
170
+ }
171
+ }
172
+ return Array.from(related);
173
+ }
174
+
175
+ function queryTokenGroups(queryTokens: string[]): string[][] {
176
+ const inputTokens = new Set(queryTokens);
177
+ const visited = new Set<string>();
178
+ const groups: string[][] = [];
179
+
180
+ for (const token of queryTokens) {
181
+ if (visited.has(token)) {
182
+ continue;
183
+ }
184
+
185
+ const group = new Set<string>();
186
+ const queue = [token];
187
+ for (let index = 0; index < queue.length; index += 1) {
188
+ const current = queue[index];
189
+ if (group.has(current)) {
190
+ continue;
191
+ }
192
+ group.add(current);
193
+
194
+ for (const related of relatedQueryTokens(current)) {
195
+ group.add(related);
196
+ if (inputTokens.has(related) && !visited.has(related)) {
197
+ queue.push(related);
198
+ }
199
+ }
200
+ }
201
+
202
+ for (const member of group) {
203
+ if (inputTokens.has(member)) {
204
+ visited.add(member);
205
+ }
206
+ }
207
+ groups.push(Array.from(group));
208
+ }
209
+
210
+ return groups;
211
+ }
212
+
213
+ function daysSince(isoDate: string, referenceTimeMs: number): number {
59
214
  const timestamp = Date.parse(isoDate);
60
215
  if (Number.isNaN(timestamp)) {
61
216
  return 3650;
62
217
  }
63
218
 
64
- const now = Date.now();
65
- return Math.max(0, (now - timestamp) / (1000 * 60 * 60 * 24));
219
+ return Math.max(0, (referenceTimeMs - timestamp) / (1000 * 60 * 60 * 24));
66
220
  }
67
221
 
68
- export function recencyScore(isoDate: string): number {
69
- const days = daysSince(isoDate);
222
+ export function recencyScore(isoDate: string, referenceTimeMs = Date.now()): number {
223
+ const days = daysSince(isoDate, referenceTimeMs);
70
224
  return 1 / (1 + days / 30);
71
225
  }
72
226
 
@@ -80,14 +234,15 @@ export function semanticScore(queryTokens: string[], queryPhrase: string, text:
80
234
  return 0;
81
235
  }
82
236
 
237
+ const tokenGroups = queryTokenGroups(queryTokens);
83
238
  let matched = 0;
84
- for (const token of queryTokens) {
85
- if (textTokenSet.has(token)) {
239
+ for (const group of tokenGroups) {
240
+ if (group.some((token) => textTokenSet.has(token))) {
86
241
  matched += 1;
87
242
  }
88
243
  }
89
244
 
90
- const overlap = matched / queryTokens.length;
245
+ const overlap = matched / tokenGroups.length;
91
246
  if (overlap <= 0) {
92
247
  return 0;
93
248
  }
@@ -97,6 +252,93 @@ export function semanticScore(queryTokens: string[], queryPhrase: string, text:
97
252
  return Math.min(1, overlap * 0.85 + phraseBonus);
98
253
  }
99
254
 
255
+ function structuralQueryTokens(queryTokens: string[]): string[] {
256
+ return queryTokens.filter((token) => token.length >= 3 && !STRUCTURAL_QUERY_STOP_TOKENS.has(token));
257
+ }
258
+
259
+ function tokenSet(value: string): Set<string> {
260
+ return new Set(tokenize(value));
261
+ }
262
+
263
+ function overlapCount(queryTokens: string[], targetTokens: Set<string>): number {
264
+ let matched = 0;
265
+ for (const token of queryTokens) {
266
+ if (targetTokens.has(token)) {
267
+ matched += 1;
268
+ }
269
+ }
270
+ return matched;
271
+ }
272
+
273
+ function basenameWithoutExtension(pathValue: string): string {
274
+ const base = pathValue.split(/[\\/]/u).pop() ?? pathValue;
275
+ const extensionIndex = base.lastIndexOf(".");
276
+ return extensionIndex > 0 ? base.slice(0, extensionIndex) : base;
277
+ }
278
+
279
+ function languageGroupsForTokens(tokens: Set<string>): Set<number> {
280
+ const groups = new Set<number>();
281
+ for (let index = 0; index < LANGUAGE_TOKEN_GROUPS.length; index += 1) {
282
+ if (LANGUAGE_TOKEN_GROUPS[index].some((token) => tokens.has(token))) {
283
+ groups.add(index);
284
+ }
285
+ }
286
+ return groups;
287
+ }
288
+
289
+ function hasLanguageGroupOverlap(a: Set<number>, b: Set<number>): boolean {
290
+ for (const value of a) {
291
+ if (b.has(value)) {
292
+ return true;
293
+ }
294
+ }
295
+ return false;
296
+ }
297
+
298
+ export function structuralSearchBoost(entity: SearchEntity, queryTokens: string[], queryPhrase: string): number {
299
+ const structuralTokens = structuralQueryTokens(queryTokens);
300
+ if (structuralTokens.length === 0) {
301
+ return 0;
302
+ }
303
+
304
+ const pathTokens = tokenSet(entity.path);
305
+ const labelTokens = tokenSet(entity.label);
306
+ const pathMatches = overlapCount(structuralTokens, pathTokens);
307
+ const basenameMatches = overlapCount(structuralTokens, tokenSet(basenameWithoutExtension(entity.path)));
308
+ const labelMatches = overlapCount(structuralTokens, labelTokens);
309
+ const kindMatches = overlapCount(structuralTokens, tokenSet(entity.kind));
310
+
311
+ let boost = 0;
312
+ boost += Math.min(0.1, pathMatches * 0.035);
313
+ boost += Math.min(0.08, basenameMatches * 0.06);
314
+ boost += Math.min(0.08, labelMatches * 0.04);
315
+ boost += Math.min(0.03, kindMatches * 0.02);
316
+
317
+ const normalizedPath = normalizeText(entity.path);
318
+ const normalizedLabel = normalizeText(entity.label);
319
+ const compactPhrase = queryPhrase.replace(/[^\p{L}\p{N}]+/gu, "");
320
+ if (compactPhrase.length >= 8) {
321
+ const compactPath = normalizedPath.replace(/[^\p{L}\p{N}]+/gu, "");
322
+ const compactLabel = normalizedLabel.replace(/[^\p{L}\p{N}]+/gu, "");
323
+ if (compactPath.includes(compactPhrase) || compactLabel.includes(compactPhrase)) {
324
+ boost += 0.04;
325
+ }
326
+ }
327
+
328
+ const queryLanguageGroups = languageGroupsForTokens(new Set(queryTokens));
329
+ const pathLanguageGroups = languageGroupsForTokens(new Set([...pathTokens, ...labelTokens]));
330
+ if (
331
+ queryLanguageGroups.size > 0 &&
332
+ pathLanguageGroups.size > 0 &&
333
+ pathTokens.has("parsers") &&
334
+ !hasLanguageGroupOverlap(queryLanguageGroups, pathLanguageGroups)
335
+ ) {
336
+ boost -= 0.08;
337
+ }
338
+
339
+ return Math.min(0.2, boost);
340
+ }
341
+
100
342
  function queryHasAnyToken(queryTokens: string[], candidates: string[]): boolean {
101
343
  return candidates.some((candidate) => queryTokens.includes(candidate));
102
344
  }
@@ -28,13 +28,25 @@ function baseChunkLabel(label: string): string {
28
28
  return markerIndex === -1 ? label : label.slice(0, markerIndex);
29
29
  }
30
30
 
31
+ export function compareText(a: string, b: string): number {
32
+ return a < b ? -1 : a > b ? 1 : 0;
33
+ }
34
+
35
+ function rankedResultKey(ranked: RankedSearchResult): string {
36
+ return `${String(ranked.result.id ?? "")}\u0000${String(ranked.result.path ?? "")}`;
37
+ }
38
+
39
+ function compareRankedResults(a: RankedSearchResult, b: RankedSearchResult): number {
40
+ return b.rankScore - a.rankScore || compareText(rankedResultKey(a), rankedResultKey(b));
41
+ }
42
+
31
43
  function pruneRankedResults(bestById: Map<string, RankedSearchResult>, limit: number): void {
32
44
  if (bestById.size <= limit) {
33
45
  return;
34
46
  }
35
47
 
36
48
  const retained = [...bestById.entries()]
37
- .sort(([, a], [, b]) => b.rankScore - a.rankScore)
49
+ .sort(([, a], [, b]) => compareRankedResults(a, b))
38
50
  .slice(0, limit);
39
51
  bestById.clear();
40
52
  for (const [id, result] of retained) {
@@ -42,6 +54,72 @@ function pruneRankedResults(bestById: Map<string, RankedSearchResult>, limit: nu
42
54
  }
43
55
  }
44
56
 
57
+ function secondarySignalScale(semantic: number): number {
58
+ return Math.min(1, 0.15 + Math.max(0, semantic) * 0.85);
59
+ }
60
+
61
+ function isTestEvidenceEntity(entity: SearchEntity): boolean {
62
+ return /(^|\/)(tests?|__tests__)\//u.test(entity.path) || /\.(test|spec)\.[^.]+$/u.test(entity.path);
63
+ }
64
+
65
+ function testEvidenceBoost(entity: SearchEntity, semantic: number, lexicalSemantic: number): number {
66
+ if (!isTestEvidenceEntity(entity)) {
67
+ return 0;
68
+ }
69
+ if (semantic >= 0.5 && lexicalSemantic >= 0.4) {
70
+ return 0.07;
71
+ }
72
+ if (semantic >= 0.4 && lexicalSemantic >= 0.25) {
73
+ return 0.04;
74
+ }
75
+ return 0;
76
+ }
77
+
78
+ function resultPathKey(result: SearchResult): string {
79
+ const path = typeof result.path === "string" ? result.path : "";
80
+ return path || String(result.id ?? "");
81
+ }
82
+
83
+ function selectDiverseResults(rankedResults: RankedSearchResult[], topK: number): RankedSearchResult[] {
84
+ const remaining = rankedResults
85
+ .map((ranked) => ({ ranked }))
86
+ .sort((a, b) => compareRankedResults(a.ranked, b.ranked));
87
+ const selected: RankedSearchResult[] = [];
88
+ const selectedByPath = new Map<string, number>();
89
+
90
+ while (selected.length < topK && remaining.length > 0) {
91
+ let bestIndex = 0;
92
+ let bestAdjustedScore = Number.NEGATIVE_INFINITY;
93
+ let bestRankScore = Number.NEGATIVE_INFINITY;
94
+ let bestKey = "";
95
+
96
+ for (let index = 0; index < remaining.length; index += 1) {
97
+ const candidate = remaining[index].ranked;
98
+ const pathCount = selectedByPath.get(resultPathKey(candidate.result)) ?? 0;
99
+ const samePathPenalty = pathCount === 0 ? 0 : Math.min(0.12, pathCount * 0.04);
100
+ const adjustedScore = candidate.rankScore - samePathPenalty;
101
+ const candidateKey = rankedResultKey(candidate);
102
+ if (
103
+ adjustedScore > bestAdjustedScore ||
104
+ (adjustedScore === bestAdjustedScore && candidate.rankScore > bestRankScore) ||
105
+ (adjustedScore === bestAdjustedScore && candidate.rankScore === bestRankScore && candidateKey < bestKey)
106
+ ) {
107
+ bestIndex = index;
108
+ bestAdjustedScore = adjustedScore;
109
+ bestRankScore = candidate.rankScore;
110
+ bestKey = candidateKey;
111
+ }
112
+ }
113
+
114
+ const [picked] = remaining.splice(bestIndex, 1);
115
+ selected.push(picked.ranked);
116
+ const pathKey = resultPathKey(picked.ranked.result);
117
+ selectedByPath.set(pathKey, (selectedByPath.get(pathKey) ?? 0) + 1);
118
+ }
119
+
120
+ return selected;
121
+ }
122
+
45
123
  function candidatesFrom(source: SearchCandidateSource): Iterable<SearchEntity> {
46
124
  return typeof source === "function" ? source() : source;
47
125
  }
@@ -63,6 +141,7 @@ export function buildSearchResultsWithStats(params: {
63
141
  semanticScorer: (queryTokens: string[], queryPhrase: string, text: string) => number;
64
142
  vectorScorer: (a: Float32Array, b: Float32Array) => number;
65
143
  recencyScorer: (isoDate: string) => number;
144
+ structuralSearchBooster?: (entity: SearchEntity, queryTokens: string[], queryPhrase: string) => number;
66
145
  legacyDataAccessBooster: (entity: SearchEntity, queryTokens: string[], queryPhrase: string) => number;
67
146
  }): { results: Record<string, unknown>[]; totalCandidates: number } {
68
147
  if (params.topK <= 0) {
@@ -112,7 +191,8 @@ export function buildSearchResultsWithStats(params: {
112
191
  };
113
192
 
114
193
  const resultLimit = Math.max(1, params.topK);
115
- const pruneThreshold = Math.max(resultLimit * 4, 64);
194
+ const diversityPoolLimit = Math.max(resultLimit * 3, resultLimit);
195
+ const pruneThreshold = Math.max(diversityPoolLimit * 2, 64);
116
196
  const bestById = new Map<string, RankedSearchResult>();
117
197
 
118
198
  for (const entity of candidatesFrom(params.candidates)) {
@@ -134,12 +214,17 @@ export function buildSearchResultsWithStats(params: {
134
214
  const graphScore = midrankPercentile(sortedDegreesByType.get(entity.entity_type) ?? [], degree);
135
215
  const trustScore = Math.max(0, Math.min(1, entity.trust_level / 100));
136
216
  const dateScore = params.recencyScorer(entity.updated_at);
217
+ const structuralBoost = params.structuralSearchBooster?.(entity, params.queryTokens, params.queryPhrase) ?? 0;
218
+ const evidenceBoost = testEvidenceBoost(entity, semantic, lexicalSemantic);
219
+ const secondaryScale = secondarySignalScale(Math.max(semantic, structuralBoost));
137
220
 
138
221
  let score = 0;
139
222
  score += params.ranking.semantic * semantic;
140
- score += params.ranking.graph * graphScore;
141
- score += params.ranking.trust * trustScore;
142
- score += params.ranking.recency * dateScore;
223
+ score += params.ranking.graph * graphScore * secondaryScale;
224
+ score += params.ranking.trust * trustScore * secondaryScale;
225
+ score += params.ranking.recency * dateScore * secondaryScale;
226
+ score += structuralBoost;
227
+ score += evidenceBoost;
143
228
  score += params.legacyDataAccessBooster(entity, params.queryTokens, params.queryPhrase);
144
229
 
145
230
  if (entity.source_of_truth) {
@@ -194,13 +279,15 @@ export function buildSearchResultsWithStats(params: {
194
279
  }
195
280
 
196
281
  if (bestById.size > pruneThreshold) {
197
- pruneRankedResults(bestById, resultLimit);
282
+ pruneRankedResults(bestById, diversityPoolLimit);
198
283
  }
199
284
  }
200
285
 
201
- const results = [...bestById.values()]
202
- .sort((a, b) => b.rankScore - a.rankScore)
203
- .slice(0, params.topK)
286
+ const rankedResults = [...bestById.values()]
287
+ .sort(compareRankedResults)
288
+ .slice(0, diversityPoolLimit);
289
+ const results = selectDiverseResults(rankedResults, params.topK)
290
+ .sort(compareRankedResults)
204
291
  .map((ranked) => ranked.result);
205
292
  return { results, totalCandidates };
206
293
  }
@@ -203,6 +203,13 @@ export type RulesParams = {
203
203
  include_inactive: boolean;
204
204
  };
205
205
 
206
+ export type PatternEvidenceParams = {
207
+ target: string;
208
+ query?: string;
209
+ top_k: number;
210
+ include_deprecated?: boolean;
211
+ };
212
+
206
213
  export type ReloadParams = {
207
214
  force: boolean;
208
215
  };
@@ -0,0 +1,41 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { execFileSync } from "node:child_process";
7
+ import { resolveChangedReviewFiles } from "../dist/enterprise/reviews/changed-files.js";
8
+
9
+ function git(cwd, args) {
10
+ execFileSync("git", args, { cwd, stdio: "ignore" });
11
+ }
12
+
13
+ test("lists staged and untracked files in a repo without commits", () => {
14
+ const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-changed-files-"));
15
+ try {
16
+ git(projectRoot, ["init"]);
17
+ fs.writeFileSync(path.join(projectRoot, ".gitignore"), "ignored.ts\n", "utf8");
18
+ fs.writeFileSync(path.join(projectRoot, "staged.ts"), "export const a = 1;\n", "utf8");
19
+ fs.writeFileSync(path.join(projectRoot, "untracked.ts"), "export const b = 2;\n", "utf8");
20
+ fs.writeFileSync(path.join(projectRoot, "ignored.ts"), "ignored\n", "utf8");
21
+ git(projectRoot, ["add", ".gitignore", "staged.ts"]);
22
+
23
+ assert.deepEqual(resolveChangedReviewFiles(projectRoot), [
24
+ ".gitignore",
25
+ "staged.ts",
26
+ "untracked.ts",
27
+ ]);
28
+ } finally {
29
+ fs.rmSync(projectRoot, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ test("returns null outside a git work tree", () => {
34
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-changed-files-plain-"));
35
+ try {
36
+ fs.writeFileSync(path.join(dir, "a.ts"), "export const a = 1;\n", "utf8");
37
+ assert.equal(resolveChangedReviewFiles(dir), null);
38
+ } finally {
39
+ fs.rmSync(dir, { recursive: true, force: true });
40
+ }
41
+ });
@@ -80,7 +80,25 @@ test("installCopilotShim: refuses to overwrite a non-shim file at shim path", ()
80
80
  searchPath: ws.searchPath,
81
81
  });
82
82
  assert.equal(result.ok, false);
83
- assert.match(result.message, /not a cortex shim/);
83
+ assert.match(result.message, /not a regular cortex shim/);
84
+ } finally {
85
+ fs.rmSync(ws.dir, { recursive: true, force: true });
86
+ }
87
+ });
88
+
89
+ test("installCopilotShim: refuses a dangling symlink without touching its target", () => {
90
+ const ws = makeWorkspace();
91
+ const target = path.join(ws.dir, "must-not-be-created");
92
+ try {
93
+ fs.symlinkSync(target, ws.shimPath);
94
+ const result = installCopilotShim({
95
+ shimPath: ws.shimPath,
96
+ searchPath: ws.searchPath,
97
+ });
98
+ assert.equal(result.ok, false);
99
+ assert.match(result.message, /regular cortex shim/);
100
+ assert.equal(fs.existsSync(target), false);
101
+ assert.equal(fs.lstatSync(ws.shimPath).isSymbolicLink(), true);
84
102
  } finally {
85
103
  fs.rmSync(ws.dir, { recursive: true, force: true });
86
104
  }
@@ -237,3 +237,40 @@ test("egress proxy: stop() closes the server", async () => {
237
237
  assert.equal(proxy.isRunning(), false);
238
238
  fs.rmSync(cwd, { recursive: true, force: true });
239
239
  });
240
+
241
+ test("egress proxy: an unattributed shared connection is not written into either project audit boundary", async () => {
242
+ const echo = await startEchoServer();
243
+ const projectA = makeProject();
244
+ const projectB = makeProject();
245
+ const proxy = await startEgressProxy({ port: 0, hostId: "test-host" });
246
+ try {
247
+ await new Promise((resolve, reject) => {
248
+ const client = net.connect(proxy.port, "127.0.0.1", () => {
249
+ client.write(
250
+ `GET http://127.0.0.1:${echo.port}/health HTTP/1.1\r\n` +
251
+ `Host: 127.0.0.1:${echo.port}\r\n\r\n`,
252
+ );
253
+ });
254
+ client.on("data", () => {
255
+ client.end();
256
+ resolve();
257
+ });
258
+ client.on("error", reject);
259
+ });
260
+ await new Promise((resolve) => setTimeout(resolve, 100));
261
+
262
+ assert.equal(
263
+ fs.existsSync(path.join(projectA, ".context", "audit")),
264
+ false,
265
+ );
266
+ assert.equal(
267
+ fs.existsSync(path.join(projectB, ".context", "audit")),
268
+ false,
269
+ );
270
+ } finally {
271
+ await proxy.stop();
272
+ await new Promise((resolve) => echo.server.close(resolve));
273
+ fs.rmSync(projectA, { recursive: true, force: true });
274
+ fs.rmSync(projectB, { recursive: true, force: true });
275
+ }
276
+ });