Changes in version 0.2.0 First systematic audit pass following the 0.1.0 pre-release. Fourteen HIGH-severity and seven MED-severity bugs were fixed across the R and Stata code, plus a new data = argument and a multi-script verification harness. A 2026-05-01 Windows verification pass uncovered three further HIGH-severity bugs (Stata weights not forwarded to bandwidth selection; R rdhte_lincom silently applied multcomp's single-step multiplicity adjustment; harness orchestrator regex/CSV-staging gaps) which are included below. A subsequent expert-review pass on the Stata code on 2026-05-01 PM uncovered six further HIGH-severity issues, surfaced one previously silent latent inference bug in the no-covs_hte path, and bumped the Stata floor from 14 to 16 (frames). All are included in this version. Stata 16+ now required (was Stata 14+). Driven by the migration from preserve to frames for the in-program working sample. New features - data = argument added to rdhte() and rdbwhte(). When supplied, y, x, covs.hte, covs.eff, weights, cluster, and subset may be given as bare variable names referring to columns of data. covs.hte additionally accepts a one-sided formula (e.g. "~ z1 + z2") whose variables are looked up in data first. Mirrors the rdrobust 4.0.0 pattern. Implemented via the internal .rdhte_resolve_data() NSE helper in R/helpers.R. Bug fixes (R) - Undefined W in no-covs.hte + covs.eff branch. The internal regression formula referenced W even though no heterogeneity variable was supplied. rdhte() now uses Y ~ T*Xp + covs (and Xq for the q-fit). Previously crashed at lm(). - Character / formula-string covs.hte path. Three separate bugs were fixed: a missing brace pair that left fml.txt undefined for ~-prefixed input; the formula's environment now uses parent.frame() so caller-frame variables resolve; and the data.frame(covs.hte = model.matrix(...)) step was rebuilt so the downstream grep("^covs.hte", ...) finds the correct columns (previously resulted in "subscript out of bounds" deeper in lm()). The illustration's covs.hte = "w_left*w_strength" example was silently affected by these and now produces correct output. - Bandwidth typo is.null(h.r) & is.null(h.r) (twice the same argument) is now is.null(h.l) & is.null(h.r). - Matrix-as-vector indexing h.lev[l] in three locations across rdhte() and rdbwhte(): the right-side effective-sample-size count was using the left bandwidth whenever bwselect = "msetwo" or per-group h.l/h.r were supplied. Now uses h.lev[l, 1] and h.lev[l, 2]. - Nh.lev always 0 in continuous-covs.hte auto-bw branch because h[1]/h[2] was NULL. Now uses h.lev[1,1]/h.lev[1,2]. - out$h shape inconsistency between branches (vector in no-covs.hte, matrix in covs.hte). All branches now return a consistent n.lev × 2 matrix. - Slot name typo W.names. = W.names (trailing dot) so summary.rdhte() could not find x$W.names for the continuous case. Renamed to W.names. - Bandwidth-vector validation check length(h.l) > 1 & length(h.r) always evaluated TRUE (bare length(h.r) coerces to TRUE when positive). Now length(h.l) > 1 | length(h.r) > 1 correctly errors on mismatched lengths. - do.call-unsafe NSE. deparse(substitute(covs.hte)) returned multi-line output when covs.hte was passed as a value rather than a symbol (e.g. via do.call), causing paste() recycling to inflate W.names to length-126 and the names(tau.hat.se) <- W.names assignment to error. The deparse output is now collapsed to one line and length-capped (>100 chars falls back to the placeholder "covs.hte"). - N.lev is now consistently c(N_l, N_r) across all branches; the categorical-covs.hte branch previously stored table(W.covs), which made summary.rdhte() show wrong "Number of obs" for any group count other than two. - subset = accepts both logical and integer indices (matches rdrobust 4.0.0); previously only logical worked. - rdbwhte() gained weights = and subset = arguments to match rdhte()'s feature surface, plus the same string-formula covs.hte handling. - weights = is now forwarded to all internal rdbwselect() calls (previously dropped silently in three locations). - rdbwhte() no-covs.hte branch's covs.cont <- FALSE flipped to TRUE so the print path matches rdhte(). - rdbwhte()'s continuous-covs.hte branch now returns Nh as a 1 x 2 matrix (consistent with the factor case) rather than a length-2 vector. - rdhte_lincom(): validate level is in [1, 100) so the common fraction-vs-percentage mistake (e.g. level = 0.95) errors clearly rather than silently producing a tiny CI. - rdhte_lincom(): column renamed t_stat -> z_stat. The values were already z-statistics (multcomp's glht uses a Gaussian approximation by default); the column label is now correct. - rdhte_lincom(): removed dead ses variable and a commented-out se column. - rdhte_lincom() p-values were silently multiplicity-adjusted. summary.glht() defaults to test = adjusted("single-step") whenever the user passes more than one hypothesis, so the p_value column in the individual data frame was an FWER-adjusted p-value rather than the per-hypothesis two-sided z-test the docstring promises (and that Stata's lincom reports). For >1 hypothesis the adjusted values were ~2x the raw values; the single-hypothesis case was already correct. Now passes test = adjusted(type = "none"). Point estimates and CI bounds are unchanged. - checkup_rdhte_basic.R was emitting r$N[1]/r$N[2] (full-sample left/right counts, post-na.omit) for the N_l/N_r cross-language diff fields, which mismatched Stata's e(tau_N) (effective N within bandwidth). Now emits r$Nh[1,1]/r$Nh[1,2] so both sides compare the same quantity. - Nh shape inconsistency in the continuous-covs.hte branch. At rdhte.R:509 the continuous (no W.lev) path returned Nh as a length-2 vector while every other branch returned a 1 x 2 (or n.lev x 2) matrix. Downstream code that indexed Nh[i, j] uniformly -- including the Akhtari/Moreira/Trucco replication script -- broke on the continuous path. Now matches the rest of the branches with matrix(c(Nh.lev.l, Nh.lev.r), 1, 2). Values are unchanged; positional access (Nh[1], Nh[2]) still works. Bug fixes (Stata) - rdhte y x, vce(hc1) errored at regress. Stata's regress only accepts vce(robust), not vce(hc1). The vce string is now translated to vce(robust) for the internal regress calls (display still shows "HC1"). - rdbwhte ... vce(cluster c) produced a different bandwidth from the auto-BW inside rdhte ... vce(cluster c) because rdbwhte was forwarding the user's raw vce string (and forcing vce_select = "hc0" internally) while rdhte built a proper vce_rdbw local. Both now use the same vce_rdbw construction. - rdbwhte ereturned e(cmd) = "rdhte"; now correctly "rdbwhte". - CI bounds at rdhte.ado:712 no longer hard-code 1.96; now use invnormal(1 - (1 - level/100)/2) so level() is honored. - Auto-i. prefix on a 0/1 variable now warns when the variable is constant (all 0 or all 1) and the heterogeneity analysis collapses to an overall RD ATE. Mirrored across rdhte.ado and rdbwhte.ado. - rdhte_lincom: lincom is now wrapped in capture so a malformed hypothesis errors with a clear message instead of silently picking up stale r() values from the previous estimation. - rdhte_lincom accepts a new level() option (default 95) -- the R version already had it; the Stata version was using the global set level only. - weights was not forwarded to bandwidth selection. rdhte.ado's two internal rdbwselect calls (the joint-bandwidth path and the per-level factor path) did not pass the user's weights() option, so rdhte ..., weights(w) bwselect(...) silently chose a different bandwidth from the R-side equivalent. Same shape of bug as the vce_rdbw cluster fix in this release. Now forwards weights() to every internal rdbwselect invocation. - rdbwhte.ado was missing the weights() option entirely. The R-side rdbwhte() already accepted weights =; the Stata syntax line did not declare weights(string) at all, and the three internal rdbwselect calls (joint-bandwidth, no-covs_hte, per-factor-level) did not forward it either. Both gaps fixed. - checkup_rdhte_stress.do had five if _rc == 0 { di "..."; local ++PASS } lines that triggered Stata's r(198) program error: code follows on the same line as open brace under batch mode. All five expanded to multi-line. The same script's NA-handling probe referenced e(N_h_l) + e(N_h_r) (neither saved by rdhte.ado); now asserts only _rc == 0. Note: rdhte.ado's e(N) is the pre-NA-drop input row count (line 158), not the post-drop count R's sum(r$N) reports -- a documented LOW-severity asymmetry left for a follow-up. Verification harness Eight new top-level scripts under the project root provide a verification gate analogous to rdrobust's: - checkup_rdhte_lm.R -- 40 R-only sanity checks. Compares each rdhte result to a manual reconstruction from coef.full / vcov.full. - checkup_rdhte_basic.{R,do} -- 14 specs of cross-language baseline parity (factor / continuous / cluster / weights / kernel / bwselect / p-q / manual h / bw.joint). - checkup_rdhte_extended.{R,do} -- 9 specs combining 3+ non-default options each, with per-(label, level) emission. - checkup_rdhte_bw.{R,do} -- 14 specs of standalone-rdbwhte parity, including the cluster path that the H10/H11/H12 fixes target directly. - checkup_rdhte_lincom.{R,do} -- 7 hypotheses across 3 specs comparing R's multcomp::glht to Stata's lincom. - checkup_rdhte_vs_rdrobust.R -- 11 R-only checks confirming the illustration's claim that rdhte reproduces rdrobust under matched settings (h fixed, rho=1, vce="hc3"). Matches to ~1e-15 on tau. - checkup_rdhte_stress.R -- 11 R-only edge cases. - checkup_rdhte_stress.do -- 9 Stata-portable stress probes. - checkup_rdhte_strformula.R -- 6 R-only checks for the string-formula covs.hte path with caller-frame variables. run_rdhte_checks.py orchestrates all of the above end-to-end and reports per-spec |R-Stata|. Marker on success: RUN_RDHTE_CHECKS_OK. Two orchestrator-level bugs found and fixed during the 2026-05-01 Windows verification pass: - The key=value parsing regex was (\w+)=(-?[0-9.eE+\-]+), which did not tolerate the whitespace that Stata's %15.10f display format inserts between = and the number. Every Stata float field (tau_cl=..., h_l=..., etc.) silently parsed as nothing -- only the unpadded integer fields (N_l=240) got through. The orchestrator was therefore comparing only N_l/N_r, which under the old full-sample-vs-effective N semantic produced ~700-unit "diffs" that masked the real silent-OK pattern. Fixed by adding \s* after =. - Each Stata batch is run from a C:\Temp\rdhte_stata_* scratch dir, but the .do files reference _rdhte_data.csv by relative path. run_STATA() now stages _rdhte_*.csv from the project root into the scratch dir before invoking Stata. Stata expert audit (2026-05-01 PM) A second pass on the Stata code, severities HIGH / MEDIUM / LOW. All HIGH fixed; most MEDIUM applied; LOW partially done. HIGH - Latent missing-e(V) bug in the no-covs_hte path (surfaced by H4). rdhte y x (no heterogeneity) had been silently returning an e(V) matrix populated with .s -- the no-covs branch never wrote tau_V[1,1]. The cap ereturn post b V masked the resulting r(504) matrix has missing values, leaving downstream test/lincom calls with broken inference. Now correct (matrix tau_V[1,1] = se^2); the cap is gone. - rdrobust version-check rewritten. The previous code read line 5 of c(sysdir_plus)/r/rdrobust.ado with file open and tokenized to extract the version. Replaced with findfile rdrobust.ado (works for any adopath location: PERSONAL, SITE, PLUS) plus a regex scan of the first *!...vN.M.K line. Robust to future rdrobust file-layout changes. - vce() parser validation restored. vce(robust foo), vce(hc1 var), vce(cluster a b), etc., previously had their validation commented out (and silently mis-parsed). Now error with r(198) and a clear message; the legal 2-token forms are exactly vce(cluster|hc2|hc3 var). - Tempname migration of all working matrices. All 14 internal matrices in rdhte.ado (tau_h, tau_hat, tau_bc, tau_se, tau_V, tau_t, tau_pv, tau_N, tau_h_l/h_r, tau_ci_l/r, CV, plus the b/V ereturn pair) and 2 in rdbwhte.ado (tau_h, tau_N) are now tempname'd. They no longer leak into the caller's namespace and cannot clobber a user matrix of the same name. - rdhte_countvars no longer double-counts. The if-elseif chain wasn't mutually exclusive: any pure factor#factor term (e.g. i.W#i.Z) incremented BOTH n_interaction_factor AND n_interaction_continuous. Restructured so each term routes to exactly one bucket. Affected the downstream quad flag and the BW-routing decision for higher-order factor interactions. - rdhte_fvexpand declared rclass. Previously the program had no class declaration and callers relied on the inner fvexpand call's r() values surviving through program return -- accidental, undocumented, and brittle to future Stata. Now rclass, captures r(varlist) / r(fvops) inside nobreak before any reset, and explicitly returns them. MEDIUM - preserve -> frames (M1). rdhte.ado and rdbwhte.ado now do all work in an isolated temporary frame: frame put * if touse, into(workframe); cwf workframe; ...; cwf orig_frame; frame drop workframe. Avoids the O(N) preserve disk round-trip and only the touse=1 subset is copied (typically a fraction of the user's dataset). The user's data and frame state are never mutated, even on error. Drives the Stata 16+ floor bump (frames were introduced in Stata 16). - e(N_h_l) and e(N_h_r) added (M2). Per-side bandwidth-effective sample sizes (summed across factor levels). Mirrors rdrobust's convention. The deeper post-NA-drop semantic for e(N) across covs_hte / weights / cluster missingness is documented as a deferred follow-up. - qui count consolidation (M3). Three qui count passes collapsed to two (left + right; total via addition). One fewer O(N) pass per call. - version 14.0 -> 16.0 (M8). Required by the frames refactor. Bumped across all 5 ados; rdhte_countvars.ado also gained the missing version directive (it had none before). LOW - Dead / commented-out code removed (L1). ~10 commented blocks across rdhte.ado (check-it-exists scaffolding for value labels, dead tau_V covariance attempts, redundant local k = i inside nested forvalues, etc.) and rdbwhte.ado (orphan */, value-label scaffolding). - e(outcomevar) removed (L2). Was advertised in the help file but the 2026-04-27 harmonization audit had already removed it; line 855 had silently reintroduced it. Users get e(depvar) per Stata convention. - rdhte.sthlp Stored results section refreshed (L5). Removed e(outcomevar); added e(N_h_l) and e(N_h_r); e(N) description now correctly says "sample size after listwise deletion on (y, x)" instead of "original number of observations". - Continuous-W discretization warning (L6). When the user passes covs_hte(W) (no i. prefix) and W has fewer than 10 unique values, rdhte now emits a note: recommending i.W for subgroup-CATE estimation rather than the (probably unintended) slope-coefficient interpretation. MEDIUM items deferred (not in this release) - M4 (length(subinstr(...)) hash-counting refactor): arithmetic is correct, just opaque. - M6 (display-formatting magic numbers): pure cleanup, high regression risk on output formatting. - M7 (DRY the 6 BW-selection branches): ~150 lines of near-duplicate code in rdhte.ado; should pull into a subprogram. - e(N) post-NA-drop for covs_hte / weights / cluster: needs careful covs_hte string parsing to markout factor variables. Changes in version 0.1.0 (2025-07-18) Initial release.