rinkaku 0.6.6

Condense PR diffs into signatures and their dependencies for LLM-friendly review
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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
140
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
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
//! Composition root for the `rinkaku` binary.
//!
//! This is the only place allowed to know about the concrete CLI wiring.
//! It stays a thin entry point: parse arguments, obtain the diff text
//! (stdin, `git diff`, or a resolved PR), read changed files, and
//! dispatch to the pure core in `lib.rs` (`pipeline::analyze_diff`,
//! `render::render`).
//!
//! The file-reading port passed to `analyze_diff` differs by input mode:
//!
//! - `--base` mode: the diff comes from `git diff <base>...<head>`, so
//!   files are read via `git show <head>:<path>` rather than off the
//!   working tree. This keeps the diff and the file content read from the
//!   exact same commit by construction, regardless of what the working
//!   tree currently holds (uncommitted changes, a dirty checkout, etc.).
//! - `--pr` mode (ADR 0004, ADR 0005, ADR 0006, ADR 0007): the PR's base
//!   branch, base commit (`baseRefOid`), and head commit are resolved via
//!   `gh pr view`; the head is fetched with `git fetch`, and the base
//!   commit is resolved via `baseRefOid` rather than the base branch's
//!   current tip (ADR 0007) — this is what makes `--pr` work on a merged
//!   PR, whose base branch has since advanced past the PR's own commits.
//!   The resulting base/head SHAs are handed to exactly the same
//!   `git show`-backed read strategy as `--base` mode — `--pr` is a
//!   resolution step in front of the `--base` pipeline, not a separate
//!   read strategy. A bare PR number requires running inside a local
//!   clone of the target repository. A PR URL also uses the current
//!   directory when its `origin` matches the URL's repository; otherwise
//!   it prefers an existing `ghq`-managed clone of the repository when one
//!   is found (ADR 0006), and only falls back to auto-cloning a blobless
//!   partial clone into a per-repository cache directory (ADR 0005) if
//!   neither the cwd nor `ghq` has one — so URL input works from any
//!   directory either way. `gh` must be installed and authenticated
//!   either way.
//! - stdin mode: the diff's provenance is unknown to rinkaku (it could be
//!   `gh pr diff`, a saved patch file, anything). Files are read off the
//!   working tree, under the assumption that **the diff is consistent
//!   with the current working tree** — i.e. applying it (or having
//!   already applied it) would reproduce the working tree's content. If
//!   that assumption doesn't hold, line numbers in the extracted symbols
//!   may not line up with the actual file content.

mod cli;
mod display;
mod generated_paths;
mod git;
mod github;
mod log_writer;
mod notes;
mod pipeline;
mod progress;
mod self_update;
mod spinner;
mod splash_progress;

#[cfg(test)]
mod test_util;

use clap::Parser;
use cli::{Cli, Command};
use display::{DisplayMode, resolve_display_mode};
use generated_paths::check_generated_paths_batch;
use git::commands::{list_repo_files_for_outline, resolve_repo_root};
use git::file_read::read_working_tree_file;
use github::base_sha::{
    fetch_branch_head, fetch_oid, fetch_pr_head, object_exists_locally, resolve_pr_base_sha,
};
use github::pr_arg::parse_pr_arg;
use github::pr_info::fetch_pr_info;
use github::workdir::resolve_pr_workdir;
use log_writer::DeferredLogSink;
use notes::{
    apply_entry_pivot, entry_pivot_empty_note, garbage_input_note, repo_outline_empty_note,
};
use pipeline::{
    build_resolver, changed_paths, read_stdin_diff, resolve_generated_paths, run_base_pipeline,
};
use progress::AnalysisProgress;
use rinkaku_core::render::{Report, render};
use rinkaku_tui::TuiSession;
use spinner::{AnalysisPhase, Spinner};
use splash_progress::SplashProgress;
use std::io::IsTerminal;
use std::path::PathBuf;

/// Shared `env_logger` setup for every display mode: `info`-level default
/// (env_logger's own default is error-only, which meant `--pr`/`--base`
/// runs — the ones slow enough to want a heartbeat, see the
/// dependency-index build below — gave no feedback at all while running;
/// `RUST_LOG` still overrides this). Timestamp and module path are
/// dropped: this is a short-lived one-shot CLI, so there is nothing to
/// correlate a timestamp against, and the binary is a single crate, making
/// the module path redundant.
fn logger_builder() -> env_logger::Builder {
    let mut builder =
        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"));
    builder.format_timestamp(None).format_target(false);
    builder
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    if let Some(Command::SelfUpdate { yes }) = cli.command {
        // Non-TUI: logs straight to stderr like every other subcommand did
        // before display-mode resolution was moved ahead of logger init.
        logger_builder().init();
        return self_update::run_self_update(yes);
    }

    // ADR 0033: the display mode is decided *before* analysis runs, not
    // after a `Report` already exists — `resolve_display_mode` only
    // depends on `cli.tui`/`cli.format`/whether stdout is a terminal, none
    // of which depend on a `Report`, so this ordering was always available
    // and is what lets the `DisplayMode::Tui` branch below enter the
    // alternate screen and start drawing a splash screen before the
    // pipeline's first byte of work runs, instead of only after it
    // finishes. Determining `display_mode` before `logger_builder().init()`
    // (ADR 0033 amendment) is what lets the `Tui` branch route the logger
    // through a deferring sink from the very first log call, instead of
    // racing the alternate-screen switch against whichever log record
    // fires first.
    let stdout_is_tty = std::io::stdout().is_terminal();
    let display_mode = resolve_display_mode(cli.tui, cli.format, stdout_is_tty);

    match display_mode {
        DisplayMode::Tui => {
            // ADR 0033 amendment: `log::` records bypass `AnalysisProgress`
            // entirely, so they need their own deferral mechanism — a
            // `DeferredLogSink` buffers every record until `release` is
            // called below, once the alternate screen has actually torn
            // down, mirroring `SplashProgress`'s buffered-notes handling
            // for the same underlying bug (raw bytes landing mid-redraw).
            let log_sink = DeferredLogSink::new();
            logger_builder()
                .target(env_logger::Target::Pipe(Box::new(log_sink.clone())))
                .init();
            // Declared before `TuiSession::init` so Rust's drop order (LIFO)
            // runs `session`'s terminal-restoring `Drop` before this guard's
            // log-release `Drop`, on *any* unwind past this point — a panic
            // inside `run_analysis`/`TuiSession::run`, or an early `?`
            // return from `TuiSession::init`/`draw_splash` below, neither of
            // which reaches the explicit `release_log_sink` calls further
            // down. Those explicit calls still run first on the normal exit
            // paths (`release` is idempotent, see `DeferredLogSink::release`),
            // giving deterministic logs-before-notes ordering there; this
            // guard is purely the safety net for the paths that skip them.
            let _log_sink_guard = log_writer::ReleaseGuard::new(log_sink.clone(), std::io::stderr);

            // No stderr spinner in this branch (ADR 0033 decision 1): the
            // splash screen drawn on the alternate screen is this run's
            // only progress feedback, replacing it rather than layering on
            // top of it.
            let mut session = TuiSession::init()?;
            session.draw_splash(&rinkaku_tui::splash::SplashState::label_only(
                spinner::phase_message(AnalysisPhase::Starting),
            ))?;
            let progress = SplashProgress::new(session);
            let outcome = run_analysis(&cli, &progress).map(|analyzed| {
                // `finish_report` is called *before* `into_session_and_notes`
                // below, while `progress` is still the active
                // `AnalysisProgress` — its own `--entry`-empty note (ADR
                // 0019) must go through the same buffering `note` does for
                // every other advisory message in this branch, or it would
                // reintroduce exactly the raw-bytes-mid-redraw bug this ADR
                // amendment fixes, just for one more call site.
                let report = finish_report(&cli, &progress, analyzed.report);
                (report, analyzed.diff_text, analyzed.resolved_workdir)
            });
            let (session, buffered_notes) = progress.into_session_and_notes();
            let (report, diff_text, resolved_workdir) = match outcome {
                Ok(analyzed) => analyzed,
                Err(err) => {
                    // `session` (and with it, `TuiSession`'s `Drop` impl)
                    // is dropped right here, before `err` propagates past
                    // this function — restoring the terminal ahead of
                    // `main`'s `anyhow` error path printing the failure to
                    // stderr, exactly like `rinkaku_tui::run`'s pre-ADR-0033
                    // `EnableMouseCapture`-failure branch already did for
                    // its own early-return case. `buffered_notes` are
                    // flushed here too (before the terminal-restoring drop
                    // completes, but after — `flush_notes` is plain
                    // `eprintln!`, so ordering against the drop itself
                    // doesn't matter for correctness, only that this runs
                    // after the alternate screen is torn down, which
                    // dropping `session` right below guarantees) so a note
                    // buffered before the error (e.g. `used_fallback`'s
                    // warning, ADR 0033) is not silently lost on an
                    // early-return failure.
                    drop(session);
                    release_log_sink(&log_sink);
                    flush_notes(buffered_notes);
                    return Err(err);
                }
            };
            let repo_root = resolve_repo_root(resolved_workdir.as_deref());
            let result = session
                .run(&report, &diff_text, cli.entry.as_deref(), &repo_root)
                .map_err(anyhow::Error::from);
            // Flushed after `TuiSession::run` has already restored the
            // terminal (its own postamble, unconditional on both the `Ok`
            // and `Err` paths — see that method's doc comment) — this is
            // the whole point of buffering in the first place: every note
            // accumulated during analysis (empty diff, garbage input, an
            // `--entry` path matching nothing, the PR base-commit
            // fallback) now reaches stderr as clean, ordinary text once
            // the alternate screen is gone, instead of corrupting a splash
            // or entry-screen frame mid-redraw.
            release_log_sink(&log_sink);
            flush_notes(buffered_notes);
            result
        }
        DisplayMode::Output(format) => {
            // Non-TUI: no alternate screen ever opens, so `log::` output
            // goes straight to stderr, same as it always has.
            logger_builder().init();

            // Started before any branch below runs and cleared right after
            // the pipeline finishes (`spinner.finish_and_clear()`), so the
            // whole synchronous analysis phase — the only part of a run
            // with no per-symbol feedback of its own — gets a visible
            // heartbeat on stderr. `Spinner::start` is a no-op-looking
            // wrapper around `indicatif`, whose stderr draw target already
            // suppresses drawing when stderr isn't a terminal (see
            // `spinner.rs`'s own doc comment), so this is safe to run
            // unconditionally in every non-TUI input mode, including piped
            // stderr.
            let spinner = Spinner::start(spinner::phase_message(AnalysisPhase::Starting));
            let analyzed = run_analysis(&cli, &spinner)?;
            // Cleared as soon as the `Report` is built, before the
            // `--entry` pivot (pure/instant) and the render call below.
            spinner.finish_and_clear();

            // Unaffected by ADR 0033's note-deferral amendment: `Spinner`
            // leaves `AnalysisProgress::note` at its default (immediate
            // `eprintln!`), since stderr is not being drawn over by
            // anything in this display mode — every note in this branch
            // still reaches stderr the instant it is produced, same as
            // before this ADR existed.
            let report = finish_report(&cli, &spinner, analyzed.report);
            let output = render(&report, format.into())?;
            print!("{output}");
            Ok(())
        }
    }
}

/// Prints every buffered note (ADR 0033's note-deferral amendment) to
/// stderr, in the order [`AnalysisProgress::note`] received them — the
/// flush half of `--tui` mode's buffer-then-flush strategy, called only
/// after the terminal has actually left the alternate screen (both of this
/// function's two call sites in `main` are positioned that way; see each
/// one's own comment).
fn flush_notes(notes: Vec<String>) {
    for note in notes {
        eprintln!("{note}");
    }
}

/// Releases a `--tui`-mode [`DeferredLogSink`] to stderr — the `log::`
/// counterpart of [`flush_notes`]. Called explicitly at `main`'s two normal
/// exit points, in the same position as each `flush_notes` call, so logs
/// drain before notes in a fixed order on those paths (`release` is
/// idempotent, so this is safe even though `_log_sink_guard`'s `Drop` will
/// also release the same sink later). Paths that skip these explicit calls
/// (a panic, or an early `?` return before either is reached) still get
/// their buffered records drained by that guard.
fn release_log_sink(sink: &DeferredLogSink<std::io::Stderr>) {
    // A failed `write_all`/`flush` to stderr here has nowhere left to be
    // reported (the process is already on its way out in every call site),
    // so it is dropped rather than propagated — same judgment call
    // `flush_notes`'s own `eprintln!` already makes implicitly.
    let _ = sink.release(std::io::stderr());
}

/// The result of [`run_analysis`]: a built [`Report`], the raw diff text
/// (empty for the whole-repo-outline branch, ADR 0017), and the resolved
/// working directory (`--pr`'s own clone/cache directory, `None` for every
/// other input mode) — grouped into a named struct rather than a tuple so
/// each field keeps its name at the one call site that destructures all
/// three (a positional tuple invites a field-order mix-up the first time
/// this return shape is touched, the same reasoning
/// `rinkaku_tui::DiffPaneSelectionEffects` documents for itself).
struct AnalyzedReport {
    report: Report,
    diff_text: String,
    resolved_workdir: Option<PathBuf>,
}

/// Runs the same `--pr`/`--base`/stdin/whole-repo input-mode chain
/// `main` always has, reporting progress through `progress` (ADR 0033) —
/// `&dyn AnalysisProgress` rather than a concrete `Spinner`/`SplashProgress`
/// so this one function serves both `DisplayMode`s in `main` without
/// duplicating the chain itself. Extracted out of `main` specifically so
/// the `DisplayMode::Tui` branch there can call it with a live
/// `SplashProgress` sitting in between `TuiSession::init` and
/// `TuiSession::run`, while the non-TUI branch calls it with a `Spinner` —
/// the input-mode logic itself does not need to know which one it got.
fn run_analysis(cli: &Cli, progress: &dyn AnalysisProgress) -> anyhow::Result<AnalyzedReport> {
    // Tracks the same `cwd`/`workdir` each branch below already resolves for
    // its own `git`/`gh` calls, so the TUI's source view (`repo_root`,
    // `main`'s own use of this result) reads files from the repository the
    // `Report` was actually built from rather than always the process's
    // current directory — `--pr` in particular can run entirely against a
    // ghq/cache clone elsewhere on disk (`resolve_pr_workdir`), and
    // `resolve_repo_root(None)` would silently resolve the *process's* repo
    // instead, showing an unrelated file if one happens to exist at the
    // same relative path there.
    let mut resolved_workdir: Option<std::path::PathBuf> = None;
    let (report, diff_text) = if let Some(pr_arg) = &cli.pr {
        // Validate the arg and derive the fetch refspec's PR number, but
        // pass the original (trimmed) value — not the parsed number — to
        // `gh pr view` (see that function's doc comment for why).
        let parsed = parse_pr_arg(pr_arg)?;
        let number = parsed.number();
        progress.set_phase(AnalysisPhase::ResolvingPr);
        let workdir = resolve_pr_workdir(&parsed)?;
        resolved_workdir = workdir.clone();
        // ADR 0033: downgraded from `log::info!` to `log::debug!` — this
        // and the sibling milestone lines below duplicate what the
        // spinner/splash's own phase label already shows on every run
        // (ADR 0032 already made the same call for the lines that overlap
        // with `phase_message`'s own text).
        log::debug!("resolving PR #{number} via gh");
        let pr_info = fetch_pr_info(pr_arg.trim())?;
        let cwd = workdir.as_deref();
        log::debug!("fetching PR #{number} head");
        let head_sha = fetch_pr_head(number, cwd)?;
        if head_sha != pr_info.head_ref_oid {
            anyhow::bail!(
                "fetched PR #{number} head ({head_sha}) does not match `gh`'s reported head \
                 ({expected}); this usually means the PR belongs to a different repository than \
                 the target clone's `origin` remote, or the PR was updated between resolving it \
                 and fetching it — verify `origin` points at the PR's repository and re-run",
                expected = pr_info.head_ref_oid,
            );
        }
        log::debug!("resolving PR #{number} base commit");
        let (base_sha, used_fallback) = resolve_pr_base_sha(
            &pr_info.base_ref_oid,
            |oid| object_exists_locally(cwd, oid),
            || fetch_branch_head(&pr_info.base_ref_name, cwd),
            |oid| fetch_oid(cwd, oid),
        )?;
        if used_fallback {
            // ADR 0033: routed through `progress.note` rather than a bare
            // `log::warn!` (which — like every other stderr write this
            // function used to make directly — would otherwise interleave
            // raw bytes into the TUI's alternate-screen frame stream mid-
            // redraw; see `AnalysisProgress::note`'s own doc comment for
            // the dynamic-verification finding that drove this).
            progress.note(format!(
                "warning: could not resolve PR #{number}'s base commit ({base_oid}) locally; \
                 falling back to the current tip of {base_branch}, which may not reproduce the \
                 original PR diff for a merged PR",
                base_oid = pr_info.base_ref_oid,
                base_branch = pr_info.base_ref_name,
            ));
        }
        run_base_pipeline(cli, &base_sha, &head_sha, cwd, progress)?
    } else if let Some(base) = &cli.base {
        run_base_pipeline(cli, base, &cli.head, None, progress)?
    } else if std::io::stdin().is_terminal() {
        // ADR 0017: this is the third arm of an `if let Some(pr) ... else if
        // let Some(base) ... else if <here>` chain, so reaching it already
        // means `cli.pr` and `cli.base` are both `None` — no need to check
        // again. With no `--base`/`--pr` and stdin attached to a terminal,
        // there is no diff to read at all, so a whole-repo outline is built
        // instead of falling through to `read_stdin_diff`'s "no diff input"
        // error. `diff_text` is empty: the TUI's diff pane (`d`) has nothing
        // to slice hunks out of in this mode and falls back to its
        // placeholder (ADR 0017's Consequences).
        //
        // `read_stdin_diff`'s own `is_terminal()` bail is unreachable via
        // this chain today (every stdin-is-a-TTY case is caught here first),
        // but is kept as a defensive check in case this `if`/`else if` chain
        // is ever restructured — e.g. a future flag added between this arm
        // and the plain stdin-read fallback below.
        log::debug!("no diff input and stdin is a terminal; building a whole-repo outline");
        progress.set_phase(AnalysisPhase::ParsingRepository);
        let paths = list_repo_files_for_outline(None)?;
        // `check_generated_paths_batch`, not `resolve_generated_paths`
        // (which shells out via `check_generated_paths`'s CLI-argument
        // form): `paths` here is every tracked file, potentially far more
        // than a diff's changed-path count, and passing thousands of paths
        // as CLI arguments risks exceeding the OS's `ARG_MAX` — the same
        // reason `build_resolver` already uses the batch/stdin form for
        // its own repo-wide scan (see that function's doc comment).
        let generated_paths = if cli.include_generated {
            std::collections::HashSet::new()
        } else {
            check_generated_paths_batch(None, &paths)
        };
        // ADR 0033: reports `(files_done, total)` back through `progress`
        // as `analyze_repo`'s rayon-parallel loop works through `paths` —
        // see `rinkaku_core::progress::OnProgress`'s own doc comment for
        // why this closure must be `Sync` (it is called from worker
        // threads), which `&dyn AnalysisProgress` already satisfies
        // (`AnalysisProgress: Sync`).
        let on_file_progress =
            |done: usize, total: usize| progress.report_file_progress(done, total);
        let report = rinkaku_core::pipeline::analyze_repo(
            &paths,
            read_working_tree_file,
            // Core's `include_tests: bool` keeps its original meaning
            // ("true means include tests"). Only the CLI-side polarity is
            // flipped by ADR 0025, so translate here.
            !cli.exclude_tests,
            &generated_paths,
            cli.include_generated,
            Some(&on_file_progress),
        );
        if let Some(note) = repo_outline_empty_note(&report) {
            progress.note(note.to_string());
        }
        (report, String::new())
    } else {
        let diff_text = read_stdin_diff()?;
        if diff_text.trim().is_empty() {
            progress.note("note: diff is empty, nothing to analyze".to_string());
        }
        let resolver = build_resolver(
            cli,
            &diff_text,
            read_working_tree_file,
            None,
            None,
            progress,
        )?;
        let changed_paths = changed_paths(&diff_text)?;
        let generated_paths = resolve_generated_paths(cli, &changed_paths, None);
        log::debug!("analyzing diff");
        progress.set_phase(AnalysisPhase::AnalyzingDiff);
        // ADR 0033 (amended): same `on_file_progress` shape as the
        // `analyze_repo`/`build_resolver` calls above — reports
        // `(files_done, total)` back through `progress` as `analyze_diff`'s
        // sequential per-file loop works through the diff's changed files.
        let on_file_progress =
            |done: usize, total: usize| progress.report_file_progress(done, total);
        let report = rinkaku_core::pipeline::analyze_diff(
            &diff_text,
            read_working_tree_file,
            // Pure stdin-pipe input has no known base commit (see this
            // module's own doc comment on stdin mode), so ADR 0014's
            // classification stays unknown for every symbol rather than
            // guessing one from partial information.
            None,
            resolver
                .as_ref()
                .map(|r| r as &dyn rinkaku_core::deps::Resolver),
            // Same translation as the `analyze_repo` call above: core's
            // `include_tests` is the semantic name, ADR 0025 flips only
            // the CLI-facing polarity.
            !cli.exclude_tests,
            &generated_paths,
            cli.include_generated,
            Some(&on_file_progress),
        )?;
        if let Some(note) = garbage_input_note(&diff_text, &report) {
            progress.note(note.to_string());
        }
        (report, diff_text)
    };

    Ok(AnalyzedReport {
        report,
        diff_text,
        resolved_workdir,
    })
}

/// Applies `--entry`'s pivot (ADR 0019) to `report`, reporting the
/// corresponding empty-result note through `progress` (ADR 0033) when
/// applicable — shared by both `DisplayMode` branches in `main`, which used
/// to inline this identically. `progress` rather than a bare `eprintln!`:
/// this function runs inside the `DisplayMode::Tui` branch too, while a
/// `SplashProgress` is still buffering notes rather than printing them
/// immediately (see `AnalysisProgress::note`'s own doc comment).
fn finish_report(cli: &Cli, progress: &dyn AnalysisProgress, report: Report) -> Report {
    if let Some(entry) = &cli.entry {
        let pivoted = apply_entry_pivot(report, entry);
        if let Some(note) = entry_pivot_empty_note(&pivoted, entry) {
            progress.note(note);
        }
        pivoted
    } else {
        report
    }
}