rubrkit 0.1.1 → 0.3.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/README.md +417 -404
- package/package.json +28 -28
- package/src/api.js +108 -101
- package/src/args.js +209 -175
- package/src/cli.js +97 -93
- package/src/config.js +186 -169
- package/src/formats.js +239 -222
- package/src/pull.js +682 -676
- package/src/sdk.js +451 -443
- package/src/testingCli.js +504 -431
package/src/formats.js
CHANGED
|
@@ -1,222 +1,239 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @param {{ payload: Record<string, any>, format?: string, output?: string | null, stdout: NodeJS.WritableStream, fsImpl?: typeof fs }} params
|
|
7
|
-
*/
|
|
8
|
-
export async function writeFormattedResult({ payload, format = 'text', output = null, stdout, fsImpl = fs }) {
|
|
9
|
-
const rendered = renderResult(payload, format);
|
|
10
|
-
|
|
11
|
-
if (output) {
|
|
12
|
-
await fsImpl.writeFile(output, rendered, 'utf8');
|
|
13
|
-
if (format !== 'json') {
|
|
14
|
-
stdout.write(`Wrote Rubrkit ${format} report to ${output}\n`);
|
|
15
|
-
}
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
stdout.write(rendered);
|
|
20
|
-
if (!rendered.endsWith('\n')) {
|
|
21
|
-
stdout.write('\n');
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param {Record<string, any>} payload
|
|
27
|
-
* @param {string} format
|
|
28
|
-
*/
|
|
29
|
-
export function renderResult(payload, format = 'text') {
|
|
30
|
-
if (format === 'json') {
|
|
31
|
-
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (format === 'junit') {
|
|
35
|
-
return renderJUnit(payload);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return renderText(payload);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @param {Record<string, any>} payload
|
|
43
|
-
*/
|
|
44
|
-
function renderText(payload) {
|
|
45
|
-
if (payload.mode === 'remote-dry-run') {
|
|
46
|
-
return [
|
|
47
|
-
`Rubrkit ${payload.command} remote dry run`,
|
|
48
|
-
`Target: ${payload.target ?? '(none)'}`,
|
|
49
|
-
`Endpoint: ${payload.endpoint}`,
|
|
50
|
-
`Method: ${payload.method}`,
|
|
51
|
-
`Polls job: ${payload.pollsJob ? 'yes' : 'no'}`,
|
|
52
|
-
payload.requiresApiKey ? 'Requires API key: yes' : 'Requires API key: no',
|
|
53
|
-
].join('\n') + '\n';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (payload.mode === 'remote') {
|
|
57
|
-
const job = payload.job ?? payload.data?.job ?? payload.data ?? {};
|
|
58
|
-
const state = job.state ?? 'unknown';
|
|
59
|
-
const score = scoreFromPayload(payload);
|
|
60
|
-
const lines = [
|
|
61
|
-
`Rubrkit ${payload.command} remote ${state}`,
|
|
62
|
-
`Job: ${job.id ?? payload.jobId ?? '(unknown)'}`,
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
lines.push(
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
lines.push(`
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
lines.push(`
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
`${
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {{ payload: Record<string, any>, format?: string, output?: string | null, stdout: NodeJS.WritableStream, fsImpl?: typeof fs }} params
|
|
7
|
+
*/
|
|
8
|
+
export async function writeFormattedResult({ payload, format = 'text', output = null, stdout, fsImpl = fs }) {
|
|
9
|
+
const rendered = renderResult(payload, format);
|
|
10
|
+
|
|
11
|
+
if (output) {
|
|
12
|
+
await fsImpl.writeFile(output, rendered, 'utf8');
|
|
13
|
+
if (format !== 'json') {
|
|
14
|
+
stdout.write(`Wrote Rubrkit ${format} report to ${output}\n`);
|
|
15
|
+
}
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
stdout.write(rendered);
|
|
20
|
+
if (!rendered.endsWith('\n')) {
|
|
21
|
+
stdout.write('\n');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Record<string, any>} payload
|
|
27
|
+
* @param {string} format
|
|
28
|
+
*/
|
|
29
|
+
export function renderResult(payload, format = 'text') {
|
|
30
|
+
if (format === 'json') {
|
|
31
|
+
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (format === 'junit') {
|
|
35
|
+
return renderJUnit(payload);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return renderText(payload);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {Record<string, any>} payload
|
|
43
|
+
*/
|
|
44
|
+
function renderText(payload) {
|
|
45
|
+
if (payload.mode === 'remote-dry-run') {
|
|
46
|
+
return [
|
|
47
|
+
`Rubrkit ${payload.command} remote dry run`,
|
|
48
|
+
`Target: ${payload.target ?? '(none)'}`,
|
|
49
|
+
`Endpoint: ${payload.endpoint}`,
|
|
50
|
+
`Method: ${payload.method}`,
|
|
51
|
+
`Polls job: ${payload.pollsJob ? 'yes' : 'no'}`,
|
|
52
|
+
payload.requiresApiKey ? 'Requires API key: yes' : 'Requires API key: no',
|
|
53
|
+
].join('\n') + '\n';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (payload.mode === 'remote') {
|
|
57
|
+
const job = payload.job ?? payload.data?.job ?? payload.data ?? {};
|
|
58
|
+
const state = job.state ?? 'unknown';
|
|
59
|
+
const score = scoreFromPayload(payload);
|
|
60
|
+
const lines = [
|
|
61
|
+
`Rubrkit ${payload.command} remote ${state}`,
|
|
62
|
+
`Job: ${job.id ?? payload.jobId ?? '(unknown)'}`,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (payload.cached) {
|
|
66
|
+
lines.push('Cached: yes (deterministic result reused; no credits charged)');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (job.phase || job.message) {
|
|
70
|
+
lines.push(`Latest: ${[job.phase, job.message].filter(Boolean).join(' - ')}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof score === 'number') {
|
|
74
|
+
lines.push(`Score: ${score}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (job.error?.message) {
|
|
78
|
+
lines.push(`Error: ${job.error.code ?? 'job_failed'} - ${job.error.message}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return `${lines.join('\n')}\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (payload.mode === 'remote-apply') {
|
|
85
|
+
const files = Array.isArray(payload.files) ? payload.files : [];
|
|
86
|
+
const count = typeof payload.appliedCount === 'number' ? payload.appliedCount : files.length;
|
|
87
|
+
const lines = [`Applied AI rewrite to ${count} file(s)`];
|
|
88
|
+
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const version = file.versionNumber != null ? ` (v${file.versionNumber})` : '';
|
|
91
|
+
lines.push(`- ${file.path ?? file.fileId ?? '(unknown)'}${version}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return `${lines.join('\n')}\n`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (payload.mode === 'report') {
|
|
98
|
+
const job = payload.job ?? payload.data?.job ?? payload;
|
|
99
|
+
return [
|
|
100
|
+
`Rubrkit report ${job.state ?? 'unknown'}`,
|
|
101
|
+
`Job: ${job.id ?? payload.id ?? '(unknown)'}`,
|
|
102
|
+
`Kind: ${job.kind ?? '(unknown)'}`,
|
|
103
|
+
`Latest: ${[job.phase, job.message].filter(Boolean).join(' - ') || '(none)'}`,
|
|
104
|
+
].join('\n') + '\n';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const summary = payload.summary ?? {};
|
|
108
|
+
const status = payload.passed ? 'PASS' : 'FAIL';
|
|
109
|
+
const lines = [
|
|
110
|
+
`Rubrkit ${payload.command ?? 'validate'} ${payload.mode ?? 'local'}`,
|
|
111
|
+
`${status} ${payload.target ?? ''} (${summary.fileCount ?? 0} files, ${summary.errorCount ?? 0} errors, ${summary.warningCount ?? 0} warnings, score ${summary.score ?? 0})`,
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (const file of payload.files ?? []) {
|
|
115
|
+
lines.push(`- ${file.passed ? 'PASS' : 'FAIL'} ${file.path}`);
|
|
116
|
+
|
|
117
|
+
for (const issue of file.issues ?? []) {
|
|
118
|
+
const location = issue.line ? ` line ${issue.line}${issue.column ? `:${issue.column}` : ''}` : '';
|
|
119
|
+
lines.push(` ${issue.severity.toUpperCase()} ${issue.code}${location}: ${issue.message}`);
|
|
120
|
+
lines.push(` fix: ${issue.fix}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const gate of payload.gates?.failures ?? []) {
|
|
125
|
+
lines.push(`Gate ${gate.code}: ${gate.message}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `${lines.join('\n')}\n`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {Record<string, any>} payload
|
|
133
|
+
*/
|
|
134
|
+
function renderJUnit(payload) {
|
|
135
|
+
const files = Array.isArray(payload.files) ? payload.files : [];
|
|
136
|
+
const tests = files.length || 1;
|
|
137
|
+
const failures = files.reduce((sum, file) => sum + Number(file.errorCount ?? 0), 0);
|
|
138
|
+
const warnings = files.reduce((sum, file) => sum + Number(file.warningCount ?? 0), 0);
|
|
139
|
+
const name = `rubrkit.${payload.command ?? 'test'}.${payload.mode ?? 'local'}`;
|
|
140
|
+
const testcases = files.length > 0 ? files.map(renderFileTestCase).join('\n') : renderRemoteTestCase(payload);
|
|
141
|
+
|
|
142
|
+
return [
|
|
143
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
144
|
+
`<testsuite name="${xml(name)}" tests="${tests}" failures="${failures}" errors="0" skipped="0">`,
|
|
145
|
+
warnings > 0 ? ` <system-out>${xml(`${warnings} Rubrkit warning${warnings === 1 ? '' : 's'}`)}</system-out>` : '',
|
|
146
|
+
testcases,
|
|
147
|
+
'</testsuite>',
|
|
148
|
+
'',
|
|
149
|
+
].filter(line => line !== '').join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {Record<string, any>} file
|
|
154
|
+
*/
|
|
155
|
+
function renderFileTestCase(file) {
|
|
156
|
+
const failures = (file.issues ?? []).filter(issue => issue.severity === 'error');
|
|
157
|
+
const warnings = (file.issues ?? []).filter(issue => issue.severity === 'warning');
|
|
158
|
+
const body = [];
|
|
159
|
+
|
|
160
|
+
if (failures.length > 0) {
|
|
161
|
+
body.push(
|
|
162
|
+
` <failure message="${xml(`${failures.length} Rubrkit validation error${failures.length === 1 ? '' : 's'}`)}">${xml(
|
|
163
|
+
failures.map(formatIssue).join('\n'),
|
|
164
|
+
)}</failure>`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (warnings.length > 0) {
|
|
169
|
+
body.push(` <system-out>${xml(warnings.map(formatIssue).join('\n'))}</system-out>`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (body.length === 0) {
|
|
173
|
+
return ` <testcase name="${xml(file.path)}" classname="RubrkitLocalChecks" />`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [` <testcase name="${xml(file.path)}" classname="RubrkitLocalChecks">`, ...body, ' </testcase>'].join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {Record<string, any>} payload
|
|
181
|
+
*/
|
|
182
|
+
function renderRemoteTestCase(payload) {
|
|
183
|
+
const job = payload.job ?? {};
|
|
184
|
+
const failed = ['failed', 'cancelled', 'paused'].includes(job.state);
|
|
185
|
+
|
|
186
|
+
if (!failed) {
|
|
187
|
+
return ` <testcase name="${xml(job.id ?? payload.jobId ?? 'remote-job')}" classname="RubrkitRemoteJob" />`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return [
|
|
191
|
+
` <testcase name="${xml(job.id ?? payload.jobId ?? 'remote-job')}" classname="RubrkitRemoteJob">`,
|
|
192
|
+
` <failure message="${xml(job.error?.message ?? 'Remote Rubrkit job did not succeed.')}">${xml(JSON.stringify(job.error ?? {}, null, 2))}</failure>`,
|
|
193
|
+
' </testcase>',
|
|
194
|
+
].join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {Record<string, any>} issue
|
|
199
|
+
*/
|
|
200
|
+
function formatIssue(issue) {
|
|
201
|
+
const location = issue.line ? `line ${issue.line}${issue.column ? `:${issue.column}` : ''}` : 'file';
|
|
202
|
+
return `${location} ${issue.severity?.toUpperCase?.() ?? 'ISSUE'} ${issue.code}: ${issue.message}\nfix: ${issue.fix}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} value
|
|
207
|
+
*/
|
|
208
|
+
function xml(value) {
|
|
209
|
+
return String(value)
|
|
210
|
+
.replace(/&/g, '&')
|
|
211
|
+
.replace(/</g, '<')
|
|
212
|
+
.replace(/>/g, '>')
|
|
213
|
+
.replace(/"/g, '"')
|
|
214
|
+
.replace(/'/g, ''');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {Record<string, any>} payload
|
|
219
|
+
*/
|
|
220
|
+
export function scoreFromPayload(payload) {
|
|
221
|
+
const candidates = [
|
|
222
|
+
payload.summary?.score,
|
|
223
|
+
payload.job?.result?.overallScore,
|
|
224
|
+
payload.job?.result?.score,
|
|
225
|
+
payload.started?.auditRun?.result?.overallScore,
|
|
226
|
+
payload.started?.auditRun?.summary?.overallScore,
|
|
227
|
+
payload.started?.evalRun?.summary?.score,
|
|
228
|
+
payload.started?.result?.overallScore,
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
for (const candidate of candidates) {
|
|
232
|
+
const value = Number(candidate);
|
|
233
|
+
if (Number.isFinite(value)) {
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
}
|