rubrkit 0.1.0 → 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/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 (job.phase || job.message) {
66
- lines.push(`Latest: ${[job.phase, job.message].filter(Boolean).join(' - ')}`);
67
- }
68
-
69
- if (typeof score === 'number') {
70
- lines.push(`Score: ${score}`);
71
- }
72
-
73
- if (job.error?.message) {
74
- lines.push(`Error: ${job.error.code ?? 'job_failed'} - ${job.error.message}`);
75
- }
76
-
77
- return `${lines.join('\n')}\n`;
78
- }
79
-
80
- if (payload.mode === 'report') {
81
- const job = payload.job ?? payload.data?.job ?? payload;
82
- return [
83
- `Rubrkit report ${job.state ?? 'unknown'}`,
84
- `Job: ${job.id ?? payload.id ?? '(unknown)'}`,
85
- `Kind: ${job.kind ?? '(unknown)'}`,
86
- `Latest: ${[job.phase, job.message].filter(Boolean).join(' - ') || '(none)'}`,
87
- ].join('\n') + '\n';
88
- }
89
-
90
- const summary = payload.summary ?? {};
91
- const status = payload.passed ? 'PASS' : 'FAIL';
92
- const lines = [
93
- `Rubrkit ${payload.command ?? 'validate'} ${payload.mode ?? 'local'}`,
94
- `${status} ${payload.target ?? ''} (${summary.fileCount ?? 0} files, ${summary.errorCount ?? 0} errors, ${summary.warningCount ?? 0} warnings, score ${summary.score ?? 0})`,
95
- ];
96
-
97
- for (const file of payload.files ?? []) {
98
- lines.push(`- ${file.passed ? 'PASS' : 'FAIL'} ${file.path}`);
99
-
100
- for (const issue of file.issues ?? []) {
101
- const location = issue.line ? ` line ${issue.line}${issue.column ? `:${issue.column}` : ''}` : '';
102
- lines.push(` ${issue.severity.toUpperCase()} ${issue.code}${location}: ${issue.message}`);
103
- lines.push(` fix: ${issue.fix}`);
104
- }
105
- }
106
-
107
- for (const gate of payload.gates?.failures ?? []) {
108
- lines.push(`Gate ${gate.code}: ${gate.message}`);
109
- }
110
-
111
- return `${lines.join('\n')}\n`;
112
- }
113
-
114
- /**
115
- * @param {Record<string, any>} payload
116
- */
117
- function renderJUnit(payload) {
118
- const files = Array.isArray(payload.files) ? payload.files : [];
119
- const tests = files.length || 1;
120
- const failures = files.reduce((sum, file) => sum + Number(file.errorCount ?? 0), 0);
121
- const warnings = files.reduce((sum, file) => sum + Number(file.warningCount ?? 0), 0);
122
- const name = `rubrkit.${payload.command ?? 'test'}.${payload.mode ?? 'local'}`;
123
- const testcases = files.length > 0 ? files.map(renderFileTestCase).join('\n') : renderRemoteTestCase(payload);
124
-
125
- return [
126
- '<?xml version="1.0" encoding="UTF-8"?>',
127
- `<testsuite name="${xml(name)}" tests="${tests}" failures="${failures}" errors="0" skipped="0">`,
128
- warnings > 0 ? ` <system-out>${xml(`${warnings} Rubrkit warning${warnings === 1 ? '' : 's'}`)}</system-out>` : '',
129
- testcases,
130
- '</testsuite>',
131
- '',
132
- ].filter(line => line !== '').join('\n');
133
- }
134
-
135
- /**
136
- * @param {Record<string, any>} file
137
- */
138
- function renderFileTestCase(file) {
139
- const failures = (file.issues ?? []).filter(issue => issue.severity === 'error');
140
- const warnings = (file.issues ?? []).filter(issue => issue.severity === 'warning');
141
- const body = [];
142
-
143
- if (failures.length > 0) {
144
- body.push(
145
- ` <failure message="${xml(`${failures.length} Rubrkit validation error${failures.length === 1 ? '' : 's'}`)}">${xml(
146
- failures.map(formatIssue).join('\n'),
147
- )}</failure>`,
148
- );
149
- }
150
-
151
- if (warnings.length > 0) {
152
- body.push(` <system-out>${xml(warnings.map(formatIssue).join('\n'))}</system-out>`);
153
- }
154
-
155
- if (body.length === 0) {
156
- return ` <testcase name="${xml(file.path)}" classname="RubrkitLocalChecks" />`;
157
- }
158
-
159
- return [` <testcase name="${xml(file.path)}" classname="RubrkitLocalChecks">`, ...body, ' </testcase>'].join('\n');
160
- }
161
-
162
- /**
163
- * @param {Record<string, any>} payload
164
- */
165
- function renderRemoteTestCase(payload) {
166
- const job = payload.job ?? {};
167
- const failed = ['failed', 'cancelled', 'paused'].includes(job.state);
168
-
169
- if (!failed) {
170
- return ` <testcase name="${xml(job.id ?? payload.jobId ?? 'remote-job')}" classname="RubrkitRemoteJob" />`;
171
- }
172
-
173
- return [
174
- ` <testcase name="${xml(job.id ?? payload.jobId ?? 'remote-job')}" classname="RubrkitRemoteJob">`,
175
- ` <failure message="${xml(job.error?.message ?? 'Remote Rubrkit job did not succeed.')}">${xml(JSON.stringify(job.error ?? {}, null, 2))}</failure>`,
176
- ' </testcase>',
177
- ].join('\n');
178
- }
179
-
180
- /**
181
- * @param {Record<string, any>} issue
182
- */
183
- function formatIssue(issue) {
184
- const location = issue.line ? `line ${issue.line}${issue.column ? `:${issue.column}` : ''}` : 'file';
185
- return `${location} ${issue.severity?.toUpperCase?.() ?? 'ISSUE'} ${issue.code}: ${issue.message}\nfix: ${issue.fix}`;
186
- }
187
-
188
- /**
189
- * @param {string} value
190
- */
191
- function xml(value) {
192
- return String(value)
193
- .replace(/&/g, '&amp;')
194
- .replace(/</g, '&lt;')
195
- .replace(/>/g, '&gt;')
196
- .replace(/"/g, '&quot;')
197
- .replace(/'/g, '&apos;');
198
- }
199
-
200
- /**
201
- * @param {Record<string, any>} payload
202
- */
203
- export function scoreFromPayload(payload) {
204
- const candidates = [
205
- payload.summary?.score,
206
- payload.job?.result?.overallScore,
207
- payload.job?.result?.score,
208
- payload.started?.auditRun?.result?.overallScore,
209
- payload.started?.auditRun?.summary?.overallScore,
210
- payload.started?.evalRun?.summary?.score,
211
- payload.started?.result?.overallScore,
212
- ];
213
-
214
- for (const candidate of candidates) {
215
- const value = Number(candidate);
216
- if (Number.isFinite(value)) {
217
- return value;
218
- }
219
- }
220
-
221
- return null;
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, '&amp;')
211
+ .replace(/</g, '&lt;')
212
+ .replace(/>/g, '&gt;')
213
+ .replace(/"/g, '&quot;')
214
+ .replace(/'/g, '&apos;');
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
+ }