Skip to content

Release v2.4.1 (dev → master, CRAN-accepted bundle 2.2.0 → 2.4.1)#132

Merged
xuyiqing merged 60 commits into
masterfrom
dev
May 1, 2026
Merged

Release v2.4.1 (dev → master, CRAN-accepted bundle 2.2.0 → 2.4.1)#132
xuyiqing merged 60 commits into
masterfrom
dev

Conversation

@xuyiqing

Copy link
Copy Markdown
Owner

Summary

Merge dev into master for the v2.4.1 CRAN release (accepted 2026-04-30). Bundles v2.2.0 → v2.4.1 in one master-side release; master was last cut at v2.2.0 (commit 731c15b, 2026-03-27).

Headline additions across the bundle:

  • v2.4.1estimand() accepts vartype = "parametric"; att.cumu populates n_cells so esplot(..., Count = "n_cells") works on cumulative plots (PR #130).
  • v2.4.0 — unified post-hoc estimand API: estimand(fit, type, by, ...) typed dispatcher (att, att.cumu, aptt, log.att) + imputed_outcomes() long-form accessor; new chapter @sec-estimands; soft-deprecates effect() and att.cumu() (PR #129). Closes issue #126.
  • v2.3.3 — bootstrap PSOCK retry + carryover.rm slot + diagtest() drop = FALSE fix (PR #128).
  • v2.3.2 — modern visual defaults for plot.fect() (white panel, dashed onset vline, peach highlight rectangle, glyph-only highlights with highlight.fill = TRUE opt-in); new selective-highlight API; legacy.style = TRUE for byte-identical pre-2.3.1 reproduction (PR #127).
  • v2.3.1 — weighted-ATT consistency fix; W.est / W.agg per-role weight arguments (PR #125).
  • v2.3.0 — rolling-window (forward-only) CV with cv.method = "rolling" default, cv.prop = 0.1, k = 20; simdata factor doubled (PR #124).

This commit-level merge head is 5d9f030 (the docs-only commit marking v2.4.1 as CRAN release in bb-updates.Rmd and dropping the (development) tag from NEWS headings).

CRAN status

Submitted to CRAN 2026-04-29; accepted 2026-04-30. gsynth (1.4.0, sole CRAN reverse-import) verified compatible against installed fect 2.4.1 (R CMD check Status OK, 0E/0W/0N).

Test plan

  • CI passes on master after merge.
  • Tag v2.4.1 on the merge commit.
  • Cut GitHub release v2.4.1 referencing this PR.
  • Update dev from master so the merge commit is reachable.

🤖 Generated with Claude Code

xuyiqing and others added 30 commits March 30, 2026 22:13
Add gsynth callout tip, link gsynth/panelView user manuals, fix table alignment and minor formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fect_nevertreated() relabelled its outgoing $method from "gsynth"
(or "cfe") to "fe" whenever cross-validation selected r.cv = 0. The
bootstrap dispatcher in fect_boot reads out$method and has branches
only for gsynth / ife / mc / cfe -- so every se = TRUE call where CV
picked the two-way-FE model crashed with:

    Error: Unsupported bootstrap method: fe

(Reported by John Macdonald against gsynth; gsynth delegates SE to
fect.) The point estimate was fine because the estimation path in
fect_nevertreated handles r = 0 internally; only the SE path tripped
on the relabel.

Fix: keep method = "gsynth" (or "cfe") regardless of r.cv. The
existing "gsynth" / "cfe" branches in fect_boot's dispatcher already
route to fect_nevertreated, which estimates correctly at r = 0.

Adds tests/testthat/test-gsynth-r0-boot.R: pure two-way-FE DGP that
deterministically drives CV to r = 0, checks $method stays "gsynth"
and the se = TRUE call returns without error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- DESCRIPTION: 2.2.0 -> 2.2.1, Date -> 2026-04-21
- NEWS.md: add 2.2.1 entry describing the bootstrap-dispatcher fix
  in cfff93e (preserve $method after CV when r.cv = 0)
- .Rbuildignore: exclude cell-D1.rds, local build tarballs, and
  .Rcheck directory from the package build

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: preserve method after CV when r.cv = 0 (gsynth/cfe boot crash)
- Add R/impute_Y0.R: dispatcher routing (method, predictive) to the
  correct underlying estimator (fect_nevertreated/fect_fe/fect_cfe)
- Add R/valid_controls.R: control-screening helper replacing inline
  r.cv+1 logic in boot.R
- R/boot.R: replace draw.error cascade and one.nonpara cascade with
  single impute_Y0 calls; add extra-FE slicing in Loop 2; update
  .export list; delete dead Branch B (L1131-1296 pre-refactor)
- R/default.R: add hasRevs+parametric hard gate after hasRevs is
  computed (L1725)
- Add tests/testthat/test-paraboot-dispatcher.R: unit tests for
  dispatcher error paths, valid_controls thresholds, and hasRevs gate

Bug fixed: Loop 2 (one.nonpara) previously called fect_nevertreated
unconditionally regardless of method/predictive. Now impute_Y0
dispatches correctly for ife+notyettreated, cfe+nevertreated,
cfe+notyettreated combinations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add §Parametric bootstrap: valid regimes to vignettes/02-fect.Rmd:
three-gate table, Monte Carlo evidence (80.6% undercoverage for
ife+notyettreated+parametric), and migration recipe. Extend
\item{vartype} in man/fect.Rd to name all three values, state the
parametric restriction (Gates A/B/C), and recommend bootstrap as
the safe default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds validation error when se=1, vartype="parametric", and
time.component.from="notyettreated". Mirrors existing hasRevs+parametric
gate (L1734-1739). Coverage-sim (2026-04-13-paraboot-coverage) showed
~20% undercoverage for ife+notyet; theoretical analysis confirms EM
shrinkage bias. Gate aligns code with stated policy (Liu-Wang-Xu 2024 H1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convert 5 spec-listed collision tests to expect_error (Phase 3a/3b/3c,
  Phase 3a-F3 in test-factors-from-refactor.R; BUG-1a in test-paraboot-parity.R)
- Convert 5 unexpected collisions discovered via grep audit (BUG-3a, BUG-1c,
  BUG-3c, BUG-4, BUG-6 in test-paraboot-parity.R) — all ife/cfe+default+parametric
  calls that now hit the notyettreated gate
- Add test-parametric-notyet-gate.R with 7 new tests:
  NGP-1/2/3/4 (gate fires for ife+default, cfe+default, ife+explicit-notyet,
  message check) and NGN-1/2/3 (gate does NOT fire for gsynth, ife+nev, cfe+nev)
- Full suite: 650/650 pass, 0 fail

StatsClaw run: 2026-04-14-notyet-parametric-gate
Keep Version at 2.2.1 (same-day release). Describe the two
backported fixes as additional bullets under the existing 2.2.1
section rather than opening a new version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
backport: paraboot dispatcher fix + notyettreated parametric gate
Mirrors the NEWS.md 2.2.1 entry: r=0 boot crash, paraboot dispatcher
refactor, notyet+parametric gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 2.2.1 changelog entry contained \`r = 0\` which knitr reads as
inline R code (the \`r <expr>\` inline-eval syntax). Replaced with
math-mode \$r = 0\$ and \$r_{\text{cv}} = 0\$ so the Quarto book renders
cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ted parallel branch

- New R/cv-helpers.R: six .fect_cv_score_one_* helper functions covering all
  four CV inner-loop call sites (cv.R IFE, cv.R MC, fect_nevertreated IFE,
  fect_nevertreated CFE), plus .CV_PARALLEL_THRESH constant.
- R/default.R: new five-value parallel API (TRUE/FALSE/"cv"/"boot"/c("cv","boot"));
  computes do_parallel_cv / do_parallel_boot; propagates to fect_cv; replaces
  parallel==TRUE/FALSE gates in bootstrap setup with do_parallel_boot.
- R/cv.R: adds do_parallel_cv/do_parallel_boot params to fect_cv signature;
  IFE notyettreated gains flat r×k future_lapply parallel branch gated by
  Nco*TT > 20000 (auto) or explicit "cv" override; MC inner loop migrated to
  helper (serial, Phase 2 will add parallel); nevertreated delegation passes
  do_parallel_cv.
- R/fect_nevertreated.R: adds do_parallel_cv param; migrates all four fold-
  parallel foreach bodies (IFE all_units, IFE treated_units, CFE all_units,
  CFE treated_units) to use helpers; serial fallback also migrated via lapply;
  parallel gate updated to honor do_parallel_cv and explicit "cv" override.
- man/fect.Rd: updated parallel parameter documentation with five-value table.
- NEWS.md: 2.2.2 entry documenting Phase 1 changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug #1 (cv.R): parallel branch assigned r.cv from r_seq[i] (unnamed
column vector element). Added names(r.cv) <- "r" after the aggregate
loop so the parallel path matches the serial path's c(r=N) form.
identical(fit_seq$r.cv, fit_par$r.cv) now TRUE.

Bug #2 (boot.R + default.R): fect_boot() did not accept do_parallel_cv
and did not forward parallel/cores/do_parallel_cv to fect_cv(). Added
do_parallel_cv parameter to fect_boot signature, threaded it from
default.R's fect_boot() call, and added parallel/cores/do_parallel_cv
to the fect_cv() call inside boot.R. CV parallel banner now appears
when se=TRUE and parallel="cv".

Bug #3 (cv.R): parallel branch guard used criterion != "PC" (uppercase)
but canonical form is "pc" (lowercase). Changed to criterion != "pc".
criterion="pc" with parallel="cv" no longer errors with "argument is
of length zero".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers all test-spec.md sections for REQ-parallel-cv-phase1:

P (numerical identity): P.1 IFE notyettreated all_units serial==parallel,
  P.2 IFE notyettreated treated_units serial==parallel, P.3-P.6 nevertreated
  helper-migration fixture regression (IFE/CFE × all_units/treated_units).

A (API back-compat): A.1 parallel=TRUE backward compat, A.2 parallel=FALSE
  determinism, A.3 invalid parallel raises error.

S (selective switches): S.1 parallel="cv" emits CV banner/no boot banner,
  S.2 parallel="boot" emits boot banner/no CV banner.

T (threshold gating): T.1 parallel=TRUE on small panel gates to serial,
  T.2 parallel="cv" overrides threshold, T.3 override produces correct output.

R (plan restoration): R.1 plan restored after normal return,
  R.2 plan restored even on error path.

D (reproducibility): D.1 same seed two parallel runs bit-identical.

L (LOO unaffected): L.1 uses time.component.from="nevertreated" (spec error
  corrected per audit — LOO+notyettreated is invalid; mirrors G.7 approach).

E (edge cases): E.1 k=1 no error, E.2 cores=1 no error, E.3 parallel=FALSE
  ignores cores, E.4 c("cv","boot") accepted, E.5 criterion="pc" no error
  (Bug #3 regression guard), E.6 boot+CV no deadlock.

Also stages four nevertreated fixture RDS files (written by tester).

Full suite: FAIL 0 | WARN 0 | SKIP 0 | PASS 699 (655 prior + 44 new).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add parallel execution branch for MC notyettreated CV in R/cv.R:
- Gate: cv_mc_parallel = do_parallel_cv && (Nco*TT > 20000 || explicit "cv")
- Flat (lambda, fold) task list dispatched via future_lapply
- Sequential master aggregation applies 1% rule; no break_check in parallel mode
- Distinct variable names: use_explicit_cv_mc, old.future.plan.mc
- on.exit(..., after=FALSE) ensures correct LIFO-equivalent plan restoration
  when method="both" (MC restore prepended, fires before IFE restore)
- Remove dead variable .inter_fe_ub_local from IFE parallel block (Phase 1 review)
- Extend NEWS.md Phase 1 entry with Phase 2 bullets
- Add MC parallel note to man/fect.Rd parallel parameter description
- Add M.1-M.6 test assertions to tests/testthat/test-cv-parallel.R
- Full suite: PASS 708 / FAIL 0 / WARN 0 / SKIP 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migrates both IFE and CFE parallel CV branches in fect_nevertreated.R
from foreach %dopar% (k-fold only) to flat r×k future_lapply dispatch.
Adds CFE nevertreated parallel support with the same pattern as IFE and
MC. Removes doFuture::registerDoFuture() calls. Uses on.exit(after=FALSE)
LIFO discipline. All eight migration regression cases pass within 1e-10.
Adds 27 new test assertions (C and N sections) to test-cv-parallel.R.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…N.6 formal test

- man/fect.Rd: add mc = 20000 to parallel arg threshold table (was missing;
  ife and cfe were listed but mc was omitted despite Phase 2 adding MC parallel)
- NEWS.md: add explicit latent-bug bullet under Phase 3 entry — prior
  foreach %dopar% parallel CV in fect_nevertreated.R (all_units) was
  non-functional; migration to future_lapply(future.packages="fect") fixes it
- R/cv.R dead variable (.inter_fe_ub_local): already removed in Phase 2
  commit 5531166; confirmed absent — no change needed
- tests/testthat/test-cv-parallel.R: add N.6 formal test_that block —
  plan restored after sequential IFE then CFE nevertreated parallel CV calls
  (was spot-check only in Phase 3 audit; now permanent, 2 expect_equal assertions)

Test count: 726 + 1 = 727 PASS, 0 FAIL (E.6 pre-existing flake not triggered)
Two unrelated cleanups packaged together since both are pre-CRAN polish for
the parallel-CV feature:

- R/cv-helpers.R: drop the `fect:::` prefix on `inter_fe_ub`, `inter_fe_mc`,
  and `complex_fe_ub` calls (4 sites). The `:::` was added defensively for
  parallel worker visibility, but `future_lapply(future.packages = "fect")`
  loads the package on workers, making internal symbols accessible without
  qualifier. R CMD check --as-cran was flagging the self-references as a
  NOTE; this clears it. cv-parallel test suite (73 tests) still passes.

- vignettes/02-fect.Rmd: extend the parallel-computing callout to document
  the new five forms of the `parallel` argument (TRUE / FALSE / "cv" /
  "boot" / c("cv", "boot")) with a behavior table.

- vignettes/03-ife-mc.Rmd: rewrite the "Parallel computing" subsection in
  the cross-validation chapter. Document method-specific auto-thresholds
  (IFE/MC = 20k, CFE = 60k), the (rank, fold) flat dispatch topology, the
  MC `break_check` tradeoff in parallel mode, and the CV-vs-bootstrap
  independent-control sugar.

- vignettes/bb-updates.Rmd: add v2.2.2 changelog entry covering the new
  parallel CV (cv.R notyettreated for IFE/MC), the fect_nevertreated.R
  flat-dispatch migration (with latent worker-visibility bug closure), and
  the five-form parallel argument.

R CMD check --as-cran on the resulting branch: 1 WARNING, 1 NOTE — exact
pre-existing baseline (macOS clang header warn + HonestDiDFEct GitHub
Suggests note). 0 new issues. devtools::test() passes 727/727.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… changelog merge

Ships parallel-CV entries under the existing v2.2.1 header (no version bump).

- 03-ife-mc.Rmd: drop space in inline code `r=c(0,5)` / `k=10` so knitr
  does not parse it as inline R (the `r<space>` trigger).
- index.qmd: add hidden setup chunk so Quarto engages the knitr engine;
  merge Source Code and Version into a single callout with an auto-derived
  "rendered against fect X.Y.Z on YYYY-MM-DD" line read from DESCRIPTION.
- 09-sens.Rmd: flip hh_honest_placebo chunk to cache=TRUE. The bootstrap
  fit re-executed on every render; other chunks in the chapter were
  already cached. Cuts re-render time on this chapter to seconds.
- bb-updates.Rmd, NEWS.md: merge the parallel-CV bullets into the v2.2.1
  entry (date 2026-04-22). Keeps version and changelog self-consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1-3 handled the new parallel API in default.R / cv.R via the
do_parallel_cv / do_parallel_boot flags, but five downstream sites kept
scalar `parallel == TRUE` / `if (parallel)` checks that errored with
"the condition has length > 1" on vector input. Caught by an Alsaadi
(2025) real-workload smoke on 2026-04-22 (gap fit 374.7s serial-CV →
212.4s with parallel=c("cv","boot")+cores=18; ATT identical).

- R/boot.R: compute do_parallel_boot once, use at both fect_boot check sites
- R/fittest.R, R/permutation.R, R/did_wrapper.R, R/fect_sens.R: use
  !identical(parallel, FALSE) as the generic "parallelize me" test
- tests/testthat/test-parallel-vector-form.R: regression test that exercises
  parallel = c("cv","boot") through fect -> fect_boot with se = TRUE

Also bundled (all motivated by the debug session that found the bug):

- Test infra: tests/testthat.R uses LocationReporter + JunitReporter for
  non-interactive captured runs; CI workflow uploads test-results.xml and
  surfaces per-test results as PR annotations on the Ubuntu leg. Gated on
  NOT_CRAN or GITHUB_ACTIONS so CRAN's default behavior is unchanged.

- Docs: v2.2.1 changelog (bb-updates.Rmd, NEWS.md) tightened into two
  subheadings plus a bullet calling out this vector-form fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parallel cross-validation + parametric-bootstrap fixes (v2.2.1)
…trap

Changes cov.ar from 0 to TT-1L at the two res.vcov() call sites for
vcov_tr and vcov_co on the unbalanced-panel code path
(R/boot.R:900,904). Prior behavior zeroed out all off-diagonal entries
of the empirical covariance matrix, causing rmvnorm() draws to be iid
across time. On serially-correlated residuals with AR(1) coefficient
rho > 0, this under-estimated ATT SEs by sqrt((1+rho)/(1-rho)).

Balanced-panel path (else branch at R/boot.R:992-1003) uses direct
whole-T vector resampling and is unaffected. The nonparametric
"bootstrap" and "jackknife" vartypes are also unaffected (both
unit-level by construction).

Empirical validation on EH 2023 edu_eq (random placebo treatment,
100 reps, true ATT = 0): GSC parametric coverage moves from 0.28 to
0.99 with this fix, matching the theoretical VIF = (1+rho)/(1-rho)
~= 27.6 at residual AR(1) = 0.93.

Tests:
- New regression test in tests/testthat/test-cov-ar-parametric-boot.R:
  - T2a: unbalanced AR(1) panel, ratio of new/old SE matches theory.
  - T2b: balanced panel, output bitwise identical (no regression).
  - T2c: rho = 0 control, no systematic effect.
- Full devtools::test(): 737 / 0 FAIL / 0 WARN / 0 SKIP.
- R CMD check --as-cran: 0 ERROR; 1 WARN + 1 NOTE both pre-existing.

Docs:
- NEWS.md: bullet under existing v2.2.1 entry (version not bumped;
  2.2.1 has not yet shipped to CRAN).
- vignettes/bb-updates.Rmd: matching changelog entry.
- vignettes/07-gsynth.Rmd: new "Parametric bootstrap on an unbalanced
  panel" subsection demonstrating vartype = "parametric" on turnout.ub
  alongside the default vartype = "bootstrap"; explains the
  Gaussian-on-pairwise-complete-empirical-covariance approximation,
  distinguishes the conditional and unconditional inferential targets
  of the two procedures, includes side-by-side gap plots.

Scoping log, spec, test-spec, and sim-spec under
statsclaw-workspace/fect/runs/2026-04-24-cov-ar-unbalanced-bootstrap-fix.md
and runs/REQ-cov-ar/.
fix(boot): preserve serial correlation on unbalanced parametric bootstrap
- entropy-regularized simplex projection for bounded loadings
- thread loading.bound through R/boot.R (main fit + bootstrap dispatch)
- harden mirror-descent solver against NaN in fallback path
- cache CV-selected gamma across bootstrap replications
xuyiqing and others added 21 commits April 25, 2026 12:02
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update the existing 2.3.0 rolling-CV entry to reflect the Stage 3 fold-in:
both method = "ife" and method = "gsynth" are supported (the latter via the
companion Y.ct.full GSC-population change in fect_nevertreated.R, see
"GSC: Y.ct.full populated at control positions" entry on the Stage 2 PR).
The deferred-dispatcher note is kept and revised to point users at the
two-step pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the codoc-mismatch WARNING on R CMD check --as-cran:
  Code: method = c("ife", "gsynth")
  Docs: method = "ife"

Triggered by the Stage 3 fold-in (a8bc1bb) that broadened method
from a free-form string with `identical(method, "ife")` guard to
match.arg'd c("ife", "gsynth"). Roxygen source in R/cv-rolling.R
was already updated; only the .Rd file lagged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(boot): Phase A foreach → future_lapply, drop doFuture pollution
feat(gsc): populate Y.ct.full at control positions with F*t(lambda_co)
feat(cv): rolling (forward-only) CV for rank selection
…aths fix

Squash of 4 commits implementing the v2.3.0 rolling-window CV API.

`r.cv.rolling()` standalone:
* Rewritten as standard ML rolling-window CV: random per-unit anchors
  + drop-after-holdout + `cv.buffer` past-side buffer + `k` folds +
  per-fold unit sampling at `cv.prop`. Replaces the prior tail-only
  deterministic implementation that shipped on the v2.3.0 dev tip via
  PR #119.
* Extended to `method = "cfe"`. CFE-specific args (Z, gamma, Q,
  Q.type, kappa, extra index columns) flow through `...` and are held
  fixed; rolling CV picks `r` only. `index` length check relaxed from
  ==2L to >=2L so extra grouping FE columns flow through unchanged.

Dispatcher integration:
* `cv.method = "rolling"` wired through the main `fect()` CV dispatcher
  for all factor-model methods (ife/cfe/mc/gsynth/both) via shared
  helper `.build_cv_mask_rolling()` in `R/cv-helpers.R`. Consumed by
  `fect_cv`, `fect_binary_cv`, `fect_nevertreated`, `fect_mspe` -- one
  identical per-fold sampling + drop-future implementation across all
  four entry points.
* `fect_mspe()` accepts `cv.method = "rolling"` for AR-leakage-resistant
  out-of-sample model comparison.

API consolidation -- `"block"` alias + soft deprecations:
* New `cv.method = "block"` value as forward-looking alias for
  `"all_units"`. Internal dispatch unchanged; legacy values still work
  for one release.
* `.fect_normalize_cv_method()` emits one-time `message()` on
  `cv.method = "all_units"` ("use 'block' instead") and
  `"treated_units"` (will be deprecated when `cv.units` lands in
  v2.4.0). Maps `"block"` -> `"all_units"` for internal dispatch.

Default flips:
* `cv.method` default flipped from `"all_units"` (or `"treated_units"`
  in `fect_nevertreated`) to `"rolling"` across all 6 entry points
  (cv.R, cv_binary.R, fect_nevertreated.R, fect_mspe.R, default.R x3,
  boot.R) + Rd files.
* `cv.donut` default raised from 0 to 1 (matches `cv.buffer = 1` for
  rolling).
* `cv.gap` -> `cv.buffer` rename (no deprecation alias since cv.gap
  was never shipped to CRAN).

Latent fixes folded in:
* PSOCK worker `.libPaths()` propagation. New
  `.fect_make_future_cluster()` in cv-helpers.R uses
  `parallelly::makeClusterPSOCK(rscript_libs = .libPaths())`,
  replacing 4 `future::multisession` call sites (cv.R x2,
  fect_nevertreated.R x2). Mirror fix for the doParallel cluster in
  R/boot.R via `clusterCall(.libPaths)`. Resolves "no package called
  'fect'" worker init errors during Quarto book render and
  intermittent CRAN-check failures.
* `loading.overlap` 1D plot histograms now use dark borders matching
  darker fill variants (were invisible against white background).
* `fect_iden.Rd` LaTeX math-macro warnings cleared.

Quarto book updates (6 chapters):
* 03-ife-mc.Rmd: rewrote CV-method table (block/rolling default/loo);
  collapsed deep dive to summary; cv-strategies figure embed; K=200
  evidence callout.
* 04-cfe.Rmd: new "Cross-validation" subsection (dispatcher only);
  placed before Summary.
* 06-plots.Rmd: `loading.overlap` promoted to L2 section with anchor
  `#sec-loading-overlap`; 1D plot bug fix; expanded discussion.
* 07-gsynth.Rmd: top callout reordered (rolling first, concise);
  CV section condensed.
* aa-cheatsheet.Rmd: new "Cross-Validation" L2 section with full
  param table + scoring criteria + workflow + "Why we prefer rolling"
  callout (K=200 evidence); Notes section moved to last.
* bb-updates.Rmd: v2.3.0 entry rewritten.

Tests:
* New `test-rolling-via-dispatcher.R` (5+ dispatcher integration tests).
* New `test-rolling-cv-methods.R` (CFE extension tests).
* `test-rolling-window-cv.R` regression suite for the standalone
  helper.
* Defensive prelude added to E.6 (state-leak fix for ordering-dependent
  failure that existed pre-rewrite).

Squashed from: f6e49e8 (docs: clear fect_iden Rd warnings + document
rolling CV + changelog), 014c33f (rewrite as standard rolling-window
CV), 9489311 (consolidate to cv.buffer + k=10 default; absorb
docs/v2.3.0-cleanup, PR #123), 2d30d4f (cv.method = "rolling" in
fect() dispatcher; "block" alias; default flips).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CV default flips paired for stable iid recovery (across all CV entry
points: fect, fect_cv, fect_binary_cv, fect_nevertreated, fect_mspe,
r.cv.rolling, default, boot):

* cv.prop default 0.2 -> 0.1
* k default 10 -> 20

The (cv.prop = 0.1, k = 20) pair gives ~2x coverage of every eligible
unit across folds while keeping per-fold mask size moderate, sharpening
the MSPE curve enough that the 1-SE rule reliably recovers r_true on
iid panels with at-least-moderate factor signal-to-noise.

simdata regenerated with the latent factor contribution scaled by 2
(vs the original LWX 2024 DGP). Doubling raises factor signal-to-noise
from 2.7 to 10.9, making the latent factor structure clearly recoverable
by cross-validated rank selection on this dataset. Original simdata's
signal was comparable to noise, so MSPE-based selection landed within
the 1-SE band of truth without converging on it. All other DGP
components (covariates, FEs, treatment effect, error) are unchanged.
Existing user code that loads simdata will see different numerical Y.

Test fixtures regenerated under the new defaults and re-pinned with
explicit cv.prop=0.1, k=20, cv.donut=1, cv.rule="1se" arguments
(future-proof against further default flips):

* phase3-baseline-{ife,cfe}-{all,tr}-serial.rds
* nevertreated_{ife,cfe}_{all,tr}.rds (regenerated under new simdata
  since att.avg depends on the new Y)

Vignette updates:

* 01-start.Rmd: simdata DGP equation reflects 2*lambda'f
* 03-ife-mc.Rmd, 07-gsynth.Rmd, aa-cheatsheet.Rmd, 04-cfe.Rmd:
  parameter references updated (cv.prop=0.1, k=20)
* 03-ife-mc.Rmd: task count text updated (k=20 -> 120 tasks vs k=10 -> 60)
* aa-cheatsheet.Rmd: sub-options table updated
* bb-updates.Rmd: v2.3.0 entry rewrites the new defaults + simdata note
* simdata.Rd: full DGP, doubled-factor explanation, all column docs

Quality pipeline:

* devtools::test(): 976 PASS / 0 FAIL / 0 WARN / 0 SKIP
* Quarto book renders cleanly (13/13 chapters)
* R CMD check --as-cran: 1 NOTE (HonestDiDFEct in Suggests, pre-existing)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(cv): rolling CV in dispatcher + cv.prop/k defaults + simdata factor doubled
Resolve a long-standing surface inconsistency: when W was non-NULL,
plot(fit) silently rendered W-weighted per-period ATTs while print(fit)
and fit$est.att returned the unweighted versions. Same fit, two answers
depending on which surface the user looked at.

Reported by Sergei Schaub against fect 2.3.0; triage at
Research_Hub/Packages/fect/triage/2026-04-27-schaub-weights/.

Single canonical aggregation surface

The fit object stores one canonical aggregation. When W is supplied,
est.att, est.avg, est.att90, att.bound, att.boot, att.vcov, est.placebo,
est.carryover, and the per-method off / on / avg / time / count
counterparts all carry the W-weighted aggregation. The redundant *.W
parallel slots (est.att.W, est.avg.W, att.W.boot, att.W.vcov,
att.W.bound, att.on.W, time.on.W, count.on.W, att.avg.W, att.placebo.W,
att.carryover.W, est.placebo.W, est.carryover.W, est.att.off.W,
att.off.W.bound, att.off.W.vcov) are no longer attached to the fit
object.

print.fect() labels the obs-level row "Tr obs sample-weighted (W)" when
W enters the aggregation, and reverts to "Tr obs equally weighted"
otherwise.

plot.fect() reads the canonical slots directly --- no auto-flip. The
weight argument is deprecated (no-op + warning); removal scheduled for
v2.5.0.

New W.est / W.agg arguments

Two new top-level arguments distinguish three weight roles:

- Survey / sample weights: W = "ws" (or W.est = W.agg = "ws"). W enters
  both the outcome-model fit and the across-treated-obs aggregation.
  Backward-compatible with v2.3.0 callers passing W alone.
- Weight only in the outcome-model fit: W.est = "wr" alone. The weight
  enters the fit but the across-treated-obs aggregation gives each
  treated cell weight 1. Use this when the weight reflects fit-side
  considerations and the estimand is the unweighted average ATT across
  treated cells.
- Weight only in the aggregation: W.agg = "your_col" alone. Outcome
  model fit unweighted; weight applied only when summarizing across
  treated cells. Common cases: calibration weights to a target
  population, or post-stratification weights.

In v2.3.1, W.est and W.agg (when both supplied) must point to the same
column. Distinct columns for fit vs. aggregation (e.g., combined survey
x IPW designs) are scheduled for v2.4.0 and currently error.

Caveat for IPW users: a fect-internal IPW-for-confounding workflow is
under development for v3.0 (cross-fit doubly-robust path). v2.3.1 does
not include a recommended IPW workflow, and W.agg should not be used as
a substitute. See statsclaw-workspace/fect/runs/REQ-cross-fit-dr/spec.md.

W.role deprecation

The W.role argument briefly introduced during v2.3.1 development is
deprecated and will be removed in v2.5.0. It is still accepted with a
one-time warning that translates to the new arguments: "both" ->
W.est = W.agg = W, "estimation" -> W.est only, "aggregation" -> W.agg
only.

Documentation

- NEWS.md, vignettes/bb-updates.Rmd: v2.3.1 entry.
- man/fect.Rd: W, W.est, W.agg documented; W.role flagged as deprecated.
- vignettes/03-ife-mc.Rmd: new "## Weights" section at 3.5 (after
  Diagnostics), describing the three roles, with a callout-warning that
  v3.0 cross-fit DR is the right home for IPW-for-confounding.
- vignettes/aa-cheatsheet.Rmd: three rows for W, W.est, W.agg.

Tests

tests/testthat/test-weights-consistency.R covers all four configs
(W=NULL, W="ws", W.est="wr", W.agg="ipw") plus the same-column
constraint and the W.role deprecation warning. Full devtools::test():
1014 / 0 / 0 / 0.

Version bump

DESCRIPTION 2.3.0 -> 2.3.1, Date 2026-04-24 -> 2026-04-27.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(weights): consistent ATT surface + W.est/W.agg per-role weights (v2.3.1)
…ght API (v2.3.2)

Visual overhaul across all 14 plot.fect() output types under
`theme.bw = TRUE` (the default): white panel, plain left-aligned title,
thin grey reference lines, dashed treatment-onset vline, pre/post lightness
contrast (grey50 / grey20), publication-sized axis text, and compact
legends. Stats annotation block moves to top-left with a symmetric 2.5%
inset and 2 × num_stat_lines top padding so it never grazes the leftmost
CI; sized at cex.text * 1.0 so it does not overpower the title.

Placebo / carryover plots render highlighted periods as a single accent
glyph (orange triangle for placebo, blue diamond for carryover, orange
triangle for carryover.rm) instead of stacked circle + accent. The
lightened-tone background rectangle is now opt-in via
`highlight.fill = TRUE` (default FALSE — keeps figures clean for print
and grayscale). The `highlight` argument additionally accepts a character
subset of `c("placebo", "carryover", "carryover.rm")` for selective
per-test-type highlighting. NULL / TRUE / FALSE behave as before.

`legacy.style = TRUE` reproduces pre-2.3.1 figures byte-identically
(bold centered title, larger axis sizes, solid vline, blue placebo
triangles, peach rectangle), independent of `theme.bw`.

`theme.bw = FALSE` is now soft-deprecated with a one-time per-session
message; removal scheduled for v2.5.0. Users who want the gray-panel
look should pass `legacy.style = TRUE`, or apply
`+ ggplot2::theme_gray()` to the returned plot.

Migrated off ggplot2 4.0's deprecated `fatten` / `lwd` arguments to the
`size` / `linewidth` aesthetics; deprecation warnings cleared.

`loadings` (ggpairs) plot's correlation panel reformatted with overall +
per-group entries; per-group label colors match the density-plot fills.

Bug fix: carryover-test plot pre-exit boundary. Legacy code took
`shift <- dim(x$est.carryover)[1]`, which was always 1 (the slot stores
the aggregate, 1 row), so the last pre-exit cell rendered at plot-x = 1
past the dashed onset line. Fix detects real `carryover.rm` from
`x$call`. (`carryover.rm` has no dedicated fit-object slot today; long
term `R/fe.R / cfe.R / mc.R` should store it explicitly.)

Quarto book: worked `highlight = ` examples in chapter 6
(carryover and carryover.rm), trim of chapter 2 §"User-supplied weights"
with deepened cross-links to chapter 3 §"Weights". 13-assertion
regression test (`test-modern-theme.R`).

R CMD check --as-cran clean (0 WARN / 0 ERROR / 1 unavoidable NOTE).
Tests: 1026 PASS / 0 FAIL / 0 WARN / 0 SKIP. Quarto book renders
end-to-end (13 chapters, 174 SVG figures).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(plot): modern visual defaults for plot.fect() + selective highlight API (v2.3.2)
… matrix-shape (v2.3.3)

* PSOCK race in CV → bootstrap chain. `parallel = TRUE` after `CV = TRUE`
  occasionally inherited a stale `future::plan()` whose underlying cluster
  had been torn down. The next `clusterCall(.libPaths)` could kill a
  worker mid-handshake; `foreach(.errorhandling = "pass")` then returned
  an error object in place of a fit, and downstream
  `array(..., dim = ...)` crashed with "incorrect number of dimensions"
  (E.6 in `test-cv-parallel.R`). Bootstrap PSOCK construction now reuses
  the shared `.fect_make_future_cluster()` helper (which bakes
  `.libPaths()` into worker startup via `rscript_libs`), wrapped in a
  3-attempt retry-with-backoff that shrinks worker count between
  attempts. If `doParallel` cluster init also fails after retries, the
  bootstrap degrades to sequential rather than crashing.

* `diagtest()` matrix-shape edge case. The all-non-NA column filter on
  `pre.att.boot` and `att.boot` could collapse the matrix to a vector
  when only one bootstrap column survived (rare but reachable when small
  `nboots` interacts with NA-filled iterations from worker errors).
  Downstream `res_boot[pre.pos, ]` then threw "incorrect number of
  dimensions". Pass `drop = FALSE` at both filter sites.

* `carryover.rm` is now stored explicitly on the fit object. Pre-fix,
  `plot.fect()` recovered `carryover.rm` from `as.list(x$call)$carryover.rm`
  + `eval(arg, envir = parent.frame())`. That call-parsing path silently
  gave the wrong K under `do.call()`, programmatic wrappers, or any
  call-rewriting code path. The fit object now stores `carryover.rm`
  directly (`R/default.R`); `R/plot.R` reads from the slot. Behaviorally
  a no-op for fits built via named-argument `fect(...)`; correct under
  any other construction.

* New `tests/testthat/test-carryover-rm-slot.R` (3 tests) verifies the
  slot is populated, NULL'd when unused, and the plot-logic recovery
  path under `x$call` wipe.

* New `E.7` in `tests/testthat/test-cv-parallel.R` simulates the
  CV → boot stale-plan PSOCK race directly: install a fresh cluster,
  kill its workers behind future's back, then run boot. The retry loop
  in `run_dopar_retry` recovers cleanly.

* Quarto book chapter 2 §Other estimands gains a new subsection on
  post-hoc estimands derived from the imputed potential-outcome
  surface, with a worked APTT (Chen & Roth 2024) example using the
  existing fit slots. Closes documentation side of issue #126
  (ajunquera).

* `DESCRIPTION` bumped to 2.3.3, `Date: 2026-04-28`. NEWS.md gets a
  "fect 2.3.3 (development)" heading covering the three fixes plus
  the documentation addition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-race

fix(boot,plot,diagtest): PSOCK retry + carryover.rm slot + APTT vignette (v2.3.3)
Adds a typed dispatcher and low-level accessor for computing alternative
estimands directly from any fect imputation fit. Replaces the four
ad-hoc paths previously available (effect(), att.cumu(), the v2.3.3
ch2 §Post-hoc estimands stopgap recipe, and the W.agg = 0/1 mask
hack for window-restricted ATT) with one coherent surface.

* New estimand(fit, type, by, cells, weights, window, direction,
  vartype, conf.level, ci.method) typed dispatcher. Closed enums for
  type and canonical by values; user-column by reserved for future
  releases. Shipped types:
    "att"      - per-cell mean treatment effect.
    "att.cumu" - cumulative ATT (replaces effect() / att.cumu()).
    "aptt"     - average proportional TE on the treated (Chen and Roth
                 2024 QJE).
    "log.att"  - mean log-scale treatment effect.
  Shipped by axes: "event.time", "overall" (with optional window).
  cohort and calendar.time reserved.

* New imputed_outcomes(fit, cells, replicates, direction) accessor
  returns a long-form data frame with documented columns
  (id, time, event.time, cohort, treated, Y_obs, Y0_hat, eff,
  eff_debias, W.agg, [replicate]). Use this for custom estimands the
  dispatcher does not ship; pipe to dplyr / data.table for arbitrary
  aggregation.

* cells = filter (NULL / logical / formula) on both functions, with
  window = c(L, R) sugar over cells = ~ event.time >= L & <= R.

* New eff_debias slot on the fit object, NULL for plain imputation
  estimators (FE / IFE / MC / CFE / GSC); reserved for future
  doubly-robust estimators so the score eff = (Y_obs - Y0_hat) +
  eff_debias can be added without breaking the long-form schema.

* Numerical-equality invariants asserted by package tests:
    estimand(fit, "att", "event.time")        == fit$est.att
    estimand(fit, "att.cumu", "event.time")   == effect(fit)$effect.est.att
    estimand(fit, "att.cumu", "overall", win) == att.cumu(fit, period = win)
  byte-identical for the relevant columns under default arguments.

* Soft-deprecation: effect() and att.cumu() emit a one-time-per-session
  message pointing at estimand(). They continue to work byte-identically
  to v2.3.x. Removal not before v3.0.0. estimand() suppresses the message
  when delegating internally so users only see it on their direct calls.

* New "Alternative estimands" chapter (vignettes/04a-estimands.Rmd) in
  the user manual with worked examples for each shipped type, the
  cells / window / direction patterns, a custom-estimand example via
  imputed_outcomes(), the keep.sims memory tradeoffs table, and the
  migration table from effect() / att.cumu(). vignettes/02-fect.Rmd
  §Post-hoc estimands is now a one-paragraph backref to the new chapter.

* Tests: 84 expectations across 6 new test files
  (test-imputed-outcomes.R, test-estimand-att.R, test-estimand-att-cumu.R,
  test-estimand-aptt.R, test-estimand-log-att.R,
  test-estimand-deprecation.R) covering schema, byte-equality
  invariants, error paths, and deprecation gating.

* DESCRIPTION bumped to 2.4.0, Date 2026-04-29.

Out of scope (deferred per design contract):
- by = "cohort" / "calendar.time" / user columns (canonical names
  reserved; implementations to follow).
- Plot integration: existing plot(fit, type = "cumul") flows through
  effect() unchanged. New plot types "aptt" / "log.att" deferred.
- DR non-linear functionals (aptt.dr, log.att.dr).
- Post-hoc linear-constraint inference.
- User-supplied closures (summarise_po(fit, fn = ...)).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(po-estimands): unified post-hoc estimand API (v2.4.0)
v2.4.1 — extend estimand() vartype enum to accept "parametric".

Audit finding: fit$eff.boot was already populated under vartype = "parametric"
when keep.sims = TRUE. The allocation at boot.R:557-566 is conditioned on
keep.sims, not vartype; the universal fill at boot.R:1756-1760 / 1871-1882
covers all vartypes. The only blocker was match.arg() at po-estimands.R:452
rejecting "parametric".

This change collapses to a one-line match.arg extension plus roxygen,
docs, and tests. Strictly additive — no fit-time machinery changed; v2.4.0
byte-equality contract intact.

Also fold in a small completeness fix on the cumulative-ATT path:
estimand(fit, "att.cumu", ...) now populates n_cells from
fit$est.att[, "count"] (event-time path: per-period count; overall
path: window-summed count) rather than NA_integer_. This lets esplot()
draw the bottom-bar treated-cell panel for cumulative plots, matching
the per-event-time level ATT behavior. ch03 + ch08 cumulative examples
updated to pass Count = "n_cells". New tests AC.6 / AC.6b lock the
invariant.

Files:
  R/po-estimands.R         — vartype enum + @param doc + att.cumu n_cells
  man/estimand.Rd          — usage + \item{vartype} match
  DESCRIPTION              — Version 2.4.1, Date 2026-04-29
  NEWS.md                  — v2.4.1 entry
  tests/testthat/test-estimand-parametric.R  — 9 test_thats / 32 expects
  tests/testthat/test-estimand-att-cumu.R    — AC.6 / AC.6b for n_cells
  vignettes/03-estimands.Rmd                 — new §Parametric variance + Count
  vignettes/08-gsynth.Rmd                    — new §Cumulative Effects + Count
  vignettes/bb-updates.Rmd                   — v2.4.1 heading
  vignettes/index.qmd                        — version bump

Validation:
  testthat:        full suite passes (estimand-att-cumu 25/0/0/0 with new
                   AC.6 / AC.6b expectations; estimand-parametric 32/0/0/0;
                   no v2.4.0 byte-equality regressions).
  R CMD check:     1 WARNING, 2 NOTEs (WARN is pre-existing toolchain
                   artifact: Apple clang 21 ↔ R_ext/Boolean.h:62 pragma —
                   verified by re-checking origin/dev at f59300a, same
                   status. CRAN's macOS-arm64 builder uses older clang
                   and does not emit it.)
  Quarto book:     14 HTML / 176 SVG / 0 errors at squash time. ch08
                   re-rendered post-n_cells fix: chunk 84/121
                   (turnout-cumulative) executes cleanly.

Contract: ref/po-estimands-contract.md
Run log:  statsclaw-workspace/fect/runs/2026-04-29-po-estimands-parametric-v241.md
feat(po-estimands): parametric vartype for estimand() (v2.4.1)
@xuyiqing xuyiqing force-pushed the dev branch 2 times, most recently from 71a0631 to 893b84d Compare April 30, 2026 20:41
xuyiqing added a commit that referenced this pull request Apr 30, 2026
The PAR-1..7 tests in test-paraboot-parity.R and the P.3..6 helper-migration tests in test-cv-parallel.R compare against RDS baselines (paraboot-baseline.rds and nevertreated_*.rds) captured on macOS/Linux during the parametric-boot refactor on 2026-04-13. Windows runners produce numerically equal but bit-different results due to BLAS implementation differences, causing 22+4 = 26 identical()/expect_identical() failures.

Add skip_on_os("windows") to the 11 affected test_that blocks. Byte-equality coverage is preserved on macOS+Ubuntu (where the baselines match); Windows users get a clean R CMD check. macOS-latest passed with these checks intact, confirming the regression-detection property still holds on the OSes where bit-equality is achievable.

Surfaces now because the R-CMD-check workflow only runs on master pushes/PRs, and PR #132 (dev -> master) is the first such PR since the parity tests were added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xuyiqing xuyiqing force-pushed the dev branch 2 times, most recently from 162c744 to 89743a1 Compare April 30, 2026 23:09
…E) + CI restructure

Squash of release-prep work for the v2.4.1 CRAN-accepted release. Pre-squash tip retained locally on tag pre-final-squash-v241 for rollback.

DOCS

- NEWS.md: drop "(development)" suffix from v2.3.2 / v2.3.3 / v2.4.0 / v2.4.1 headings now that the bundled release is on CRAN.
- vignettes/bb-updates.Rmd: mark v2.4.1 as "(2026-04-30) CRAN release." per the convention used for v2.2.0 (commit 07da03c). Trim v2.4.1 entry to user-visible enhancement + bug fix; drop slot-contract / match.arg / byte-equality internals.
- vignettes/01-start.Rmd: new chunk after the installed-version check displays live CRAN release version (via available.packages()) and dev branch DESCRIPTION version (via GitHub raw), with tryCatch fallbacks for offline renders. Lets users compare their installed version against both reference points.
- README.Rmd / README.md: bump Chiu et al. citation from "First View" to APSR 120(1): 245-266 (matches bib entry CLLX2026). Convert CRAN status / downloads badges from inline <img> tags to standard markdown image syntax. Strip trailing whitespace, drop trailing blank line. README.Rmd gains <!-- markdownlint-disable MD041 --> (YAML frontmatter + setup chunk push the H1 down; structural to Rmd).
- ARCHITECTURE.md: full scriber regeneration on dev for v2.4.1. Refreshed module-structure / function-call / data-flow Mermaid graphs. Eight new R files added to the module table (cv-rolling.R, cv-rule-helpers.R, cv-helpers.R, impute_Y0.R, valid_controls.R, loading_bound.R, theme-helpers.R, plot_return.R). Two new architectural patterns: rolling-window CV (v2.3.0) and per-role weight + modern-theme split (v2.3.1/v2.3.2). Line counts updated across all existing files; total R source line count corrected to 32,047. Run log at statsclaw-workspace/fect/runs/2026-04-30-architecture-regen-v241.md.

CI

- .github/workflows/R-CMD-check.yaml: gate cost and strictness by event type.
  * pull_request to master: matrix collapses to ubuntu-latest only with NOT_CRAN="false" (CRAN-style; skip_on_cran tests are skipped). Fast feedback (~10-15 min) and matches CRAN's farm behavior so PR reviews catch CRAN-relevant issues.
  * push to master / workflow_dispatch: full 3-OS matrix.
    - ubuntu-latest, NOT_CRAN="false" (CRAN-style)
    - macos-latest,  NOT_CRAN="true" (full suite, including byte-equality fixture tests)
    - windows-latest, NOT_CRAN="false" (CRAN-style)
- Per-OS NOT_CRAN gating ensures the byte-equality fixture tests in test-paraboot-parity.R (PAR-1..7) and test-cv-parallel.R (P.3..6) only run on macOS, where their RDS baselines were captured (non-Apple BLAS produces bit-different results). On Ubuntu and Windows these tests skip via skip_on_cran(), matching how CRAN's win-builder and farm see them.

Implementation: dynamic matrix via fromJSON of a conditional string keyed on github.event_name; per-OS not_cran field threaded through to rcmdcheck via the `env=` parameter (verified: the actual rcmdcheck signature has `env = character()`, not `env_vars=`). r-lib/actions/setup-r-dependencies@v2 unconditionally writes NOT_CRAN=true to $GITHUB_ENV during its run, so the parent R process always sees NOT_CRAN=true regardless of workflow-level env: settings; the only reliable override is via callr's env vector, which rcmdcheck forwards. callr::rcmd_safe_env() lists 4 baseline keys (CYGWIN, R_TESTS, R_BROWSER, R_PDFVIEWER); rcmdcheck appends our `env` on top, and callr passes that complete vector as the child R CMD check process env. Empirically verified: parent NOT_CRAN=true, env=c(NOT_CRAN="false") -> child sees "false".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xuyiqing xuyiqing merged commit e497195 into master May 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant