# tplyr2 0.2.0

**First CRAN release.**

tplyr2 builds clinical summary tables from a declarative specification. A
`tplyr_spec()` describes what to compute — count, descriptive, shift, and
custom analysis layers, with population-based denominators, formatting, sorting,
and traceability — and `tplyr_build()` executes it against data. The spec is
pure configuration, so it can be written, serialized to JSON or YAML, reviewed,
and re-run independently of any dataset.

tplyr2 is a ground-up successor to the
[Tplyr](https://cran.r-project.org/package=Tplyr) package rather than a new
version of it. The two have different APIs and are intended to coexist while
users migrate; `vignette("migration")` maps one onto the other.

Versions before 0.2.0 were development releases available only from GitHub. The
sections below record what changed since 0.1.0, and are relevant to anyone who
installed the package that way.

## Breaking changes since 0.1.0

Several settings that were previously accepted and silently ignored now error.
Each of these produced a plausible-looking but wrong table, which is the wrong
default for a clinical reporting package.

- Unknown `...` overrides passed to `tplyr_build()` error instead of being
  ignored (#73). `tplyr_build(spec, adsl, wher = "SAFFL == 'Y'")` used to build
  on **unfiltered** data; it now errors and lists the valid override names.
- Unknown option names passed to `tplyr2_options()` error (#74). A misspelled
  `IBMrounding` used to set a dead option and leave the whole output package on
  banker's rounding.
- `result_order_var` naming a statistic the layer does not compute errors
  instead of falling back to `"n"`, and `ordering_cols` matching none of the
  observed column levels errors instead of zeroing every sort key (#78). A
  partially unmatched `ordering_cols` warns and sorts on the levels that matched.
- `denoms_by` must name the layer's own grouping variables — the column
  variables, `by` variables, and (for count and shift layers) the target
  variable (#77). An unrecognized name used to shrink the join key set silently,
  either multiplying table rows or attaching another group's denominator.
- Unrecognized `missing_count` keys error (#80).
- `tplyr_stats_data()` now returns the grouping columns plus the requested
  statistic, as documented, rather than the entire layer frame (#79). Use
  `tplyr_numeric_data()` for every statistic.
- Count and shift layers now render a percentage with no usable denominator as
  blank rather than `0` (#76), matching what desc layers already did. A genuine
  zero count against a real denominator still renders `0.0%`.

## New features

- New single-proportion confidence-interval statistic for count layers (#44).
  Four `f_str` keywords — `ci_lower`/`ci_upper` (from `n`/`total`) and
  `distinct_ci_lower`/`distinct_ci_upper` (from `distinct_n`/`distinct_total`) —
  are computed per column-by-target-level cell on the percentage scale, so an
  incidence CI drops straight into a count-layer format string, e.g.
  `f_str("xx (xx.x%) [xx.x, xx.x]", "distinct_n", "distinct_pct", "distinct_ci_lower", "distinct_ci_upper")`.
  Two new `layer_settings()` controls choose the method and coverage:
  `ci_method` (`"clopper_pearson"` default / exact, matching SAS `PROC FREQ
  EXACT` and `stats::binom.test()`; plus `"wilson"`, `"wald"`,
  `"agresti_coull"`, `"jeffreys"`) and `ci_level` (default `0.95`). The bounds
  are computed lazily (only when a format references a CI keyword) and appear on
  Total/Missing rows just like `pct`. The underlying vectorized helper,
  `proportion_ci()`, is also exported.
- `apply_formats()` gains `na`, `width`, and `pad` arguments (#41). `na` is a
  string substituted for cells whose format-group inputs are all NA, used
  instead of the blank-width fill (`na = ""` yields a truly empty cell, `nchar`
  0; `na = "NE"` renders `"NE"`), letting `apply_formats()` replace hand-rolled
  fixed-width formatters for externally row-bound statistics. `width` pads each
  formatted token to a fixed total width (`pad = "right"`/`"left"`); when the
  `na` substitution applies, it wins and the cell is not padded. The defaults
  (`NULL`) preserve existing behavior.
- New `as_display()` helper returning a display-ready frame — the `rowlabel*`,
  `res*`, and `rdiff*` columns only, with the internal `ord*` (and `row_id`)
  columns dropped, ready to hand to a table-rendering package (#36). Pass
  `labels = TRUE` to rename the result columns to their column-group header
  labels.
- New `n_records` descriptive statistic keyword for `group_desc()` — the number
  of records assessed (non-missing + missing), for tables that report an `n` of
  records/subjects assessed rather than the non-missing analysis count (#34).
  The existing `n` (non-missing) keyword is unchanged.
- New `denom_row` setting for shift layers — emits the per-baseline-group
  denominator (the `shift_denom = "column"` denominator) as an integer `n` row
  above the shift-to rows, instead of forcing callers to recompute it (#35).
  The label defaults to `"n"` (`denom_row_label`).
- New `assoc_test()` for count and shift layers — an omnibus association-test
  column (#37). It runs a caller-supplied function once per `by` group over the
  raw source-data subset for that group (all `cols` levels), so a Fisher's
  exact or CMH test can tabulate across the treatment columns, and lands the
  formatted result as a single trailing `pval1` column (one value per by-group,
  on the group's first row). Attach via `layer_settings(assoc_test = ...)`.
- `assoc_test()` gains a pairwise / per-level mode for count layers (#40).
  Supplying `comparisons` (with an optional `reference`, defaulting to the first
  `cols` level) compares the reference arm to each named arm and emits one
  `pval` column per comparison, each with a value on every target-level row
  (like `risk_diff`'s `rdiff` columns) — the standard AE-by-SOC/PT layout. In
  this mode the caller-supplied `fn` receives a 2x2 incidence matrix
  `matrix(c(n_ref, n_cmp, N_ref - n_ref, N_cmp - n_cmp), nrow = 2)` (distinct
  counts/denominators when `distinct_by` is set) and returns a scalar p-value,
  so any test — `fisher.test`, and beyond — can be used. `label` may be a
  per-comparison vector; the default is `"<reference> vs <comparison>"`.
- `assoc_test()`'s `fn` may now return a **character** string, passed through
  to the `pval` cell verbatim (#47); `format` applies only when `fn` returns a
  numeric. This completes the arbitrary-`fn` design — the function that computes
  the test can also supply the finished display, so conditional p-value
  conventions (a significance flag like `0.031*`, a `>.99`/`<.0001`
  ceiling/floor, an `"NE"`/`"N/A"` sentinel, trailing-space alignment) all live
  in the `fn`. Works in both omnibus and pairwise modes; `NA` (numeric or
  character) still renders a blank. Existing numeric-returning fns are
  unaffected.
- Pairwise `assoc_test()` now works on **nested** count layers (#49), where it
  was previously a no-op. It emits a `pval` column per comparison with a value
  on every row of every level — each inner (e.g. preferred-term) row and each
  outer (e.g. system-organ-class subtotal) row — building each row's 2x2 from
  that row's own distinct counts and the `pop_data` denominators (the outer row
  uses the level's "any event in that group" subject count). This is the
  canonical AE-incidence-by-SOC/PT Fisher layout. A new `total_row` argument to
  `assoc_test()` (default `TRUE`) also lands a p-value on the layer's total
  ("any event anywhere") row; set `total_row = FALSE` to leave it blank.
  Missing rows are always blank. Combined with the character-return display
  (#47), the `fn` can supply the exact `* / >.99 / trailing-space / blank`
  cell text on every nested row. A zero-event arm is handled correctly: its
  2x2 denominator is taken from `pop_data` (subjects at risk), so a sparse or
  empty **reference** arm still yields a valid `0`-vs-`k` test on every row
  instead of blanking the column.
- `assoc_test()` now works on **`group_desc`** layers in omnibus mode (#51),
  giving a continuous-variable comparison across arms — ANOVA, Kruskal-Wallis,
  a t-test — a native home. Same contract as count/shift: `fn` receives the
  by-group's raw source subset (all `cols` levels) and returns a scalar p
  (formatted by `format`) or a verbatim character string (#47), placed on the
  by-group's first statistic row (`NA` → blank). A demographics table can now
  produce its comparison p-values — continuous *and* categorical characteristics
  sharing one `pval` column — entirely through tplyr2 instead of a hand-rolled
  `aov`/`kruskal.test` side pipeline. Pairwise/per-level mode remains count-layer
  only; supplying `comparisons` on a desc layer is now a clear error rather than
  silently ignored.
- `assoc_test()`'s `fn` may now return **multiple values** rendered into one
  cell (#60). When `format` references more than one variable, `fn` returns a
  numeric vector of matching length, mapped positionally onto the format — so a
  procedure that emits a tuple (an odds ratio with its confidence interval, an
  estimate with a p-value) lands as a single formatted cell, e.g.
  `f_str("xx.xx (xx.xx, xx.xx)", "or", "lo", "hi")`. A scalar return with a
  one-variable format is unchanged; an all-`NA` return or an arity mismatch
  renders a blank; the character-return passthrough (#47) still wins for a
  finished display string.
- New `shift_denom` setting for shift layers (#18). `shift_denom = "column"`
  computes percentages column-wise — out of each shift column group (the
  "from"/baseline group) within the treatment arm — the standard
  "% within the from group" shift display, and the header `(N=)` labels then
  reflect those per-column-group denominators. The default `"total"` keeps the
  arm-total denominator.
- New `pct_lt` and `pct_gt` count-layer settings for the regulatory
  "less-than / greater-than" percent conventions (#14). A cell with a nonzero
  count whose percent would display below `pct_lt` renders as `"<"` + the
  threshold (e.g. `1 ( <1%)` instead of `1 (  0%)`); a percent below 100 that
  would display above `pct_gt` renders as `">"` + the threshold (e.g. `>99`).
  The comparison is against the rounded display value, so a percent that
  rounds up to the threshold keeps its number.
- New `zero_count_display` count-layer setting (#14) controlling how cells
  with a zero count render: `"full"` (default, unchanged), `"count_only"`
  (just the count field, e.g. `" 0"`), or `"blank"` (empty string).
- New `stat_columns` layer setting for count layers (#10). Passing a named
  list of `f_str()` objects produces one result column per statistic per
  column group — for example, a distinct-subject "n (%)" column beside a raw
  event-count "E" column under each treatment arm. Column label attributes
  follow the pattern `"<column group> (N=n) | <stat name>"` so renderers can
  span the group over its stat sub-columns. Works with nested count layers,
  by variables, total/missing rows, risk difference, cell metadata,
  JSON/YAML serialization, and ARD conversion.
- `tplyr_meta` objects gain an optional `statistic` field recording which
  statistic a cell displays (populated for `stat_columns` layers).
- `missing_count` gains `denom_exclude` (#80), which was previously accepted in
  config and never implemented. With `denom_exclude = TRUE` the rows folded into
  the Missing row (`NA` plus anything in `missing_values`) leave the layer's
  denominator, so percentages are of the non-missing population. Every key
  `missing_count` accepts is now documented.
- Failures in user-supplied code no longer discard their message (#75). Custom
  summaries and `assoc_test` functions still cannot abort a build and still
  render `NA` as a blank cell, but the reasons are now collected and reported as
  one warning per build, deduplicated and naming the summary or test and the
  group affected. Previously a partial failure — real numbers everywhere and one
  blank cell where the expression errored — was indistinguishable from data
  legitimately missing. An `assoc_test` function whose return does not match its
  format's variable count is reported as the caller bug it is.
- Missing and zero denominators are no longer silent (#76). A count with
  `n > 0` against an `NA` or zero denominator warns, naming the layer and the
  affected groups, and `tplyr_build()` warns when `pop_data` has no rows for a
  column level present in the analysis data.

## Bug fixes

- A layer whose `where` clause left a column group with no rows emitted fewer
  result columns than its sibling layers, and those columns were then aligned
  positionally — putting that layer's values **under the wrong treatment arm**.
  In `tplyr_adae`, for example, a `where = AESEV == "SEVERE"` layer alongside an
  unfiltered one reported Xanomeline Low Dose's severe events under the Placebo
  label. The column-variable level set is now captured from the table's full data
  and pinned for every layer, so an empty column group completes with zeros in
  its own position.
- A `total_group()` combined with a `custom_group()` on the same column variable
  double-counted the pooled subjects: the total duplicated the custom group's
  copies as well as the originals, so a 254-subject study reported `Total (N=422)`
  and a sex count of 233 where 143 was correct. Duplicated rows now record which
  column variable they were created for, so a total group skips copies made on
  its own variable while still spanning copies made for a different one.
- A shift layer or a `stats_as_columns` desc layer combined with a standard layer
  produced a table whose `res` columns meant different things in different row
  blocks, keeping only the first layer's column labels. Those combinations are now
  rejected by `validate_spec()` with a message pointing at separate specs. (This
  replaces a silently mislabeled table, so a spec that "worked" before may now
  error — it was not producing a correct table.)
- `tplyr_meta_subset()` treated an empty filter set as "nothing matches" and
  returned zero rows. A cell can legitimately have no filters — a `total_group()`
  column crossed with a total row, or with a desc statistic in a layer that has no
  `by` variable — and those cells describe the whole dataset. It now returns all
  rows.
- Cell metadata dropped the `by` filter when a `by` level was an empty string, and
  aborted the entire metadata build with "missing value where TRUE/FALSE needed"
  when a `by` level was `NA`. Both are real levels: an empty string now filters on
  `""` and `NA` filters with `is.na()`. A nested layer's structurally absent inner
  label still contributes no filter, as before.
- Cell metadata compared `by` values against the *trimmed* row label, so a `by`
  variable whose values carry leading or trailing whitespace (common in
  SAS-derived character data) produced filters matching zero rows. Filters now use
  the untrimmed value.
- `generate_row_ids()` silently produced duplicate IDs when row labels had been
  blanked by `apply_row_masks()` or when a target level collided with a
  `total_row_label`, so metadata lookups resolved to the wrong cell. It now warns.
- A missing-subjects row built without `distinct_by` carried filters resolving to
  the subjects that *do* appear — the exact complement of what the cell counts.
  Row-level missing-subjects counting is a population-minus-target difference that
  no filter set can express, so no metadata is emitted for it and the build warns.
- Cell metadata for a `stats_as_columns` desc layer resolved to nothing. That
  layout labels its result columns `"<arm> (N=n) | <statistic>"`, the same grammar
  count-layer `stat_columns` uses, but the trailing statistic segment was stripped
  only for count layers — so every filter read `TRT == "A | n"` and matched zero
  rows. `tplyr_meta_subset()` now returns the correct source rows.
- A count layer's total row rendered a **blank** instead of `0` for any column
  group with no rows in the analysis data, while the category rows above it
  correctly showed `0` (#66). `n` is counted from the raw analysis rows, so an
  empty column group never appears there; the denominator join now brings it in
  from the completed category counts and zero-fills it. Nothing errored or
  warned, so the value silently vanished from a delivered table.
- A count layer's total row counted `n` by summing the category rows while
  `distinct_n` counted from the raw data, so the two disagreed in the same cell
  and `total_row_count_missings` had no effect on `n`. Category rows exclude NA
  target values (data completion drops them) and any level folded into the Missing
  row, so the sum silently omitted them. `n` is now counted from the raw rows with
  the same missing handling as `distinct_n`, making `n`, `distinct_n`, and the
  cell's metadata agree. **A total row over data with missing target values will
  change**: with the default `total_row_count_missings = TRUE` it now includes
  them, as documented.
- `tplyr_build(metadata = TRUE)` now warns when a `stats_as_columns` desc layer
  has no `by` variable. That layout names its result columns after the statistics
  rather than `res1`, `res2`, ..., and cell metadata is keyed on `res` columns, so
  none was produced — previously without any indication.
- `missing_count`'s `missing_values` no longer double-counts. Values named there
  are folded into the Missing row, but they also kept their own category row, so
  the same records were counted twice and the column summed past 100%. They are
  now removed from the category rows, matching Tplyr v1's `set_missing_count()`
  and the exclusion `tplyr_meta()` already assumed. On a nested count layer,
  naming an outer-level value removes its inner rows along with it. **This changes
  the numbers in any table that used `missing_values`** — previously those tables
  were wrong.
- `f_str(empty = )` now honors its unnamed form. Only `c(.overall = "...")` was
  implemented; an unnamed `empty = "NA"` was silently ignored. It now fills each
  NA format group in place, right-justified to that group's field width, so
  `f_str("xx (xxx)", "n", "pct", empty = "NA")` renders `"NA ( NA)"` and a
  partially missing cell keeps its alignment. This restores v1 parity; `.overall`
  is unchanged and still replaces the whole cell only when every group is NA.
- Shift layers now compute the single-proportion confidence-interval keywords
  (`ci_lower`, `ci_upper`, `distinct_ci_lower`, `distinct_ci_upper`). They were
  accepted by validation but never computed, so a shift format referencing one
  rendered an empty field with no warning. Bounds follow whichever denominator
  `shift_denom` selects.
- `as_display()` no longer discards the result columns of a `stats_as_columns`
  desc layer built without a `by` variable. That layout names its columns after
  the statistics rather than `res1`, `res2`, ..., and the whitelist dropped them,
  returning row labels alone. It now removes the internal `ord_layer_*`/`row_id`
  helpers and keeps everything else.
- `stats_as_columns` with no `by` variable now orders its columns by
  format-string order rather than alphabetically by statistic label.
- `collect_precision()` now warns when `precision_data` does not cover every
  `precision_by` group present in the data (those cells render blank), and when
  `precision_data` omits the `precision_by` columns entirely (its widths are
  applied to every group). Both were silent.
- `f_str()` now warns when a format group requests parenthesis hugging (`X`/`A`)
  but has no literal text in front of it — there is nothing to hug, and the
  number is left-justified with trailing spaces instead.

- Count-layer row ordering now honors the sort settings it advertised (#57).
  `order_count_method = "bycount"` actually sorts by descending count (it
  previously fell back to the default order); `ordering_cols` selects which
  column's count drives that sort, and `result_order_var` which statistic;
  `outer_sort_position = "desc"` reverses a nested layer's outer level. Any
  explicit `order_count_method` (and the default) now also keeps `by`-groups
  **blocked** instead of interleaving them, and the default respects the target
  variable's factor levels (previously it ordered the target alphabetically
  even when it was a factor). The target sort key is threaded through the cast
  so all methods compose correctly with by-groups and special (total/missing)
  rows.
- `order_count_method = "bycount"` now also reaches the **inner** level of a
  nested count layer (#64), sorting (e.g.) preferred terms by descending count
  within each system organ class -- the AE-by-SOC/PT convention -- with
  `result_order_var`/`ordering_cols` honored. Previously it only affected
  single-level layers. The outer level keeps its own order (controlled by
  `outer_sort_position`), so the useful "outer alphabetical, inner by descending
  count" layout comes for free.
- `risk_diff` on a **nested** count layer now errors instead of silently
  emitting an all-blank column (#58). Risk difference is computed only on
  single-level count layers; on a nested SOC/PT layer the setting previously
  produced an empty `rdiff` column with no warning. The error points to
  pairwise `assoc_test()`, which does compute a per-level comparison on nested
  layers.
- `group_shift(denom_row = TRUE)` no longer renders the literal string `"NA"`
  for a baseline (shift-column) group that is absent within a `by` group (#55);
  an absent group's denominator is zero, so the cell now reads `0` (consistent
  with `zero_count_display` on the shift-to rows). A new `denom_row_format`
  setting also lets the denominator row carry its own `f_str` width independent
  of the `n_counts` cells (e.g. `denom_row_format = f_str("xx", "n")` for a plain
  narrow integer) instead of inheriting their padding.
- Omnibus `assoc_test()` no longer lets `total_group()` / `custom_group()`
  duplicate rows leak into the `fn`'s `.data` (#53). Those rows are a display
  construct for the count columns; including them double-counted every subject
  and silently returned a wrong p-value (no error, no warning). The synthetic
  rows — and their now-unused factor levels (e.g. a phantom `"Total"` level that
  made `chisq.test()` return `NaN`) — are dropped before `fn` runs, so it sees
  only the real observations.
- Omnibus `assoc_test()` now places its p-value on the layer's **first display
  row**, not the arbitrary pre-sort (dcast) row (#54). The value was written
  before the `ord*` reorder (e.g. `order_count_method = "byfactor"`), so it could
  strand on the wrong category (landing on `65-80` instead of `<65`, etc.);
  placement is now derived from the ordering columns, per by-group.
- `group_count()` `missing_count` now always emits the Missing row when set,
  zero-filling every column/by group that has no missing values, so the row
  reads `0 ( 0%)` throughout instead of being dropped (when the total missing
  count is zero) or leaving empty cells (when only some columns have missings)
  (#33). Matches classic Tplyr `set_missing_count()`.
- `group_shift(shift_denom = "column")` with a `by` variable now scopes the
  column (from-group) denominator within each by-group instead of pooling it
  across them (#28). A shift-by-visit table now gets per-visit percentages.
  With a `by` variable the header `(N=)` reflects the arm total (the
  per-column-group denominator varies by by-group, so no single header N can
  represent it); the no-`by` behavior (from-group N in the header) is unchanged.
- `group_shift()` now honors the `zero_count_display`, `pct_lt`, and `pct_gt`
  layer settings, applying them the same way `group_count()` does (#31).
  Previously a shift layer ignored them (e.g. a zero cell always rendered as
  `0 (  0%)` even with `zero_count_display = "count_only"`).
- Descriptive statistics that round to negative zero now display as `0.0`
  instead of `-0.0`, matching base R `format()` (#29).
- Result and risk-difference columns are now ordered by their numeric suffix
  when layers are combined and when metadata is built. Previously tables
  with more than 9 result columns sorted them lexicographically
  (`res10` before `res2`), scrambling column order.
- Count layers now order their `res*` columns by the `cols` variable's factor
  levels, matching `group_desc()` (#13). Previously count layers ordered
  result columns alphabetically by the `cols` value, so a spec mixing count
  and desc layers (or any renderer assuming `res1` is the first `cols` level)
  could get inconsistent column order. Shift layers likewise order their
  column dimension by the shift variable's factor levels.
- `group_count()` now orders its `by`-group rows by the `by` variable's factor
  levels (then a VARN companion, then alphabetically) instead of always
  alphabetically (#24). Previously a factor `by` such as visits came out
  mis-ordered (e.g. `Week 12` before `Week 2`), matching `group_shift()` and
  `group_desc()`.
- Fixed `group_count()` total/missing rows with a `by` variable (#24): each
  by-group's `Total` (or `Missing`) row is now labelled with its by-group value
  instead of a blank, and special rows now sort after the normal rows within
  each group (previously e.g. `Total` was interleaved alphabetically among the
  target values, and with a `by` variable the row label was dropped entirely).
- `group_count(order_count_method = "byfactor")` now orders category rows by the
  target variable's factor levels instead of alphabetically (#16). The target
  column is coerced to character while counts are built, so the level order is
  now recovered from the source data (`compute_var_order()`). Nested count
  layers likewise order their outer and inner categories by factor levels
  (previously they fell back to the dcast's alphabetical row order).
- `group_desc(stats_as_columns = TRUE)` combined with a `by` variable no longer
  drops the by-groups and returns only the last group's statistics (#20). It
  now keeps the by-groups as rows and produces one result column per
  treatment x statistic (labelled `"<arm> | <stat>"`). Behavior without a `by`
  variable (treatment groups as rows, statistics as columns) is unchanged.
- `group_desc()` now orders its `by`-group rows by the `by` variable's factor
  levels (then a VARN companion, then alphabetically) instead of always
  alphabetically (#20). Previously a factor `by` such as visits came out
  mis-ordered (e.g. `Week 12` before `Week 2`); this applies to both the
  standard stats-as-rows output and `stats_as_columns = TRUE`.
- Risk difference and pairwise `assoc_test` columns came out **entirely blank**
  when `by` led with a string label (#72). Both merge functions assumed the `by`
  data variables occupied the first `rowlabel` columns, so with
  `by = c("Age Group", "SEX")` the join keyed the constant-label column against
  `SEX` values and matched nothing. They now share one helper that offsets past
  the label columns.
- A `where` clause longer than about 60 characters could not be read back from a
  spec file (#70). `rlang::expr_deparse()` wraps at that width, and the
  resulting multi-element array was not something `parse_expr()` could accept —
  so any realistic multi-condition ADaM filter broke the spec file in both
  formats. Files written by the old code still read.
- `precision_cap` was silently dropped on a spec-file round trip in **both**
  formats (#69). Both writers dropped the names of the named numeric vector, and
  `apply_precision_cap()` dispatches on those names, so a round-tripped spec
  rendered different numbers than the original with no error or warning.
  `apply_precision_cap()` now also warns when given a cap carrying neither an
  `int` nor a `dec` name.
- Multi-element character settings (`denoms_by`, `keep_levels`, `precision_by`,
  and friends) deserialized as lists from JSON and broke the build (#68); YAML
  was unaffected only because it auto-simplifies. A `denoms_by` list made
  data.table's `by=` error out. One field-type table now restores every plain
  setting's vector type, and a test asserts that no `layer_settings()` parameter
  is missing from it.
- A `denom_where` expression read back from a spec file was evaluated as a call
  rather than stored, erroring on the first variable name it contained.
- `tplyr_from_ard()` re-defaulted desc-layer format strings instead of sharing
  `get_desc_formats()` with the build path (#71), reconstructing a 1-row table at
  a different width than the 6-row build. Format-string rows also kept `dcast`'s
  alphabetical order rather than their declared order, which affected
  ARD-reconstructed desc layers and analyze layers alike.
- Unknown keys in a spec file are no longer dropped silently (#81). Hand-editing
  spec files is supported, and a typo'd key such as `total_rows` built a table
  without the requested behavior and said nothing. Unknown layer settings and
  unknown top-level spec keys now warn, naming the layer.
- A `pop_data` that renames the column variable — `pop_data(c(TRTA = "TRT01P"))` —
  skipped `total_group()` and `custom_group()` on the population side, because
  the rename ran after those were applied. The Total column had no population
  rows, so every nonzero count in it displayed `0.0%`.
- `compute_risk_diff()` computed the plain difference inside the same
  `tryCatch()` as the confidence interval, so a `prop.test()` failure blanked the
  difference along with the CI even though the difference needs no test (#76). It
  also now pre-validates that counts do not exceed their denominators instead of
  letting `prop.test()`'s error be swallowed into an all-`NA` row.

## Documentation

- **The formatting vignettes have been reorganized.** The two previous articles,
  *"General String Formatting"* and *"Advanced Descriptive Statistics
  Formatting"*, are replaced by three organized around what the user is trying to
  do rather than by layer type:
  - `vignette("format_strings")` — the format string grammar, where format
    strings attach per layer type, the complete statistic keyword reference for
    count/shift/desc layers, rounding, missing-value handling, and standalone
    `apply_formats()`.
  - `vignette("precision_alignment")` — auto-precision (`a`/`A`, `+N`,
    `precision_on`, `precision_by`, `precision_cap`, `precision_data`) and
    parenthesis hugging (`X`/`A`).
  - `vignette("display_conventions")` — the display rules imposed by shells and
    SAPs: `pct_lt`/`pct_gt`, `zero_count_display`, `stat_columns` and
    `stats_as_columns`, `keep_levels`, `missing_count`, shift denominators, and
    indenting/wrapping nested terms.

  The old vignettes duplicated auto-precision, hugging, and `empty` at similar
  depth while disagreeing about what hugging does, and their four statistic
  keyword lists contradicted each other and the source. The following were
  previously documented in no vignette at all and are now covered: `pct_lt`,
  `pct_gt`, `zero_count_display`, `keep_levels`, `missing_count`,
  `total_row_count_missings`, `stats_as_columns`, `shift_denom`, `denom_row`,
  `denom_row_label`, `denom_row_format`, the desc-layer `total`/`pct` keywords,
  and `apply_formats()`'s `na`/`width`/`pad`/`lt`/`gt` arguments.
- Corrected the description of parenthesis hugging. tplyr2 moves a hugged group's
  leading spaces to just inside the trailing literal's last character
  (`12 (34.5% )`); Tplyr v1 moved them to the left of the opening delimiter
  (`12  (34.5%)`). Two vignettes described v1's behavior. The difference is now
  stated explicitly for anyone reconciling output against v1.
- Corrected the description of `shift_denom = "column"`: it denominates by the
  result column group (arm × post-baseline category), so each result *column*
  sums to 100%. `vignette("shift")` now shows all three shift denominators,
  including how to get row-wise percentages with `denoms_by`.
- Documented that auto-precision (`a`/`A`) resolves against the data only in
  `group_desc()` layers; elsewhere it degrades to a fixed width equal to the
  number of characters written.
- Documented several other scope limits that were previously unstated: the four
  confidence-interval keywords are count-layer only (a shift layer accepts them
  and renders them empty); the desc-layer `total` keyword is a **record** count,
  so `pct` is a share of the arm only on one-row-per-subject data; `keep_levels`
  filters after the denominators are computed, so the kept percentages do not
  re-base; `precision_data` validates only `max_int`/`max_dec`, rendering a blank
  cell for any group it fails to cover; `pct_lt`/`pct_gt` and
  `zero_count_display` target the first matching format group; and `str_indent_wrap()`
  charges an existing indent against `width` twice.
- Documented that literal text in a format string cannot contain `x`, `X`, `a`,
  or `A` — those characters are always parsed as format groups, so a template
  like `"xx days"` silently gains a second group.
- `vignette("sort")` now covers `order_count_method = "bycount"` on nested count
  layers, including that it sorts the inner level only and that
  `outer_sort_position` reverses the outer order rather than ranking it by count.
- `vignette("post_processing")` now covers `as_display()`,
  `collapse_row_labels(nest = TRUE)`, and the `apply_formats()` `na`/`width`
  arguments, and points at the declarative `pct_lt`/`zero_count_display`
  settings before `apply_conditional_format()`.
- `vignette("riskdiff")` now states that `risk_diff` errors on nested count
  layers and points to pairwise `assoc_test()`.
- Fixed the IBM-rounding example in `vignette("options")`, which showed two
  identical tables under captions promising a difference.
- Fixed a broken `vignette("serialize")` cross-reference in `vignette("ard")`,
  and expanded the vignette indexes in the README and
  `vignette("tplyr2")`, which listed 10 and 8 of the 19 articles respectively.
- `print()` on an `f_str` object no longer runs its fields together on one line.
- New vignette *"Comparative Statistics and Binding External Results"*
  (`vignette("binding-statistics")`) — how to attach cross-arm comparisons
  (`assoc_test()`, `risk_diff`, single-proportion CIs) and how to bind
  externally-computed model results (MMRM/ANCOVA/Cox/logistic p-values,
  LS-means, CIs) onto an assembled table via `apply_formats()` and
  `as_display()`, plus where `group_analyze()` fits.
- Clarified in `?layer_settings` and the denominators vignette that `denoms_by`
  **replaces** (does not augment) the default `cols`-based denominator
  grouping, so you must include the `cols` variable(s) to get per-column
  denominators (#19).
