tlc-claude-code 1.6.4 → 1.8.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.
- package/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
- package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
- package/package.json +1 -1
- package/server/lib/access-control.test.js +1 -1
- package/server/lib/agents-cancel-command.test.js +1 -1
- package/server/lib/agents-get-command.test.js +1 -1
- package/server/lib/agents-list-command.test.js +1 -1
- package/server/lib/agents-logs-command.test.js +1 -1
- package/server/lib/agents-retry-command.test.js +1 -1
- package/server/lib/budget-limits.test.js +2 -2
- package/server/lib/code-gate/bypass-logger.js +129 -0
- package/server/lib/code-gate/bypass-logger.test.js +142 -0
- package/server/lib/code-gate/first-commit-audit.js +138 -0
- package/server/lib/code-gate/first-commit-audit.test.js +203 -0
- package/server/lib/code-gate/gate-command.js +114 -0
- package/server/lib/code-gate/gate-command.test.js +111 -0
- package/server/lib/code-gate/gate-config.js +163 -0
- package/server/lib/code-gate/gate-config.test.js +181 -0
- package/server/lib/code-gate/gate-engine.js +193 -0
- package/server/lib/code-gate/gate-engine.test.js +258 -0
- package/server/lib/code-gate/gate-reporter.js +123 -0
- package/server/lib/code-gate/gate-reporter.test.js +159 -0
- package/server/lib/code-gate/hooks-generator.js +149 -0
- package/server/lib/code-gate/hooks-generator.test.js +142 -0
- package/server/lib/code-gate/llm-reviewer.js +176 -0
- package/server/lib/code-gate/llm-reviewer.test.js +161 -0
- package/server/lib/code-gate/multi-model-reviewer.js +172 -0
- package/server/lib/code-gate/multi-model-reviewer.test.js +217 -0
- package/server/lib/code-gate/push-gate.js +133 -0
- package/server/lib/code-gate/push-gate.test.js +190 -0
- package/server/lib/code-gate/rules/architecture-rules.js +228 -0
- package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
- package/server/lib/code-gate/rules/client-rules.js +120 -0
- package/server/lib/code-gate/rules/client-rules.test.js +121 -0
- package/server/lib/code-gate/rules/config-rules.js +140 -0
- package/server/lib/code-gate/rules/config-rules.test.js +103 -0
- package/server/lib/code-gate/rules/database-rules.js +158 -0
- package/server/lib/code-gate/rules/database-rules.test.js +119 -0
- package/server/lib/code-gate/rules/docker-rules.js +201 -0
- package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
- package/server/lib/code-gate/rules/quality-rules.js +304 -0
- package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
- package/server/lib/code-gate/rules/security-rules.js +228 -0
- package/server/lib/code-gate/rules/security-rules.test.js +131 -0
- package/server/lib/code-gate/rules/structure-rules.js +155 -0
- package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
- package/server/lib/code-gate/rules/test-rules.js +93 -0
- package/server/lib/code-gate/rules/test-rules.test.js +97 -0
- package/server/lib/code-gate/typescript-gate.js +128 -0
- package/server/lib/code-gate/typescript-gate.test.js +131 -0
- package/server/lib/code-generator.test.js +1 -1
- package/server/lib/cost-command.test.js +1 -1
- package/server/lib/cost-optimizer.test.js +1 -1
- package/server/lib/cost-projections.test.js +1 -1
- package/server/lib/cost-reports.test.js +1 -1
- package/server/lib/cost-tracker.test.js +1 -1
- package/server/lib/crypto-patterns.test.js +1 -1
- package/server/lib/design-command.test.js +1 -1
- package/server/lib/design-parser.test.js +1 -1
- package/server/lib/gemini-vision.test.js +1 -1
- package/server/lib/infra/infra-generator.js +331 -0
- package/server/lib/infra/infra-generator.test.js +146 -0
- package/server/lib/input-validator.test.js +1 -1
- package/server/lib/litellm-client.test.js +1 -1
- package/server/lib/litellm-command.test.js +1 -1
- package/server/lib/litellm-config.test.js +1 -1
- package/server/lib/llm/adapters/api-adapter.js +95 -0
- package/server/lib/llm/adapters/api-adapter.test.js +81 -0
- package/server/lib/llm/adapters/codex-adapter.js +85 -0
- package/server/lib/llm/adapters/codex-adapter.test.js +54 -0
- package/server/lib/llm/adapters/gemini-adapter.js +100 -0
- package/server/lib/llm/adapters/gemini-adapter.test.js +54 -0
- package/server/lib/llm/index.js +109 -0
- package/server/lib/llm/index.test.js +147 -0
- package/server/lib/llm/provider-executor.js +168 -0
- package/server/lib/llm/provider-executor.test.js +244 -0
- package/server/lib/llm/provider-registry.js +104 -0
- package/server/lib/llm/provider-registry.test.js +157 -0
- package/server/lib/llm/review-service.js +222 -0
- package/server/lib/llm/review-service.test.js +220 -0
- package/server/lib/model-pricing.test.js +1 -1
- package/server/lib/models-command.test.js +1 -1
- package/server/lib/optimize-command.test.js +1 -1
- package/server/lib/orchestration-integration.test.js +1 -1
- package/server/lib/output-encoder.test.js +1 -1
- package/server/lib/quality-evaluator.test.js +1 -1
- package/server/lib/quality-gate-command.test.js +1 -1
- package/server/lib/quality-gate-scorer.test.js +1 -1
- package/server/lib/quality-history.test.js +1 -1
- package/server/lib/quality-presets.test.js +1 -1
- package/server/lib/quality-retry.test.js +1 -1
- package/server/lib/quality-thresholds.test.js +1 -1
- package/server/lib/secure-auth.test.js +1 -1
- package/server/lib/secure-code-command.test.js +1 -1
- package/server/lib/secure-errors.test.js +1 -1
- package/server/lib/security/auth-security.test.js +4 -3
- package/server/lib/shame/shame-registry.js +224 -0
- package/server/lib/shame/shame-registry.test.js +202 -0
- package/server/lib/standards/cleanup-dry-run.js +254 -0
- package/server/lib/standards/cleanup-dry-run.test.js +220 -0
- package/server/lib/vision-command.test.js +1 -1
- package/server/lib/visual-command.test.js +1 -1
- package/server/lib/visual-testing.test.js +1 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Engine Tests
|
|
3
|
+
*
|
|
4
|
+
* The gate engine accepts changed files and runs configurable rule sets
|
|
5
|
+
* against each file, returning pass/fail with detailed findings.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
createGateEngine,
|
|
11
|
+
runGate,
|
|
12
|
+
aggregateFindings,
|
|
13
|
+
calculateScore,
|
|
14
|
+
SEVERITY,
|
|
15
|
+
} = require('./gate-engine.js');
|
|
16
|
+
|
|
17
|
+
describe('Gate Engine', () => {
|
|
18
|
+
describe('SEVERITY', () => {
|
|
19
|
+
it('defines all severity levels', () => {
|
|
20
|
+
expect(SEVERITY.BLOCK).toBe('block');
|
|
21
|
+
expect(SEVERITY.WARN).toBe('warn');
|
|
22
|
+
expect(SEVERITY.INFO).toBe('info');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('createGateEngine', () => {
|
|
27
|
+
it('creates engine with default options', () => {
|
|
28
|
+
const engine = createGateEngine();
|
|
29
|
+
expect(engine).toBeDefined();
|
|
30
|
+
expect(engine.rules).toEqual([]);
|
|
31
|
+
expect(engine.options).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('creates engine with custom rules', () => {
|
|
35
|
+
const mockRule = { id: 'test-rule', check: () => [] };
|
|
36
|
+
const engine = createGateEngine({ rules: [mockRule] });
|
|
37
|
+
expect(engine.rules).toHaveLength(1);
|
|
38
|
+
expect(engine.rules[0].id).toBe('test-rule');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('accepts ignore patterns', () => {
|
|
42
|
+
const engine = createGateEngine({ ignore: ['*.md', 'dist/*'] });
|
|
43
|
+
expect(engine.options.ignore).toEqual(['*.md', 'dist/*']);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('runGate', () => {
|
|
48
|
+
let engine;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
engine = createGateEngine();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns pass for empty changeset', async () => {
|
|
55
|
+
const result = await runGate(engine, []);
|
|
56
|
+
expect(result.passed).toBe(true);
|
|
57
|
+
expect(result.findings).toEqual([]);
|
|
58
|
+
expect(result.summary.total).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('runs rules against each file', async () => {
|
|
62
|
+
const ruleCallCount = { count: 0 };
|
|
63
|
+
const mockRule = {
|
|
64
|
+
id: 'counter-rule',
|
|
65
|
+
check: (file, content) => {
|
|
66
|
+
ruleCallCount.count++;
|
|
67
|
+
return [];
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
engine = createGateEngine({ rules: [mockRule] });
|
|
71
|
+
|
|
72
|
+
const files = [
|
|
73
|
+
{ path: 'src/a.js', content: 'const a = 1;' },
|
|
74
|
+
{ path: 'src/b.js', content: 'const b = 2;' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
await runGate(engine, files);
|
|
78
|
+
expect(ruleCallCount.count).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('collects findings from all rules', async () => {
|
|
82
|
+
const rule1 = {
|
|
83
|
+
id: 'rule-a',
|
|
84
|
+
check: () => [
|
|
85
|
+
{ severity: 'block', rule: 'rule-a', line: 1, message: 'Issue A', fix: 'Fix A' },
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
const rule2 = {
|
|
89
|
+
id: 'rule-b',
|
|
90
|
+
check: () => [
|
|
91
|
+
{ severity: 'warn', rule: 'rule-b', line: 5, message: 'Issue B', fix: 'Fix B' },
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
engine = createGateEngine({ rules: [rule1, rule2] });
|
|
95
|
+
|
|
96
|
+
const result = await runGate(engine, [{ path: 'src/a.js', content: 'code' }]);
|
|
97
|
+
expect(result.findings).toHaveLength(2);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('fails when any finding has block severity', async () => {
|
|
101
|
+
const rule = {
|
|
102
|
+
id: 'blocker',
|
|
103
|
+
check: () => [
|
|
104
|
+
{ severity: 'block', rule: 'blocker', line: 1, message: 'Blocked', fix: 'Fix it' },
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
engine = createGateEngine({ rules: [rule] });
|
|
108
|
+
|
|
109
|
+
const result = await runGate(engine, [{ path: 'src/a.js', content: 'code' }]);
|
|
110
|
+
expect(result.passed).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('passes when findings are warn-only', async () => {
|
|
114
|
+
const rule = {
|
|
115
|
+
id: 'warner',
|
|
116
|
+
check: () => [
|
|
117
|
+
{ severity: 'warn', rule: 'warner', line: 1, message: 'Warning', fix: 'Maybe fix' },
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
engine = createGateEngine({ rules: [rule] });
|
|
121
|
+
|
|
122
|
+
const result = await runGate(engine, [{ path: 'src/a.js', content: 'code' }]);
|
|
123
|
+
expect(result.passed).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('passes when findings are info-only', async () => {
|
|
127
|
+
const rule = {
|
|
128
|
+
id: 'informer',
|
|
129
|
+
check: () => [
|
|
130
|
+
{ severity: 'info', rule: 'informer', line: 1, message: 'FYI', fix: 'Optional' },
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
engine = createGateEngine({ rules: [rule] });
|
|
134
|
+
|
|
135
|
+
const result = await runGate(engine, [{ path: 'src/a.js', content: 'code' }]);
|
|
136
|
+
expect(result.passed).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('attaches file path to each finding', async () => {
|
|
140
|
+
const rule = {
|
|
141
|
+
id: 'path-test',
|
|
142
|
+
check: () => [
|
|
143
|
+
{ severity: 'warn', rule: 'path-test', line: 1, message: 'X', fix: 'Y' },
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
engine = createGateEngine({ rules: [rule] });
|
|
147
|
+
|
|
148
|
+
const result = await runGate(engine, [{ path: 'src/deep/file.js', content: 'x' }]);
|
|
149
|
+
expect(result.findings[0].file).toBe('src/deep/file.js');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('skips files matching ignore patterns', async () => {
|
|
153
|
+
const rule = {
|
|
154
|
+
id: 'skip-test',
|
|
155
|
+
check: () => [
|
|
156
|
+
{ severity: 'block', rule: 'skip-test', line: 1, message: 'Bad', fix: 'Fix' },
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
engine = createGateEngine({ rules: [rule], ignore: ['*.md', '*.json'] });
|
|
160
|
+
|
|
161
|
+
const files = [
|
|
162
|
+
{ path: 'README.md', content: '# Hello' },
|
|
163
|
+
{ path: 'package.json', content: '{}' },
|
|
164
|
+
{ path: 'src/app.js', content: 'code' },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const result = await runGate(engine, files);
|
|
168
|
+
// Only src/app.js should be checked
|
|
169
|
+
expect(result.findings).toHaveLength(1);
|
|
170
|
+
expect(result.findings[0].file).toBe('src/app.js');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('includes summary with counts per severity', async () => {
|
|
174
|
+
const rule = {
|
|
175
|
+
id: 'multi',
|
|
176
|
+
check: () => [
|
|
177
|
+
{ severity: 'block', rule: 'multi', line: 1, message: 'A', fix: 'A' },
|
|
178
|
+
{ severity: 'warn', rule: 'multi', line: 2, message: 'B', fix: 'B' },
|
|
179
|
+
{ severity: 'info', rule: 'multi', line: 3, message: 'C', fix: 'C' },
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
engine = createGateEngine({ rules: [rule] });
|
|
183
|
+
|
|
184
|
+
const result = await runGate(engine, [{ path: 'x.js', content: '' }]);
|
|
185
|
+
expect(result.summary.total).toBe(3);
|
|
186
|
+
expect(result.summary.block).toBe(1);
|
|
187
|
+
expect(result.summary.warn).toBe(1);
|
|
188
|
+
expect(result.summary.info).toBe(1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('handles rule that throws gracefully', async () => {
|
|
192
|
+
const rule = {
|
|
193
|
+
id: 'crasher',
|
|
194
|
+
check: () => { throw new Error('Rule crashed'); },
|
|
195
|
+
};
|
|
196
|
+
engine = createGateEngine({ rules: [rule] });
|
|
197
|
+
|
|
198
|
+
const result = await runGate(engine, [{ path: 'x.js', content: '' }]);
|
|
199
|
+
// Should not throw - engine catches rule errors
|
|
200
|
+
expect(result).toBeDefined();
|
|
201
|
+
expect(result.findings).toHaveLength(1);
|
|
202
|
+
expect(result.findings[0].rule).toBe('crasher');
|
|
203
|
+
expect(result.findings[0].severity).toBe('warn');
|
|
204
|
+
expect(result.findings[0].message).toContain('Rule crashed');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('measures execution duration', async () => {
|
|
208
|
+
engine = createGateEngine();
|
|
209
|
+
const result = await runGate(engine, []);
|
|
210
|
+
expect(typeof result.duration).toBe('number');
|
|
211
|
+
expect(result.duration).toBeGreaterThanOrEqual(0);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('aggregateFindings', () => {
|
|
216
|
+
it('groups findings by file', () => {
|
|
217
|
+
const findings = [
|
|
218
|
+
{ file: 'a.js', rule: 'r1', severity: 'block', message: 'A' },
|
|
219
|
+
{ file: 'b.js', rule: 'r2', severity: 'warn', message: 'B' },
|
|
220
|
+
{ file: 'a.js', rule: 'r3', severity: 'info', message: 'C' },
|
|
221
|
+
];
|
|
222
|
+
const grouped = aggregateFindings(findings);
|
|
223
|
+
expect(grouped['a.js']).toHaveLength(2);
|
|
224
|
+
expect(grouped['b.js']).toHaveLength(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns empty object for no findings', () => {
|
|
228
|
+
const grouped = aggregateFindings([]);
|
|
229
|
+
expect(Object.keys(grouped)).toHaveLength(0);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('calculateScore', () => {
|
|
234
|
+
it('returns 100 for no findings', () => {
|
|
235
|
+
expect(calculateScore([])).toBe(100);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('deducts points for block findings', () => {
|
|
239
|
+
const findings = [
|
|
240
|
+
{ severity: 'block' },
|
|
241
|
+
];
|
|
242
|
+
const score = calculateScore(findings);
|
|
243
|
+
expect(score).toBeLessThan(100);
|
|
244
|
+
expect(score).toBeGreaterThanOrEqual(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('deducts fewer points for warnings', () => {
|
|
248
|
+
const blockFindings = [{ severity: 'block' }];
|
|
249
|
+
const warnFindings = [{ severity: 'warn' }];
|
|
250
|
+
expect(calculateScore(warnFindings)).toBeGreaterThan(calculateScore(blockFindings));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('floors at zero', () => {
|
|
254
|
+
const manyFindings = Array.from({ length: 50 }, () => ({ severity: 'block' }));
|
|
255
|
+
expect(calculateScore(manyFindings)).toBe(0);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Reporter
|
|
3
|
+
*
|
|
4
|
+
* Formats gate results into clear, actionable terminal output
|
|
5
|
+
* with severity badges, fix suggestions, and summary line.
|
|
6
|
+
*
|
|
7
|
+
* @module code-gate/gate-reporter
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format a single finding as a readable string.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} finding
|
|
14
|
+
* @param {string} finding.severity
|
|
15
|
+
* @param {string} finding.rule
|
|
16
|
+
* @param {string} finding.file
|
|
17
|
+
* @param {number} [finding.line]
|
|
18
|
+
* @param {string} finding.message
|
|
19
|
+
* @param {string} finding.fix
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function formatFinding(finding) {
|
|
23
|
+
const badge = `[${finding.severity.toUpperCase()}]`;
|
|
24
|
+
const location = finding.line ? ` (line ${finding.line})` : '';
|
|
25
|
+
const rule = finding.rule;
|
|
26
|
+
|
|
27
|
+
let output = ` ${badge} ${rule}${location}\n`;
|
|
28
|
+
output += ` ${finding.message}\n`;
|
|
29
|
+
if (finding.fix) {
|
|
30
|
+
output += ` Fix: ${finding.fix}\n`;
|
|
31
|
+
}
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Group findings by file path.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array} findings
|
|
39
|
+
* @returns {Object.<string, Array>}
|
|
40
|
+
*/
|
|
41
|
+
function groupByFile(findings) {
|
|
42
|
+
const groups = {};
|
|
43
|
+
for (const finding of findings) {
|
|
44
|
+
const key = finding.file;
|
|
45
|
+
if (!groups[key]) groups[key] = [];
|
|
46
|
+
groups[key].push(finding);
|
|
47
|
+
}
|
|
48
|
+
return groups;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format the summary line.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} summary - { total, block, warn, info }
|
|
55
|
+
* @param {boolean} passed
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function formatSummary(summary, passed) {
|
|
59
|
+
if (passed && summary.total === 0) {
|
|
60
|
+
return 'All clear — gate passed.';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const parts = [];
|
|
64
|
+
if (summary.block > 0) parts.push(`${summary.block} blocking`);
|
|
65
|
+
if (summary.warn > 0) parts.push(`${summary.warn} warning${summary.warn > 1 ? 's' : ''}`);
|
|
66
|
+
if (summary.info > 0) parts.push(`${summary.info} info`);
|
|
67
|
+
|
|
68
|
+
const status = passed ? 'passed' : 'blocked';
|
|
69
|
+
return `Summary: ${parts.join(' | ')} — gate ${status}.`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format a complete gate report.
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} result - Gate engine result
|
|
76
|
+
* @param {boolean} result.passed
|
|
77
|
+
* @param {Array} result.findings
|
|
78
|
+
* @param {Object} result.summary
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
function formatReport(result) {
|
|
82
|
+
const { passed, findings, summary } = result;
|
|
83
|
+
|
|
84
|
+
let report = '';
|
|
85
|
+
|
|
86
|
+
// Header
|
|
87
|
+
const status = passed ? 'Passed' : 'Blocked';
|
|
88
|
+
report += `TLC Code Gate — ${status}\n`;
|
|
89
|
+
report += '─'.repeat(42) + '\n\n';
|
|
90
|
+
|
|
91
|
+
if (findings.length === 0) {
|
|
92
|
+
report += 'All clear — no issues found.\n\n';
|
|
93
|
+
report += formatSummary(summary, passed) + '\n';
|
|
94
|
+
return report;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Group and display findings by file
|
|
98
|
+
const grouped = groupByFile(findings);
|
|
99
|
+
for (const [file, fileFindings] of Object.entries(grouped)) {
|
|
100
|
+
report += `${file}\n`;
|
|
101
|
+
for (const finding of fileFindings) {
|
|
102
|
+
report += formatFinding(finding);
|
|
103
|
+
}
|
|
104
|
+
report += '\n';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Summary
|
|
108
|
+
report += formatSummary(summary, passed) + '\n';
|
|
109
|
+
|
|
110
|
+
// Bypass hint (only when blocked)
|
|
111
|
+
if (!passed) {
|
|
112
|
+
report += 'Fix blocking issues or use --no-verify (logged).\n';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return report;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
formatReport,
|
|
120
|
+
formatSummary,
|
|
121
|
+
formatFinding,
|
|
122
|
+
groupByFile,
|
|
123
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Reporter Tests
|
|
3
|
+
*
|
|
4
|
+
* Formats gate results into clear, actionable terminal output
|
|
5
|
+
* with severity badges, fix suggestions, and summary.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
formatReport,
|
|
11
|
+
formatSummary,
|
|
12
|
+
formatFinding,
|
|
13
|
+
groupByFile,
|
|
14
|
+
} = require('./gate-reporter.js');
|
|
15
|
+
|
|
16
|
+
describe('Gate Reporter', () => {
|
|
17
|
+
describe('formatFinding', () => {
|
|
18
|
+
it('formats a block finding with line number', () => {
|
|
19
|
+
const finding = {
|
|
20
|
+
severity: 'block',
|
|
21
|
+
rule: 'no-eval',
|
|
22
|
+
file: 'src/app.js',
|
|
23
|
+
line: 12,
|
|
24
|
+
message: "eval() is not allowed",
|
|
25
|
+
fix: 'Use a safe alternative',
|
|
26
|
+
};
|
|
27
|
+
const output = formatFinding(finding);
|
|
28
|
+
expect(output).toContain('[BLOCK]');
|
|
29
|
+
expect(output).toContain('no-eval');
|
|
30
|
+
expect(output).toContain('line 12');
|
|
31
|
+
expect(output).toContain('eval()');
|
|
32
|
+
expect(output).toContain('Use a safe alternative');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('formats a warn finding', () => {
|
|
36
|
+
const finding = {
|
|
37
|
+
severity: 'warn',
|
|
38
|
+
rule: 'max-function-length',
|
|
39
|
+
file: 'src/big.js',
|
|
40
|
+
line: 45,
|
|
41
|
+
message: 'Function too long',
|
|
42
|
+
fix: 'Extract helper functions',
|
|
43
|
+
};
|
|
44
|
+
const output = formatFinding(finding);
|
|
45
|
+
expect(output).toContain('[WARN]');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('formats info finding', () => {
|
|
49
|
+
const finding = {
|
|
50
|
+
severity: 'info',
|
|
51
|
+
rule: 'docs-hint',
|
|
52
|
+
file: 'src/x.js',
|
|
53
|
+
message: 'Consider adding docs',
|
|
54
|
+
fix: 'Add JSDoc',
|
|
55
|
+
};
|
|
56
|
+
const output = formatFinding(finding);
|
|
57
|
+
expect(output).toContain('[INFO]');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('handles finding without line number', () => {
|
|
61
|
+
const finding = {
|
|
62
|
+
severity: 'block',
|
|
63
|
+
rule: 'require-test-file',
|
|
64
|
+
file: 'src/x.js',
|
|
65
|
+
message: 'No test file found',
|
|
66
|
+
fix: 'Create test file',
|
|
67
|
+
};
|
|
68
|
+
const output = formatFinding(finding);
|
|
69
|
+
expect(output).not.toContain('line');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('groupByFile', () => {
|
|
74
|
+
it('groups findings by file path', () => {
|
|
75
|
+
const findings = [
|
|
76
|
+
{ file: 'a.js', rule: 'r1', severity: 'block', message: 'A' },
|
|
77
|
+
{ file: 'b.js', rule: 'r2', severity: 'warn', message: 'B' },
|
|
78
|
+
{ file: 'a.js', rule: 'r3', severity: 'info', message: 'C' },
|
|
79
|
+
];
|
|
80
|
+
const groups = groupByFile(findings);
|
|
81
|
+
expect(Object.keys(groups)).toHaveLength(2);
|
|
82
|
+
expect(groups['a.js']).toHaveLength(2);
|
|
83
|
+
expect(groups['b.js']).toHaveLength(1);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('formatSummary', () => {
|
|
88
|
+
it('shows blocking count and pass status', () => {
|
|
89
|
+
const summary = { total: 0, block: 0, warn: 0, info: 0 };
|
|
90
|
+
const output = formatSummary(summary, true);
|
|
91
|
+
expect(output).toContain('passed');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('shows blocking message when blocked', () => {
|
|
95
|
+
const summary = { total: 3, block: 2, warn: 1, info: 0 };
|
|
96
|
+
const output = formatSummary(summary, false);
|
|
97
|
+
expect(output).toContain('2 blocking');
|
|
98
|
+
expect(output).toContain('1 warning');
|
|
99
|
+
expect(output).toContain('blocked');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('formatReport', () => {
|
|
104
|
+
it('formats complete report with header', () => {
|
|
105
|
+
const result = {
|
|
106
|
+
passed: false,
|
|
107
|
+
findings: [
|
|
108
|
+
{ file: 'src/app.js', severity: 'block', rule: 'no-eval', line: 5, message: 'eval found', fix: 'Remove eval' },
|
|
109
|
+
],
|
|
110
|
+
summary: { total: 1, block: 1, warn: 0, info: 0 },
|
|
111
|
+
};
|
|
112
|
+
const output = formatReport(result);
|
|
113
|
+
expect(output).toContain('Code Gate');
|
|
114
|
+
expect(output).toContain('src/app.js');
|
|
115
|
+
expect(output).toContain('[BLOCK]');
|
|
116
|
+
expect(output).toContain('blocked');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('formats all-clear report', () => {
|
|
120
|
+
const result = {
|
|
121
|
+
passed: true,
|
|
122
|
+
findings: [],
|
|
123
|
+
summary: { total: 0, block: 0, warn: 0, info: 0 },
|
|
124
|
+
};
|
|
125
|
+
const output = formatReport(result);
|
|
126
|
+
expect(output).toContain('passed');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('groups findings by file in report', () => {
|
|
130
|
+
const result = {
|
|
131
|
+
passed: false,
|
|
132
|
+
findings: [
|
|
133
|
+
{ file: 'a.js', severity: 'block', rule: 'r1', message: 'X', fix: 'Y' },
|
|
134
|
+
{ file: 'b.js', severity: 'block', rule: 'r2', message: 'X', fix: 'Y' },
|
|
135
|
+
{ file: 'a.js', severity: 'warn', rule: 'r3', message: 'X', fix: 'Y' },
|
|
136
|
+
],
|
|
137
|
+
summary: { total: 3, block: 2, warn: 1, info: 0 },
|
|
138
|
+
};
|
|
139
|
+
const output = formatReport(result);
|
|
140
|
+
// a.js should appear before its findings
|
|
141
|
+
const aIndex = output.indexOf('a.js');
|
|
142
|
+
const bIndex = output.indexOf('b.js');
|
|
143
|
+
expect(aIndex).toBeGreaterThan(-1);
|
|
144
|
+
expect(bIndex).toBeGreaterThan(-1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('includes bypass hint in blocked report', () => {
|
|
148
|
+
const result = {
|
|
149
|
+
passed: false,
|
|
150
|
+
findings: [
|
|
151
|
+
{ file: 'x.js', severity: 'block', rule: 'r1', message: 'Bad', fix: 'Fix' },
|
|
152
|
+
],
|
|
153
|
+
summary: { total: 1, block: 1, warn: 0, info: 0 },
|
|
154
|
+
};
|
|
155
|
+
const output = formatReport(result);
|
|
156
|
+
expect(output).toContain('--no-verify');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates and installs git hooks that run the TLC code gate.
|
|
5
|
+
* Hooks are portable sh scripts (not bash-specific).
|
|
6
|
+
*
|
|
7
|
+
* @module code-gate/hooks-generator
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
/** Marker comment to identify TLC-generated hooks */
|
|
14
|
+
const TLC_HOOK_MARKER = '# TLC Code Gate';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate a pre-commit hook script.
|
|
18
|
+
* Runs fast static analysis (< 3s) on staged files.
|
|
19
|
+
*
|
|
20
|
+
* @returns {string} Shell script content
|
|
21
|
+
*/
|
|
22
|
+
function generatePreCommitHook() {
|
|
23
|
+
return `#!/bin/sh
|
|
24
|
+
${TLC_HOOK_MARKER} — pre-commit
|
|
25
|
+
# Runs static code gate on staged files.
|
|
26
|
+
# To bypass: git commit --no-verify (bypass is logged)
|
|
27
|
+
|
|
28
|
+
# Get the project root
|
|
29
|
+
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
|
30
|
+
|
|
31
|
+
# Run the TLC gate check on staged files
|
|
32
|
+
if command -v node > /dev/null 2>&1; then
|
|
33
|
+
node "$PROJECT_ROOT/node_modules/.bin/tlc-gate" check --hook pre-commit
|
|
34
|
+
EXIT_CODE=$?
|
|
35
|
+
|
|
36
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
37
|
+
echo ""
|
|
38
|
+
echo "Commit blocked by TLC Code Gate."
|
|
39
|
+
echo "Fix the issues above or use: git commit --no-verify"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
else
|
|
43
|
+
echo "Warning: Node.js not found. Skipping TLC Code Gate."
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
exit 0
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate a pre-push hook script.
|
|
52
|
+
* Runs full analysis including LLM-powered review.
|
|
53
|
+
*
|
|
54
|
+
* @returns {string} Shell script content
|
|
55
|
+
*/
|
|
56
|
+
function generatePrePushHook() {
|
|
57
|
+
return `#!/bin/sh
|
|
58
|
+
${TLC_HOOK_MARKER} — pre-push
|
|
59
|
+
# Runs full code gate including LLM review before push.
|
|
60
|
+
# To bypass: git push --no-verify (bypass is logged)
|
|
61
|
+
|
|
62
|
+
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
|
63
|
+
|
|
64
|
+
if command -v node > /dev/null 2>&1; then
|
|
65
|
+
node "$PROJECT_ROOT/node_modules/.bin/tlc-gate" check --hook pre-push --full
|
|
66
|
+
EXIT_CODE=$?
|
|
67
|
+
|
|
68
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
69
|
+
echo ""
|
|
70
|
+
echo "Push blocked by TLC Code Gate."
|
|
71
|
+
echo "Fix the issues above or use: git push --no-verify"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
else
|
|
75
|
+
echo "Warning: Node.js not found. Skipping TLC Code Gate."
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
exit 0
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Install git hooks into the project's .git/hooks/ directory.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} projectPath - Path to project root
|
|
86
|
+
* @param {Object} [options] - Options
|
|
87
|
+
* @param {Object} [options.fs] - File system module (for testing)
|
|
88
|
+
* @param {string[]} [options.hooks] - Which hooks to install (default: both)
|
|
89
|
+
* @returns {Promise<{installed: string[]}>}
|
|
90
|
+
*/
|
|
91
|
+
async function installHooks(projectPath, options = {}) {
|
|
92
|
+
const fsModule = options.fs || fs;
|
|
93
|
+
const hooks = options.hooks || ['pre-commit', 'pre-push'];
|
|
94
|
+
const gitDir = path.join(projectPath, '.git');
|
|
95
|
+
|
|
96
|
+
if (!fsModule.existsSync(gitDir)) {
|
|
97
|
+
throw new Error('Not a git repository: .git directory not found');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
101
|
+
const installed = [];
|
|
102
|
+
|
|
103
|
+
const generators = {
|
|
104
|
+
'pre-commit': generatePreCommitHook,
|
|
105
|
+
'pre-push': generatePrePushHook,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (const hookName of hooks) {
|
|
109
|
+
const generator = generators[hookName];
|
|
110
|
+
if (!generator) continue;
|
|
111
|
+
|
|
112
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
113
|
+
const content = generator();
|
|
114
|
+
|
|
115
|
+
fsModule.writeFileSync(hookPath, content, 'utf-8');
|
|
116
|
+
fsModule.chmodSync(hookPath, '755');
|
|
117
|
+
installed.push(hookName);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { installed };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if a TLC code gate hook is installed.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} projectPath
|
|
127
|
+
* @param {string} hookName - Hook name (pre-commit, pre-push)
|
|
128
|
+
* @param {Object} [options]
|
|
129
|
+
* @param {Object} [options.fs] - File system module
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
function isHookInstalled(projectPath, hookName, options = {}) {
|
|
133
|
+
const fsModule = options.fs || fs;
|
|
134
|
+
const hookPath = path.join(projectPath, '.git', 'hooks', hookName);
|
|
135
|
+
|
|
136
|
+
if (!fsModule.existsSync(hookPath)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const content = fsModule.readFileSync(hookPath, 'utf-8');
|
|
141
|
+
return content.includes(TLC_HOOK_MARKER);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
generatePreCommitHook,
|
|
146
|
+
generatePrePushHook,
|
|
147
|
+
installHooks,
|
|
148
|
+
isHookInstalled,
|
|
149
|
+
};
|