@danielblomma/cortex-mcp 2.4.0 → 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 (173) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +19 -3
  3. package/bin/cortex.mjs +308 -75
  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 +321 -200
  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/run.ts +53 -19
  124. package/scaffold/mcp/src/cli/stage.ts +8 -3
  125. package/scaffold/mcp/src/core/config.ts +9 -2
  126. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  127. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  128. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  129. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  130. package/scaffold/mcp/src/core/license.ts +186 -17
  131. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  132. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  133. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  134. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  135. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  136. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  137. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  138. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  139. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  140. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  142. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  143. package/scaffold/mcp/src/daemon/main.ts +99 -25
  144. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  145. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  146. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  147. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  148. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  149. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  150. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  151. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  152. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  153. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  154. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  155. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  156. package/scaffold/mcp/src/plugin.ts +20 -0
  157. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  158. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  159. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  160. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  161. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  162. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  163. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  164. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  165. package/scaffold/mcp/tests/license.test.mjs +367 -0
  166. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  167. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  168. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  169. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  170. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  171. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  172. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  173. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
@@ -0,0 +1,184 @@
1
+ const REPO_METADATA = [
2
+ "repo",
3
+ "instance_id",
4
+ "session_id",
5
+ ];
6
+ export const OUTBOUND_DATA_BOUNDARY = {
7
+ version: 3,
8
+ excludes: [
9
+ "source_code",
10
+ "raw_prompts",
11
+ "raw_queries",
12
+ "embeddings",
13
+ "graph_data",
14
+ "full_file_contents",
15
+ ],
16
+ telemetry: {
17
+ retention_days: 30,
18
+ payload_type: "counts_and_metadata_only",
19
+ allowed_fields: [
20
+ "period_start",
21
+ "period_end",
22
+ "total_tool_calls",
23
+ "successful_tool_calls",
24
+ "failed_tool_calls",
25
+ "total_duration_ms",
26
+ "session_starts",
27
+ "session_ends",
28
+ "session_duration_ms_total",
29
+ "searches",
30
+ "related_lookups",
31
+ "caller_lookups",
32
+ "trace_lookups",
33
+ "impact_analyses",
34
+ "rule_lookups",
35
+ "reloads",
36
+ "total_results_returned",
37
+ "estimated_tokens_saved",
38
+ "estimated_tokens_total",
39
+ "client_version",
40
+ "repo",
41
+ "instance_id",
42
+ "session_id",
43
+ "tool_metrics",
44
+ ],
45
+ },
46
+ audit: {
47
+ required_retention_days: 365,
48
+ diagnostic_retention_days: 30,
49
+ redaction: "string values are summarized to counts/lengths before outbound push",
50
+ metadata_fields: REPO_METADATA,
51
+ },
52
+ reviews: {
53
+ metadata_fields: REPO_METADATA,
54
+ },
55
+ violations: {
56
+ metadata_fields: REPO_METADATA,
57
+ },
58
+ workflow: {
59
+ metadata_fields: REPO_METADATA,
60
+ },
61
+ };
62
+ const MAX_OBJECT_KEYS = 12;
63
+ const MAX_ARRAY_ITEMS = 12;
64
+ const SENSITIVE_KEY_RE = /^(?:query|prompt|content|code|diff|patch|body|text|embedding|embeddings|graph|raw_query|raw_prompt|raw_code|raw_content)$/i;
65
+ function summarizeString(value) {
66
+ return {
67
+ type: "string",
68
+ length: value.length,
69
+ redacted: true,
70
+ };
71
+ }
72
+ function summarizeArray(value, depth) {
73
+ if (depth >= 2) {
74
+ return {
75
+ type: "array",
76
+ count: value.length,
77
+ redacted: true,
78
+ };
79
+ }
80
+ return {
81
+ type: "array",
82
+ count: value.length,
83
+ sample: value.slice(0, MAX_ARRAY_ITEMS).map((item) => summarizeValue(item, depth + 1)),
84
+ };
85
+ }
86
+ function summarizeObject(value, depth) {
87
+ const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS);
88
+ if (depth >= 2) {
89
+ return {
90
+ type: "object",
91
+ keys: entries.map(([key]) => key),
92
+ key_count: Object.keys(value).length,
93
+ redacted: true,
94
+ };
95
+ }
96
+ return Object.fromEntries(entries.map(([key, item]) => [
97
+ key,
98
+ SENSITIVE_KEY_RE.test(key)
99
+ ? summarizeSensitiveValue(item)
100
+ : summarizeValue(item, depth + 1),
101
+ ]));
102
+ }
103
+ function summarizeSensitiveValue(value) {
104
+ if (typeof value === "string")
105
+ return summarizeString(value);
106
+ if (Array.isArray(value)) {
107
+ return {
108
+ type: "array",
109
+ count: value.length,
110
+ redacted: true,
111
+ };
112
+ }
113
+ if (value && typeof value === "object") {
114
+ return {
115
+ type: "object",
116
+ key_count: Object.keys(value).length,
117
+ redacted: true,
118
+ };
119
+ }
120
+ return {
121
+ type: typeof value,
122
+ redacted: true,
123
+ };
124
+ }
125
+ function summarizeValue(value, depth = 0) {
126
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
127
+ return value;
128
+ }
129
+ if (typeof value === "string") {
130
+ return summarizeString(value);
131
+ }
132
+ if (Array.isArray(value)) {
133
+ return summarizeArray(value, depth);
134
+ }
135
+ if (value && typeof value === "object") {
136
+ return summarizeObject(value, depth);
137
+ }
138
+ return {
139
+ type: typeof value,
140
+ redacted: true,
141
+ };
142
+ }
143
+ export function sanitizeOutboundRecord(record) {
144
+ if (!record)
145
+ return {};
146
+ return summarizeObject(record, 0);
147
+ }
148
+ export function sanitizeAuditEntryForPush(entry) {
149
+ return {
150
+ ...entry,
151
+ input: sanitizeOutboundRecord(entry.input),
152
+ error: entry.error ? `[redacted:${entry.error.length}]` : undefined,
153
+ metadata: entry.metadata ? sanitizeOutboundRecord(entry.metadata) : undefined,
154
+ };
155
+ }
156
+ export function buildTelemetryPushPayload(metrics, context = {}) {
157
+ return {
158
+ period_start: metrics.period_start,
159
+ period_end: metrics.period_end,
160
+ total_tool_calls: metrics.total_tool_calls,
161
+ successful_tool_calls: metrics.successful_tool_calls,
162
+ failed_tool_calls: metrics.failed_tool_calls,
163
+ total_duration_ms: metrics.total_duration_ms,
164
+ session_starts: metrics.session_starts,
165
+ session_ends: metrics.session_ends,
166
+ session_duration_ms_total: metrics.session_duration_ms_total,
167
+ searches: metrics.searches,
168
+ related_lookups: metrics.related_lookups,
169
+ caller_lookups: metrics.caller_lookups,
170
+ trace_lookups: metrics.trace_lookups,
171
+ impact_analyses: metrics.impact_analyses,
172
+ rule_lookups: metrics.rule_lookups,
173
+ reloads: metrics.reloads,
174
+ total_results_returned: metrics.total_results_returned,
175
+ estimated_tokens_saved: metrics.estimated_tokens_saved,
176
+ estimated_tokens_total: metrics.estimated_tokens_total,
177
+ client_version: metrics.client_version,
178
+ repo: context.repo,
179
+ instance_id: metrics.instance_id,
180
+ session_id: context.session_id,
181
+ push_id: context.push_id,
182
+ tool_metrics: metrics.tool_metrics,
183
+ };
184
+ }
@@ -0,0 +1,33 @@
1
+ import { execFileSync } from "node:child_process";
2
+ function gitLines(projectRoot, args) {
3
+ const output = execFileSync("git", args, {
4
+ cwd: projectRoot,
5
+ encoding: "utf8",
6
+ timeout: 5000,
7
+ stdio: ["ignore", "pipe", "ignore"],
8
+ });
9
+ return output.split("\n").map((value) => value.trim()).filter(Boolean);
10
+ }
11
+ function trackedChangedFiles(projectRoot) {
12
+ try {
13
+ return gitLines(projectRoot, ["diff", "--name-only", "HEAD"]);
14
+ }
15
+ catch {
16
+ // No HEAD yet (repo without commits): every tracked file is new.
17
+ return [
18
+ ...gitLines(projectRoot, ["diff", "--name-only"]),
19
+ ...gitLines(projectRoot, ["ls-files"]),
20
+ ];
21
+ }
22
+ }
23
+ export function resolveChangedReviewFiles(projectRoot) {
24
+ try {
25
+ gitLines(projectRoot, ["rev-parse", "--is-inside-work-tree"]);
26
+ const tracked = trackedChangedFiles(projectRoot);
27
+ const untracked = gitLines(projectRoot, ["ls-files", "--others", "--exclude-standard"]);
28
+ return [...new Set([...tracked, ...untracked])].sort();
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
@@ -0,0 +1,202 @@
1
+ import { loadContextData } from "../../graph.js";
2
+ import { normalizeRepoPath, runLocalPatternEvidence } from "../../patternEvidence.js";
3
+ import { compareText } from "../../searchResults.js";
4
+ // Loads context data lazily on first use and shares it across all targets in
5
+ // one review, instead of re-reading the index from disk per target. Load
6
+ // failures surface per target, matching the per-file error handling below.
7
+ export function createLocalPatternRunner(loadData = loadContextData) {
8
+ let shared;
9
+ return async (params) => {
10
+ shared ??= loadData();
11
+ return runLocalPatternEvidence(params, { data: await shared });
12
+ };
13
+ }
14
+ export const PATTERN_REVIEW_QUESTION = "Does this change follow the established pattern for this kind of problem in this repository, or does it introduce a second way to solve something that already has a local convention?";
15
+ const PATTERN_EVIDENCE_ORDER = ["same_file", "same_module", "same_feature_area", "repo_wide"];
16
+ // Canonicalizes like the pattern evidence engine, then rejects paths that
17
+ // must never appear in review citations (absolute, drive-letter, or
18
+ // parent-escaping paths).
19
+ function normalizeReviewPath(value) {
20
+ const normalized = normalizeRepoPath(value);
21
+ if (!normalized ||
22
+ normalized.startsWith("/") ||
23
+ /^[A-Za-z]:\//.test(normalized) ||
24
+ normalized.split("/").includes("..")) {
25
+ return null;
26
+ }
27
+ return normalized;
28
+ }
29
+ function safeIdentifier(value) {
30
+ if (typeof value !== "string" || value.length === 0 || value.length > 1000)
31
+ return null;
32
+ if (/(?:^|:)\/|[A-Za-z]:[\\/]|(?:^|[:/])\.\.(?:[:/]|$)/u.test(value))
33
+ return null;
34
+ return value;
35
+ }
36
+ function emptyTiers() {
37
+ return PATTERN_EVIDENCE_ORDER.map((name) => ({ name, evidence: [] }));
38
+ }
39
+ function sanitizeEvidenceTiers(value) {
40
+ const inputTiers = Array.isArray(value) ? value : [];
41
+ let dropped = 0;
42
+ const tiers = PATTERN_EVIDENCE_ORDER.map((name) => {
43
+ const source = inputTiers.find((candidate) => candidate && typeof candidate === "object" && candidate.name === name);
44
+ const rawEvidence = Array.isArray(source?.evidence) ? source.evidence : [];
45
+ const evidence = [];
46
+ for (const candidate of rawEvidence) {
47
+ if (!candidate || typeof candidate !== "object") {
48
+ dropped += 1;
49
+ continue;
50
+ }
51
+ const row = candidate;
52
+ const citationPath = typeof row.path === "string" ? normalizeReviewPath(row.path) : null;
53
+ const id = safeIdentifier(row.id);
54
+ if (!citationPath || !id) {
55
+ dropped += 1;
56
+ continue;
57
+ }
58
+ evidence.push({
59
+ id,
60
+ entity_type: typeof row.entity_type === "string" ? row.entity_type.slice(0, 100) : "",
61
+ kind: typeof row.kind === "string" ? row.kind.slice(0, 100) : "",
62
+ title: typeof row.title === "string" ? row.title.slice(0, 500) : id,
63
+ path: citationPath,
64
+ start_line: Number.isInteger(row.start_line) ? row.start_line : null,
65
+ end_line: Number.isInteger(row.end_line) ? row.end_line : null,
66
+ excerpt: typeof row.excerpt === "string" ? row.excerpt.slice(0, 1000) : "",
67
+ score: typeof row.score === "number" && Number.isFinite(row.score) ? row.score : null,
68
+ matched_rules: Array.isArray(row.matched_rules)
69
+ ? row.matched_rules.filter((item) => typeof item === "string").slice(0, 50)
70
+ : [],
71
+ });
72
+ }
73
+ return { name, evidence };
74
+ });
75
+ return { tiers, dropped };
76
+ }
77
+ function emptyTarget(file, status, query) {
78
+ return {
79
+ path: file,
80
+ status,
81
+ review_question: PATTERN_REVIEW_QUESTION,
82
+ query: query ?? null,
83
+ query_source: query ? "explicit" : null,
84
+ local_pattern_found: false,
85
+ fallback_used: false,
86
+ evidence_order: [...PATTERN_EVIDENCE_ORDER],
87
+ tiers: emptyTiers(),
88
+ warning: null,
89
+ message: status === "not_indexed"
90
+ ? "Target is not present as a file-backed entity in the current Cortex index. Run cortex update before reviewing it."
91
+ : "Pattern evidence could not be produced for this target.",
92
+ citations_dropped: 0,
93
+ };
94
+ }
95
+ function disabledPatternReview(requested, limit, topK) {
96
+ return {
97
+ enabled: false,
98
+ non_blocking: true,
99
+ affects_policy_summary: false,
100
+ review_question: PATTERN_REVIEW_QUESTION,
101
+ limit,
102
+ top_k_per_tier: topK,
103
+ targets: [],
104
+ summary: {
105
+ requested,
106
+ eligible: 0,
107
+ analyzed: 0,
108
+ local_evidence: 0,
109
+ repo_fallback: 0,
110
+ no_evidence: 0,
111
+ not_indexed: 0,
112
+ errors: 0,
113
+ omitted: 0,
114
+ invalid_paths: 0,
115
+ },
116
+ };
117
+ }
118
+ export async function buildPatternReviewContext(input) {
119
+ if (!input.enabled) {
120
+ return disabledPatternReview(input.files.length, input.limit, input.topK);
121
+ }
122
+ const normalized = input.files.map(normalizeReviewPath);
123
+ const invalidPaths = normalized.filter((value) => value === null).length;
124
+ const eligible = [...new Set(normalized.filter((value) => value !== null))]
125
+ .sort(compareText);
126
+ const selected = eligible.slice(0, input.limit);
127
+ const runner = input.runner ?? createLocalPatternRunner();
128
+ const targets = [];
129
+ const counts = {
130
+ local_evidence: 0,
131
+ repo_fallback: 0,
132
+ no_evidence: 0,
133
+ not_indexed: 0,
134
+ errors: 0,
135
+ };
136
+ for (const file of selected) {
137
+ try {
138
+ const evidence = await runner({
139
+ target: file,
140
+ query: input.query,
141
+ top_k: input.topK,
142
+ include_deprecated: false,
143
+ });
144
+ const sanitized = sanitizeEvidenceTiers(evidence.tiers);
145
+ const localPatternFound = sanitized.tiers.slice(0, 3).some((tier) => Array.isArray(tier.evidence) && tier.evidence.length > 0);
146
+ const repoEvidence = sanitized.tiers[3].evidence;
147
+ const fallbackUsed = !localPatternFound && Array.isArray(repoEvidence) && repoEvidence.length > 0;
148
+ const status = localPatternFound
149
+ ? "local_evidence"
150
+ : fallbackUsed
151
+ ? "repo_fallback"
152
+ : "no_evidence";
153
+ counts[status] += 1;
154
+ const warning = status === "local_evidence"
155
+ ? typeof evidence.warning === "string" ? "Pattern evidence completed with local runtime warnings." : null
156
+ : status === "repo_fallback"
157
+ ? "No applicable local pattern evidence was found; repository-wide fallback evidence is provided."
158
+ : "No applicable local pattern evidence was found.";
159
+ targets.push({
160
+ path: file,
161
+ status,
162
+ review_question: PATTERN_REVIEW_QUESTION,
163
+ query: typeof evidence.query === "string" ? evidence.query.slice(0, 1000) : input.query ?? null,
164
+ query_source: evidence.query_source === "explicit" || evidence.query_source === "derived_from_target"
165
+ ? evidence.query_source
166
+ : input.query ? "explicit" : null,
167
+ local_pattern_found: localPatternFound,
168
+ fallback_used: fallbackUsed,
169
+ evidence_order: [...PATTERN_EVIDENCE_ORDER],
170
+ tiers: sanitized.tiers,
171
+ warning,
172
+ message: null,
173
+ citations_dropped: sanitized.dropped,
174
+ });
175
+ }
176
+ catch (error) {
177
+ const message = error instanceof Error ? error.message : String(error);
178
+ const notIndexed = /not found in indexed context|not file-backed/u.test(message);
179
+ const status = notIndexed ? "not_indexed" : "error";
180
+ counts[notIndexed ? "not_indexed" : "errors"] += 1;
181
+ targets.push(emptyTarget(file, status, input.query));
182
+ }
183
+ }
184
+ return {
185
+ enabled: true,
186
+ non_blocking: true,
187
+ affects_policy_summary: false,
188
+ review_question: PATTERN_REVIEW_QUESTION,
189
+ query: input.query ?? null,
190
+ limit: input.limit,
191
+ top_k_per_tier: input.topK,
192
+ targets,
193
+ summary: {
194
+ requested: input.files.length,
195
+ eligible: eligible.length,
196
+ analyzed: selected.length,
197
+ ...counts,
198
+ omitted: Math.max(0, eligible.length - selected.length),
199
+ invalid_paths: invalidPaths,
200
+ },
201
+ };
202
+ }
@@ -0,0 +1,46 @@
1
+ import { getGenericEvaluator, getValidator, } from "../../core/validators/engine.js";
2
+ export const DEFERRED_CODE_REVIEW_REASON = "Current context.review invocation is the review being recorded; validate this policy from workflow state on the next run.";
3
+ export function partitionReviewPolicies(policies) {
4
+ const enforced = [];
5
+ const skipped = [];
6
+ for (const policy of policies) {
7
+ if (!policy.enforce)
8
+ continue;
9
+ if (policy.id === "require-code-review") {
10
+ skipped.push({
11
+ policy_id: policy.id,
12
+ kind: policy.kind ?? null,
13
+ type: policy.type ?? null,
14
+ reason: DEFERRED_CODE_REVIEW_REASON,
15
+ });
16
+ continue;
17
+ }
18
+ if (policy.type) {
19
+ if (!getGenericEvaluator(policy.type)) {
20
+ skipped.push({
21
+ policy_id: policy.id,
22
+ kind: policy.kind ?? null,
23
+ type: policy.type,
24
+ reason: `No evaluator registered for type "${policy.type}"`,
25
+ });
26
+ continue;
27
+ }
28
+ }
29
+ else if (!getValidator(policy.id)) {
30
+ skipped.push({
31
+ policy_id: policy.id,
32
+ kind: policy.kind ?? null,
33
+ type: null,
34
+ reason: "No executable validator registered for this policy",
35
+ });
36
+ continue;
37
+ }
38
+ enforced.push({
39
+ id: policy.id,
40
+ type: policy.type ?? null,
41
+ config: policy.config ?? null,
42
+ severity: policy.severity ?? "block",
43
+ });
44
+ }
45
+ return { enforced, skipped };
46
+ }
@@ -0,0 +1,102 @@
1
+ import { recordReviewDeliveryStatus } from "./trust-state.js";
2
+ import { isAllowedEnterpriseEndpoint } from "../../core/secure-endpoint.js";
3
+ const pending = [];
4
+ let activeContext = {};
5
+ export function setReviewPushContext(context) {
6
+ activeContext = { ...context };
7
+ }
8
+ export function getReviewPushContext() {
9
+ return { ...activeContext };
10
+ }
11
+ export function queueReviewResult(item) {
12
+ pending.push(item);
13
+ }
14
+ export function pendingCount() {
15
+ return pending.length;
16
+ }
17
+ export async function pushReviewResults(baseUrl, apiKey) {
18
+ if (pending.length === 0) {
19
+ return { success: true, count: 0, attempted: false };
20
+ }
21
+ if (!isAllowedEnterpriseEndpoint(baseUrl)) {
22
+ return {
23
+ success: false,
24
+ count: 0,
25
+ error: "insecure or invalid enterprise endpoint",
26
+ attempted: false,
27
+ };
28
+ }
29
+ const reviewsUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/reviews/push`;
30
+ const batch = pending.splice(0, 100);
31
+ const attemptedAt = new Date().toISOString();
32
+ if (activeContext.context_dir) {
33
+ recordReviewDeliveryStatus(activeContext.context_dir, "pushed", {
34
+ reviewCount: batch.length,
35
+ attemptedAt,
36
+ });
37
+ }
38
+ try {
39
+ const response = await fetch(reviewsUrl, {
40
+ method: "POST",
41
+ headers: {
42
+ "Authorization": `Bearer ${apiKey}`,
43
+ "Content-Type": "application/json",
44
+ "Accept": "application/json",
45
+ },
46
+ body: JSON.stringify({
47
+ repo: activeContext.repo,
48
+ instance_id: activeContext.instance_id,
49
+ session_id: activeContext.session_id,
50
+ reviews: batch,
51
+ }),
52
+ signal: AbortSignal.timeout(10_000),
53
+ });
54
+ if (!response.ok) {
55
+ pending.unshift(...batch);
56
+ if (activeContext.context_dir) {
57
+ recordReviewDeliveryStatus(activeContext.context_dir, "failed", {
58
+ reviewCount: batch.length,
59
+ attemptedAt,
60
+ error: `HTTP ${response.status}`,
61
+ });
62
+ }
63
+ return {
64
+ success: false,
65
+ count: 0,
66
+ error: `HTTP ${response.status}`,
67
+ attempted: true,
68
+ delivery_status: "failed",
69
+ };
70
+ }
71
+ if (activeContext.context_dir) {
72
+ recordReviewDeliveryStatus(activeContext.context_dir, "accepted", {
73
+ reviewCount: batch.length,
74
+ attemptedAt,
75
+ });
76
+ }
77
+ return {
78
+ success: true,
79
+ count: batch.length,
80
+ attempted: true,
81
+ delivery_status: "accepted",
82
+ };
83
+ }
84
+ catch (err) {
85
+ pending.unshift(...batch);
86
+ const error = err instanceof Error ? err.message : "unknown error";
87
+ if (activeContext.context_dir) {
88
+ recordReviewDeliveryStatus(activeContext.context_dir, "failed", {
89
+ reviewCount: batch.length,
90
+ attemptedAt,
91
+ error,
92
+ });
93
+ }
94
+ return {
95
+ success: false,
96
+ count: 0,
97
+ error,
98
+ attempted: true,
99
+ delivery_status: "failed",
100
+ };
101
+ }
102
+ }