@renseiai/agentfactory 0.8.19 → 0.8.21
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/dist/src/config/repository-config.d.ts +7 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +15 -1
- package/dist/src/config/repository-config.test.js +1 -1
- package/dist/src/governor/decision-engine-adapter.js +5 -10
- package/dist/src/governor/decision-engine-adapter.test.js +13 -14
- package/dist/src/governor/decision-engine.js +3 -7
- package/dist/src/governor/decision-engine.test.js +5 -5
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/merge-queue/adapters/local.d.ts +68 -0
- package/dist/src/merge-queue/adapters/local.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/local.js +136 -0
- package/dist/src/merge-queue/adapters/local.test.d.ts +2 -0
- package/dist/src/merge-queue/adapters/local.test.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/local.test.js +176 -0
- package/dist/src/merge-queue/index.d.ts +13 -5
- package/dist/src/merge-queue/index.d.ts.map +1 -1
- package/dist/src/merge-queue/index.js +13 -6
- package/dist/src/merge-queue/merge-queue.integration.test.js +19 -0
- package/dist/src/merge-queue/merge-worker.d.ts.map +1 -1
- package/dist/src/merge-queue/merge-worker.js +29 -0
- package/dist/src/merge-queue/types.d.ts +1 -1
- package/dist/src/merge-queue/types.d.ts.map +1 -1
- package/dist/src/orchestrator/index.d.ts +4 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +3 -0
- package/dist/src/orchestrator/orchestrator.d.ts +31 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +263 -11
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +3 -1
- package/dist/src/orchestrator/parse-work-result.test.js +6 -0
- package/dist/src/orchestrator/quality-baseline.d.ts +83 -0
- package/dist/src/orchestrator/quality-baseline.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-baseline.js +313 -0
- package/dist/src/orchestrator/quality-baseline.test.d.ts +2 -0
- package/dist/src/orchestrator/quality-baseline.test.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-baseline.test.js +448 -0
- package/dist/src/orchestrator/quality-ratchet.d.ts +70 -0
- package/dist/src/orchestrator/quality-ratchet.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-ratchet.js +162 -0
- package/dist/src/orchestrator/quality-ratchet.test.d.ts +2 -0
- package/dist/src/orchestrator/quality-ratchet.test.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-ratchet.test.js +335 -0
- package/dist/src/orchestrator/types.d.ts +2 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -1
- package/dist/src/providers/codex-app-server-provider.d.ts +37 -1
- package/dist/src/providers/codex-app-server-provider.d.ts.map +1 -1
- package/dist/src/providers/codex-app-server-provider.js +290 -35
- package/dist/src/providers/codex-app-server-provider.test.js +72 -12
- package/dist/src/providers/codex-approval-bridge.d.ts +49 -0
- package/dist/src/providers/codex-approval-bridge.d.ts.map +1 -0
- package/dist/src/providers/codex-approval-bridge.js +117 -0
- package/dist/src/providers/codex-approval-bridge.test.d.ts +2 -0
- package/dist/src/providers/codex-approval-bridge.test.d.ts.map +1 -0
- package/dist/src/providers/codex-approval-bridge.test.js +188 -0
- package/dist/src/providers/types.d.ts +25 -0
- package/dist/src/providers/types.d.ts.map +1 -1
- package/dist/src/routing/types.d.ts +1 -1
- package/dist/src/templates/adapters.d.ts +25 -0
- package/dist/src/templates/adapters.d.ts.map +1 -1
- package/dist/src/templates/adapters.js +70 -0
- package/dist/src/templates/adapters.test.js +49 -0
- package/dist/src/templates/index.d.ts +1 -0
- package/dist/src/templates/index.d.ts.map +1 -1
- package/dist/src/templates/registry.d.ts +8 -0
- package/dist/src/templates/registry.d.ts.map +1 -1
- package/dist/src/templates/registry.js +11 -0
- package/dist/src/templates/types.d.ts +22 -0
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +12 -0
- package/dist/src/tools/index.d.ts +2 -0
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +1 -0
- package/dist/src/tools/registry.d.ts +9 -1
- package/dist/src/tools/registry.d.ts.map +1 -1
- package/dist/src/tools/registry.js +13 -1
- package/dist/src/tools/stdio-server-entry.d.ts +25 -0
- package/dist/src/tools/stdio-server-entry.d.ts.map +1 -0
- package/dist/src/tools/stdio-server-entry.js +205 -0
- package/dist/src/tools/stdio-server.d.ts +87 -0
- package/dist/src/tools/stdio-server.d.ts.map +1 -0
- package/dist/src/tools/stdio-server.js +138 -0
- package/dist/src/workflow/workflow-types.d.ts +3 -3
- package/package.json +3 -2
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { captureQualityBaseline, computeQualityDelta, formatQualityReport, parseVitestJson, countTypescriptErrors, loadBaseline, saveBaseline, } from './quality-baseline.js';
|
|
3
|
+
// Mock child_process and fs
|
|
4
|
+
vi.mock('node:child_process', () => ({
|
|
5
|
+
execSync: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('node:fs', () => ({
|
|
8
|
+
readFileSync: vi.fn(),
|
|
9
|
+
writeFileSync: vi.fn(),
|
|
10
|
+
existsSync: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
14
|
+
const mockExecSync = vi.mocked(execSync);
|
|
15
|
+
const mockReadFileSync = vi.mocked(readFileSync);
|
|
16
|
+
const mockWriteFileSync = vi.mocked(writeFileSync);
|
|
17
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.resetAllMocks();
|
|
20
|
+
});
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// captureQualityBaseline
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
describe('captureQualityBaseline', () => {
|
|
25
|
+
it('captures test counts from vitest JSON output', () => {
|
|
26
|
+
// git rev-parse HEAD
|
|
27
|
+
mockExecSync.mockReturnValueOnce('abc123\n');
|
|
28
|
+
// test command with --reporter=json
|
|
29
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({
|
|
30
|
+
numTotalTests: 100,
|
|
31
|
+
numPassedTests: 98,
|
|
32
|
+
numFailedTests: 2,
|
|
33
|
+
}));
|
|
34
|
+
// typecheck command
|
|
35
|
+
mockExecSync.mockReturnValueOnce('');
|
|
36
|
+
// no lint command configured → skipped
|
|
37
|
+
const baseline = captureQualityBaseline('/work', {
|
|
38
|
+
testCommand: 'pnpm test',
|
|
39
|
+
validateCommand: 'pnpm typecheck',
|
|
40
|
+
});
|
|
41
|
+
expect(baseline.commitSha).toBe('abc123');
|
|
42
|
+
expect(baseline.tests.total).toBe(100);
|
|
43
|
+
expect(baseline.tests.passed).toBe(98);
|
|
44
|
+
expect(baseline.tests.failed).toBe(2);
|
|
45
|
+
expect(baseline.typecheck.errorCount).toBe(0);
|
|
46
|
+
expect(baseline.typecheck.exitCode).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
it('falls back to text parsing when JSON reporter fails', () => {
|
|
49
|
+
// git rev-parse HEAD
|
|
50
|
+
mockExecSync.mockReturnValueOnce('abc123\n');
|
|
51
|
+
// JSON reporter fails
|
|
52
|
+
mockExecSync.mockImplementationOnce(() => { throw new Error('json reporter not found'); });
|
|
53
|
+
// text output fallback
|
|
54
|
+
mockExecSync.mockReturnValueOnce('Tests 42 passed | 3 failed | 45 total');
|
|
55
|
+
// typecheck
|
|
56
|
+
mockExecSync.mockReturnValueOnce('');
|
|
57
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
58
|
+
expect(baseline.tests.total).toBe(45);
|
|
59
|
+
expect(baseline.tests.passed).toBe(42);
|
|
60
|
+
expect(baseline.tests.failed).toBe(3);
|
|
61
|
+
});
|
|
62
|
+
it('captures typecheck errors from stderr', () => {
|
|
63
|
+
// git rev-parse HEAD
|
|
64
|
+
mockExecSync.mockReturnValueOnce('abc123\n');
|
|
65
|
+
// test JSON reporter
|
|
66
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ numTotalTests: 10, numPassedTests: 10, numFailedTests: 0 }));
|
|
67
|
+
// typecheck fails
|
|
68
|
+
const tscError = new Error('tsc failed');
|
|
69
|
+
tscError.stdout = '';
|
|
70
|
+
tscError.stderr = 'src/a.ts(1,1): error TS2304: Cannot find name\nsrc/b.ts(5,3): error TS2345: Argument of type';
|
|
71
|
+
tscError.status = 2;
|
|
72
|
+
mockExecSync.mockImplementationOnce(() => { throw tscError; });
|
|
73
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
74
|
+
expect(baseline.typecheck.errorCount).toBe(2);
|
|
75
|
+
expect(baseline.typecheck.exitCode).toBe(2);
|
|
76
|
+
});
|
|
77
|
+
it('handles complete test failure gracefully', () => {
|
|
78
|
+
// git rev-parse HEAD
|
|
79
|
+
mockExecSync.mockReturnValueOnce('abc123\n');
|
|
80
|
+
// JSON reporter fails
|
|
81
|
+
mockExecSync.mockImplementationOnce(() => { throw new Error('crash'); });
|
|
82
|
+
// text fallback also fails
|
|
83
|
+
mockExecSync.mockImplementationOnce(() => { throw new Error('crash'); });
|
|
84
|
+
// typecheck passes
|
|
85
|
+
mockExecSync.mockReturnValueOnce('');
|
|
86
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
87
|
+
// Should record at least 1 failure, not throw
|
|
88
|
+
expect(baseline.tests.failed).toBeGreaterThanOrEqual(1);
|
|
89
|
+
});
|
|
90
|
+
it('returns unknown commit SHA when git fails', () => {
|
|
91
|
+
mockExecSync.mockImplementationOnce(() => { throw new Error('not a git repo'); });
|
|
92
|
+
// JSON reporter
|
|
93
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ numTotalTests: 1, numPassedTests: 1, numFailedTests: 0 }));
|
|
94
|
+
// typecheck
|
|
95
|
+
mockExecSync.mockReturnValueOnce('');
|
|
96
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
97
|
+
expect(baseline.commitSha).toBe('unknown');
|
|
98
|
+
});
|
|
99
|
+
it('captures lint metrics when lintCommand is provided', () => {
|
|
100
|
+
mockExecSync.mockReturnValueOnce('abc123\n');
|
|
101
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ numTotalTests: 5, numPassedTests: 5, numFailedTests: 0 }));
|
|
102
|
+
mockExecSync.mockReturnValueOnce('');
|
|
103
|
+
// lint command output
|
|
104
|
+
mockExecSync.mockReturnValueOnce('\n✖ 10 problems (6 errors, 4 warnings)\n');
|
|
105
|
+
const baseline = captureQualityBaseline('/work', {
|
|
106
|
+
lintCommand: 'pnpm lint',
|
|
107
|
+
});
|
|
108
|
+
expect(baseline.lint.errorCount).toBe(6);
|
|
109
|
+
expect(baseline.lint.warningCount).toBe(4);
|
|
110
|
+
});
|
|
111
|
+
it('returns zero lint counts when no lintCommand is configured', () => {
|
|
112
|
+
mockExecSync.mockReturnValueOnce('abc123\n');
|
|
113
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ numTotalTests: 1, numPassedTests: 1, numFailedTests: 0 }));
|
|
114
|
+
mockExecSync.mockReturnValueOnce('');
|
|
115
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
116
|
+
expect(baseline.lint.errorCount).toBe(0);
|
|
117
|
+
expect(baseline.lint.warningCount).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
it('returns errorCount=1 when typecheck exits non-zero but has no parseable errors', () => {
|
|
120
|
+
mockExecSync.mockReturnValueOnce('abc123\n');
|
|
121
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ numTotalTests: 1, numPassedTests: 1, numFailedTests: 0 }));
|
|
122
|
+
const tscError = new Error('tsc failed');
|
|
123
|
+
tscError.stdout = '';
|
|
124
|
+
tscError.stderr = 'Some unparseable error output';
|
|
125
|
+
tscError.status = 1;
|
|
126
|
+
mockExecSync.mockImplementationOnce(() => { throw tscError; });
|
|
127
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
128
|
+
expect(baseline.typecheck.errorCount).toBe(1);
|
|
129
|
+
expect(baseline.typecheck.exitCode).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
it('uses custom packageManager for default commands', () => {
|
|
132
|
+
mockExecSync.mockReturnValueOnce('abc123\n');
|
|
133
|
+
// JSON reporter with npm
|
|
134
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ numTotalTests: 1, numPassedTests: 1, numFailedTests: 0 }));
|
|
135
|
+
// typecheck
|
|
136
|
+
mockExecSync.mockReturnValueOnce('');
|
|
137
|
+
captureQualityBaseline('/work', { packageManager: 'npm' });
|
|
138
|
+
// First call after git rev-parse should use npm for test command
|
|
139
|
+
const testCall = mockExecSync.mock.calls[1];
|
|
140
|
+
expect(testCall[0]).toContain('npm test');
|
|
141
|
+
});
|
|
142
|
+
it('captures lint errors from failing lint command', () => {
|
|
143
|
+
mockExecSync.mockReturnValueOnce('abc123\n');
|
|
144
|
+
mockExecSync.mockReturnValueOnce(JSON.stringify({ numTotalTests: 1, numPassedTests: 1, numFailedTests: 0 }));
|
|
145
|
+
mockExecSync.mockReturnValueOnce('');
|
|
146
|
+
// lint command fails
|
|
147
|
+
const lintError = new Error('lint failed');
|
|
148
|
+
lintError.stdout = '\n✖ 3 problems (3 errors, 0 warnings)\n';
|
|
149
|
+
lintError.stderr = '';
|
|
150
|
+
mockExecSync.mockImplementationOnce(() => { throw lintError; });
|
|
151
|
+
const baseline = captureQualityBaseline('/work', { lintCommand: 'pnpm lint' });
|
|
152
|
+
expect(baseline.lint.errorCount).toBe(3);
|
|
153
|
+
expect(baseline.lint.warningCount).toBe(0);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// computeQualityDelta
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
describe('computeQualityDelta', () => {
|
|
160
|
+
const makeBaseline = (overrides) => ({
|
|
161
|
+
timestamp: '2026-01-01T00:00:00Z',
|
|
162
|
+
commitSha: 'base',
|
|
163
|
+
tests: { total: 100, passed: 95, failed: 5, skipped: 0 },
|
|
164
|
+
typecheck: { errorCount: 3, exitCode: 0 },
|
|
165
|
+
lint: { errorCount: 2, warningCount: 10 },
|
|
166
|
+
...overrides,
|
|
167
|
+
});
|
|
168
|
+
it('passes when agent improves all metrics', () => {
|
|
169
|
+
const baseline = makeBaseline();
|
|
170
|
+
const current = makeBaseline({
|
|
171
|
+
tests: { total: 105, passed: 102, failed: 3, skipped: 0 },
|
|
172
|
+
typecheck: { errorCount: 1, exitCode: 0 },
|
|
173
|
+
lint: { errorCount: 0, warningCount: 5 },
|
|
174
|
+
});
|
|
175
|
+
const delta = computeQualityDelta(baseline, current);
|
|
176
|
+
expect(delta.passed).toBe(true);
|
|
177
|
+
expect(delta.testFailuresDelta).toBe(-2);
|
|
178
|
+
expect(delta.typeErrorsDelta).toBe(-2);
|
|
179
|
+
expect(delta.lintErrorsDelta).toBe(-2);
|
|
180
|
+
expect(delta.testCountDelta).toBe(5);
|
|
181
|
+
});
|
|
182
|
+
it('fails when agent introduces new test failures', () => {
|
|
183
|
+
const baseline = makeBaseline();
|
|
184
|
+
const current = makeBaseline({
|
|
185
|
+
tests: { total: 100, passed: 92, failed: 8, skipped: 0 },
|
|
186
|
+
});
|
|
187
|
+
const delta = computeQualityDelta(baseline, current);
|
|
188
|
+
expect(delta.passed).toBe(false);
|
|
189
|
+
expect(delta.testFailuresDelta).toBe(3);
|
|
190
|
+
});
|
|
191
|
+
it('fails when agent introduces new typecheck errors', () => {
|
|
192
|
+
const baseline = makeBaseline();
|
|
193
|
+
const current = makeBaseline({
|
|
194
|
+
typecheck: { errorCount: 5, exitCode: 1 },
|
|
195
|
+
});
|
|
196
|
+
const delta = computeQualityDelta(baseline, current);
|
|
197
|
+
expect(delta.passed).toBe(false);
|
|
198
|
+
expect(delta.typeErrorsDelta).toBe(2);
|
|
199
|
+
});
|
|
200
|
+
it('fails when agent introduces new lint errors', () => {
|
|
201
|
+
const baseline = makeBaseline();
|
|
202
|
+
const current = makeBaseline({
|
|
203
|
+
lint: { errorCount: 4, warningCount: 10 },
|
|
204
|
+
});
|
|
205
|
+
const delta = computeQualityDelta(baseline, current);
|
|
206
|
+
expect(delta.passed).toBe(false);
|
|
207
|
+
expect(delta.lintErrorsDelta).toBe(2);
|
|
208
|
+
});
|
|
209
|
+
it('passes when baseline and current are identical', () => {
|
|
210
|
+
const baseline = makeBaseline();
|
|
211
|
+
const current = makeBaseline();
|
|
212
|
+
const delta = computeQualityDelta(baseline, current);
|
|
213
|
+
expect(delta.passed).toBe(true);
|
|
214
|
+
expect(delta.testFailuresDelta).toBe(0);
|
|
215
|
+
expect(delta.typeErrorsDelta).toBe(0);
|
|
216
|
+
expect(delta.lintErrorsDelta).toBe(0);
|
|
217
|
+
expect(delta.testCountDelta).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
it('tracks test removal as negative testCountDelta', () => {
|
|
220
|
+
const baseline = makeBaseline();
|
|
221
|
+
const current = makeBaseline({
|
|
222
|
+
tests: { total: 90, passed: 90, failed: 0, skipped: 0 },
|
|
223
|
+
});
|
|
224
|
+
const delta = computeQualityDelta(baseline, current);
|
|
225
|
+
expect(delta.passed).toBe(true); // fewer failures is good
|
|
226
|
+
expect(delta.testCountDelta).toBe(-10); // but removing tests is a warning
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// formatQualityReport
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
describe('formatQualityReport', () => {
|
|
233
|
+
const makeBaseline = () => ({
|
|
234
|
+
timestamp: '2026-01-01T00:00:00Z',
|
|
235
|
+
commitSha: 'base',
|
|
236
|
+
tests: { total: 100, passed: 95, failed: 5, skipped: 0 },
|
|
237
|
+
typecheck: { errorCount: 3, exitCode: 0 },
|
|
238
|
+
lint: { errorCount: 2, warningCount: 10 },
|
|
239
|
+
});
|
|
240
|
+
it('formats a passing report', () => {
|
|
241
|
+
const baseline = makeBaseline();
|
|
242
|
+
const current = { ...makeBaseline(), tests: { total: 100, passed: 98, failed: 2, skipped: 0 } };
|
|
243
|
+
const delta = computeQualityDelta(baseline, current);
|
|
244
|
+
const report = formatQualityReport(baseline, current, delta);
|
|
245
|
+
expect(report).toContain('**PASSED**');
|
|
246
|
+
expect(report).toContain('Test failures');
|
|
247
|
+
expect(report).toContain('Typecheck errors');
|
|
248
|
+
});
|
|
249
|
+
it('formats a failing report', () => {
|
|
250
|
+
const baseline = makeBaseline();
|
|
251
|
+
const current = { ...makeBaseline(), tests: { total: 100, passed: 90, failed: 10, skipped: 0 } };
|
|
252
|
+
const delta = computeQualityDelta(baseline, current);
|
|
253
|
+
const report = formatQualityReport(baseline, current, delta);
|
|
254
|
+
expect(report).toContain('**FAILED**');
|
|
255
|
+
expect(report).toContain('+5');
|
|
256
|
+
});
|
|
257
|
+
it('warns about removed tests', () => {
|
|
258
|
+
const baseline = makeBaseline();
|
|
259
|
+
const current = { ...makeBaseline(), tests: { total: 80, passed: 80, failed: 0, skipped: 0 } };
|
|
260
|
+
const delta = computeQualityDelta(baseline, current);
|
|
261
|
+
const report = formatQualityReport(baseline, current, delta);
|
|
262
|
+
expect(report).toContain('20 test(s) were removed');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// parseVitestJson
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
describe('parseVitestJson', () => {
|
|
269
|
+
it('parses standard vitest JSON output', () => {
|
|
270
|
+
const json = JSON.stringify({
|
|
271
|
+
numTotalTests: 50,
|
|
272
|
+
numPassedTests: 48,
|
|
273
|
+
numFailedTests: 2,
|
|
274
|
+
});
|
|
275
|
+
const result = parseVitestJson(json);
|
|
276
|
+
expect(result).toEqual({ total: 50, passed: 48, failed: 2, skipped: 0 });
|
|
277
|
+
});
|
|
278
|
+
it('handles JSON with non-JSON prefix', () => {
|
|
279
|
+
const output = 'Some vitest output\n' + JSON.stringify({
|
|
280
|
+
numTotalTests: 10,
|
|
281
|
+
numPassedTests: 10,
|
|
282
|
+
numFailedTests: 0,
|
|
283
|
+
});
|
|
284
|
+
const result = parseVitestJson(output);
|
|
285
|
+
expect(result?.total).toBe(10);
|
|
286
|
+
});
|
|
287
|
+
it('returns null for non-JSON output', () => {
|
|
288
|
+
expect(parseVitestJson('not json at all')).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
it('returns null for empty string', () => {
|
|
291
|
+
expect(parseVitestJson('')).toBeNull();
|
|
292
|
+
});
|
|
293
|
+
it('parses vitest v2+ format with testResults array', () => {
|
|
294
|
+
const output = JSON.stringify({
|
|
295
|
+
testResults: [
|
|
296
|
+
{
|
|
297
|
+
assertionResults: [
|
|
298
|
+
{ status: 'passed' },
|
|
299
|
+
{ status: 'passed' },
|
|
300
|
+
{ status: 'failed' },
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
assertionResults: [
|
|
305
|
+
{ status: 'passed' },
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
const result = parseVitestJson(output);
|
|
311
|
+
expect(result).toEqual({ total: 4, passed: 3, failed: 1, skipped: 0 });
|
|
312
|
+
});
|
|
313
|
+
it('returns null for JSON without recognized fields', () => {
|
|
314
|
+
const output = JSON.stringify({ unrelated: 'data' });
|
|
315
|
+
expect(parseVitestJson(output)).toBeNull();
|
|
316
|
+
});
|
|
317
|
+
it('computes skipped count correctly', () => {
|
|
318
|
+
const json = JSON.stringify({
|
|
319
|
+
numTotalTests: 20,
|
|
320
|
+
numPassedTests: 15,
|
|
321
|
+
numFailedTests: 2,
|
|
322
|
+
});
|
|
323
|
+
const result = parseVitestJson(json);
|
|
324
|
+
expect(result?.skipped).toBe(3); // 20 - 15 - 2
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// countTypescriptErrors
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
describe('countTypescriptErrors', () => {
|
|
331
|
+
it('counts TypeScript errors in tsc output', () => {
|
|
332
|
+
const output = [
|
|
333
|
+
'src/a.ts(1,1): error TS2304: Cannot find name',
|
|
334
|
+
'src/b.ts(5,3): error TS2345: Argument of type',
|
|
335
|
+
'src/c.ts(10,1): error TS2322: Type is not assignable',
|
|
336
|
+
].join('\n');
|
|
337
|
+
expect(countTypescriptErrors(output)).toBe(3);
|
|
338
|
+
});
|
|
339
|
+
it('returns 0 for clean output', () => {
|
|
340
|
+
expect(countTypescriptErrors('')).toBe(0);
|
|
341
|
+
expect(countTypescriptErrors('All good')).toBe(0);
|
|
342
|
+
});
|
|
343
|
+
it('handles mixed output with errors and warnings', () => {
|
|
344
|
+
const output = [
|
|
345
|
+
'warning TS6059: File not under rootDir',
|
|
346
|
+
'src/a.ts(1,1): error TS2304: Cannot find name',
|
|
347
|
+
'Found 1 error.',
|
|
348
|
+
].join('\n');
|
|
349
|
+
expect(countTypescriptErrors(output)).toBe(1); // only counts error TS, not warnings
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Text output parsing (tested via captureQualityBaseline fallback)
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
describe('test text output parsing', () => {
|
|
356
|
+
it('parses vitest text with skipped tests', () => {
|
|
357
|
+
mockExecSync.mockReturnValueOnce('abc\n');
|
|
358
|
+
mockExecSync.mockImplementationOnce(() => { throw new Error('no json'); });
|
|
359
|
+
mockExecSync.mockReturnValueOnce('Tests 10 passed | 2 failed | 1 skipped | 13 total');
|
|
360
|
+
mockExecSync.mockReturnValueOnce('');
|
|
361
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
362
|
+
expect(baseline.tests.total).toBe(13);
|
|
363
|
+
expect(baseline.tests.passed).toBe(10);
|
|
364
|
+
expect(baseline.tests.failed).toBe(2);
|
|
365
|
+
expect(baseline.tests.skipped).toBe(1);
|
|
366
|
+
});
|
|
367
|
+
it('parses jest text format', () => {
|
|
368
|
+
mockExecSync.mockReturnValueOnce('abc\n');
|
|
369
|
+
mockExecSync.mockImplementationOnce(() => { throw new Error('no json'); });
|
|
370
|
+
mockExecSync.mockReturnValueOnce('Tests: 2 failed, 42 passed, 44 total');
|
|
371
|
+
mockExecSync.mockReturnValueOnce('');
|
|
372
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
373
|
+
expect(baseline.tests.total).toBe(44);
|
|
374
|
+
expect(baseline.tests.passed).toBe(42);
|
|
375
|
+
expect(baseline.tests.failed).toBe(2);
|
|
376
|
+
});
|
|
377
|
+
it('parses jest text format with no failures', () => {
|
|
378
|
+
mockExecSync.mockReturnValueOnce('abc\n');
|
|
379
|
+
mockExecSync.mockImplementationOnce(() => { throw new Error('no json'); });
|
|
380
|
+
mockExecSync.mockReturnValueOnce('Tests: 42 passed, 42 total');
|
|
381
|
+
mockExecSync.mockReturnValueOnce('');
|
|
382
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
383
|
+
expect(baseline.tests.total).toBe(42);
|
|
384
|
+
expect(baseline.tests.passed).toBe(42);
|
|
385
|
+
expect(baseline.tests.failed).toBe(0);
|
|
386
|
+
});
|
|
387
|
+
it('parses vitest compact format "Tests 42 passed (44)"', () => {
|
|
388
|
+
mockExecSync.mockReturnValueOnce('abc\n');
|
|
389
|
+
mockExecSync.mockImplementationOnce(() => { throw new Error('no json'); });
|
|
390
|
+
mockExecSync.mockReturnValueOnce('Tests 42 passed (44)');
|
|
391
|
+
mockExecSync.mockReturnValueOnce('');
|
|
392
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
393
|
+
expect(baseline.tests.total).toBe(44);
|
|
394
|
+
expect(baseline.tests.passed).toBe(42);
|
|
395
|
+
});
|
|
396
|
+
it('parses test counts from error output when command fails', () => {
|
|
397
|
+
mockExecSync.mockReturnValueOnce('abc\n');
|
|
398
|
+
mockExecSync.mockImplementationOnce(() => { throw new Error('no json'); });
|
|
399
|
+
const testError = new Error('tests failed');
|
|
400
|
+
testError.stdout = 'Tests 8 passed | 2 failed | 10 total';
|
|
401
|
+
testError.stderr = '';
|
|
402
|
+
mockExecSync.mockImplementationOnce(() => { throw testError; });
|
|
403
|
+
mockExecSync.mockReturnValueOnce('');
|
|
404
|
+
const baseline = captureQualityBaseline('/work', {});
|
|
405
|
+
expect(baseline.tests.total).toBe(10);
|
|
406
|
+
expect(baseline.tests.failed).toBe(2);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// saveBaseline / loadBaseline
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
describe('saveBaseline', () => {
|
|
413
|
+
it('writes baseline JSON to .agent/ directory', () => {
|
|
414
|
+
const baseline = {
|
|
415
|
+
timestamp: '2026-01-01T00:00:00Z',
|
|
416
|
+
commitSha: 'abc',
|
|
417
|
+
tests: { total: 10, passed: 10, failed: 0, skipped: 0 },
|
|
418
|
+
typecheck: { errorCount: 0, exitCode: 0 },
|
|
419
|
+
lint: { errorCount: 0, warningCount: 0 },
|
|
420
|
+
};
|
|
421
|
+
saveBaseline('/work', baseline);
|
|
422
|
+
expect(mockWriteFileSync).toHaveBeenCalledWith(expect.stringContaining('quality-baseline.json'), expect.stringContaining('"commitSha": "abc"'));
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
describe('loadBaseline', () => {
|
|
426
|
+
it('loads baseline from .agent/ directory', () => {
|
|
427
|
+
const baseline = {
|
|
428
|
+
timestamp: '2026-01-01T00:00:00Z',
|
|
429
|
+
commitSha: 'abc',
|
|
430
|
+
tests: { total: 10, passed: 10, failed: 0, skipped: 0 },
|
|
431
|
+
typecheck: { errorCount: 0, exitCode: 0 },
|
|
432
|
+
lint: { errorCount: 0, warningCount: 0 },
|
|
433
|
+
};
|
|
434
|
+
mockExistsSync.mockReturnValue(true);
|
|
435
|
+
mockReadFileSync.mockReturnValue(JSON.stringify(baseline));
|
|
436
|
+
const loaded = loadBaseline('/work');
|
|
437
|
+
expect(loaded).toEqual(baseline);
|
|
438
|
+
});
|
|
439
|
+
it('returns null when no baseline exists', () => {
|
|
440
|
+
mockExistsSync.mockReturnValue(false);
|
|
441
|
+
expect(loadBaseline('/work')).toBeNull();
|
|
442
|
+
});
|
|
443
|
+
it('returns null on parse error', () => {
|
|
444
|
+
mockExistsSync.mockReturnValue(true);
|
|
445
|
+
mockReadFileSync.mockReturnValue('not json');
|
|
446
|
+
expect(loadBaseline('/work')).toBeNull();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Ratchet — Monotonic Quality Thresholds
|
|
3
|
+
*
|
|
4
|
+
* A quality ratchet is a committed JSON file that stores the best-known quality
|
|
5
|
+
* thresholds for the repository. Thresholds can only tighten (improve), never
|
|
6
|
+
* loosen. This prevents cumulative quality drift across many agent sessions.
|
|
7
|
+
*
|
|
8
|
+
* File location: .agentfactory/quality-ratchet.json (committed to repo)
|
|
9
|
+
*
|
|
10
|
+
* The ratchet is enforced at two points:
|
|
11
|
+
* 1. Merge queue — blocks merge if ratchet thresholds are violated
|
|
12
|
+
* 2. CI — runs as a required status check on every PR
|
|
13
|
+
*/
|
|
14
|
+
import type { QualityBaseline } from './quality-baseline.js';
|
|
15
|
+
export interface QualityRatchet {
|
|
16
|
+
version: 1;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
updatedBy: string;
|
|
19
|
+
thresholds: {
|
|
20
|
+
testCount: {
|
|
21
|
+
min: number;
|
|
22
|
+
};
|
|
23
|
+
testFailures: {
|
|
24
|
+
max: number;
|
|
25
|
+
};
|
|
26
|
+
typecheckErrors: {
|
|
27
|
+
max: number;
|
|
28
|
+
};
|
|
29
|
+
lintErrors: {
|
|
30
|
+
max: number;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface RatchetCheckResult {
|
|
35
|
+
passed: boolean;
|
|
36
|
+
violations: Array<{
|
|
37
|
+
metric: string;
|
|
38
|
+
threshold: number;
|
|
39
|
+
actual: number;
|
|
40
|
+
direction: 'above-max' | 'below-min';
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Load the quality ratchet from disk.
|
|
45
|
+
* Returns null if the ratchet file does not exist.
|
|
46
|
+
* Throws if the file exists but is invalid.
|
|
47
|
+
*/
|
|
48
|
+
export declare function loadQualityRatchet(repoRoot: string): QualityRatchet | null;
|
|
49
|
+
/**
|
|
50
|
+
* Check current quality metrics against ratchet thresholds.
|
|
51
|
+
* Returns a result with pass/fail and any violations.
|
|
52
|
+
*/
|
|
53
|
+
export declare function checkQualityRatchet(ratchet: QualityRatchet, current: QualityBaseline): RatchetCheckResult;
|
|
54
|
+
/**
|
|
55
|
+
* Tighten the quality ratchet if current metrics are better than thresholds.
|
|
56
|
+
* The ratchet only moves in the direction of improvement (monotonic).
|
|
57
|
+
*
|
|
58
|
+
* Returns true if the ratchet was updated, false if no improvement was found.
|
|
59
|
+
*/
|
|
60
|
+
export declare function updateQualityRatchet(repoRoot: string, current: QualityBaseline, identifier: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Initialize a new quality ratchet file from a baseline snapshot.
|
|
63
|
+
* Use this when setting up quality gates for the first time.
|
|
64
|
+
*/
|
|
65
|
+
export declare function initializeQualityRatchet(repoRoot: string, baseline: QualityBaseline): QualityRatchet;
|
|
66
|
+
/**
|
|
67
|
+
* Format a ratchet check result into a human-readable string.
|
|
68
|
+
*/
|
|
69
|
+
export declare function formatRatchetResult(result: RatchetCheckResult): string;
|
|
70
|
+
//# sourceMappingURL=quality-ratchet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quality-ratchet.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/quality-ratchet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAM5D,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,CAAC,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE;QACV,SAAS,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QAC1B,YAAY,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QAC7B,eAAe,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QAChC,UAAU,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KAC5B,CAAA;CACF;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAA;IACf,UAAU,EAAE,KAAK,CAAC;QAChB,MAAM,EAAE,MAAM,CAAA;QACd,SAAS,EAAE,MAAM,CAAA;QACjB,MAAM,EAAE,MAAM,CAAA;QACd,SAAS,EAAE,WAAW,GAAG,WAAW,CAAA;KACrC,CAAC,CAAA;CACH;AAiBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAa1E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,eAAe,GACvB,kBAAkB,CA6CpB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,GACjB,OAAO,CAsCT;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,eAAe,GACxB,cAAc,CAehB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAYtE"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Ratchet — Monotonic Quality Thresholds
|
|
3
|
+
*
|
|
4
|
+
* A quality ratchet is a committed JSON file that stores the best-known quality
|
|
5
|
+
* thresholds for the repository. Thresholds can only tighten (improve), never
|
|
6
|
+
* loosen. This prevents cumulative quality drift across many agent sessions.
|
|
7
|
+
*
|
|
8
|
+
* File location: .agentfactory/quality-ratchet.json (committed to repo)
|
|
9
|
+
*
|
|
10
|
+
* The ratchet is enforced at two points:
|
|
11
|
+
* 1. Merge queue — blocks merge if ratchet thresholds are violated
|
|
12
|
+
* 2. CI — runs as a required status check on every PR
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { resolve } from 'node:path';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// File path
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const RATCHET_FILENAME = 'quality-ratchet.json';
|
|
20
|
+
const RATCHET_DIR = '.agentfactory';
|
|
21
|
+
function ratchetPath(repoRoot) {
|
|
22
|
+
return resolve(repoRoot, RATCHET_DIR, RATCHET_FILENAME);
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Public API
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Load the quality ratchet from disk.
|
|
29
|
+
* Returns null if the ratchet file does not exist.
|
|
30
|
+
* Throws if the file exists but is invalid.
|
|
31
|
+
*/
|
|
32
|
+
export function loadQualityRatchet(repoRoot) {
|
|
33
|
+
const filePath = ratchetPath(repoRoot);
|
|
34
|
+
if (!existsSync(filePath))
|
|
35
|
+
return null;
|
|
36
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
37
|
+
const parsed = JSON.parse(content);
|
|
38
|
+
// Basic validation
|
|
39
|
+
if (parsed.version !== 1 || !parsed.thresholds) {
|
|
40
|
+
throw new Error(`Invalid quality ratchet file: missing version or thresholds`);
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check current quality metrics against ratchet thresholds.
|
|
46
|
+
* Returns a result with pass/fail and any violations.
|
|
47
|
+
*/
|
|
48
|
+
export function checkQualityRatchet(ratchet, current) {
|
|
49
|
+
const violations = [];
|
|
50
|
+
const { thresholds } = ratchet;
|
|
51
|
+
if (current.tests.total < thresholds.testCount.min) {
|
|
52
|
+
violations.push({
|
|
53
|
+
metric: 'testCount',
|
|
54
|
+
threshold: thresholds.testCount.min,
|
|
55
|
+
actual: current.tests.total,
|
|
56
|
+
direction: 'below-min',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (current.tests.failed > thresholds.testFailures.max) {
|
|
60
|
+
violations.push({
|
|
61
|
+
metric: 'testFailures',
|
|
62
|
+
threshold: thresholds.testFailures.max,
|
|
63
|
+
actual: current.tests.failed,
|
|
64
|
+
direction: 'above-max',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (current.typecheck.errorCount > thresholds.typecheckErrors.max) {
|
|
68
|
+
violations.push({
|
|
69
|
+
metric: 'typecheckErrors',
|
|
70
|
+
threshold: thresholds.typecheckErrors.max,
|
|
71
|
+
actual: current.typecheck.errorCount,
|
|
72
|
+
direction: 'above-max',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (current.lint.errorCount > thresholds.lintErrors.max) {
|
|
76
|
+
violations.push({
|
|
77
|
+
metric: 'lintErrors',
|
|
78
|
+
threshold: thresholds.lintErrors.max,
|
|
79
|
+
actual: current.lint.errorCount,
|
|
80
|
+
direction: 'above-max',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
passed: violations.length === 0,
|
|
85
|
+
violations,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Tighten the quality ratchet if current metrics are better than thresholds.
|
|
90
|
+
* The ratchet only moves in the direction of improvement (monotonic).
|
|
91
|
+
*
|
|
92
|
+
* Returns true if the ratchet was updated, false if no improvement was found.
|
|
93
|
+
*/
|
|
94
|
+
export function updateQualityRatchet(repoRoot, current, identifier) {
|
|
95
|
+
const existing = loadQualityRatchet(repoRoot);
|
|
96
|
+
if (!existing)
|
|
97
|
+
return false;
|
|
98
|
+
const updated = { ...existing, thresholds: { ...existing.thresholds } };
|
|
99
|
+
let changed = false;
|
|
100
|
+
// Test count: min can only go up
|
|
101
|
+
if (current.tests.total > existing.thresholds.testCount.min) {
|
|
102
|
+
updated.thresholds.testCount = { min: current.tests.total };
|
|
103
|
+
changed = true;
|
|
104
|
+
}
|
|
105
|
+
// Test failures: max can only go down
|
|
106
|
+
if (current.tests.failed < existing.thresholds.testFailures.max) {
|
|
107
|
+
updated.thresholds.testFailures = { max: current.tests.failed };
|
|
108
|
+
changed = true;
|
|
109
|
+
}
|
|
110
|
+
// Typecheck errors: max can only go down
|
|
111
|
+
if (current.typecheck.errorCount < existing.thresholds.typecheckErrors.max) {
|
|
112
|
+
updated.thresholds.typecheckErrors = { max: current.typecheck.errorCount };
|
|
113
|
+
changed = true;
|
|
114
|
+
}
|
|
115
|
+
// Lint errors: max can only go down
|
|
116
|
+
if (current.lint.errorCount < existing.thresholds.lintErrors.max) {
|
|
117
|
+
updated.thresholds.lintErrors = { max: current.lint.errorCount };
|
|
118
|
+
changed = true;
|
|
119
|
+
}
|
|
120
|
+
if (changed) {
|
|
121
|
+
updated.updatedAt = new Date().toISOString();
|
|
122
|
+
updated.updatedBy = identifier;
|
|
123
|
+
writeFileSync(ratchetPath(repoRoot), JSON.stringify(updated, null, 2) + '\n');
|
|
124
|
+
}
|
|
125
|
+
return changed;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Initialize a new quality ratchet file from a baseline snapshot.
|
|
129
|
+
* Use this when setting up quality gates for the first time.
|
|
130
|
+
*/
|
|
131
|
+
export function initializeQualityRatchet(repoRoot, baseline) {
|
|
132
|
+
const ratchet = {
|
|
133
|
+
version: 1,
|
|
134
|
+
updatedAt: new Date().toISOString(),
|
|
135
|
+
updatedBy: 'manual',
|
|
136
|
+
thresholds: {
|
|
137
|
+
testCount: { min: baseline.tests.total },
|
|
138
|
+
testFailures: { max: baseline.tests.failed },
|
|
139
|
+
typecheckErrors: { max: baseline.typecheck.errorCount },
|
|
140
|
+
lintErrors: { max: baseline.lint.errorCount },
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
writeFileSync(ratchetPath(repoRoot), JSON.stringify(ratchet, null, 2) + '\n');
|
|
144
|
+
return ratchet;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Format a ratchet check result into a human-readable string.
|
|
148
|
+
*/
|
|
149
|
+
export function formatRatchetResult(result) {
|
|
150
|
+
if (result.passed)
|
|
151
|
+
return 'Quality ratchet check passed.';
|
|
152
|
+
const lines = ['Quality ratchet check **FAILED**:', ''];
|
|
153
|
+
for (const v of result.violations) {
|
|
154
|
+
if (v.direction === 'above-max') {
|
|
155
|
+
lines.push(`- ${v.metric}: ${v.actual} exceeds maximum threshold of ${v.threshold}`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
lines.push(`- ${v.metric}: ${v.actual} is below minimum threshold of ${v.threshold}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|