@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,321 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
classifyPatternEvidence,
|
|
5
|
+
contextReferenceTimeMs,
|
|
6
|
+
runLocalPatternEvidence,
|
|
7
|
+
runPatternEvidence,
|
|
8
|
+
} from "../dist/patternEvidence.js";
|
|
9
|
+
|
|
10
|
+
function chunk(id, fileId, startLine, endLine) {
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
file_id: fileId,
|
|
14
|
+
name: id,
|
|
15
|
+
kind: "function",
|
|
16
|
+
signature: "",
|
|
17
|
+
body: "",
|
|
18
|
+
description: "",
|
|
19
|
+
start_line: startLine,
|
|
20
|
+
end_line: endLine,
|
|
21
|
+
language: "typescript",
|
|
22
|
+
exported: false,
|
|
23
|
+
updated_at: "2026-07-12T00:00:00.000Z",
|
|
24
|
+
source_of_truth: false,
|
|
25
|
+
trust_level: 60,
|
|
26
|
+
status: "active",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function document(id, filePath, kind, content = "") {
|
|
31
|
+
return {
|
|
32
|
+
id,
|
|
33
|
+
path: filePath,
|
|
34
|
+
kind,
|
|
35
|
+
updated_at: "2026-07-12T00:00:00.000Z",
|
|
36
|
+
source_of_truth: false,
|
|
37
|
+
trust_level: kind === "DOC" ? 80 : 60,
|
|
38
|
+
status: "active",
|
|
39
|
+
excerpt: content,
|
|
40
|
+
content,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function contextData({ documents, chunks = [] }) {
|
|
45
|
+
return {
|
|
46
|
+
documents,
|
|
47
|
+
chunks,
|
|
48
|
+
rules: [],
|
|
49
|
+
adrs: [],
|
|
50
|
+
modules: [],
|
|
51
|
+
projects: [],
|
|
52
|
+
relations: [],
|
|
53
|
+
ranking: { semantic: 0.4, graph: 0.25, trust: 0.2, recency: 0.15 },
|
|
54
|
+
source: "cache",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test("classifies helper, error, and config evidence in local-first order with line citations", () => {
|
|
59
|
+
const target = {
|
|
60
|
+
input: "src/features/auth/controller.ts",
|
|
61
|
+
entity_id: "file:src/features/auth/controller.ts",
|
|
62
|
+
entity_type: "File",
|
|
63
|
+
path: "src/features/auth/controller.ts",
|
|
64
|
+
};
|
|
65
|
+
const results = [
|
|
66
|
+
{
|
|
67
|
+
id: target.entity_id,
|
|
68
|
+
entity_type: "File",
|
|
69
|
+
kind: "CODE",
|
|
70
|
+
title: target.path,
|
|
71
|
+
path: target.path,
|
|
72
|
+
excerpt: "target must not cite itself",
|
|
73
|
+
score: 0.99,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "chunk:helper",
|
|
77
|
+
entity_type: "Chunk",
|
|
78
|
+
kind: "function",
|
|
79
|
+
title: "normalizeUser",
|
|
80
|
+
path: "src\\features\\auth\\controller.ts",
|
|
81
|
+
excerpt: "file-local helper shape",
|
|
82
|
+
score: 0.9,
|
|
83
|
+
matched_rules: ["rule.repo_local_pattern_review", "rule.repo_local_pattern_review"],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "chunk:error",
|
|
87
|
+
entity_type: "Chunk",
|
|
88
|
+
kind: "function",
|
|
89
|
+
title: "toAuthError",
|
|
90
|
+
path: "src/features/auth/errors.ts",
|
|
91
|
+
excerpt: "module-local error handling",
|
|
92
|
+
score: 0.8,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "chunk:config",
|
|
96
|
+
entity_type: "Chunk",
|
|
97
|
+
kind: "function",
|
|
98
|
+
title: "parseFeatureEnv",
|
|
99
|
+
path: "src/features/config/env.ts",
|
|
100
|
+
excerpt: "feature-local config parsing",
|
|
101
|
+
score: 0.7,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "file:docs/conventions.md",
|
|
105
|
+
entity_type: "File",
|
|
106
|
+
kind: "DOC",
|
|
107
|
+
title: "docs/conventions.md",
|
|
108
|
+
path: "docs/conventions.md",
|
|
109
|
+
excerpt: "repository fallback",
|
|
110
|
+
score: 0.6,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
const chunks = [
|
|
114
|
+
chunk("chunk:helper", "file:src/features/auth/controller.ts", 10, 18),
|
|
115
|
+
chunk("chunk:error", "file:src/features/auth/errors.ts", 4, 12),
|
|
116
|
+
chunk("chunk:config", "file:src/features/config/env.ts", 20, 31),
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const classified = classifyPatternEvidence({ target, results, chunks, topK: 3 });
|
|
120
|
+
|
|
121
|
+
assert.deepEqual(classified.tiers.map((tier) => tier.name), [
|
|
122
|
+
"same_file",
|
|
123
|
+
"same_module",
|
|
124
|
+
"same_feature_area",
|
|
125
|
+
"repo_wide",
|
|
126
|
+
]);
|
|
127
|
+
assert.deepEqual(classified.tiers.map((tier) => tier.evidence.map((item) => item.id)), [
|
|
128
|
+
["chunk:helper"],
|
|
129
|
+
["chunk:error"],
|
|
130
|
+
["chunk:config"],
|
|
131
|
+
["file:docs/conventions.md"],
|
|
132
|
+
]);
|
|
133
|
+
assert.equal(classified.tiers[0].evidence[0].path, "src/features/auth/controller.ts");
|
|
134
|
+
assert.equal(classified.tiers[0].evidence[0].start_line, 10);
|
|
135
|
+
assert.equal(classified.tiers[0].evidence[0].end_line, 18);
|
|
136
|
+
assert.deepEqual(classified.tiers[0].evidence[0].matched_rules, ["rule.repo_local_pattern_review"]);
|
|
137
|
+
assert.equal(classified.localPatternFound, true);
|
|
138
|
+
assert.equal(classified.fallbackUsed, false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("reports repository fallback without claiming a local pattern", () => {
|
|
142
|
+
const classified = classifyPatternEvidence({
|
|
143
|
+
target: {
|
|
144
|
+
input: "src/isolated.ts",
|
|
145
|
+
entity_id: "file:src/isolated.ts",
|
|
146
|
+
entity_type: "File",
|
|
147
|
+
path: "src/isolated.ts",
|
|
148
|
+
},
|
|
149
|
+
results: [
|
|
150
|
+
{
|
|
151
|
+
id: "file:docs/general-practices.md",
|
|
152
|
+
entity_type: "File",
|
|
153
|
+
kind: "DOC",
|
|
154
|
+
title: "docs/general-practices.md",
|
|
155
|
+
path: "docs/general-practices.md",
|
|
156
|
+
excerpt: "general fallback only",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
chunks: [],
|
|
160
|
+
topK: 3,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
assert.equal(classified.localPatternFound, false);
|
|
164
|
+
assert.equal(classified.fallbackUsed, true);
|
|
165
|
+
assert.deepEqual(classified.tiers.slice(0, 3).flatMap((tier) => tier.evidence), []);
|
|
166
|
+
assert.equal(classified.tiers[3].evidence.length, 1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("filters chunk evidence when indexed line bounds are missing or invalid", () => {
|
|
170
|
+
const target = {
|
|
171
|
+
input: "src/a.ts",
|
|
172
|
+
entity_id: "file:src/a.ts",
|
|
173
|
+
entity_type: "File",
|
|
174
|
+
path: "src/a.ts",
|
|
175
|
+
};
|
|
176
|
+
const results = [
|
|
177
|
+
{
|
|
178
|
+
id: "chunk:missing",
|
|
179
|
+
entity_type: "Chunk",
|
|
180
|
+
kind: "function",
|
|
181
|
+
title: "missing",
|
|
182
|
+
path: "src/b.ts",
|
|
183
|
+
excerpt: "missing metadata",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "chunk:invalid",
|
|
187
|
+
entity_type: "Chunk",
|
|
188
|
+
kind: "function",
|
|
189
|
+
title: "invalid",
|
|
190
|
+
path: "src/c.ts",
|
|
191
|
+
excerpt: "invalid metadata",
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const classified = classifyPatternEvidence({
|
|
196
|
+
target,
|
|
197
|
+
results,
|
|
198
|
+
chunks: [chunk("chunk:invalid", "file:src/c.ts", 0, 0)],
|
|
199
|
+
topK: 3,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
assert.deepEqual(classified.tiers.flatMap((tier) => tier.evidence), []);
|
|
203
|
+
assert.equal(classified.localPatternFound, false);
|
|
204
|
+
assert.equal(classified.fallbackUsed, false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("retrieves each locality tier before cutoff and stays deterministic", async () => {
|
|
208
|
+
const target = document("file:src/auth/handler.ts", "src/auth/handler.ts", "CODE");
|
|
209
|
+
const localFile = document("file:src/auth/config.ts", "src/auth/config.ts", "CODE");
|
|
210
|
+
const localChunk = {
|
|
211
|
+
...chunk("chunk:local-config", localFile.id, 7, 14),
|
|
212
|
+
name: "parseLocalSetting",
|
|
213
|
+
body: "environment",
|
|
214
|
+
description: "module-local environment parsing",
|
|
215
|
+
};
|
|
216
|
+
const repoDocuments = Array.from({ length: 60 }, (_, index) =>
|
|
217
|
+
document(
|
|
218
|
+
`file:docs/pattern-${index}.md`,
|
|
219
|
+
`docs/pattern-${index}.md`,
|
|
220
|
+
"DOC",
|
|
221
|
+
"environment variable parsing configuration pattern",
|
|
222
|
+
));
|
|
223
|
+
const data = contextData({ documents: [target, localFile, ...repoDocuments], chunks: [localChunk] });
|
|
224
|
+
const input = {
|
|
225
|
+
target: target.path,
|
|
226
|
+
query: "environment variable parsing configuration pattern",
|
|
227
|
+
top_k: 2,
|
|
228
|
+
include_deprecated: false,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const first = await runPatternEvidence(input, { data });
|
|
232
|
+
const second = await runPatternEvidence(input, { data });
|
|
233
|
+
const moduleTier = first.tiers.find((tier) => tier.name === "same_module");
|
|
234
|
+
|
|
235
|
+
assert.ok(moduleTier.evidence.some((evidence) => evidence.id === localChunk.id));
|
|
236
|
+
assert.equal(first.local_pattern_found, true);
|
|
237
|
+
assert.equal(first.ranking_reference_time, "2026-07-12T00:00:00.000Z");
|
|
238
|
+
assert.deepEqual(second, first);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("runtime response exposes repository-only fallback and warning", async () => {
|
|
242
|
+
const target = document("file:src/isolated.ts", "src/isolated.ts", "CODE");
|
|
243
|
+
const fallback = document(
|
|
244
|
+
"file:docs/general-practices.md",
|
|
245
|
+
"docs/general-practices.md",
|
|
246
|
+
"DOC",
|
|
247
|
+
"general retry convention",
|
|
248
|
+
);
|
|
249
|
+
const result = await runPatternEvidence({
|
|
250
|
+
target: target.path,
|
|
251
|
+
query: "general retry convention",
|
|
252
|
+
top_k: 2,
|
|
253
|
+
include_deprecated: false,
|
|
254
|
+
}, {
|
|
255
|
+
data: contextData({ documents: [target, fallback] }),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
assert.equal(result.local_pattern_found, false);
|
|
259
|
+
assert.equal(result.fallback_used, true);
|
|
260
|
+
assert.match(result.warning, /No applicable file-local, module-local, or feature-local pattern/);
|
|
261
|
+
assert.equal(result.tiers[3].evidence[0].id, fallback.id);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("reference time calculation stays bounded for large indexes", () => {
|
|
265
|
+
const documents = Array.from({ length: 200_000 }, (_, index) => ({
|
|
266
|
+
updated_at: index === 199_999 ? "2026-07-13T00:00:00.000Z" : "2026-07-12T00:00:00.000Z",
|
|
267
|
+
}));
|
|
268
|
+
const data = contextData({ documents });
|
|
269
|
+
|
|
270
|
+
assert.equal(contextReferenceTimeMs(data), Date.parse("2026-07-13T00:00:00.000Z"));
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("equal-score evidence is stable across reversed source order", async () => {
|
|
274
|
+
const target = document("file:src/isolated.ts", "src/isolated.ts", "CODE");
|
|
275
|
+
const a = document("file:docs/a.md", "docs/a.md", "DOC", "shared convention");
|
|
276
|
+
const b = document("file:docs/b.md", "docs/b.md", "DOC", "shared convention");
|
|
277
|
+
const input = {
|
|
278
|
+
target: target.path,
|
|
279
|
+
query: "shared convention",
|
|
280
|
+
top_k: 1,
|
|
281
|
+
include_deprecated: false,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const forward = await runPatternEvidence(input, {
|
|
285
|
+
data: contextData({ documents: [target, a, b] }),
|
|
286
|
+
});
|
|
287
|
+
const reversed = await runPatternEvidence(input, {
|
|
288
|
+
data: contextData({ documents: [target, b, a] }),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
assert.deepEqual(reversed, forward);
|
|
292
|
+
assert.equal(forward.tiers[3].evidence[0].id, a.id);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("local-only pattern evidence uses lexical search without network fetch", async () => {
|
|
296
|
+
const target = document("file:src/a.ts", "src/a.ts", "CODE");
|
|
297
|
+
const fallback = document("file:docs/pattern.md", "docs/pattern.md", "DOC", "shared retry convention");
|
|
298
|
+
const originalFetch = globalThis.fetch;
|
|
299
|
+
let fetchCalls = 0;
|
|
300
|
+
globalThis.fetch = async () => {
|
|
301
|
+
fetchCalls += 1;
|
|
302
|
+
throw new Error("network access is forbidden in local review");
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const result = await runLocalPatternEvidence({
|
|
307
|
+
target: target.path,
|
|
308
|
+
query: "shared retry convention",
|
|
309
|
+
top_k: 1,
|
|
310
|
+
include_deprecated: false,
|
|
311
|
+
}, {
|
|
312
|
+
data: contextData({ documents: [target, fallback] }),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
assert.equal(fetchCalls, 0);
|
|
316
|
+
assert.equal(result.semantic_engine, "lexical-only");
|
|
317
|
+
assert.equal(result.fallback_used, true);
|
|
318
|
+
} finally {
|
|
319
|
+
globalThis.fetch = originalFetch;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
|
|
7
|
+
import {
|
|
8
|
+
normalizeProjectCwd,
|
|
9
|
+
ProjectServiceRegistry,
|
|
10
|
+
} from "../dist/daemon/project-service-registry.js";
|
|
11
|
+
|
|
12
|
+
function makeProject() {
|
|
13
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-daemon-project-"));
|
|
14
|
+
fs.mkdirSync(path.join(cwd, ".context"));
|
|
15
|
+
return cwd;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("ProjectServiceRegistry: registers each normalized project exactly once", async () => {
|
|
19
|
+
const first = makeProject();
|
|
20
|
+
const second = makeProject();
|
|
21
|
+
const created = [];
|
|
22
|
+
const stopped = [];
|
|
23
|
+
const registry = new ProjectServiceRegistry((cwd) => {
|
|
24
|
+
created.push(cwd);
|
|
25
|
+
return [{
|
|
26
|
+
stop() {
|
|
27
|
+
stopped.push(cwd);
|
|
28
|
+
},
|
|
29
|
+
}];
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
assert.equal(registry.register(first), true);
|
|
34
|
+
assert.equal(registry.register(path.join(first, ".")), false);
|
|
35
|
+
assert.equal(registry.register(second), true);
|
|
36
|
+
assert.equal(registry.size(), 2);
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
registry.projectCwds().sort(),
|
|
39
|
+
[fs.realpathSync(first), fs.realpathSync(second)].sort(),
|
|
40
|
+
);
|
|
41
|
+
assert.deepEqual(created.sort(), registry.projectCwds().sort());
|
|
42
|
+
|
|
43
|
+
await registry.stopAll();
|
|
44
|
+
assert.equal(registry.size(), 0);
|
|
45
|
+
assert.deepEqual(stopped.sort(), created.sort());
|
|
46
|
+
} finally {
|
|
47
|
+
fs.rmSync(first, { recursive: true, force: true });
|
|
48
|
+
fs.rmSync(second, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("ProjectServiceRegistry: ignores roots without a Cortex context directory", () => {
|
|
53
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-daemon-project-"));
|
|
54
|
+
let factoryCalls = 0;
|
|
55
|
+
const registry = new ProjectServiceRegistry(() => {
|
|
56
|
+
factoryCalls += 1;
|
|
57
|
+
return [];
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
assert.equal(registry.register(cwd), false);
|
|
62
|
+
assert.equal(registry.register(path.join(cwd, "missing")), false);
|
|
63
|
+
assert.equal(registry.register(""), false);
|
|
64
|
+
assert.equal(registry.size(), 0);
|
|
65
|
+
assert.equal(factoryCalls, 0);
|
|
66
|
+
} finally {
|
|
67
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("normalizeProjectCwd: returns a physical Cortex project root", () => {
|
|
72
|
+
const cwd = makeProject();
|
|
73
|
+
try {
|
|
74
|
+
assert.equal(
|
|
75
|
+
normalizeProjectCwd(path.join(cwd, ".")),
|
|
76
|
+
fs.realpathSync(cwd),
|
|
77
|
+
);
|
|
78
|
+
assert.equal(normalizeProjectCwd(path.join(cwd, "missing")), null);
|
|
79
|
+
} finally {
|
|
80
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("ProjectServiceRegistry: resolves project symlinks to one service identity", {
|
|
85
|
+
skip: process.platform === "win32",
|
|
86
|
+
}, () => {
|
|
87
|
+
const cwd = makeProject();
|
|
88
|
+
const linkRoot = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-daemon-link-"));
|
|
89
|
+
const linked = path.join(linkRoot, "project");
|
|
90
|
+
fs.symlinkSync(cwd, linked, "dir");
|
|
91
|
+
const registry = new ProjectServiceRegistry(() => []);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
assert.equal(registry.register(linked), true);
|
|
95
|
+
assert.equal(registry.register(cwd), false);
|
|
96
|
+
assert.equal(registry.size(), 1);
|
|
97
|
+
assert.equal(registry.has(cwd), true);
|
|
98
|
+
} finally {
|
|
99
|
+
fs.rmSync(linkRoot, { recursive: true, force: true });
|
|
100
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("ProjectServiceRegistry: starts host-wide services once for projects sharing one Enterprise identity", async () => {
|
|
105
|
+
const first = makeProject();
|
|
106
|
+
const second = makeProject();
|
|
107
|
+
const hostStarts = [];
|
|
108
|
+
const projectStarts = [];
|
|
109
|
+
const registry = new ProjectServiceRegistry(
|
|
110
|
+
(cwd) => {
|
|
111
|
+
projectStarts.push(cwd);
|
|
112
|
+
return [];
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
credentialIdForCwd: () => "same-identity",
|
|
116
|
+
hostFactory: (cwd, credentialId) => {
|
|
117
|
+
hostStarts.push({ cwd, credentialId });
|
|
118
|
+
return [];
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
assert.equal(registry.register(first), true);
|
|
125
|
+
assert.equal(registry.register(second), true);
|
|
126
|
+
assert.equal(hostStarts.length, 1);
|
|
127
|
+
assert.equal(hostStarts[0].cwd, fs.realpathSync(first));
|
|
128
|
+
assert.equal(hostStarts[0].credentialId, "same-identity");
|
|
129
|
+
assert.deepEqual(
|
|
130
|
+
projectStarts.sort(),
|
|
131
|
+
[fs.realpathSync(first), fs.realpathSync(second)].sort(),
|
|
132
|
+
);
|
|
133
|
+
} finally {
|
|
134
|
+
await registry.stopAll();
|
|
135
|
+
fs.rmSync(first, { recursive: true, force: true });
|
|
136
|
+
fs.rmSync(second, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("ProjectServiceRegistry: rejects a second Enterprise identity before starting project or host services", async () => {
|
|
141
|
+
const first = makeProject();
|
|
142
|
+
const second = makeProject();
|
|
143
|
+
let hostStarts = 0;
|
|
144
|
+
let projectStarts = 0;
|
|
145
|
+
const registry = new ProjectServiceRegistry(
|
|
146
|
+
() => {
|
|
147
|
+
projectStarts += 1;
|
|
148
|
+
return [];
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
credentialIdForCwd: (cwd) =>
|
|
152
|
+
cwd === fs.realpathSync(first) ? "identity-a" : "identity-b",
|
|
153
|
+
hostFactory: () => {
|
|
154
|
+
hostStarts += 1;
|
|
155
|
+
return [];
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
assert.equal(registry.register(first), true);
|
|
162
|
+
assert.equal(registry.register(second), false);
|
|
163
|
+
assert.equal(registry.size(), 1);
|
|
164
|
+
assert.equal(hostStarts, 1);
|
|
165
|
+
assert.equal(projectStarts, 1);
|
|
166
|
+
assert.equal(registry.activeCredentialId(), "identity-a");
|
|
167
|
+
} finally {
|
|
168
|
+
await registry.stopAll();
|
|
169
|
+
fs.rmSync(first, { recursive: true, force: true });
|
|
170
|
+
fs.rmSync(second, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
@@ -4,6 +4,7 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
|
|
6
6
|
const QUERY_MODULE = fileURLToPath(new URL("../dist/cli/query.js", import.meta.url));
|
|
7
|
+
const PROJECT_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
7
8
|
|
|
8
9
|
function runQuery(args) {
|
|
9
10
|
const script = [
|
|
@@ -20,7 +21,11 @@ function runQuery(args) {
|
|
|
20
21
|
pathToFileURL(QUERY_MODULE).href,
|
|
21
22
|
JSON.stringify(args),
|
|
22
23
|
],
|
|
23
|
-
{
|
|
24
|
+
{
|
|
25
|
+
cwd: PROJECT_ROOT,
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
env: { ...process.env, CORTEX_PROJECT_ROOT: PROJECT_ROOT },
|
|
28
|
+
},
|
|
24
29
|
);
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -105,6 +110,73 @@ test("explain --json enables scores and matched rules", () => {
|
|
|
105
110
|
assert.ok(Array.isArray(parsed.data.results));
|
|
106
111
|
});
|
|
107
112
|
|
|
113
|
+
test("pattern-evidence --json emits ordered cited evidence tiers", () => {
|
|
114
|
+
const parsed = runJson([
|
|
115
|
+
"pattern-evidence",
|
|
116
|
+
"mcp/src/cli/query.ts",
|
|
117
|
+
"--query",
|
|
118
|
+
"CLI argument parsing error handling",
|
|
119
|
+
"--top-k",
|
|
120
|
+
"2",
|
|
121
|
+
"--json",
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
assert.equal(parsed.ok, true);
|
|
125
|
+
assert.equal(parsed.command, "pattern-evidence");
|
|
126
|
+
assert.equal(parsed.input.target, "mcp/src/cli/query.ts");
|
|
127
|
+
assert.equal(parsed.input.top_k, 2);
|
|
128
|
+
assert.deepEqual(parsed.data.evidence_order, [
|
|
129
|
+
"same_file",
|
|
130
|
+
"same_module",
|
|
131
|
+
"same_feature_area",
|
|
132
|
+
"repo_wide",
|
|
133
|
+
]);
|
|
134
|
+
assert.equal(typeof parsed.data.local_pattern_found, "boolean");
|
|
135
|
+
assert.equal(parsed.data.tiers.length, 4);
|
|
136
|
+
for (const tier of parsed.data.tiers) {
|
|
137
|
+
assert.ok(Array.isArray(tier.evidence));
|
|
138
|
+
for (const evidence of tier.evidence) {
|
|
139
|
+
assert.equal(typeof evidence.path, "string");
|
|
140
|
+
assert.ok(evidence.path.length > 0);
|
|
141
|
+
if (evidence.entity_type === "Chunk") {
|
|
142
|
+
assert.equal(Number.isInteger(evidence.start_line), true);
|
|
143
|
+
assert.equal(Number.isInteger(evidence.end_line), true);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("pattern-evidence --json rejects targets that are not file-backed", () => {
|
|
150
|
+
const parsed = runJson(["pattern-evidence", "rule.source_of_truth", "--json"], 1);
|
|
151
|
+
|
|
152
|
+
assert.equal(parsed.ok, false);
|
|
153
|
+
assert.equal(parsed.command, "pattern-evidence");
|
|
154
|
+
assert.match(parsed.error.message, /not file-backed/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("pattern-evidence derives a query from the target when --query is omitted", () => {
|
|
158
|
+
const parsed = runJson(["pattern-evidence", "mcp/src/cli/query.ts", "--top-k", "1", "--json"]);
|
|
159
|
+
|
|
160
|
+
assert.equal(parsed.ok, true);
|
|
161
|
+
assert.equal(parsed.data.query_source, "derived_from_target");
|
|
162
|
+
assert.equal(typeof parsed.data.query, "string");
|
|
163
|
+
assert.ok(parsed.data.query.length > 0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("pattern-evidence rejects malformed --top-k values", () => {
|
|
167
|
+
const parsed = runJson([
|
|
168
|
+
"pattern-evidence",
|
|
169
|
+
"mcp/src/cli/query.ts",
|
|
170
|
+
"--top-k",
|
|
171
|
+
"2junk",
|
|
172
|
+
"--json",
|
|
173
|
+
], 1);
|
|
174
|
+
|
|
175
|
+
assert.equal(parsed.ok, false);
|
|
176
|
+
assert.equal(parsed.command, "pattern-evidence");
|
|
177
|
+
assert.match(parsed.error.message, /must be an integer/);
|
|
178
|
+
});
|
|
179
|
+
|
|
108
180
|
test("json validation errors emit an error envelope", () => {
|
|
109
181
|
const parsed = runJson(["impact", "--json"], 1);
|
|
110
182
|
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
recordReviewDeliveryStatus,
|
|
13
13
|
} from "../dist/enterprise/reviews/trust-state.js";
|
|
14
14
|
import { syncedWorkflowsCachePath } from "../dist/core/workflow/synced-registry.js";
|
|
15
|
+
import { enterpriseCredentialId } from "../dist/core/license.js";
|
|
16
|
+
|
|
17
|
+
const ENTERPRISE_ENDPOINT = "https://licenses.example.com";
|
|
18
|
+
const ENTERPRISE_KEY = "ent_review_contract_12345678";
|
|
15
19
|
|
|
16
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
21
|
const reviewFixture = JSON.parse(
|
|
@@ -25,6 +29,16 @@ function makeProject() {
|
|
|
25
29
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-review-contract-"));
|
|
26
30
|
const contextDir = path.join(root, ".context");
|
|
27
31
|
fs.mkdirSync(contextDir, { recursive: true });
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(contextDir, "enterprise.yml"),
|
|
34
|
+
[
|
|
35
|
+
"enterprise:",
|
|
36
|
+
` api_key: ${ENTERPRISE_KEY}`,
|
|
37
|
+
` endpoint: ${ENTERPRISE_ENDPOINT}`,
|
|
38
|
+
"",
|
|
39
|
+
].join("\n"),
|
|
40
|
+
"utf8",
|
|
41
|
+
);
|
|
28
42
|
const taskDir = path.join(root, ".agents", "task-1");
|
|
29
43
|
fs.mkdirSync(taskDir, { recursive: true });
|
|
30
44
|
fs.writeFileSync(
|
|
@@ -49,6 +63,10 @@ function enableSyncedSecureBuild(homeDir) {
|
|
|
49
63
|
syncedWorkflowsCachePath(syncedDir),
|
|
50
64
|
JSON.stringify(
|
|
51
65
|
{
|
|
66
|
+
credential_id: enterpriseCredentialId(
|
|
67
|
+
ENTERPRISE_ENDPOINT,
|
|
68
|
+
ENTERPRISE_KEY,
|
|
69
|
+
),
|
|
52
70
|
workflows: {
|
|
53
71
|
"secure-build": {
|
|
54
72
|
workflow_id: "secure-build",
|