@contextrail/code-review-agent 0.1.1-alpha.0

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 (84) hide show
  1. package/LICENSE +26 -0
  2. package/MODEL_RECOMMENDATIONS.md +178 -0
  3. package/README.md +177 -0
  4. package/dist/config/defaults.d.ts +72 -0
  5. package/dist/config/defaults.js +113 -0
  6. package/dist/config/index.d.ts +34 -0
  7. package/dist/config/index.js +89 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +603 -0
  10. package/dist/llm/factory.d.ts +21 -0
  11. package/dist/llm/factory.js +50 -0
  12. package/dist/llm/index.d.ts +3 -0
  13. package/dist/llm/index.js +2 -0
  14. package/dist/llm/service.d.ts +38 -0
  15. package/dist/llm/service.js +191 -0
  16. package/dist/llm/types.d.ts +119 -0
  17. package/dist/llm/types.js +1 -0
  18. package/dist/logging/logger.d.ts +9 -0
  19. package/dist/logging/logger.js +52 -0
  20. package/dist/mcp/client.d.ts +429 -0
  21. package/dist/mcp/client.js +173 -0
  22. package/dist/mcp/mcp-tools.d.ts +292 -0
  23. package/dist/mcp/mcp-tools.js +40 -0
  24. package/dist/mcp/token-validation.d.ts +31 -0
  25. package/dist/mcp/token-validation.js +57 -0
  26. package/dist/mcp/tools-provider.d.ts +18 -0
  27. package/dist/mcp/tools-provider.js +24 -0
  28. package/dist/observability/index.d.ts +2 -0
  29. package/dist/observability/index.js +1 -0
  30. package/dist/observability/metrics.d.ts +48 -0
  31. package/dist/observability/metrics.js +86 -0
  32. package/dist/orchestrator/agentic-orchestrator.d.ts +29 -0
  33. package/dist/orchestrator/agentic-orchestrator.js +136 -0
  34. package/dist/orchestrator/prompts.d.ts +25 -0
  35. package/dist/orchestrator/prompts.js +98 -0
  36. package/dist/orchestrator/validation.d.ts +2 -0
  37. package/dist/orchestrator/validation.js +7 -0
  38. package/dist/orchestrator/writer.d.ts +4 -0
  39. package/dist/orchestrator/writer.js +17 -0
  40. package/dist/output/aggregator.d.ts +30 -0
  41. package/dist/output/aggregator.js +132 -0
  42. package/dist/output/prompts.d.ts +32 -0
  43. package/dist/output/prompts.js +153 -0
  44. package/dist/output/schema.d.ts +1515 -0
  45. package/dist/output/schema.js +224 -0
  46. package/dist/output/writer.d.ts +31 -0
  47. package/dist/output/writer.js +120 -0
  48. package/dist/review-inputs/chunking.d.ts +29 -0
  49. package/dist/review-inputs/chunking.js +113 -0
  50. package/dist/review-inputs/diff-summary.d.ts +52 -0
  51. package/dist/review-inputs/diff-summary.js +83 -0
  52. package/dist/review-inputs/file-patterns.d.ts +40 -0
  53. package/dist/review-inputs/file-patterns.js +182 -0
  54. package/dist/review-inputs/filtering.d.ts +31 -0
  55. package/dist/review-inputs/filtering.js +53 -0
  56. package/dist/review-inputs/git-diff-provider.d.ts +2 -0
  57. package/dist/review-inputs/git-diff-provider.js +42 -0
  58. package/dist/review-inputs/index.d.ts +46 -0
  59. package/dist/review-inputs/index.js +122 -0
  60. package/dist/review-inputs/path-validation.d.ts +10 -0
  61. package/dist/review-inputs/path-validation.js +37 -0
  62. package/dist/review-inputs/surrounding-context.d.ts +35 -0
  63. package/dist/review-inputs/surrounding-context.js +180 -0
  64. package/dist/review-inputs/triage.d.ts +57 -0
  65. package/dist/review-inputs/triage.js +81 -0
  66. package/dist/reviewers/executor.d.ts +41 -0
  67. package/dist/reviewers/executor.js +357 -0
  68. package/dist/reviewers/findings-merge.d.ts +9 -0
  69. package/dist/reviewers/findings-merge.js +131 -0
  70. package/dist/reviewers/iteration.d.ts +17 -0
  71. package/dist/reviewers/iteration.js +95 -0
  72. package/dist/reviewers/persistence.d.ts +17 -0
  73. package/dist/reviewers/persistence.js +55 -0
  74. package/dist/reviewers/progress-tracker.d.ts +115 -0
  75. package/dist/reviewers/progress-tracker.js +194 -0
  76. package/dist/reviewers/prompt.d.ts +42 -0
  77. package/dist/reviewers/prompt.js +246 -0
  78. package/dist/reviewers/tool-call-tracker.d.ts +18 -0
  79. package/dist/reviewers/tool-call-tracker.js +40 -0
  80. package/dist/reviewers/types.d.ts +12 -0
  81. package/dist/reviewers/types.js +1 -0
  82. package/dist/reviewers/validation-rules.d.ts +27 -0
  83. package/dist/reviewers/validation-rules.js +189 -0
  84. package/package.json +79 -0
@@ -0,0 +1,40 @@
1
+ export const collectToolCalls = (toolCalls) => {
2
+ const collected = [];
3
+ const contextIds = [];
4
+ if (!Array.isArray(toolCalls)) {
5
+ return { toolCalls: collected, contextIds };
6
+ }
7
+ for (const call of toolCalls) {
8
+ collected.push({ tool: call.toolName, input: call.input });
9
+ if (call.toolName === 'get_context' && call.input && typeof call.input === 'object') {
10
+ const maybeId = call.input.contextId;
11
+ if (maybeId) {
12
+ contextIds.push(maybeId);
13
+ }
14
+ }
15
+ }
16
+ return { toolCalls: collected, contextIds };
17
+ };
18
+ export const mergeIterationTracking = (current, iteration) => {
19
+ current.toolCalls.push(...iteration.toolCalls);
20
+ for (const id of iteration.contextIds) {
21
+ if (!current.contextIds.includes(id)) {
22
+ current.contextIds.push(id);
23
+ }
24
+ }
25
+ };
26
+ /**
27
+ * Merge token usage from multiple sources.
28
+ * Sums promptTokens, completionTokens, and totalTokens.
29
+ */
30
+ export const mergeTokenUsage = (usages) => {
31
+ const validUsages = usages.filter((u) => u !== undefined);
32
+ if (validUsages.length === 0) {
33
+ return undefined;
34
+ }
35
+ return {
36
+ promptTokens: validUsages.reduce((sum, u) => sum + u.promptTokens, 0),
37
+ completionTokens: validUsages.reduce((sum, u) => sum + u.completionTokens, 0),
38
+ totalTokens: validUsages.reduce((sum, u) => sum + u.totalTokens, 0),
39
+ };
40
+ };
@@ -0,0 +1,12 @@
1
+ import type { ReviewerFindings, TokenUsage } from '../output/schema.js';
2
+ export type { TokenUsage };
3
+ export type ToolCallEntry = {
4
+ tool: string;
5
+ input?: unknown;
6
+ };
7
+ export type ReviewerIterationResult = {
8
+ findings: ReviewerFindings;
9
+ toolCalls: ToolCallEntry[];
10
+ contextIds: string[];
11
+ usage?: TokenUsage;
12
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import type { Finding } from '../output/schema.js';
2
+ export type ValidationResult = {
3
+ passed: Finding[];
4
+ rejected: Array<{
5
+ finding: Finding;
6
+ reason: string;
7
+ }>;
8
+ };
9
+ /**
10
+ * Apply non-negotiable attribution guardrails.
11
+ * These run on every iteration to prevent unsupported ContextRail claims.
12
+ */
13
+ export declare const applyAttributionGuardrails: (findings: Finding[]) => ValidationResult;
14
+ /**
15
+ * Apply deterministic validation rules to findings.
16
+ * These rules run before the LLM critic pass to filter out obvious issues.
17
+ *
18
+ * Rules:
19
+ * 1. Reject findings where file is empty or line is undefined (unless it's a general finding)
20
+ * 2. Reject findings where severity is "critical" but description lacks security/integrity keywords
21
+ * 3. Reject findings that duplicate another finding's file:line:title
22
+ * 4. Reject findings where rationale is empty or <20 characters
23
+ *
24
+ * @param findings - Findings to validate
25
+ * @returns Validation result with passed and rejected findings
26
+ */
27
+ export declare const applyDeterministicValidationRules: (findings: Finding[]) => ValidationResult;
@@ -0,0 +1,189 @@
1
+ const CONTEXT_ID_PREFIX = 'context://';
2
+ /**
3
+ * Security/integrity keywords that should be present in critical findings.
4
+ * Critical findings without these keywords may be downgraded.
5
+ */
6
+ const CRITICAL_KEYWORDS = [
7
+ 'exploit',
8
+ 'vulnerability',
9
+ 'security',
10
+ 'injection',
11
+ 'bypass',
12
+ 'unauthorized',
13
+ 'secret',
14
+ 'credential',
15
+ 'password',
16
+ 'token',
17
+ 'authentication',
18
+ 'authorization',
19
+ 'corruption',
20
+ 'integrity',
21
+ 'data loss',
22
+ 'breach',
23
+ 'attack',
24
+ 'malicious',
25
+ 'xss',
26
+ 'csrf',
27
+ 'sql injection',
28
+ 'command injection',
29
+ 'path traversal',
30
+ 'hardcoded',
31
+ ];
32
+ /**
33
+ * Check if a finding's description/rationale contains security/integrity keywords.
34
+ */
35
+ const hasSecurityKeywords = (finding) => {
36
+ const text = `${finding.description} ${finding.rationale}`.toLowerCase();
37
+ return CRITICAL_KEYWORDS.some((keyword) => text.includes(keyword));
38
+ };
39
+ /**
40
+ * Generate a unique key for a finding based on file, line, and title.
41
+ * Used for duplicate detection.
42
+ */
43
+ const getFindingKey = (finding) => {
44
+ const file = finding.file ?? '';
45
+ const line = finding.line ?? '';
46
+ const title = finding.title.toLowerCase().trim();
47
+ return `${file}:${line}:${title}`;
48
+ };
49
+ /**
50
+ * Check if an attribution field is effectively empty (null or empty array).
51
+ * Empty arrays are treated as equivalent to null since cleanFinding normalizes them.
52
+ */
53
+ const isEmptyAttributionField = (value) => {
54
+ return value === null || value === undefined || (Array.isArray(value) && value.length === 0);
55
+ };
56
+ const hasRetrievedContextAttribution = (finding) => !isEmptyAttributionField(finding.contextIdsUsed) || !isEmptyAttributionField(finding.contextIdsViolated);
57
+ const mentionsContextRailInText = (finding) => {
58
+ const text = `${finding.title} ${finding.description} ${finding.rationale}`.toLowerCase();
59
+ return text.includes('contextrail') || text.includes(CONTEXT_ID_PREFIX);
60
+ };
61
+ const hasInvalidContextIdFormat = (finding) => {
62
+ const ids = [
63
+ ...(isEmptyAttributionField(finding.contextIdsUsed) ? [] : (finding.contextIdsUsed ?? [])),
64
+ ...(isEmptyAttributionField(finding.contextIdsViolated) ? [] : (finding.contextIdsViolated ?? [])),
65
+ ];
66
+ return ids.some((id) => !id.startsWith(CONTEXT_ID_PREFIX));
67
+ };
68
+ const hasAnyContextIds = (finding) => !isEmptyAttributionField(finding.contextIdsUsed) ||
69
+ !isEmptyAttributionField(finding.contextIdsViolated) ||
70
+ hasRetrievedContextAttribution(finding);
71
+ const hasContextTitles = (finding) => !isEmptyAttributionField(finding.contextTitles);
72
+ const getAttributionInconsistencyReason = (finding) => {
73
+ // Treat empty arrays as equivalent to null (matches cleanFinding normalization behavior).
74
+ const hasIds = hasRetrievedContextAttribution(finding);
75
+ const hasTitles = hasContextTitles(finding);
76
+ if (hasTitles && !hasIds) {
77
+ return 'contextTitles provided without corresponding contextIdsUsed/contextIdsViolated';
78
+ }
79
+ if (hasIds && !hasTitles) {
80
+ return 'Context IDs provided without contextTitles';
81
+ }
82
+ // Empty arrays are normalized to null by cleanFinding, so validation accepts them.
83
+ // This ensures validation is aligned with schema normalization behavior.
84
+ return null;
85
+ };
86
+ /**
87
+ * Apply non-negotiable attribution guardrails.
88
+ * These run on every iteration to prevent unsupported ContextRail claims.
89
+ */
90
+ export const applyAttributionGuardrails = (findings) => {
91
+ const passed = [];
92
+ const rejected = [];
93
+ for (const finding of findings) {
94
+ let shouldReject = false;
95
+ let rejectReason = '';
96
+ // Guardrail 1: ContextRail claims require retrieved attribution IDs.
97
+ if (mentionsContextRailInText(finding) && !hasRetrievedContextAttribution(finding)) {
98
+ shouldReject = true;
99
+ rejectReason = 'ContextRail claim without retrieved contextIds attribution';
100
+ }
101
+ // Guardrail 2: Context attribution fields must be internally consistent.
102
+ if (!shouldReject) {
103
+ const inconsistency = getAttributionInconsistencyReason(finding);
104
+ if (inconsistency) {
105
+ shouldReject = true;
106
+ rejectReason = inconsistency;
107
+ }
108
+ }
109
+ // Guardrail 3: Context IDs must use context:// format.
110
+ if (!shouldReject && hasInvalidContextIdFormat(finding)) {
111
+ shouldReject = true;
112
+ rejectReason = `Context ID format invalid; expected prefix "${CONTEXT_ID_PREFIX}"`;
113
+ }
114
+ if (shouldReject) {
115
+ rejected.push({ finding, reason: rejectReason });
116
+ }
117
+ else {
118
+ passed.push(finding);
119
+ }
120
+ }
121
+ return { passed, rejected };
122
+ };
123
+ /**
124
+ * Apply deterministic validation rules to findings.
125
+ * These rules run before the LLM critic pass to filter out obvious issues.
126
+ *
127
+ * Rules:
128
+ * 1. Reject findings where file is empty or line is undefined (unless it's a general finding)
129
+ * 2. Reject findings where severity is "critical" but description lacks security/integrity keywords
130
+ * 3. Reject findings that duplicate another finding's file:line:title
131
+ * 4. Reject findings where rationale is empty or <20 characters
132
+ *
133
+ * @param findings - Findings to validate
134
+ * @returns Validation result with passed and rejected findings
135
+ */
136
+ export const applyDeterministicValidationRules = (findings) => {
137
+ const passed = [];
138
+ const rejected = [];
139
+ const seenKeys = new Set();
140
+ for (const finding of findings) {
141
+ let shouldReject = false;
142
+ let rejectReason = '';
143
+ // Rule 1: Reject findings missing file+line (unless it's a general/architectural finding)
144
+ // Allow findings without file/line only if they're architectural or general concerns
145
+ const hasFileButNoLine = finding.file && !finding.line;
146
+ if (hasFileButNoLine) {
147
+ shouldReject = true;
148
+ rejectReason = 'Missing line number for file-specific finding';
149
+ }
150
+ // Rule 2: Reject critical findings without security/integrity keywords
151
+ if (!shouldReject && finding.severity === 'critical' && !hasSecurityKeywords(finding)) {
152
+ shouldReject = true;
153
+ rejectReason =
154
+ 'Critical finding lacks security/integrity keywords (exploit path, vulnerability, data corruption, etc.)';
155
+ }
156
+ // Rule 3: Reject duplicate findings (same file:line:title)
157
+ if (!shouldReject) {
158
+ const key = getFindingKey(finding);
159
+ if (seenKeys.has(key)) {
160
+ shouldReject = true;
161
+ rejectReason = `Duplicate finding: same file:line:title as another finding`;
162
+ }
163
+ else {
164
+ seenKeys.add(key);
165
+ }
166
+ }
167
+ // Rule 4: Reject findings with empty or very short rationale
168
+ if (!shouldReject && (!finding.rationale || finding.rationale.trim().length < 20)) {
169
+ shouldReject = true;
170
+ rejectReason = `Rationale too short (${finding.rationale?.length ?? 0} chars, minimum 20)`;
171
+ }
172
+ // Rule 5: Reject malformed attribution payloads when attribution fields are present.
173
+ // Check both context IDs and titles to catch inconsistencies (e.g., titles without IDs).
174
+ if (!shouldReject && (hasAnyContextIds(finding) || hasContextTitles(finding))) {
175
+ const inconsistency = getAttributionInconsistencyReason(finding);
176
+ if (inconsistency) {
177
+ shouldReject = true;
178
+ rejectReason = inconsistency;
179
+ }
180
+ }
181
+ if (shouldReject) {
182
+ rejected.push({ finding, reason: rejectReason });
183
+ }
184
+ else {
185
+ passed.push(finding);
186
+ }
187
+ }
188
+ return { passed, rejected };
189
+ };
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@contextrail/code-review-agent",
3
+ "version": "0.1.1-alpha.0",
4
+ "description": "CLI tool for orchestrating ContextRail-powered code reviews",
5
+ "homepage": "https://contextrail.app",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/contextrail/contextrail-monorepo.git",
9
+ "directory": "packages/code-review-agent"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/contextrail/contextrail/issues"
13
+ },
14
+ "main": "dist/index.js",
15
+ "type": "module",
16
+ "bin": {
17
+ "code-review-agent": "dist/index.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "MODEL_RECOMMENDATIONS.md",
23
+ "LICENSE"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "clean": "node --eval \"const fs=require('node:fs');fs.rmSync('dist',{recursive:true,force:true});\"",
30
+ "build": "pnpm run clean && tsc",
31
+ "start": "node dist/index.js",
32
+ "dev": "pnpm run build && pnpm run start",
33
+ "prepublishOnly": "pnpm run build",
34
+ "pack:preview": "pnpm run build && pnpm pack --pack-destination ./.pack",
35
+ "typecheck": "tsc --noEmit",
36
+ "lint": "eslint src",
37
+ "lint:fix": "eslint src --fix",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest watch",
40
+ "test:coverage": "vitest run --coverage",
41
+ "check": "pnpm run typecheck && pnpm run lint && pnpm run test",
42
+ "format": "prettier . --write",
43
+ "format:check": "prettier . --check",
44
+ "publish:alpha": "pnpm run check && npm version prerelease --preid=alpha && npm publish --tag alpha --access public",
45
+ "publish:patch": "pnpm run check && npm version patch && npm publish --access public",
46
+ "publish:minor": "pnpm run check && npm version minor && npm publish --access public",
47
+ "publish:major": "pnpm run check && npm version major && npm publish --access public"
48
+ },
49
+ "keywords": [
50
+ "cli",
51
+ "ai",
52
+ "code-review",
53
+ "standards",
54
+ "contextrail"
55
+ ],
56
+ "author": "ContextRail",
57
+ "license": "SEE LICENSE IN LICENSE",
58
+ "dependencies": {
59
+ "@modelcontextprotocol/sdk": "^1.24.3",
60
+ "@openrouter/ai-sdk-provider": "^2.1.1",
61
+ "ai": "^6.0.67",
62
+ "dedent": "^1.7.1",
63
+ "dotenv": "^16.6.1",
64
+ "jose": "^6.1.3",
65
+ "minimatch": "^10.1.1",
66
+ "simple-git": "^3.30.0",
67
+ "zod": "^3.23.8"
68
+ },
69
+ "devDependencies": {
70
+ "@types/node": "^22.10.2",
71
+ "@vitest/coverage-v8": "^4.0.16",
72
+ "tsx": "^4.20.6",
73
+ "typescript": "^5.7.2",
74
+ "vitest": "^4.0.16"
75
+ },
76
+ "engines": {
77
+ "node": ">=20"
78
+ }
79
+ }