@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.
- package/CHANGELOG.md +201 -0
- package/README.md +73 -3
- package/bin/cortex.mjs +339 -242
- package/bin/daemon-control.mjs +162 -0
- package/package.json +7 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +336 -212
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/query.ts +36 -2
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/embed.ts +211 -10
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/index.ts +1 -1
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
- package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/paths.ts +3 -5
- package/scaffold/mcp/src/patternEvidence.ts +347 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/src/search.ts +35 -9
- package/scaffold/mcp/src/searchCore.ts +254 -12
- package/scaffold/mcp/src/searchResults.ts +96 -9
- package/scaffold/mcp/src/types.ts +7 -0
- package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/paths.test.mjs +11 -3
- package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
- package/scaffold/scripts/dashboard.mjs +29 -18
- package/scaffold/scripts/doctor.sh +26 -8
- package/scaffold/scripts/ingest.mjs +37 -4
- 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 (
|
|
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(
|
|
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(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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:
|
|
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
|
|