@ngcompass/reporters 0.1.1-beta

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.
@@ -0,0 +1,466 @@
1
+ import { RuleResult, ConfigReport, InitResult, HealthReport, RuleListEntry } from '@ngcompass/common';
2
+ import { CacheInfo } from '@ngcompass/cache';
3
+
4
+ /**
5
+ * Reporter contracts and DTOs for ngcompass CLI output.
6
+ *
7
+ * Design goals:
8
+ * - Tool-agnostic contracts (no ESLint coupling).
9
+ * - Explicit, semantic naming.
10
+ * - Composable interfaces (ISP, DIP).
11
+ * - Clear public API documentation.
12
+ */
13
+
14
+ /** Supported output encodings for analysis results. */
15
+ type ReporterFormat = 'console' | 'json' | 'html' | 'ui' | 'sarif';
16
+ interface ConsoleReporterOptions {
17
+ /**
18
+ * Uses compact, standard one-line-per-violation layout.
19
+ * Suitable for CI environments and editor integration.
20
+ */
21
+ readonly compact?: boolean;
22
+ /**
23
+ * Output file path for the HTML reporter.
24
+ * Defaults to `ngcompass-report.html` in the current working directory.
25
+ * Only used when `ReporterFormat` is `'html'` or `'ui'`.
26
+ */
27
+ readonly outputPath?: string;
28
+ /**
29
+ * TTY stream used for in-place phase updates.
30
+ * Defaults to process.stdout. Override to process.stderr for non-console formats.
31
+ */
32
+ readonly phaseStream?: NodeJS.WriteStream;
33
+ /** Suppress violation details — show only the summary counts. */
34
+ readonly quiet?: boolean;
35
+ /** Suppress fix/recommendation text from violation output. */
36
+ readonly noRecommendation?: boolean;
37
+ }
38
+ /**
39
+ * Summary statistics produced at the end of an analysis run.
40
+ *
41
+ * Consumed by `ProgressReporter.summary()`.
42
+ */
43
+ interface ResultSummary {
44
+ /** Total number of source files that were scanned (passed to the planner). */
45
+ readonly scannedFiles: number;
46
+ /** Total files found by the scanner (all file types, including HTML/SCSS/JSON). */
47
+ readonly discoveredFiles?: number;
48
+ /** Files that contained at least one violation. */
49
+ readonly totalFiles: number;
50
+ readonly totalTasks: number;
51
+ readonly cachedTasks?: number;
52
+ readonly totalErrors: number;
53
+ readonly totalWarnings: number;
54
+ readonly failOnSeverity?: 'warn' | 'error';
55
+ readonly maxWarnings?: number;
56
+ readonly duration: number;
57
+ }
58
+ /**
59
+ * Core analysis reporter contract.
60
+ *
61
+ * @remarks
62
+ * Implementations receive analysis results and surface them to the user.
63
+ * `report()` must handle an empty array gracefully (emit a "no violations" message).
64
+ * None of the methods should throw — errors are surfaced via `error()`.
65
+ */
66
+ interface AnalysisReporter {
67
+ /**
68
+ * Renders all rule results.
69
+ * @param results - May be empty; reporters must handle this gracefully.
70
+ */
71
+ report(results: ReadonlyArray<RuleResult>): void;
72
+ /**
73
+ * Called when an unexpected runtime failure occurs during analysis.
74
+ * @param error - The caught exception. Implementations should surface both
75
+ * `error.message` and (optionally) `error.stack`.
76
+ */
77
+ error(error: Error): void;
78
+ /**
79
+ * Called when one or more files could not be parsed or analyzed.
80
+ * @param errors - Array of parse failures; implementations should no-op on empty arrays.
81
+ */
82
+ parseErrors(errors: ReadonlyArray<ParseError>): void;
83
+ }
84
+ /**
85
+ * Progressive reporting contract for interactive / TTY environments.
86
+ *
87
+ * @remarks
88
+ * Non-interactive reporters (e.g. `JsonReporter`) implement this with no-ops.
89
+ * Callers that only need analysis output should accept `AnalysisReporter`, not `Reporter`.
90
+ */
91
+ interface ProgressReporter {
92
+ /** Emits a bold status step, e.g. "Analyzing 42 files…". */
93
+ summary(stats: ResultSummary): void;
94
+ /** Emits a high-visibility progress message. */
95
+ step(message: string): void;
96
+ /** Emits a dim informational message. */
97
+ info(message: string): void;
98
+ /** Emits a debug message; may be suppressed unless verbose mode is active. */
99
+ debug(message: string): void;
100
+ /** Erases the current in-place phase line (TTY only). No-op on non-TTY. */
101
+ clearLine?(): void;
102
+ }
103
+ /**
104
+ * Combined reporter contract expected by the CLI orchestrator.
105
+ *
106
+ * @remarks
107
+ * Prefer the narrower `AnalysisReporter` or `ProgressReporter` types for
108
+ * functions that only need one facet, to avoid forcing callers to satisfy
109
+ * the full combined surface.
110
+ */
111
+ type Reporter = AnalysisReporter & ProgressReporter;
112
+ /**
113
+ * Reporter contract for configuration-related commands (`compass config`, `compass health`).
114
+ *
115
+ * Separated from `Reporter` because config commands do not produce `RuleResult` output
116
+ * and must not be forced to implement the full analysis surface.
117
+ */
118
+ interface ConfigReporter {
119
+ /**
120
+ * Renders full config validation output.
121
+ * @param report - The validation result from the config loader.
122
+ */
123
+ reportConfig(report: ConfigReport): void;
124
+ /**
125
+ * Renders the outcome of `compass init`.
126
+ * @param result - The init operation result (success, already-exists, or failure).
127
+ */
128
+ renderInitResult(result: InitResult): void;
129
+ /**
130
+ * Renders the full project health report.
131
+ * @param report - Aggregated health issues, one per config concern.
132
+ */
133
+ renderHealthReport(report: HealthReport): void;
134
+ }
135
+ /**
136
+ * Represents a file that could not be parsed during analysis.
137
+ */
138
+ interface ParseError {
139
+ readonly filePath: string;
140
+ readonly message: string;
141
+ }
142
+ /**
143
+ * A single diagnostic message in structured JSON format.
144
+ *
145
+ * Severity encoding:
146
+ * - `2` → error (corresponds to `isErrorSeverity() === true`)
147
+ * - `1` → warning
148
+ *
149
+ * Named constants (`JSON_SEVERITY_ERROR`, `JSON_SEVERITY_WARNING`) live in
150
+ * `json-reporter.ts` to avoid exposing implementation details in the public type.
151
+ */
152
+ interface DiagnosticMessage {
153
+ readonly ruleId: string;
154
+ readonly severity: 1 | 2;
155
+ readonly message: string;
156
+ readonly line: number;
157
+ readonly column: number;
158
+ }
159
+ /**
160
+ * Per-file group of diagnostic messages.
161
+ *
162
+ * Produced by `JsonReporter` to allow downstream tooling (e.g. reviewdog,
163
+ * other formatters) to consume ngcompass output without coupling to its
164
+ * internal `RuleResult` shape.
165
+ */
166
+ interface FileDiagnosticResult {
167
+ readonly filePath: string;
168
+ readonly messages: readonly DiagnosticMessage[];
169
+ readonly errorCount: number;
170
+ readonly warningCount: number;
171
+ }
172
+ /**
173
+ * Reporter contract for cache-related commands (`compass cache`).
174
+ */
175
+ interface CacheReporter {
176
+ /**
177
+ * Renders the clear cache result.
178
+ * @param type - The type of cache cleared (ast, config, results, all).
179
+ */
180
+ renderClearResult(type: 'ast' | 'config' | 'results' | 'all'): void;
181
+ /**
182
+ * Renders cache statistics and information.
183
+ * @param info - The cache information object.
184
+ */
185
+ renderCacheInfo(info: CacheInfo): void;
186
+ }
187
+
188
+ interface RulesReporterOptions {
189
+ preset?: string;
190
+ }
191
+ declare class RulesReporter {
192
+ private readonly options;
193
+ constructor(options?: RulesReporterOptions);
194
+ render(entries: RuleListEntry[]): void;
195
+ renderSingleRule(rule: RuleListEntry): void;
196
+ }
197
+
198
+ declare function getReporter(format?: ReporterFormat, options?: ConsoleReporterOptions): Reporter;
199
+ declare function getConfigReporter(): ConfigReporter;
200
+ declare function getCacheReporter(): CacheReporter;
201
+ declare function getRulesReporter(options?: RulesReporterOptions): RulesReporter;
202
+
203
+ /**
204
+ * Core output abstraction used by every reporter.
205
+ *
206
+ * Why this interface exists:
207
+ * - Decouples reporters from Node.js `process` globals.
208
+ * - Enables deterministic unit tests via in-memory implementations.
209
+ * - Opens the door to future output targets (files, network, IDE protocol).
210
+ *
211
+ * Contract:
212
+ * - `write` → standard output (stdout in the default implementation).
213
+ * - `error` → diagnostic / error output (stderr in the default implementation).
214
+ * - Neither method should throw under normal operation.
215
+ */
216
+ interface ReporterOutput {
217
+ write(line: string): void;
218
+ error(line: string): void;
219
+ }
220
+ /**
221
+ * Process-backed output singleton, wired to `process.stdout` / `process.stderr`.
222
+ *
223
+ * Why a singleton constant instead of a factory function:
224
+ * - Reporters are created once per analysis run; a singleton avoids repeated
225
+ * allocation with identical behaviour.
226
+ * - Makes the default explicit at call-sites without verbosity.
227
+ *
228
+ * Do NOT use in tests — use `createTestOutput()` for hermetic assertions.
229
+ */
230
+ declare const processOutput: ReporterOutput;
231
+ /**
232
+ * Captured output produced by a reporter under test.
233
+ *
234
+ * `lines` — every call to `output.write()`
235
+ * `errors` — every call to `output.error()`
236
+ *
237
+ * Arrays are live references; assertions can be made at any point after writes.
238
+ */
239
+ interface TestOutput {
240
+ readonly output: ReporterOutput;
241
+ readonly lines: readonly string[];
242
+ readonly errors: readonly string[];
243
+ }
244
+ /**
245
+ * Creates an in-memory output capture for use in unit tests.
246
+ *
247
+ * Usage:
248
+ * ```typescript
249
+ * const out = createTestOutput();
250
+ * const reporter = new ConsoleReporter(out.output);
251
+ * reporter.report(results);
252
+ * expect(out.lines).toContain('...');
253
+ * ```
254
+ *
255
+ * Call `createTestOutput()` in `beforeEach` to get a fresh capture per test,
256
+ * preventing cross-test pollution.
257
+ */
258
+ declare function createTestOutput(): TestOutput;
259
+
260
+ /**
261
+ * Abstraction over file-system source reading.
262
+ *
263
+ * Providing this interface allows callers (reporters) to inject a test double
264
+ * without touching the module-level cache, keeping each test hermetic and
265
+ * avoiding real filesystem access during unit tests.
266
+ */
267
+ interface SourceReader {
268
+ readLines(filePath: string): string[];
269
+ }
270
+ /**
271
+ * Reads a source file and splits it into lines.
272
+ *
273
+ * Returns an empty array when the file cannot be read (permissions, not found, etc.).
274
+ * Results are cached for the lifetime of the process (see `sourceCache`).
275
+ *
276
+ * @param filePath - Absolute path to the source file.
277
+ * @returns The file split into lines, or an empty array on read failure.
278
+ */
279
+ declare function readSourceLines(filePath: string): string[];
280
+ /** Clears the in-memory source cache. Call in `beforeEach` for hermetic tests. */
281
+ declare function clearSourceCache(): void;
282
+ /**
283
+ * Default SourceReader backed by the process-lifetime filesystem cache.
284
+ * Inject a different implementation in tests to avoid real filesystem access.
285
+ */
286
+ declare const defaultSourceReader: SourceReader;
287
+ /**
288
+ * Renders a code frame for a single violation using @babel/code-frame.
289
+ *
290
+ * @param lines - All source lines for the file (from `readSourceLines`).
291
+ * @param targetLine - 1-indexed line number of the violation.
292
+ * @param _targetColumn - 1-indexed column number of the violation (ignored for full-line highlight).
293
+ * @param filePath - Optional path to determine language highlighting.
294
+ * @returns Coloured frame strings ready to be written to the output.
295
+ */
296
+ declare function renderCodeFrame(lines: string[], targetLine: number, _targetColumn: number, filePath?: string): string[];
297
+
298
+ declare class TextConfigReporter implements ConfigReporter {
299
+ private readonly out;
300
+ constructor(out?: ReporterOutput);
301
+ /**
302
+ * Renders configuration validation errors.
303
+ * Only produces output when the config is invalid; succeeds silently.
304
+ *
305
+ * @param report - The validation result from the config loader.
306
+ */
307
+ reportConfig(report: ConfigReport): void;
308
+ /**
309
+ * Renders the outcome of `compass init`.
310
+ *
311
+ * @param result - The init operation result (success, already-exists, or failure).
312
+ */
313
+ renderInitResult(result: InitResult): void;
314
+ /**
315
+ * Renders the full project health report.
316
+ *
317
+ * @param report - Aggregated health issues, one per config concern.
318
+ */
319
+ renderHealthReport(report: HealthReport): void;
320
+ private renderIssues;
321
+ private renderSummary;
322
+ }
323
+
324
+ declare class TextCacheReporter implements CacheReporter {
325
+ renderClearResult(type: 'ast' | 'config' | 'results' | 'all'): void;
326
+ renderCacheInfo(info: CacheInfo): void;
327
+ private printInfoRow;
328
+ private formatSize;
329
+ }
330
+
331
+ declare class ConsoleReporter implements Reporter {
332
+ private readonly out;
333
+ private readonly compact;
334
+ private readonly quiet;
335
+ private readonly noRecommendation;
336
+ private readonly sourceReader;
337
+ private readonly cwd;
338
+ private lastSummary?;
339
+ constructor(out?: ReporterOutput, options?: ConsoleReporterOptions,
340
+ /**
341
+ * Injected source reader — override in tests to avoid real filesystem access.
342
+ * Defaults to the process-lifetime fs cache provided by code-frame.ts.
343
+ */
344
+ sourceReader?: SourceReader,
345
+ /**
346
+ * Working directory used to compute relative file paths in the output.
347
+ * Injected (rather than read via `process.cwd()` at call-time) to keep
348
+ * the reporter deterministic and hermetically testable without patching globals.
349
+ *
350
+ * @default process.cwd() — evaluated once at construction, not per-render.
351
+ */
352
+ cwd?: string);
353
+ /**
354
+ * Renders all rule results to the output.
355
+ *
356
+ * @param results - May be empty; a "no violations" message is emitted in that case.
357
+ */
358
+ report(results: ReadonlyArray<RuleResult>): void;
359
+ summary(stats: ResultSummary): void;
360
+ error(error: Error): void;
361
+ step(message: string): void;
362
+ info(message: string): void;
363
+ clearLine(): void;
364
+ /**
365
+ * No-op: debug logging is not handled by the reporter.
366
+ */
367
+ debug(_message: string): void;
368
+ /**
369
+ * Surfaces parse failures. No-ops gracefully on an empty array.
370
+ *
371
+ * @param errors - Files that could not be parsed during analysis.
372
+ */
373
+ parseErrors(errors: ReadonlyArray<ParseError>): void;
374
+ /** Flattens results into a single typed failure list (no narrowing needed later). */
375
+ private extractAllFailures;
376
+ /**
377
+ * Dispatches rendering to either compact or rich mode and appends the
378
+ * summary divider (rich mode only). Separating dispatch from rendering
379
+ * satisfies SRP and makes each mode independently testable.
380
+ *
381
+ * @param total - Total failure count; threaded through to rich-mode cards
382
+ * so each card's indexed separator can display `[X/TOTAL]`.
383
+ */
384
+ private renderFileBlocks;
385
+ private renderCompactBlocks;
386
+ private renderCompactFileBlock;
387
+ private renderRichBlocks;
388
+ /**
389
+ * Renders all failure cards for a single file.
390
+ *
391
+ * Returns the updated global index so the caller can pass it to the next file
392
+ * without relying on external mutable state.
393
+ */
394
+ private renderRichFileBlock;
395
+ /**
396
+ * Renders one failure card in the new compact style:
397
+ *
398
+ * ────────────────────────────────────[3/33]─
399
+ * ERROR src\app\violations\rule01.ts:33:5
400
+ * × Avoid manual change detection component-no-manual-detect-changes
401
+ *
402
+ * 31 │ this.counter++;
403
+ * > 33 │ this.cdr.detectChanges();
404
+ * ^^^^^^^^^^^^^^^^^^^^^^
405
+ * 35 │ }
406
+ *
407
+ * i Add changeDetection: ChangeDetectionStrategy.OnPush
408
+ *
409
+ * Layout decisions vs the old design:
410
+ * - Indexed separator `[X/TOTAL]` replaces the plain `────` divider and
411
+ * the `1 ██ ERROR ██ rule-name` header line — fewer total lines.
412
+ * - Badge + filepath:line:col on one line (no separate `--> path` line).
413
+ * - Message + rule name on one line (dimmed rule name on the right).
414
+ * - Fix text appears immediately after the code frame (no extra blank line
415
+ * between frame and fix when fix is absent — card ends cleanly).
416
+ */
417
+ private renderRichCard;
418
+ private renderCardCodeFrame;
419
+ private renderCardRecommendation;
420
+ }
421
+
422
+ declare class JsonReporter implements Reporter {
423
+ private readonly out;
424
+ private readonly accumulatedResults;
425
+ private readonly accumulatedParseErrors;
426
+ constructor(out?: ReporterOutput);
427
+ report(results: ReadonlyArray<RuleResult>): void;
428
+ summary(stats: ResultSummary): void;
429
+ error(err: Error): void;
430
+ step(_message: string): void;
431
+ info(_message: string): void;
432
+ debug(_message: string): void;
433
+ parseErrors(errors: ReadonlyArray<ParseError>): void;
434
+ }
435
+
436
+ declare class HtmlReporter implements Reporter {
437
+ private readonly outputPath;
438
+ private readonly out;
439
+ private readonly autoOpen;
440
+ private readonly accumulatedResults;
441
+ private readonly accumulatedParseErrors;
442
+ constructor(outputPath?: string, out?: ReporterOutput, autoOpen?: boolean);
443
+ report(results: ReadonlyArray<RuleResult>): void;
444
+ parseErrors(errors: ReadonlyArray<ParseError>): void;
445
+ error(error: Error): void;
446
+ summary(stats: ResultSummary): void;
447
+ step(_message: string): void;
448
+ info(_message: string): void;
449
+ debug(_message: string): void;
450
+ }
451
+
452
+ declare class SarifReporter implements Reporter {
453
+ private readonly out;
454
+ private readonly parseErrorBuffer;
455
+ private readonly resultBuffer;
456
+ constructor(out?: ReporterOutput);
457
+ report(results: ReadonlyArray<RuleResult>): void;
458
+ parseErrors(errors: ReadonlyArray<ParseError>): void;
459
+ error(error: Error): void;
460
+ summary(stats: ResultSummary): void;
461
+ step(_message: string): void;
462
+ info(_message: string): void;
463
+ debug(_message: string): void;
464
+ }
465
+
466
+ export { type AnalysisReporter, type CacheReporter, type ConfigReporter, ConsoleReporter, type ConsoleReporterOptions, type DiagnosticMessage, type FileDiagnosticResult, HtmlReporter, JsonReporter, type ParseError, type ProgressReporter, type Reporter, type ReporterFormat, type ReporterOutput, type ResultSummary, RulesReporter, type RulesReporterOptions, SarifReporter, type SourceReader, type TestOutput, TextCacheReporter, TextConfigReporter, clearSourceCache, createTestOutput, defaultSourceReader, getCacheReporter, getConfigReporter, getReporter, getRulesReporter, processOutput, readSourceLines, renderCodeFrame };