---
name: rounding
description: >
  Audit R code that prepares CSR/TLF statistics for SAS-compatible rounding
  compliance (ties away from zero, round-once-at-display, fixed trailing-zero
  precision). Make sure to use this skill whenever the user asks to review,
  check, verify, or fix rounding, mentions SAS rounding, half-away-from-zero,
  display precision, trailing zeros, early rounding, mock tables showing 76
  not 76.00, percentages off by 1 at the half point, or names round(),
  formatC(), sprintf(), format(), signif(), prettyNum(), cards::round5,
  tidytlg::roundSAS, or janitor::round_half_up in a clinical-report context --
  even if they don't say the word rounding.
license: MIT
metadata:
  author: Pharma Skills community
  version: "0.11"
  rules-version: "BR-001/002/003 v1.0"
---

# Rounding compliance review

Default to an advisory report. Do not edit source, change policy, or approve a
classification. When the user explicitly asks to file a finding, create one
fix-ready issue in the **audited repository**, not in this skill's repository.

The point of the review is not to find `round()`. It is to establish which
operations can change a number a reader sees or a number that decides which
rows they see, and then to prove what each one actually does. Those are
different questions, and only the second one needs a machine.

## When to Use

Use this skill when the user asks to:

- audit, check, verify, or fix rounding in R reporting code (even if they
  don't say the word "rounding")
- compare R and SAS rounding, or mentions ties-away-from-zero / half-away
- debug trailing-zero or fixed-precision display (e.g. mock tables show
  `76` where the spec requires `76.00`, or `2%` where it requires `2.0%`)
- investigate percentages or statistics "off by 1 at the half point"
- review TLF/CSR/display code that calls `round()`, `formatC()`, `sprintf()`,
  `format()`, `signif()`, `prettyNum()`, `cards::round5`, `tidytlg::roundSAS`,
  or `janitor::round_half_up`, or describes early rounding / round-once-at-display

See also *When NOT to use this skill* at the end.

## Scope example

A repository audit is not limited to installed package code. Treat executable
reporting examples in `Rmd`/`qmd` as report entry paths too: a vignette that
computes a mean, percentage, CI, or p-value and sends it to `rtf_*()`,
`write_rtf()`, a table, or a listing is in scope even when no exported
`report_*()` function calls it. Tests and pure rendering/layout examples stay
visible in Coverage and are excluded unless they themselves generate report
statistics.

## Bundled resources

| File | What it contains | When to use |
|------|-----------------|-------------|
| `references/function-catalog.md` | In-scope/excluded functions, where rounding hides | Step 2 |
| `references/br-001-tie-method.md` | Tie rule, two causes, FAIL signature, versioned-helper fix | Step 4, when BR-001 is in scope |
| `references/br-002-rounding-stage.md` | Stage rule, FAIL signature, remove-early-rounding fix | Step 4, when BR-002 is in scope |
| `references/br-003-display-precision.md` | Display rule, FAIL signature, fixed-character fix | Step 4, when BR-003 is in scope |
| `scripts/scan-rounding-calls.R` | Parse-tree inventory: catalog calls, quantizing operators, wrapper closure | Step 2; its output is never complete coverage |
| `scripts/probe-tie-behavior.R` | Executed tie/stage/display witnesses; `--digits` for per-site precision | Step 5 |
| `assets/report-template.md` | `report.md` structure | Step 6 |

Do NOT read or run these upfront. Use each only when the step directs.

## Required inputs

Collect these before scanning. A rule the request is silent on is
`NOT ASSESSABLE` -- name it as unchecked rather than inventing it.

- **Target**: a local folder or a repo link pinned to a commit, read-only. Pin
  it: an unpinned reference silently stops reproducing when the source moves.
- **Precision spec**: preferred digits and trailing-zero expectation per
  reported statistic. If absent, infer only from non-circular reporting context
  (table labels, paired examples, or a stated default); code's own `digits` or
  comments set probe precision but cannot establish compliance. Default trailing
  zeros to required when the context supports a precision; otherwise use `NOT
  ASSESSABLE`. Record the inference and its source.
- **Tie policy and its version**, plus the comparison helper the rule owner
  selected and that package's version.
- **Entry paths**: exported report functions, scripts, and executable chunks in
  `.Rmd`/`.qmd`/`.Rnw` that calculate or prepare reported statistics. A caller
  need not be exported: observable reporting behavior makes it in scope.
- **Rule owner**: the named person who will classify each finding. Record the
  name in the report. An unnamed gate is not a gate.
- **Allowlist** (optional): sites the rule owner has already classified. See
  below.

## Procedure

1. **Confirm inputs.** Resolve the list above and the `report.md` path, using
   the source to settle whatever it can settle. Ask about a gap you cannot
   close, but do not let one unanswered question stop the parts of the review
   it does not touch -- see *Missing inputs* below.
2. **Scan the whole repository (first pass only).** Read
   `references/function-catalog.md`, then run `scripts/scan-rounding-calls.R`
   at the repository root—not just `R/`. Confirm Coverage accounts separately
   for every discovered `.R`, `.r`, `.Rmd`, `.rmd`, `.qmd`, and `.Rnw` file;
   use `--include-tests` when tests contain reporting fixtures or executable
   examples. Preserve the full output. The scanner never clears a file, so
   inspect zero-hit literate files and infer where dynamic rounding can hide.
   Label candidates `catalog`, `wrapper`, `operator`, or `exploratory`.
3. **Trace and triage.** Follow each reporting path, including standalone
   vignette/helper chunks that compute statistics before table or file output.
   Export status is irrelevant. Keep each reachable reporting operation as a
   row; move layout, encoding, solver, plotting, and ordinary test-only hits to
   Coverage with reasons. Trace paired sites (early rounding + downstream
   display or decision) together.
4. **Resolve against the rules.** For each row state namespace, method/class,
   and version, then read the matching `references/br-00x-*.md` for the FAIL
   signature and fix.
5. **Prove.** Run `scripts/probe-tie-behavior.R`, then
   re-run it with `--digits N` for each distinct precision the target displays
   -- a witness at the wrong precision does not test the site. Unexecuted
   claims are not evidence. Embed `R.version.string` plus the probe
   `checks_run / invariants_held / unexpected` line inline; keep full logs
   as sidecars. State explicitly that a numeric-only helper cannot fix
   trailing zeros.

   Write `report.md` incrementally -- skeleton first, witnesses second. Draft
   the inventory table and Coverage before perfecting any witness script, so
   a run always delivers a report even if time runs short. Keep witness
   scripts small: source the target read-only and reuse the probe's
   dependency-free half-away arithmetic inline (`sign(x) *
   trunc(abs(x) * 10^d + 0.5) / 10^d`, plus `+ 0` for the signed-zero guard)
   rather than building a large bespoke harness. A report with before-only
   witnesses and a stated fix pattern beats no report.

   Every finding needs two executed witnesses: the **before**, showing the
   observed value against the policy value, and the **after**, showing that the
   fix you recommend actually produces the policy value. A recommendation no one
   has run is a guess, and it is the part of the report a reader is most likely
   to paste into the codebase. If the selected package helper cannot be run,
   demonstrate the fix *pattern* with the probe's dependency-free half-away
   arithmetic and say that is what you did -- still recommend the versioned
   package, and note that its exact behavior at inexact ties was not confirmed
   here.
6. **Verdict and write.** Per row: Tie / Stage / Display as `PASS` / `FAIL` /
   `NOT ASSESSABLE`; Overall is `FAIL` if any rule fails, `NOT ASSESSABLE`
   if none fails and at least one is uncheckable, else `PASS`. Compliant
   helper-plus-formatter paths pass -- do not rewrite them.

   ALWAYS use the exact structure in `assets/report-template.md` (see
   *Report structure* below) for the audit deliverable; do not invent or rename
   report sections.

7. **File a target-repository issue only when explicitly authorized.** First
   search the target repository's open issues for the exact paths/rules to
   avoid duplicates. For each actionable FAIL, create at most one issue in the
   target repository with: affected `file:line` paths; actual versus required
   behavior; a minimal executed reproduction; a bounded remediation pattern;
   acceptance criteria covering positive and negative ties, round-once stage
   where applicable, fixed-width display, and signed zero; and the pinned
   target commit. Link the audit report, verify the issue after posting, and
   report its URL. If the target is read-only, authorization is missing, or the
   finding is only `NOT ASSESSABLE`, leave a local issue draft instead.

## Report structure

ALWAYS use this exact template from `assets/report-template.md`:

```markdown
# Rounding compliance report -- <package> <version/commit>
Target / Environment / Policy / Status / Verdict (one line each)
## Summary -- one row per (operation, entry path) with Tie/Stage/Display/Overall
## Appendix A. Coverage -- files scanned, hits, excluded sites with file:line + reason
## Appendix B. Evidence (executed) -- policy-vs-actual witnesses per row
## Appendix C. Limitations -- missing specs and blind spots
```

Fill every section from executed output; an empty section is a missing section,
not a clean section.

## One row per operation *and* entry path

The same site can be compliant on one path and defective on another. A shared
formatter that receives a value already rounded half-away is a no-op; the same
formatter receiving an unrounded value is itself the rounding step, and
`formatC()` and `sprintf()` are not half-away. Give each
(operation, entry path) pair its own row. A single collapsed verdict for a
shared helper is wrong in one direction or the other.

Split by *distinct behavior*, not by syntax. Two calls to the same operation on
the same path with the same precision -- both bounds of a confidence interval,
say -- share one row; note that it covers two call sites. Splitting them inflates
the inventory without adding a verdict. Conversely, quantization and display
formatting in the same function are distinct behaviors and get separate rows:
`(x * 10) %/% 1 / 10` (Tie) vs `paste0(n, "%")` (Display) is two rows, not one.

Every in-scope operation gets a row, including compliant and allowlisted ones.
Give the allowlisted helper its own definition-site row (e.g.
`R/rounding-helpers.R:10` `trunc(abs(x) * scale + 0.5)` as a reviewed `PASS`
with tie-vector evidence), in addition to the per-call-site rows that use it.
Recording it only in the allowlist table drops it out of the count a reader
uses to check your coverage, which is the opposite of what an allowlist is for.

When early rounding and its downstream display are paired (BR-002), mark
Stage `FAIL` on both rows and state they are one finding -- the early site is
the primary defect, the downstream row carries the same Stage verdict so the
pair stays together when sorted or filtered.

In Coverage, quote the scanner's `files_scanned` and `total_hits` verbatim;
do not recount `R/` files by hand.

Signed zero is part of BR-001, not a fourth rule. A site that renders `-0`
fails the tie rule; do not open a separate verdict column for it, and do not
fail a downstream no-op formatter for a signed zero its caller produced --
the finding belongs to the operation that created the value.

## Two causes, never one

When a formatting call diverges from policy, say which of these is acting --
usually both, and they can push in opposite directions:

1. **Tie mode.** `base::round()` is half-to-even at a true tie.
2. **Binary representation.** Most decimal ties are not stored exactly, so the
   result follows the stored value rather than the printed one.

Do not compress this into "the function uses banker's rounding". That claim is
false for `formatC()` and `sprintf()`, and it predicts the wrong direction for
half the inputs. At zero decimals every `x.5` tie is stored exactly, so cause 1
acts alone; decimals bring in cause 2. This is why the probe, not source
reading, is the evidence.

Also check that no displayed value is a signed zero: a small negative value
formatted at the display precision can render as `-0` or `-0.0`. Probe this
only with in-domain inputs for that entry path. An out-of-domain negative
probe (e.g. negative counts for a rate that takes counts and durations) is
recorded as not scored -- input unreachable -- never as a `FAIL`. Score `FAIL`
only when a reachable input can render a signed zero.

## Allowlist

An allowlist entry is a classification the rule owner already made, not a
reason to stop looking. An entry needs a reason, a source version, and an
approver. Report the site as a reviewed `PASS`, keep it visible in Coverage,
and return it to full review if the named source has changed since approval --
otherwise the allowlist quietly becomes a blind spot. Never add, edit, or infer
an entry yourself; proposing one to the rule owner is the most you may do.

## Missing inputs: narrow the verdict, don't abandon the review

First close gaps from the source: `NAMESPACE` and `@export` tags settle entry
points, while literal `digits` selects a probe precision. They do not establish
Display compliance: code describing its own decimals is circular evidence.

Audit every unaffected rule. Infer Display only from non-circular reporting
context; otherwise mark that cell `NOT ASSESSABLE`, name the missing item, and
deliver the partial report.
## Stop and escalate

Stop the whole review only when no trustworthy evidence is obtainable at all:

- the source cannot be read or located;
- no report entry point can be identified, even from `NAMESPACE`;
- `probe-tie-behavior.R` reports an unexpected result -- this environment does
  not behave as the rule references describe, so no witness from it can be
  trusted;
- the target documents a convention that conflicts with the stated policy, so
  which rule applies is a question for the rule owner, not for you.

Record the blocker and stop. Everything else is a `NOT ASSESSABLE` cell in a
report you still deliver.

## Target-issue mode

Use this mode only for a user-authorized, evidence-backed `FAIL`. The issue is
a handoff to an implementer, not another audit: give it one defect cluster,
source anchors, the exact observed/policy values, a non-prescriptive fix
boundary, and executable acceptance tests. Never open an issue merely to
repeat scanner candidates, layout/encoding exclusions, or missing policy.

## When NOT to use this skill

Do not use for non-numeric reports, statistical-method validation,
or independent QC replacement. If the user only wants "why do
R and SAS differ in rounding" explained, answer directly. If stage or precision
are out of scope, name the unchecked rules instead of narrowing silently.
