@alex-dbs/coderuler 0.0.0-stage → 1.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/LICENSE +175 -0
- package/README.md +218 -2
- package/dist/adapter/cli/commands/check.d.ts +3 -0
- package/dist/adapter/cli/commands/check.js +93 -0
- package/dist/adapter/cli/commands/deps.d.ts +3 -0
- package/dist/adapter/cli/commands/deps.js +4 -0
- package/dist/adapter/cli/commands/inspect.d.ts +3 -0
- package/dist/adapter/cli/commands/inspect.js +59 -0
- package/dist/adapter/cli/commands/rules.d.ts +10 -0
- package/dist/adapter/cli/commands/rules.js +17 -0
- package/dist/adapter/cli/commands/skill.d.ts +4 -0
- package/dist/adapter/cli/commands/skill.js +7 -0
- package/dist/adapter/cli/commands/test.d.ts +3 -0
- package/dist/adapter/cli/commands/test.js +78 -0
- package/dist/adapter/cli/commands/update.d.ts +3 -0
- package/dist/adapter/cli/commands/update.js +7 -0
- package/dist/adapter/cli/commands/validate.d.ts +3 -0
- package/dist/adapter/cli/commands/validate.js +12 -0
- package/dist/adapter/cli/exit.d.ts +7 -0
- package/dist/adapter/cli/exit.js +10 -0
- package/dist/adapter/cli/inspect-json.d.ts +9 -0
- package/dist/adapter/cli/inspect-json.js +160 -0
- package/dist/adapter/cli/inspect-usage.d.ts +9 -0
- package/dist/adapter/cli/inspect-usage.js +14 -0
- package/dist/adapter/cli/program.d.ts +4 -0
- package/dist/adapter/cli/program.js +216 -0
- package/dist/adapter/cli/progress.d.ts +22 -0
- package/dist/adapter/cli/progress.js +79 -0
- package/dist/adapter/cli/reports.d.ts +47 -0
- package/dist/adapter/cli/reports.js +735 -0
- package/dist/adapter/cli/self-update.d.ts +1 -0
- package/dist/adapter/cli/self-update.js +89 -0
- package/dist/adapter/cli/services.d.ts +101 -0
- package/dist/adapter/cli/services.js +1 -0
- package/dist/adapter/cli/text.d.ts +5 -0
- package/dist/adapter/cli/text.js +36 -0
- package/dist/adapter/cli/version.d.ts +1 -0
- package/dist/adapter/cli/version.js +3 -0
- package/dist/adapter/decision/jev.d.ts +11 -0
- package/dist/adapter/decision/jev.js +138 -0
- package/dist/adapter/filesystem/dependencies.d.ts +26 -0
- package/dist/adapter/filesystem/dependencies.js +577 -0
- package/dist/adapter/filesystem/dependency-installation.d.ts +11 -0
- package/dist/adapter/filesystem/dependency-installation.js +373 -0
- package/dist/adapter/filesystem/project.d.ts +14 -0
- package/dist/adapter/filesystem/project.js +387 -0
- package/dist/adapter/filesystem/rule-resolution.d.ts +4 -0
- package/dist/adapter/filesystem/rule-resolution.js +516 -0
- package/dist/adapter/filesystem/rule-test-worker.d.ts +17 -0
- package/dist/adapter/filesystem/rule-test-worker.js +47 -0
- package/dist/adapter/filesystem/rule-tests.d.ts +3 -0
- package/dist/adapter/filesystem/rule-tests.js +264 -0
- package/dist/adapter/filesystem/rules.d.ts +13 -0
- package/dist/adapter/filesystem/rules.js +242 -0
- package/dist/adapter/git/changes.d.ts +1 -0
- package/dist/adapter/git/changes.js +54 -0
- package/dist/adapter/git/environment.d.ts +1 -0
- package/dist/adapter/git/environment.js +10 -0
- package/dist/adapter/github/api.d.ts +5 -0
- package/dist/adapter/github/api.js +40 -0
- package/dist/adapter/github/comments.d.ts +3 -0
- package/dist/adapter/github/comments.js +235 -0
- package/dist/adapter/github/pull-request.d.ts +16 -0
- package/dist/adapter/github/pull-request.js +139 -0
- package/dist/adapter/scope/astgrep-languages.d.ts +5 -0
- package/dist/adapter/scope/astgrep-languages.js +119 -0
- package/dist/adapter/scope/astgrep.d.ts +14 -0
- package/dist/adapter/scope/astgrep.js +183 -0
- package/dist/adapter/scope/csv.d.ts +4 -0
- package/dist/adapter/scope/csv.js +158 -0
- package/dist/adapter/scope/file.d.ts +2 -0
- package/dist/adapter/scope/file.js +4 -0
- package/dist/adapter/scope/json.d.ts +7 -0
- package/dist/adapter/scope/json.js +132 -0
- package/dist/adapter/scope/lines.d.ts +10 -0
- package/dist/adapter/scope/lines.js +42 -0
- package/dist/adapter/scope/protobuf-options.d.ts +29 -0
- package/dist/adapter/scope/protobuf-options.js +64 -0
- package/dist/adapter/scope/protobuf-parser.d.ts +30 -0
- package/dist/adapter/scope/protobuf-parser.js +772 -0
- package/dist/adapter/scope/protobuf.d.ts +5 -0
- package/dist/adapter/scope/protobuf.js +53 -0
- package/dist/adapter/scope/structured.d.ts +10 -0
- package/dist/adapter/scope/structured.js +84 -0
- package/dist/adapter/scope/yaml.d.ts +7 -0
- package/dist/adapter/scope/yaml.js +224 -0
- package/dist/adapter/skills/cli.d.ts +10 -0
- package/dist/adapter/skills/cli.js +77 -0
- package/dist/app/concurrency.d.ts +1 -0
- package/dist/app/concurrency.js +44 -0
- package/dist/app/evaluators.d.ts +14 -0
- package/dist/app/evaluators.js +175 -0
- package/dist/app/execute.d.ts +2 -0
- package/dist/app/execute.js +427 -0
- package/dist/app/jsonpath.d.ts +1 -0
- package/dist/app/jsonpath.js +8 -0
- package/dist/app/ports.d.ts +165 -0
- package/dist/app/ports.js +1 -0
- package/dist/app/rule-tests.d.ts +20 -0
- package/dist/app/rule-tests.js +11 -0
- package/dist/app/rule.d.ts +121 -0
- package/dist/app/rule.js +24 -0
- package/dist/app/selection.d.ts +2 -0
- package/dist/app/selection.js +23 -0
- package/dist/app/usage.d.ts +2 -0
- package/dist/app/usage.js +13 -0
- package/dist/app/validate.d.ts +11 -0
- package/dist/app/validate.js +415 -0
- package/dist/app/violation-comments.d.ts +10 -0
- package/dist/app/violation-comments.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +11 -0
- package/dist/infra/runtime.d.ts +3 -0
- package/dist/infra/runtime.js +168 -0
- package/dist/infra/settings.d.ts +5 -0
- package/dist/infra/settings.js +91 -0
- package/package.json +86 -4
- package/skills/coderuler-new-rule/SKILL.md +141 -0
- package/skills/coderuler-new-rule/reference.md +447 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { stripVTControlCharacters, styleText } from 'node:util';
|
|
3
|
+
import { applyEdits, format, visit } from 'jsonc-parser';
|
|
4
|
+
import stringWidth from 'string-width';
|
|
5
|
+
import { addUsage } from '../../app/usage.js';
|
|
6
|
+
import { createUsageTracker } from './inspect-usage.js';
|
|
7
|
+
import { escapeText } from './text.js';
|
|
8
|
+
const MAX_VIOLATIONS = 10;
|
|
9
|
+
const MAX_SOURCE_LINES = 5;
|
|
10
|
+
const MAX_GROUP_MEMBERS = 5;
|
|
11
|
+
const TAB_WIDTH = 4;
|
|
12
|
+
const GRAPHEMES = new Intl.Segmenter();
|
|
13
|
+
const STYLE_CLOSE = { 1: 22, 2: 22, 31: 39, 32: 39, 33: 39, 36: 39, 90: 39 };
|
|
14
|
+
function paint(text, style, color) {
|
|
15
|
+
return color ? styleText(style, text, { validateStream: false }) : text;
|
|
16
|
+
}
|
|
17
|
+
function sourceText(line) {
|
|
18
|
+
let tab = line.indexOf('\t');
|
|
19
|
+
if (tab === -1)
|
|
20
|
+
return escapeText(line);
|
|
21
|
+
let rendered = '';
|
|
22
|
+
let column = 0;
|
|
23
|
+
let start = 0;
|
|
24
|
+
while (tab !== -1) {
|
|
25
|
+
const span = escapeText(line.slice(start, tab));
|
|
26
|
+
rendered += span;
|
|
27
|
+
column += stringWidth(span);
|
|
28
|
+
const spaces = TAB_WIDTH - (column % TAB_WIDTH);
|
|
29
|
+
rendered += ' '.repeat(spaces);
|
|
30
|
+
column += spaces;
|
|
31
|
+
start = tab + 1;
|
|
32
|
+
tab = line.indexOf('\t', start);
|
|
33
|
+
}
|
|
34
|
+
return rendered + escapeText(line.slice(start));
|
|
35
|
+
}
|
|
36
|
+
export function renderRuleTestHeading(ruleName, redact, color) {
|
|
37
|
+
return `${paint(escapeText(redact(ruleName)), 'bold', color)}\n`;
|
|
38
|
+
}
|
|
39
|
+
export function renderRuleTestResult(result, redact, color, lastInRule) {
|
|
40
|
+
const safe = (text) => escapeText(redact(text));
|
|
41
|
+
const matches = result.expected === result.actual;
|
|
42
|
+
const label = paint(safe(matches ? 'PASS' : 'FAIL'), matches ? 'green' : 'red', color);
|
|
43
|
+
const branch = paint(lastInRule ? '└─ ' : '├─ ', 'dim', color);
|
|
44
|
+
const detail = matches
|
|
45
|
+
? ''
|
|
46
|
+
: safe(` (expected ${result.expected}, actual ${result.actual})`);
|
|
47
|
+
const duration = paint(safe(` (${formatDuration(result.durationMs)})`), 'dim', color);
|
|
48
|
+
return `${branch}${label} ${safe(result.name)}${detail}${duration}\n`;
|
|
49
|
+
}
|
|
50
|
+
export function renderRuleTestSummary(passed, failed, durationMs, redact, color) {
|
|
51
|
+
const safe = (text) => escapeText(redact(text));
|
|
52
|
+
const passing = paint(safe(`${passed} passed`), 'green', color);
|
|
53
|
+
const failing = paint(safe(`${failed} failed`), failed === 0 ? 'dim' : 'red', color);
|
|
54
|
+
return `${passing}, ${failing}${safe(` Time: ${formatDuration(durationMs)}`)}\n`;
|
|
55
|
+
}
|
|
56
|
+
function reasonSuffix(reason) {
|
|
57
|
+
return reason === undefined ? '' : ` (${escapeText(reason)})`;
|
|
58
|
+
}
|
|
59
|
+
function sourceLines(source) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
let current = '';
|
|
62
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
63
|
+
const code = source.charCodeAt(index);
|
|
64
|
+
if (code === 10 || code === 13) {
|
|
65
|
+
lines.push(current);
|
|
66
|
+
current = '';
|
|
67
|
+
if (code === 13 && source.charCodeAt(index + 1) === 10)
|
|
68
|
+
index += 1;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
current += source.charAt(index);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (current !== '')
|
|
75
|
+
lines.push(current);
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
function reportFlags(outputAll) {
|
|
79
|
+
return outputAll ? '--json --output-all or --jsonl --output-all' : '--json or --jsonl';
|
|
80
|
+
}
|
|
81
|
+
function excerptLines(subject, limit, redact, color = false, outputAll = false) {
|
|
82
|
+
const lines = sourceLines(redact(subject.source));
|
|
83
|
+
const shown = lines.slice(0, limit);
|
|
84
|
+
const start = subject.lines?.start ?? 1;
|
|
85
|
+
const width = String(start + Math.max(shown.length - 1, 0)).length;
|
|
86
|
+
const rendered = shown.map((line, index) => `${paint(` ${String(start + index).padStart(width)} |`, 'dim', color)} ${sourceText(line)}`);
|
|
87
|
+
const hidden = lines.length - shown.length;
|
|
88
|
+
if (hidden > 0) {
|
|
89
|
+
rendered.push(paint(` ... ${hidden} more ${hidden === 1 ? 'line' : 'lines'}; rerun with ${reportFlags(outputAll)} for the complete source.`, 'dim', color));
|
|
90
|
+
}
|
|
91
|
+
return rendered;
|
|
92
|
+
}
|
|
93
|
+
function subjectLabel(subject) {
|
|
94
|
+
return subject.kind === 'group'
|
|
95
|
+
? `group ${subject.group.type} ${escapeText(subject.group.path)}`
|
|
96
|
+
: `${escapeText(subject.filepath)} ${escapeText(subject.id)}`;
|
|
97
|
+
}
|
|
98
|
+
export function createRuleHeadings(rules) {
|
|
99
|
+
const byName = new Map(rules.map(entry => [entry.name, entry]));
|
|
100
|
+
return name => {
|
|
101
|
+
const entry = byName.get(name);
|
|
102
|
+
if (entry === undefined)
|
|
103
|
+
return [];
|
|
104
|
+
const fields = [
|
|
105
|
+
['files', entry.rule.files],
|
|
106
|
+
['exclude', entry.rule.exclude],
|
|
107
|
+
['group', entry.rule.group],
|
|
108
|
+
['settings', entry.rule.settings],
|
|
109
|
+
];
|
|
110
|
+
return [
|
|
111
|
+
` file: ${escapeText(entry.ruleFile)}`,
|
|
112
|
+
...fields.map(([label, value]) => ` ${label}: ${escapeText(JSON.stringify(value) ?? String(value))}`),
|
|
113
|
+
];
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function createMetadataRenderer(write, redact) {
|
|
117
|
+
return event => {
|
|
118
|
+
if (event.type === 'usage')
|
|
119
|
+
return;
|
|
120
|
+
write(`${redact(metadataLine(event))}\n`);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function metadataLine(event) {
|
|
124
|
+
switch (event.type) {
|
|
125
|
+
case 'progress':
|
|
126
|
+
return `progress ${event.phase} ${event.completed}/${event.total}`;
|
|
127
|
+
case 'rule':
|
|
128
|
+
return `rule ${escapeText(event.name)} ${escapeText(event.ruleFile)}`;
|
|
129
|
+
case 'gate':
|
|
130
|
+
return `gate ${escapeText(event.name)} enabled=${String(event.enabled)} applies=${String(event.applies)}${reasonSuffix(event.reason)}`;
|
|
131
|
+
case 'file':
|
|
132
|
+
return `file ${escapeText(event.name)} ${escapeText(event.filepath)} ${event.selected ? 'selected' : 'excluded'}${reasonSuffix(event.reason)}`;
|
|
133
|
+
case 'scope':
|
|
134
|
+
return `scope ${escapeText(event.name)} ${subjectLabel(event.subject)} ${event.accepted ? 'accepted' : 'filtered'}${reasonSuffix(event.reason)}`;
|
|
135
|
+
case 'group':
|
|
136
|
+
return `group ${escapeText(event.name)} ${subjectLabel(event.subject)} members=${event.subject.members.length}`;
|
|
137
|
+
case 'evaluation':
|
|
138
|
+
return `evaluation ${event.evaluationId} ${escapeText(event.name)} ${subjectLabel(event.subject)}`;
|
|
139
|
+
case 'request':
|
|
140
|
+
return `request ${escapeText(event.name)} ${subjectLabel(event.subject)} ${Buffer.byteLength(event.body, 'utf8')} bytes`;
|
|
141
|
+
case 'response':
|
|
142
|
+
return `response ${escapeText(event.name)} ${subjectLabel(event.subject)} HTTP ${event.status} ${Buffer.byteLength(event.body, 'utf8')} bytes`;
|
|
143
|
+
case 'usage':
|
|
144
|
+
return `usage ${escapeText(event.name)} ${subjectLabel(event.subject)} calls=${event.usage.calls}`;
|
|
145
|
+
case 'result':
|
|
146
|
+
return `result ${escapeText(event.name)} ${subjectLabel(event.subject)} ${event.result.status}`;
|
|
147
|
+
case 'trace': {
|
|
148
|
+
const summary = `trace ${escapeText(event.name)} ${escapeText(event.message)}`;
|
|
149
|
+
const details = event.details;
|
|
150
|
+
if (details === undefined)
|
|
151
|
+
return summary;
|
|
152
|
+
const shape = typeof details !== 'object' || details === null
|
|
153
|
+
? typeof details
|
|
154
|
+
: Array.isArray(details)
|
|
155
|
+
? `array[${details.length}]`
|
|
156
|
+
: Object.keys(details).join(',');
|
|
157
|
+
return `${summary} details=${escapeText(shape)}`;
|
|
158
|
+
}
|
|
159
|
+
case 'error':
|
|
160
|
+
return event.name === undefined
|
|
161
|
+
? `error stage=${escapeText(event.stage)} ${escapeText(event.message)}`
|
|
162
|
+
: `error ${escapeText(event.name)} stage=${escapeText(event.stage)} ${escapeText(event.message)}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function walkthroughText(value, redact, color, style) {
|
|
166
|
+
const text = escapeText(redact(value));
|
|
167
|
+
return style === undefined ? text : paint(text, style, color);
|
|
168
|
+
}
|
|
169
|
+
function walkthroughField(label, value, redact, color, style) {
|
|
170
|
+
return ` ${label}: ${walkthroughText(value, redact, color, style)}`;
|
|
171
|
+
}
|
|
172
|
+
function walkthroughPayload(serialized, redact, raw = false) {
|
|
173
|
+
let formatted = redact(serialized);
|
|
174
|
+
let valid = true;
|
|
175
|
+
if (!raw)
|
|
176
|
+
visit(formatted, { onError: () => { valid = false; } }, {
|
|
177
|
+
disallowComments: true, allowTrailingComma: false, allowEmptyContent: false,
|
|
178
|
+
});
|
|
179
|
+
if (!raw && valid) {
|
|
180
|
+
formatted = applyEdits(formatted, format(formatted, undefined, {
|
|
181
|
+
insertSpaces: true, tabSize: 2, eol: '\n',
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
return redact(formatted).split('\n').map(line => ` ${escapeText(line)}`);
|
|
185
|
+
}
|
|
186
|
+
function walkthroughSubject(subject, redact, color) {
|
|
187
|
+
if (subject.kind === 'group') {
|
|
188
|
+
return [
|
|
189
|
+
walkthroughField('group', subject.group.type, redact, color),
|
|
190
|
+
walkthroughField('path', subject.group.path, redact, color, 'dim'),
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
const location = subject.lines === undefined
|
|
194
|
+
? subject.filepath
|
|
195
|
+
: `${subject.filepath}:${subject.lines.start}-${subject.lines.end}`;
|
|
196
|
+
return [
|
|
197
|
+
walkthroughField('file', location, redact, color, 'dim'),
|
|
198
|
+
walkthroughField('scope', subject.id, redact, color, 'cyan'),
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
function writeWalkthroughBlock(write, lines) {
|
|
202
|
+
write(`${lines.join('\n')}\n\n`);
|
|
203
|
+
}
|
|
204
|
+
function wrapCardLine(line, width) {
|
|
205
|
+
const styles = [];
|
|
206
|
+
let text = '';
|
|
207
|
+
let cursor = 0;
|
|
208
|
+
for (const match of line.matchAll(/\u001b\[(\d+)m/g)) {
|
|
209
|
+
text += line.slice(cursor, match.index);
|
|
210
|
+
styles.push({ offset: text.length, sequence: match[0], code: Number(match[1]) });
|
|
211
|
+
cursor = match.index + match[0].length;
|
|
212
|
+
}
|
|
213
|
+
text += line.slice(cursor);
|
|
214
|
+
const active = new Set();
|
|
215
|
+
const rows = [];
|
|
216
|
+
let row = '';
|
|
217
|
+
let cells = 0;
|
|
218
|
+
let styleIndex = 0;
|
|
219
|
+
const appendStyle = () => {
|
|
220
|
+
const style = styles[styleIndex++];
|
|
221
|
+
row += style.sequence;
|
|
222
|
+
if (STYLE_CLOSE[style.code] !== undefined) {
|
|
223
|
+
if (STYLE_CLOSE[style.code] === 39) {
|
|
224
|
+
for (const code of active)
|
|
225
|
+
if (STYLE_CLOSE[code] === 39)
|
|
226
|
+
active.delete(code);
|
|
227
|
+
}
|
|
228
|
+
active.add(style.code);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
for (const code of active)
|
|
232
|
+
if (STYLE_CLOSE[code] === style.code)
|
|
233
|
+
active.delete(code);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
for (const { segment, index } of GRAPHEMES.segment(text)) {
|
|
237
|
+
const size = stringWidth(segment);
|
|
238
|
+
if (cells > 0 && cells + size > width) {
|
|
239
|
+
rows.push(row + (active.size === 0 ? '' : '\u001b[0m'));
|
|
240
|
+
row = [...active].map(code => `\u001b[${code}m`).join('');
|
|
241
|
+
cells = 0;
|
|
242
|
+
}
|
|
243
|
+
const end = index + segment.length;
|
|
244
|
+
let start = index;
|
|
245
|
+
while (styleIndex < styles.length && styles[styleIndex].offset < end) {
|
|
246
|
+
row += text.slice(start, styles[styleIndex].offset);
|
|
247
|
+
start = styles[styleIndex].offset;
|
|
248
|
+
appendStyle();
|
|
249
|
+
}
|
|
250
|
+
row += text.slice(start, end);
|
|
251
|
+
cells += size;
|
|
252
|
+
}
|
|
253
|
+
while (styleIndex < styles.length)
|
|
254
|
+
appendStyle();
|
|
255
|
+
rows.push(row + (active.size === 0 ? '' : '\u001b[0m'));
|
|
256
|
+
return rows;
|
|
257
|
+
}
|
|
258
|
+
export function createWalkthroughRenderer(options, inspectUsage = false) {
|
|
259
|
+
const { write, headings, redact } = options;
|
|
260
|
+
const raw = options.raw === true;
|
|
261
|
+
const color = options.color === true && !raw;
|
|
262
|
+
const buffered = (options.concurrency ?? 1) > 1;
|
|
263
|
+
const cards = new Map();
|
|
264
|
+
const progress = new Map();
|
|
265
|
+
const pending = [];
|
|
266
|
+
let finished = false;
|
|
267
|
+
const field = (label, value, style) => walkthroughField(label, value, redact, color, style);
|
|
268
|
+
const section = (card, title, lines) => {
|
|
269
|
+
const heading = ` ${title}`;
|
|
270
|
+
if (raw)
|
|
271
|
+
return ['', heading, ...lines];
|
|
272
|
+
const remaining = card.width - 4 - stringWidth(heading) - 1;
|
|
273
|
+
const divider = remaining > 0 ? ` ${paint('─'.repeat(remaining), 'dim', color)}` : '';
|
|
274
|
+
return ['', paint(heading, 'bold', color) + divider, ...lines];
|
|
275
|
+
};
|
|
276
|
+
const columns = () => {
|
|
277
|
+
const width = options.columns?.() ?? 80;
|
|
278
|
+
return Number.isFinite(width) ? Math.max(8, Math.floor(width)) : 80;
|
|
279
|
+
};
|
|
280
|
+
const border = (card, top) => raw ? '' : `${top ? '╭' : '╰'}${'─'.repeat(card.width - 2)}${top ? '╮' : '╯'}\n`;
|
|
281
|
+
const framed = (card, lines) => raw ? `${lines.join('\n')}\n` : lines.flatMap(line => wrapCardLine(line, card.width - 4)).map(line => `│ ${line}${' '.repeat(Math.max(0, card.width - 4 - stringWidth(line)))} │\n`).join('');
|
|
282
|
+
const append = (card, lines) => {
|
|
283
|
+
const text = framed(card, lines);
|
|
284
|
+
if (buffered)
|
|
285
|
+
card.lines.push(text);
|
|
286
|
+
else
|
|
287
|
+
write(text);
|
|
288
|
+
};
|
|
289
|
+
const standalone = (event) => {
|
|
290
|
+
writeWalkthroughBlock(write, walkthroughLines(event, headings, redact, color, raw));
|
|
291
|
+
};
|
|
292
|
+
const drain = () => {
|
|
293
|
+
if (!buffered && cards.size > 0)
|
|
294
|
+
return;
|
|
295
|
+
for (const event of pending.splice(0))
|
|
296
|
+
standalone(event);
|
|
297
|
+
for (const event of progress.values())
|
|
298
|
+
standalone(event);
|
|
299
|
+
progress.clear();
|
|
300
|
+
};
|
|
301
|
+
const close = (id, incomplete = false) => {
|
|
302
|
+
const card = cards.get(id);
|
|
303
|
+
if (incomplete)
|
|
304
|
+
append(card, section(card, 'Incomplete', [` ${field('status', 'incomplete', 'yellow')}`]));
|
|
305
|
+
if (inspectUsage && card.usage !== undefined) {
|
|
306
|
+
append(card, section(card, 'AI Usage', usageFields(card.usage).map(([label, value]) => ` ${field(label, value)}`)));
|
|
307
|
+
}
|
|
308
|
+
if (buffered)
|
|
309
|
+
write(border(card, true) + card.lines.join('') + border(card, false) + '\n');
|
|
310
|
+
else
|
|
311
|
+
write(border(card, false) + '\n');
|
|
312
|
+
cards.delete(id);
|
|
313
|
+
drain();
|
|
314
|
+
};
|
|
315
|
+
const source = (subject) => subject.kind === 'scope'
|
|
316
|
+
? excerptLines(subject, Number.POSITIVE_INFINITY, redact, color)
|
|
317
|
+
: subject.members.flatMap(member => [
|
|
318
|
+
...walkthroughSubject(member, redact, color),
|
|
319
|
+
...excerptLines(member, Number.POSITIVE_INFINITY, redact, color),
|
|
320
|
+
'',
|
|
321
|
+
]);
|
|
322
|
+
const nested = (card, event) => {
|
|
323
|
+
switch (event.type) {
|
|
324
|
+
case 'request':
|
|
325
|
+
return section(card, 'Request', [
|
|
326
|
+
paint(' body:', 'gray', color),
|
|
327
|
+
...walkthroughPayload(event.body, redact, raw).map(line => paint(` ${line}`, 'gray', color)),
|
|
328
|
+
]);
|
|
329
|
+
case 'response':
|
|
330
|
+
return section(card, 'Response', [
|
|
331
|
+
` ${field('status', `HTTP ${event.status}`, event.status >= 400 ? 'red' : event.status >= 300 ? 'yellow' : 'green')}`,
|
|
332
|
+
paint(' body:', 'gray', color),
|
|
333
|
+
...walkthroughPayload(event.body, redact, raw).map(line => paint(` ${line}`, 'gray', color)),
|
|
334
|
+
]);
|
|
335
|
+
case 'result':
|
|
336
|
+
return section(card, 'Result', [
|
|
337
|
+
` ${field('status', event.result.status, event.result.status === 'pass' ? 'green' : 'red')}`,
|
|
338
|
+
...(event.result.message === undefined ? [] : [` ${field('message', event.result.message)}`]),
|
|
339
|
+
...(event.result.probability === undefined ? [] : [` ${field('probability', String(event.result.probability))}`]),
|
|
340
|
+
...(event.result.threshold === undefined ? [] : [` ${field('threshold', String(event.result.threshold))}`]),
|
|
341
|
+
...(event.result.model === undefined ? [] : [` ${field('model', event.result.model)}`]),
|
|
342
|
+
]);
|
|
343
|
+
case 'trace':
|
|
344
|
+
return section(card, 'Trace', [
|
|
345
|
+
` ${field('message', event.message)}`,
|
|
346
|
+
...(event.details === undefined ? [] : [
|
|
347
|
+
' details:', ...walkthroughPayload(JSON.stringify(event.details), redact, raw).map(line => ` ${line}`),
|
|
348
|
+
]),
|
|
349
|
+
]);
|
|
350
|
+
case 'file':
|
|
351
|
+
return section(card, 'File', [
|
|
352
|
+
` ${field('file', event.filepath, 'dim')}`,
|
|
353
|
+
` ${field('status', event.selected ? 'selected' : 'excluded', event.selected ? 'green' : 'yellow')}`,
|
|
354
|
+
...(event.reason === undefined ? [] : [` ${field('reason', event.reason)}`]),
|
|
355
|
+
]);
|
|
356
|
+
case 'error':
|
|
357
|
+
return section(card, 'Error', [
|
|
358
|
+
` ${field('stage', event.stage)}`,
|
|
359
|
+
` ${field('message', event.message, 'red')}`,
|
|
360
|
+
]);
|
|
361
|
+
default:
|
|
362
|
+
throw new Error(`Unexpected evaluation event: ${event.type}`);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
emit: event => {
|
|
367
|
+
if (finished)
|
|
368
|
+
return;
|
|
369
|
+
if (event.type === 'usage') {
|
|
370
|
+
if (inspectUsage) {
|
|
371
|
+
const card = cards.get(event.evaluationId);
|
|
372
|
+
if (card === undefined)
|
|
373
|
+
throw new Error(`Missing evaluation start: ${event.evaluationId}`);
|
|
374
|
+
card.usage = addUsage(card.usage, event.usage);
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if ((event.type === 'scope' && event.accepted) || event.type === 'group')
|
|
379
|
+
return;
|
|
380
|
+
if (event.type === 'evaluation') {
|
|
381
|
+
if (cards.has(event.evaluationId))
|
|
382
|
+
throw new Error(`Duplicate evaluation start: ${event.evaluationId}`);
|
|
383
|
+
if (!buffered && cards.size > 0)
|
|
384
|
+
throw new Error('Concurrent evaluations require buffered walkthrough rendering');
|
|
385
|
+
const card = { width: columns(), lines: [] };
|
|
386
|
+
cards.set(event.evaluationId, card);
|
|
387
|
+
if (!buffered)
|
|
388
|
+
write(border(card, true));
|
|
389
|
+
append(card, [
|
|
390
|
+
`${paint('evaluation', 'cyan', color)} ${walkthroughText(event.name, redact, color, 'bold')}`,
|
|
391
|
+
...walkthroughSubject(event.subject, redact, color),
|
|
392
|
+
...(event.subject.kind === 'group' ? [field('members', String(event.subject.members.length))] : []),
|
|
393
|
+
...section(card, 'Source', source(event.subject)),
|
|
394
|
+
]);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if ('evaluationId' in event && event.evaluationId !== undefined) {
|
|
398
|
+
const card = cards.get(event.evaluationId);
|
|
399
|
+
if (card === undefined)
|
|
400
|
+
throw new Error(`Missing evaluation start: ${event.evaluationId}`);
|
|
401
|
+
append(card, nested(card, event));
|
|
402
|
+
if (event.type === 'result' || event.type === 'error')
|
|
403
|
+
close(event.evaluationId);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (event.type === 'request' || event.type === 'response' || event.type === 'result') {
|
|
407
|
+
throw new Error(`Missing evaluation ID for ${event.type}`);
|
|
408
|
+
}
|
|
409
|
+
if (event.type === 'error' && cards.size > 0) {
|
|
410
|
+
for (const id of cards.keys())
|
|
411
|
+
close(id, true);
|
|
412
|
+
standalone(event);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!buffered && cards.size > 0) {
|
|
416
|
+
if (event.type === 'progress')
|
|
417
|
+
progress.set(event.phase, event);
|
|
418
|
+
else
|
|
419
|
+
pending.push(event);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
standalone(event);
|
|
423
|
+
},
|
|
424
|
+
finish: () => {
|
|
425
|
+
if (finished)
|
|
426
|
+
return;
|
|
427
|
+
finished = true;
|
|
428
|
+
for (const id of cards.keys())
|
|
429
|
+
close(id, true);
|
|
430
|
+
drain();
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function walkthroughLines(event, headings, redact, color, raw = false) {
|
|
435
|
+
const header = (name, style = 'cyan') => `${paint(event.type, style, color)}${name === undefined ? '' : ` ${walkthroughText(name, redact, color, 'bold')}`}`;
|
|
436
|
+
const field = (label, value, style) => walkthroughField(label, value, redact, color, style);
|
|
437
|
+
const reason = (value) => value === undefined ? [] : [field('reason', value)];
|
|
438
|
+
switch (event.type) {
|
|
439
|
+
case 'progress':
|
|
440
|
+
return [paint(` progress ${event.phase} ${event.completed}/${event.total}`, 'dim', color)];
|
|
441
|
+
case 'rule': {
|
|
442
|
+
const metadata = headings(event.name);
|
|
443
|
+
return [
|
|
444
|
+
header(event.name),
|
|
445
|
+
...(metadata.some(line => line.startsWith(' file: ')) ? [] : [field('file', event.ruleFile, 'dim')]),
|
|
446
|
+
...metadata.flatMap(line => {
|
|
447
|
+
const match = /^ ([a-z]+): (.*)$/.exec(line);
|
|
448
|
+
if (match === null)
|
|
449
|
+
return [walkthroughText(line, redact, color)];
|
|
450
|
+
const label = match[1];
|
|
451
|
+
const value = match[2];
|
|
452
|
+
return value.startsWith('{') || value.startsWith('[')
|
|
453
|
+
? [` ${label}:`, ...walkthroughPayload(value, redact, raw)]
|
|
454
|
+
: [field(label, value, label === 'file' ? 'dim' : undefined)];
|
|
455
|
+
}),
|
|
456
|
+
];
|
|
457
|
+
}
|
|
458
|
+
case 'gate':
|
|
459
|
+
return [
|
|
460
|
+
header(event.name),
|
|
461
|
+
field('status', event.enabled ? (event.applies ? 'applies' : 'skipped') : 'disabled', event.enabled && event.applies ? 'green' : 'yellow'),
|
|
462
|
+
...reason(event.reason),
|
|
463
|
+
];
|
|
464
|
+
case 'file':
|
|
465
|
+
return [
|
|
466
|
+
header(event.name),
|
|
467
|
+
field('file', event.filepath, 'dim'),
|
|
468
|
+
field('status', event.selected ? 'selected' : 'excluded', event.selected ? 'green' : 'yellow'),
|
|
469
|
+
...reason(event.reason),
|
|
470
|
+
];
|
|
471
|
+
case 'scope':
|
|
472
|
+
return [
|
|
473
|
+
header(event.name),
|
|
474
|
+
...walkthroughSubject(event.subject, redact, color),
|
|
475
|
+
field('status', 'filtered', 'yellow'),
|
|
476
|
+
...reason(event.reason),
|
|
477
|
+
' source:', ...excerptLines(event.subject, Number.POSITIVE_INFINITY, redact, color),
|
|
478
|
+
];
|
|
479
|
+
case 'trace':
|
|
480
|
+
return [
|
|
481
|
+
header(event.name),
|
|
482
|
+
field('message', event.message),
|
|
483
|
+
...(event.details === undefined ? [] : [
|
|
484
|
+
' details:', ...walkthroughPayload(JSON.stringify(event.details), redact, raw),
|
|
485
|
+
]),
|
|
486
|
+
];
|
|
487
|
+
case 'error':
|
|
488
|
+
return [header(event.name, 'red'), field('stage', event.stage), field('message', event.message, 'red')];
|
|
489
|
+
default:
|
|
490
|
+
throw new Error(`Evaluation event rendered outside its card: ${event.type}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function usageFields(usage) {
|
|
494
|
+
const money = (value) => `$${value > 0 && value < 1e-8 ? value.toPrecision(8) : value.toFixed(8)}`;
|
|
495
|
+
return [
|
|
496
|
+
['calls', String(usage.calls)],
|
|
497
|
+
...(usage.unreportedCalls === 0 ? [] : [
|
|
498
|
+
['unreported calls', String(usage.unreportedCalls)],
|
|
499
|
+
['totals', 'reported calls only; unreported tokens and costs are unknown'],
|
|
500
|
+
]),
|
|
501
|
+
['input tokens', String(usage.inputTokens)],
|
|
502
|
+
['output tokens', String(usage.outputTokens)],
|
|
503
|
+
['input cost (USD)', money(usage.inputCost)],
|
|
504
|
+
['output cost (USD)', money(usage.outputCost)],
|
|
505
|
+
['total cost (USD)', money(usage.inputCost + usage.outputCost)],
|
|
506
|
+
];
|
|
507
|
+
}
|
|
508
|
+
export function createInspectRenderer(options) {
|
|
509
|
+
const render = createWalkthroughRenderer({ ...options, concurrency: 1 }, true);
|
|
510
|
+
const usage = createUsageTracker();
|
|
511
|
+
const color = options.color === true && options.raw !== true;
|
|
512
|
+
const gateExplained = new Set();
|
|
513
|
+
let finished = false;
|
|
514
|
+
return {
|
|
515
|
+
emit: event => {
|
|
516
|
+
if (finished)
|
|
517
|
+
return;
|
|
518
|
+
usage.emit(event);
|
|
519
|
+
const skippedGate = event.type === 'gate' && (!event.enabled || !event.applies);
|
|
520
|
+
if (skippedGate)
|
|
521
|
+
gateExplained.add(event.name);
|
|
522
|
+
render.emit(event);
|
|
523
|
+
},
|
|
524
|
+
finish: reports => {
|
|
525
|
+
if (finished)
|
|
526
|
+
return;
|
|
527
|
+
finished = true;
|
|
528
|
+
render.finish();
|
|
529
|
+
for (const report of reports) {
|
|
530
|
+
if (report.status !== 'skipped' || report.evaluations.length > 0)
|
|
531
|
+
continue;
|
|
532
|
+
if (gateExplained.has(report.name))
|
|
533
|
+
continue;
|
|
534
|
+
writeWalkthroughBlock(options.write, [
|
|
535
|
+
`${paint('result', 'cyan', color)} ${walkthroughText(report.name, options.redact, color, 'bold')}`,
|
|
536
|
+
walkthroughField('status', 'skipped', options.redact, color, 'yellow'),
|
|
537
|
+
walkthroughField('reason', report.reason ?? 'no matching subjects', options.redact, color),
|
|
538
|
+
]);
|
|
539
|
+
}
|
|
540
|
+
const summary = usage.summary();
|
|
541
|
+
if (summary !== undefined) {
|
|
542
|
+
writeWalkthroughBlock(options.write, [
|
|
543
|
+
paint('AI Usage', 'bold', color),
|
|
544
|
+
walkthroughField('verifications', String(summary.verifications), options.redact, color),
|
|
545
|
+
...usageFields(summary).map(([label, value]) => walkthroughField(label, value, options.redact, color)),
|
|
546
|
+
]);
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
export function countViolations(reports) {
|
|
552
|
+
let total = 0;
|
|
553
|
+
for (const report of reports) {
|
|
554
|
+
for (const evaluation of report.evaluations) {
|
|
555
|
+
if (evaluation.result.status === 'violation')
|
|
556
|
+
total += 1;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return total;
|
|
560
|
+
}
|
|
561
|
+
export function renderViolationComments(reports, redact, rules) {
|
|
562
|
+
const overrides = new Map();
|
|
563
|
+
if (rules !== undefined) {
|
|
564
|
+
for (const entry of rules) {
|
|
565
|
+
const comment = entry.rule.comment;
|
|
566
|
+
if (comment !== undefined)
|
|
567
|
+
overrides.set(entry.name, redact(stripVTControlCharacters(comment)));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const safe = (text) => escapeText(redact(stripVTControlCharacters(text)))
|
|
571
|
+
.replace(/[\\`*_{}\[\]()#+\-.!|~]/g, '\\$&')
|
|
572
|
+
.replace(/&/g, '&')
|
|
573
|
+
.replace(/</g, '<')
|
|
574
|
+
.replace(/>/g, '>')
|
|
575
|
+
.replace(/@/g, '@');
|
|
576
|
+
const location = (value) => safe(value.lines === undefined
|
|
577
|
+
? value.filepath
|
|
578
|
+
: `${value.filepath}:${value.lines.start}-${value.lines.end}`);
|
|
579
|
+
const comments = [];
|
|
580
|
+
for (const report of reports) {
|
|
581
|
+
const override = overrides.get(report.name);
|
|
582
|
+
for (const { subject, result } of report.evaluations) {
|
|
583
|
+
if (result.status !== 'violation')
|
|
584
|
+
continue;
|
|
585
|
+
let body;
|
|
586
|
+
if (override !== undefined) {
|
|
587
|
+
body = override;
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
const label = subject.kind === 'scope'
|
|
591
|
+
? `scope ${location(subject)} (${safe(subject.id)})`
|
|
592
|
+
: safe(`group ${subject.group.type} ${subject.group.path} (${subject.members.length} scopes)`);
|
|
593
|
+
const lines = [`- **${safe(report.name)}** — ${label}`];
|
|
594
|
+
if (result.locations !== undefined) {
|
|
595
|
+
for (const entry of result.locations) {
|
|
596
|
+
const scope = entry.scopeId === undefined ? '' : ` (scope ${safe(entry.scopeId)})`;
|
|
597
|
+
lines.push(` - Location: ${location(entry)}${scope}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (result.message !== undefined)
|
|
601
|
+
lines.push(` - ${safe(result.message)}`);
|
|
602
|
+
body = `${lines.join('\n')}\n`;
|
|
603
|
+
}
|
|
604
|
+
const candidates = result.locations
|
|
605
|
+
?? (subject.kind === 'scope' ? [subject] : []);
|
|
606
|
+
const locations = [];
|
|
607
|
+
const seen = new Set();
|
|
608
|
+
for (const candidate of candidates) {
|
|
609
|
+
if (candidate.lines === undefined)
|
|
610
|
+
continue;
|
|
611
|
+
const key = JSON.stringify([candidate.filepath, candidate.lines.start, candidate.lines.end]);
|
|
612
|
+
if (seen.has(key))
|
|
613
|
+
continue;
|
|
614
|
+
seen.add(key);
|
|
615
|
+
locations.push({ filepath: candidate.filepath, lines: candidate.lines });
|
|
616
|
+
}
|
|
617
|
+
comments.push({ body, locations });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return comments;
|
|
621
|
+
}
|
|
622
|
+
function includedReports(reports, outputAll) {
|
|
623
|
+
if (outputAll)
|
|
624
|
+
return reports;
|
|
625
|
+
const included = [];
|
|
626
|
+
for (const report of reports) {
|
|
627
|
+
if (report.status !== 'violation')
|
|
628
|
+
continue;
|
|
629
|
+
included.push({
|
|
630
|
+
...report,
|
|
631
|
+
evaluations: report.evaluations.filter(evaluation => evaluation.result.status === 'violation'),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return included;
|
|
635
|
+
}
|
|
636
|
+
export function renderCheckJson(reports, options) {
|
|
637
|
+
return `${JSON.stringify({
|
|
638
|
+
durationMs: options.durationMs,
|
|
639
|
+
reports: includedReports(reports, options.outputAll === true),
|
|
640
|
+
}, null, 2)}\n`;
|
|
641
|
+
}
|
|
642
|
+
export function renderCheckJsonl(reports, options) {
|
|
643
|
+
let output = '';
|
|
644
|
+
for (const report of includedReports(reports, options.outputAll === true)) {
|
|
645
|
+
output += `${JSON.stringify({ type: 'rule', ...report })}\n`;
|
|
646
|
+
}
|
|
647
|
+
return output;
|
|
648
|
+
}
|
|
649
|
+
function formatDuration(durationMs) {
|
|
650
|
+
if (durationMs >= 86_400_000)
|
|
651
|
+
return `${(durationMs / 86_400_000).toFixed(2)}d`;
|
|
652
|
+
if (durationMs >= 3_600_000)
|
|
653
|
+
return `${(durationMs / 3_600_000).toFixed(2)}h`;
|
|
654
|
+
if (durationMs >= 60_000)
|
|
655
|
+
return `${(durationMs / 60_000).toFixed(2)}min`;
|
|
656
|
+
if (durationMs >= 1_000)
|
|
657
|
+
return `${(durationMs / 1_000).toFixed(2)}s`;
|
|
658
|
+
return `${durationMs.toFixed(2)}ms`;
|
|
659
|
+
}
|
|
660
|
+
export function renderCheckSummary(reports, redact, options) {
|
|
661
|
+
const color = options.color === true;
|
|
662
|
+
const outputAll = options.outputAll === true;
|
|
663
|
+
const safe = (text) => escapeText(redact(text));
|
|
664
|
+
const muted = (text) => paint(text, 'dim', color);
|
|
665
|
+
const name = (text) => paint(safe(text), 'bold', color);
|
|
666
|
+
const skipped = reports.filter(report => report.status === 'skipped');
|
|
667
|
+
const verifications = reports.reduce((total, report) => total + report.evaluations.length, 0);
|
|
668
|
+
const violations = countViolations(reports);
|
|
669
|
+
const lines = [
|
|
670
|
+
[
|
|
671
|
+
`Rules: ${reports.length}`,
|
|
672
|
+
paint(`Skipped: ${skipped.length}`, skipped.length > 0 ? 'yellow' : 'dim', color),
|
|
673
|
+
`Verifications: ${verifications}`,
|
|
674
|
+
paint(`Violations: ${violations}`, violations > 0 ? 'red' : 'green', color),
|
|
675
|
+
`Time: ${formatDuration(options.durationMs)}`,
|
|
676
|
+
].join(' '),
|
|
677
|
+
'',
|
|
678
|
+
];
|
|
679
|
+
if (outputAll && skipped.length > 0) {
|
|
680
|
+
lines.push(paint('Skipped:', 'yellow', color));
|
|
681
|
+
for (const report of skipped) {
|
|
682
|
+
lines.push(` ${name(report.name)}: ${safe(report.reason ?? 'skipped')}`);
|
|
683
|
+
}
|
|
684
|
+
lines.push('');
|
|
685
|
+
}
|
|
686
|
+
const entries = [];
|
|
687
|
+
for (const report of reports) {
|
|
688
|
+
for (const evaluation of report.evaluations) {
|
|
689
|
+
if (!outputAll && evaluation.result.status !== 'violation')
|
|
690
|
+
continue;
|
|
691
|
+
entries.push({
|
|
692
|
+
name: report.name,
|
|
693
|
+
subject: evaluation.subject,
|
|
694
|
+
message: evaluation.result.message,
|
|
695
|
+
status: evaluation.result.status,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (entries.length > 0) {
|
|
700
|
+
lines.push(outputAll ? 'Verifications:' : paint('🚓 Violations:', 'red', color));
|
|
701
|
+
const shown = outputAll ? entries : entries.slice(0, MAX_VIOLATIONS);
|
|
702
|
+
for (const entry of shown) {
|
|
703
|
+
const subject = entry.subject;
|
|
704
|
+
const prefix = entry.status === 'pass' ? `${paint('✓', 'green', color)} ` : '';
|
|
705
|
+
const ruleName = `${prefix}${paint(name(entry.name), entry.status === 'violation' ? 'red' : 'green', color)}`;
|
|
706
|
+
if (subject.kind === 'group') {
|
|
707
|
+
lines.push(` ${ruleName}: ${muted(`group ${subject.group.type} ${safe(subject.group.path)} (${subject.members.length} members)`)}`);
|
|
708
|
+
if (entry.message !== undefined)
|
|
709
|
+
lines.push(` ${safe(entry.message)}`);
|
|
710
|
+
const members = subject.members.slice(0, MAX_GROUP_MEMBERS);
|
|
711
|
+
for (const member of members) {
|
|
712
|
+
lines.push(muted(` ${safe(member.filepath)}${member.lines === undefined ? '' : `:${member.lines.start}-${member.lines.end}`} ${safe(member.id)}`));
|
|
713
|
+
lines.push(...excerptLines(member, MAX_SOURCE_LINES, redact, color, outputAll));
|
|
714
|
+
}
|
|
715
|
+
const hiddenMembers = subject.members.length - members.length;
|
|
716
|
+
if (hiddenMembers > 0) {
|
|
717
|
+
lines.push(muted(` ... ${hiddenMembers} more ${hiddenMembers === 1 ? 'member' : 'members'}; rerun with ${reportFlags(outputAll)} for the complete report.`));
|
|
718
|
+
}
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const location = `${safe(subject.filepath)}${subject.lines === undefined ? '' : `:${subject.lines.start}-${subject.lines.end}`}`;
|
|
722
|
+
lines.push(` ${ruleName}: ${muted(location)}`);
|
|
723
|
+
if (entry.message !== undefined)
|
|
724
|
+
lines.push(` ${safe(entry.message)}`);
|
|
725
|
+
lines.push(...excerptLines(subject, MAX_SOURCE_LINES, redact, color, outputAll));
|
|
726
|
+
}
|
|
727
|
+
if (!outputAll && entries.length > MAX_VIOLATIONS) {
|
|
728
|
+
lines.push(muted(` ${entries.length - MAX_VIOLATIONS} more violations; rerun with ${reportFlags(false)} for the complete report.`));
|
|
729
|
+
}
|
|
730
|
+
lines.push('');
|
|
731
|
+
}
|
|
732
|
+
if (verifications === 0)
|
|
733
|
+
lines.push(paint('No verifications were run.', 'yellow', color));
|
|
734
|
+
return `${lines.join('\n')}\n`;
|
|
735
|
+
}
|