Changelog
Source:NEWS.md
hvtiRtemplates 1.4.2
dc-tablesdraws the%summarytabletable the team asked for on 2026-10-16: one table with continuous and categorical variables together, and, withSTDDIFF <- TRUE, the standardized difference%stddiffprinted beside the p-value. The difference comes fromhvtiRpropensity::ps_stddiff()throughhvtiRtables::hv_tbl_compare().MEAN_SDshows named continuous variables as mean ± SD and the rest asCONTINUOUS_STAT, with every one still tested by Wilcoxon.ORDINALnames ordered categorical variables, which%stddiffranks rather than treats as nominal.GROUP_1names the treated group whenBYis not 0/1.SHOW_VARIABLE <- FALSEdrops the new Variable column for a manuscript. The report now shows the table exactly as it is saved to Word, with its notes, not gtsummary’s working copy. Needs hvtiRtables 1.0.6, and broom forCOMPARE <- "pvalue".migrate_job()on a%desc_tabjob now writes the table that job printed:COMPARE <- "pvalue"whenby=named a group,CONTINUOUS_STAT <- "both"for mean ± SD beside the percentiles, andPERCENTILESfromperctile=when every call agrees on a low/median/high triple. Until now it wrote “none”, “median” and 15, 85 whatever the SAS said.exposure=0(the group as an outcome, incidence by row) is listed as unresolved, sincedc-tablesdoes not draw it. The report lists where the R table differs from%desc_tabon purpose: Fisher’s exact test for sparse categorical rows, the Wilcoxon test (exact in small groups) where%desc_tabreported Kruskal-Wallis for two groups, and percentiles:%desc_tabcomputed them withPCTLDEF=4, where the R table uses SAS’s default definition, so the low and high percentiles can differ in small groups.nb-boostmtree’s partial-effects pages are drawn at their own height. Since 1.4.1 every page was emitted as tall as it was wide, because onesprintf()call built both chunk options and handed each the page width, so a one-panel page came out 3.25 by 3.25 in rather than 3.25 by 3. The saved files were already the right size; only the figure in the report was wrong, and the call raised a warning on every page.dc-tables’ report shows the table with the same header marks, notes and abbreviation key as the saved Word table, throughhvtiRtables::hv_man_table_display(), so anABBREVIATIONSentry or a header asterisk is explained in the HTML as well. Needs hvtiRtables 1.0.7.migrate_job()now migrates a%desc_tabjob in the shapetp.dc.tables.ods.sasships: each table written twice, overall (by=blank) and by group. It stopped on that pair before, so the mapping added for%desc_tabjobs never reached a typical one. The group becomesBYand the overall calls become the table’s Overall column, which the report says, but only when every patient has a group: where the registered data has patients with noBYvalue, which SAS’s overall calls counted and the R table’s Overall column leaves out, the report lists it as unresolved with the count instead. A group variable an overall call summarized as a row is taken out ofGROUPS, since its levels are now the columns. Calls naming two different groups still stop.nb-boostmtree’s variable importance is seeded, so a re-render reproduces it.vimp.boostmtree()permutes each predictor at random and the template called it unseeded, so the importance figure, and the six most important variables the partial effects are drawn for, could change from one render of the same fit to the next. It now runs underSEEDthroughcache_fit(), as the fit does. The seed-hygiene test now listsvimp.boostmtree()among the calls that draw from R’s generator, which is how it had been missed.nb-boostmtreecaches its partial effects beside the fit, asnb-boostmtree-partial.rds.partial.plot()makes one prediction over every patient for each of 25 grid values per continuous covariate, and was the slowest step after the fit; it re-ran on every render, even one that changed only prose. A changedEFFECT_VARIABLESor a refit stops the render untilREFIT = TRUE, as a changed fit setting does. The importances are cached too, asnb-boostmtree-vimp.rds.nb-boostmtreeruns its cross-validated fit on several cores, through a new study choice,CORES(4, or 1 on Windows, which cannot fork). The fit’s cost grows with the square of the number of patients, and most of it now runs in parallel: at 600 patients, 4 cores fit 3.3 times faster than before. The fit is identical on any number of cores, soCORESis left out of the cache key and changing it never forces a refit. This needsboostmtree2.0.4 from the CCF fork (v2.0.4-ccf), which the setup chunk now requires. A job scaffolded before this change keeps its own copy of the fit call, so its cached fit is unaffected until the job is upgraded; the first render after an upgrade finds the cached fit stale and stops untilREFIT = TRUE, because the call itself changed. The upgraded job also carriesCORESunder its ownEDIT:marker, so it renders as a draft until the marker is resolved.The study-setup article’s “If EDA uses a subset” example now registers the subset with
kind = "subset", and points to an analysis set (hvtiRdatabuild::write_analysis_set()) as the alternative for a subset of the study dataset that several jobs share.nb-boostmtree’s setup chunk, when boostmtree is missing or too old, now gives the install line for boostmtree 2.0.5 (v2.0.5-ccf), whose cross-validation is faster for an ordinal response; the fit is the same. The floor stays at 2.0.4.nb-boostmtree’sCORESchoice now says what to set it to. Its comment carries a table, measured on the server, of peak memory atM = 10and hours scaled toM = 1000, by patients and cores, with the smallest session each needs. The cross-validated fit’s parallel share levels off near 80%, so speed stops improving past 4 to 5 times, and memory is what limitsCORESon a large study: at 4,000 patients andM = 10, 4 cores needed about 70 GB and 8 ran out. The fit itself keeps every step, so memory grows withMand the sessions are a floor atM = 1000, not a fit.nb-boostmtree’s setup chunk, when boostmtree is missing or too old, now gives the install line for boostmtree 2.0.6 (v2.0.6-ccf), whose forked workers need about a third of the memory, so moreCORESfit in a session. The fit is the same. The floor stays at 2.0.4.Tests and examples pass
renvandcleanuptohvtiRutilities::study_setup()explicitly, ahead ofhvtiRutilitiesturning both on by default. A new test fails on any call that leaves them implicit.The
hvtiRutilitiesfloor rises to 1.5.2, the first release whosestudy_setup()takesrenvandcleanup.nb-boostmtree’s setup chunk, when boostmtree is missing or too old, now gives the install line for boostmtree 2.0.7 (v2.0.7-ccf). Its forked workers each use one BLAS thread, so a server whose R links a multithreaded OpenBLAS is no longer overloaded: at 1,000 patients on 4 cores, 616 s where 2.0.6 took 1,488 s. The floor stays at 2.0.4.add_job()installs the packages a template needs into the study’s library, and records them inrenv.lock, when the study is a renv project. It asks first, and writes nothing if the packages cannot be had. It needs hvtiRutilities 1.5.7 or later, the first release whosestudy_need()leaves the study unchanged when an install is declined.nb-boostmtreegains a study choice,MTRY: the number of predictors tried at each split of each tree, passed toboostmtree(mtry = ).NULL, the default, tries every predictor, as the template has always done, so existing fits are unchanged. Set it to reproduce a study fitted with a smallermtry. It is part of the cache key, because a differentMTRYis a different fit. A job upgraded to this version finds its cached fit stale once, because the fit call changed.dc-tablessaves its Word table in hvtiRtables’ manuscript style: a full grid, a two-row header when the table is grouped, with the grouping spanning its levels, shaded section rows that span the table, the notes below it, and a caption above it. Three study choices set it:MANUSCRIPT_LAYOUT("compare", onencolumn beside the groups, or"describe", a count beside each group),MANUSCRIPT_CAPTIONandMANUSCRIPT_FILL. The report keeps the report style, with the Variable column, and shows the Word table beneath it.SHOW_VARIABLEnow applies to the report only.migrate_job()writesMANUSCRIPT_LAYOUT <- "describe"for a%desc_tabjob that printed mean ± SD beside the percentiles, which the compare layout cannot draw. Needs hvtiRtables 1.0.9. A job scaffolded before this keeps working and keeps its report-style Word table;upgrade_job()adds the three choices, each with itsEDIT:marker. An upgraded job withCONTINUOUS_STAT "both"stops at its Word table untilMANUSCRIPT_LAYOUTis set to"describe". hvtiRtables’ error nameslayout =; the study choice that sets it isMANUSCRIPT_LAYOUT.The
hvtiRtablesfloor is now 1.0.9, inDESCRIPTIONand indc-tables’ setup check. In 1.0.7 and 1.0.8,hv_tbl_summary()failed with “Value must be a named list of functions” on any mean±SD or median row when the study’sR/folder definedsdormedian, and every job sourcesR/into the global environment. 1.0.9 fixes that.
hvtiRtemplates 1.4.1
The
rfs-explain,rfc-explainandrfr-explaindependence figures, andnb-boostmtree’s partial effects, are drawn on pages no larger than the printed page, 6.5 by 9 in, rather than as one figure that grew with the number of predictors:rfs-explain’s marginal dependence was 24 in tall in the template gallery and 48 in at the defaultTOP_K = 8, and the partial and VarPro dependence figures were 9 in wide. Panels sit two across; more than fit go on further pages, each a numbered figure whose caption names its predictors. Saved files gain a-page-01suffix. Therfc-explainandnb-boostmtreeimportance figures are 6.5 in wide rather than 9.dc-edadraws its pages on the printed page, 6.5 by 9 in portrait, rather than 11 by 8.5 in landscape, and its grid defaults to two panels across (GRID_NCOL <- 2L), so each 8 pt panel’s title and year axis stay readable. A section takes more pages than before. A job that setsGRID_NCOLkeeps its choice; more than two columns crowd the panels. Migrating a legacy EDA job that sets no grid now gives two columns.dc-eda’s categorical panels, percent and count, no longer name their categories, in a legend or beside the panel: the names took width the bars needed, so the bars now fill the panel. The house colors tell the categories apart, each panel’s title still names its variable, and the section’s frequency table names the categories.dc-stddiffdraws its balance figure 6.5 in wide, not 7, and puts at most 30 variables on a page of at most 9 in; more variables go on further pages, each a numbered figure (fig-balance-1, …) whose caption names its first and last variable. The saved file isdc-stddiff-balance-page-01.pngand so on, notdc-stddiff-balance.png.upgrade_job()’s help page now says first that it upgrades the one job named inpath, shows a real job path and how to upgrade several jobs one call each, and says what to check afterwards.dc-edadraws against the surgery date by default,X_VAR <- "dt_surg", rather than ayearcolumn no study dataset carries, which stopped a fresh job with “Unknown EDA column(s): year”. A date column is drawn as a decimal calendar year: whole years for the percent and count bars, the point in the year for the continuous panels. Any other column is drawn as it is. The newX_LABELchoice titles the axis, “Year of operation” by default; set it andX_VARwhen time 0 is not an operation, such as an enrollment date. A missingX_VARcolumn now stops with “Set X_VAR in the study choices to the study’s reference date or year column.”In the HTML report,
dc-eda’s percent and count pages carry hover labels. The pages name no categories, so a reader hovers a bar to see its category, year and count and percent. Each page is a ggiraph widget of about 120 KB, with no embedded fonts. The saved PNG and PDF figures are unchanged. The hover labels need ggiraph, now in Suggests, and a hvtiPlotR that hashv_girafe(). Without either, the pages are images as before.A selected figure is saved as a TIFF too, beside its PNG and PDF: 300 dpi and LZW-compressed, through
ragg::agg_tiff(), sincegrDevices::tiff()cannot compress on a Mac without XQuartz. Journals ask for a PDF or a TIFF, and the TIFF is the raster format the family delivers by default, as hvtiPlotR’ssave_manuscript()does. The PNG stays the image a report embeds. ragg is suggested: without it the TIFF is skipped, with a message once a session. ragg’s floor is 1.2.1, which fixed a crash writing a TIFF with a transparent background. A transparent plot stays transparent in its TIFF, and saving a figure leaves the graphics device that was active before it active afterwards.The
hmtemplate’s note onvars.sascolumns (mean imputation, thems_*flags, and why R reading a column as a factor would quietly change the model) is rewritten in plainer terms for readers new to R.
hvtiRtemplates 1.4.0
dc-generalnow says plainly that its tables are checks on the data, and points todc-tablesfor a manuscript table saved to Word, with theadd_job()call that scaffolds one (#308).New
upgrade_job()rewrites a job from the current version of its template and carries the study’s choices across: the upper-case assignments in itsedit-*chunks, multi-line values included. A choice the current template no longer has is kept in the job, commented out under anEDIT:marker, and the-upgrade.mdreport beside the job lists what was carried, not placed and new, and which chunks to review, by name and never by value. The old job is kept as<file>.pre-upgrade(#307).add_job()now stamps each job with the template and package version it was copied from, as one HTML comment after the front matter, whichupgrade_job()reads.hmoffers each covariate only to the phases whose block names it. It built one formula from every block, so every covariate entered every phase and the estimates were not comparable with the SAS model it replaces (#171). A block naming a phase thehzfit does not have now stops the job.hmno longer needs a companion SAS job: withSAS_JOB <- NULLthe covariates are listed per phase in R, and the covariate table says which source was used (#178).The
lm-*andrf*-fittemplates now show a table of missing values right after the data are read: each model variable that has any, with its count, and the cohort the fit will use (“725 of 800 rows analyzed; 75 dropped”, or underna.impute, how many rows are imputed). Until now the drop showed only as a smaller sample size in the fit’s own output.hmstops at that point instead, naming each column and its count, wherehazard()had failed with “Predictor rows must match the length of ‘time’” (#174).hs-setupderives the life tables’ 0/1 male and non-white indicators from the study’s own coding. New settingsMALE_LEVELandOTHER_LEVELSname the values that count as male and as non-white, so afemalecolumn (MALE_LEVEL <- 0), a sex factor or a race factor is used as it is, without first deriving the indicators by hand. Both default to 1, which leaves a job with 0/1maleandothercolumns unchanged. A level the column never takes stops the job, and a new table shows how each value was counted, so a race level left out ofOTHER_LEVELS, and therefore counted as white, is visible (#183).bl,brandbcname the bag they read withBOOT_PREFIX, asbhdoes, in place ofBOOT_FILE: each reads<BOOT_PREFIX>.rds, defaulting to its own prefix, and its runner ininst/runners/now writesbl.rds,br.rdsorbc.rdsrather thanbagging.rds. One setting across the family lets a runner write where every report looks, and the three no longer overwrite one another when they share a subject and type. A job scaffolded earlier keeps its ownBOOT_FILE; a runner still writingbagging.rdsis read by settingBOOT_PREFIX <- "bagging"(#189).Every template now follows one figure standard. A single-panel figure is 6 in wide by 4 in tall, in the report through
fig-widthandfig-heightin each template’sformat:block and in the saved PNG and PDF throughsave_figure()’s default. Faceted figures and plots with one row per variable keep a size of their own, and say why. Type is 12 pt in hvtiPlotR’s theme,theme_hv_manuscript(base_size = 12), on every ggplot, including those returned by a package’splot()method, andtheme_minimal()is gone. The one exception isdc-eda’s pages of small panels, four to a row, which use 8 pt and name each category beside its panel in its own color instead of a legend, with about five labels on the year axis. Every figure’s numbered caption now names the shape or line and the color of each mark, so the figure can be read from its legend alone.test-figure-standard.Rguards the size, the theme and the captions (dev/specs/2026-10-13-figure-standard-design.md).lgsizes its figure with hvtiPlotR’shv_panel_size(), so it now needs hvtiPlotR 2.8.8, whose 6.5 inch page fits three columns, and the package requires 2.8.9, which stops that function leaving a graphics device open. Its six scales keep their two rows of three, each panel shrunk together to fit the page, with about three ticks on each x axis so the labels of panels under 2 inches wide do not run together.hs-setupaccepts a cohort with no one in a life-table stratum, all female or all white, asus_matched()does. Under the defaultMALE_LEVEL <- 1, an all-zeromalecolumn stopped, because the level never appeared in it. A level the column could take is now accepted: 0 or 1 in a 0/1 column, or a level a factor declares. An emptyOTHER_LEVELScounts no one as non-white. A level the column could not take still stops, because it is a typo (#183).bdno longer publishes catalog releases, which hvtiRutilities 1.5.3 retired. It writes the study’s working file,datasets/<DATASET_ID>.rds, with a schema 1 build record beside it,<DATASET_ID>.build.yml, and withREGISTER <- TRUEregisters that file as a dated version withregister_data()the first time andupdate_manifest()after that. The version’s manifest provenance then readsorigin: bd. The study choicePUBLISHis nowREGISTER, and thewrite-draftandpublishchunks are nowwrite-builtandregister. The build record keeps a hash of the cohort rules, never their text, so anEXCLUDErule that names a patient stays in the job file. A study registered under a file this job does not write still stops, now inregister. ADATASET_IDending in a date, or naming a filedataset-catalog.ymllists as a published release, stops before anything is written, so a working file never overwrites a release.bdneeds hvtiRutilities 1.5.3 and checks for it in its setup chunk. Its tests no longer callpublish_dataset(),register_data(release_id =)oradopt_data_update(), so they run with no deprecation warning.Two regression-model templates:
rm-balancing_continuous, a least-squares balancing score for a continuous exposure, andrm-linear, an ordinary least-squares outcome model. Both pool stacked imputations with Rubin’s rules, and both model the response untransformed or on the log scale through theRESPONSE_SCALEstudy choice; the report states the formula it fitted and reads the coefficients on that scale. They needhvtiRpropensity0.2.0, and thelmtemplates,dc-stddiffandlp-mirrornow ask for it too.The study-setup vignette’s “When registered data change” passage now reviews a rebuilt dataset with
hvtiRutilities::review_built()and registers it withhvtiRutilities::update_manifest(dataset = "study"), replacing the retired catalog-release pairreview_data_update()andadopt_data_update().The
lmtemplates read stacked multiple imputations again. Since 1.3.0 their data table stopped with “This job models one row per patient” on anyIMPUTATION, becauseread_job_data()counted a patient once per row rather than once per imputation. It takes a newimputationargument, which eachlmtemplate passes asIMPUTATION, and counts a patient within each imputation: a patient once in each passes, a patient twice in one imputation, or in a long dataset with no imputation, still stops. The data table now says how many imputations the rows hold, for example “240 rows on 120 patients, in 2 imputations”.read_job_data()pairs an ID with its imputation by value, so abit64integer64ID is no longer taken for one repeated patient, and records the imputation column as the data spell it (selection$imputation). Thelmtemplates read it back, soIMPUTATION <- "IMP"works for a columnimp, asKEYdoes.rm-linearandrm-balancing_continuousread a namedRESPONSE_SCALE, such as one looked up withscales["outcome"], as its value. Before, a named"identity"fittedlog()while the report read the coefficients as measured, and a named"log"was read as measured and left the residual axes unlabeled.read_job_data(), so every template, now drops direct identifiers beyond MRN and eMRN, unless one is the job’s ID or a KEY column, including a KEY that names a joined column:ccfid, Social Security numbers (ssn,soc_sec,social_security, each with or without a_no,_numor_numbersuffix), and patient names and contact details held as text, by name (a 0/1 or Y/N contact indicator stays, however it is stored), and any column whose values are shaped like Social Security numbers, whatever its name. The data table lists every column dropped and notes the ones dropped by their values.bddrops the same columns from the built it writes, sodc-edacan no longer draw an SSN carried in from an SSDI join.A website article, “From vars.sas to R: study-build and variable actions”, explains the design in
dev/specs/2026-10-14-bd-vars-action-items-design.mdfor analysts who know SASvars.sas: where each%varsswitch,tp.vars.<umbrella>module, correction and deletion now lives, with illustrative R. The functions it describes are not built yet. Articles undervignettes/articles/are now left out of the package tarball.The README’s Installation section now leads with
hvtiR::install(), which installs the whole HVTI R family in one call, and then givespak::pak("ehrlinger/hvtiRtemplates")for this package alone.dc-tablesandDESCRIPTIONnow require hvtiRtables 1.0.5, whose Word tables follow the SAS%summarytablelayout: section rows in the Characteristic column instead of agroupname_colcolumn, no%sign in the cells, a dagger and sub-header that name the statistic each row shows, and a superscriptaon eachnwith its footnote.lg’s calibration figure says it needs hvtiPlotR when hvtiPlotR is not installed, as it already did for ggplot2, instead of stopping on R’s “there is no package called” error. The figure has usedhvtiPlotR::theme_hv_manuscript()since the figure standard.
hvtiRtemplates 1.3.1
lm-binaryno longer prints the saved model’s full path in its report. A bareMODEL_PATHline showed the absolute path, study folder included, in a report meant to be shared. A test now stops any template from printing a path variable this way.dp-postageis removed, as its deprecation promised for the release after 1.2.3. Usedc-eda:add_job("dc.eda", subject, type)withSECTIONS <- c("continuous", "percent", "count")draws the pages it drew. Asking fordp.postagenow stops with the usual unknown-template error.Every
lmtemplate now checks the columns its study choices name before it fits, and a missing one stops with the setting to change, such as “OUTCOME names a column this dataset does not have: severity. Change OUTCOME in edit-study-choices.”OUTCOMEorTREATMENTandPREDICTORSare checked, including a variable used inside a term such asI(age^2);IDandKEYwere already checked byread_job_data(). Before, onlylm-binarychecked, and elsewhere a wrong name failed inside hvtiRpropensity with an error naming one of its functions (#179).lm-checkpredcan read a model saved in another set. A new optionalMODEL_SETnames the training job’s set, such as"stroke-model", so a validation job no longer has to share the subject and type of itslm-binaryjob.MODEL_FILEmust be a file name andMODEL_SETa set name, so neither can reach outside the study’s estimates. The template now says how to register a validation cohort as a dataset of its own and read it withDATASET; it is checked against the training patients like any other cohort (#180).lm-propensity_ordinalandlm-propensity_nominalprint a balance table, the standardized mean difference of each covariate between each treatment level and the reference level, in the placelm-propensity_binaryprints its own.lm-propensity_ordinalalso prints the cumulative direction of its proportional-odds model, aslm-ordinaldoes, which a reader needs to interpret the signs of its coefficients (#191).hvtiRpropensity 0.1.10 or later is now required, up from 0.1.7, by
DESCRIPTIONand by the version check of everylmtemplate and ofdc-stddiff. 0.1.10 returns the multi-level balance tables, labels the count family’s exponentiated coefficientsrate_ratiorather thanodds_ratio, and names the declared treatment levels inlm-propensity_binary’s group counts rather than “control” and “treated” (#206).dp-edaanddp-trendsare renameddc-edaanddc-trends, so every descriptive template now carries thedcprefix. The old names,dp.edaanddp.trends, still work inadd_job(),open_job(),template_path()andmigrate_job()through the next release, each call warning once that the name is deprecated in favor of itsdcreplacement, and are removed in the release after. A job already scaffolded asdp.eda.*ordp.trends.*(or*-dp-eda.qmd,*-dp-trends.qmd) keeps its name and keeps rendering:open_job()opens it under either name, andadd_job()refuses to write a second copy under the new one. A legacy SAS source nameddp.trends.*migrates intodc-trendswithout a warning. The taxonomy keeps itsdprow, since legacy jobs carry that prefix; the queueddp-variableanddp-boxplottemplates are planned asdc-variableanddc-boxplot.A new
dc-trendsjob is written to the study’s descriptive folder (10_descriptive/, ordescriptive/in a legacy study), wheredp-trendswrote it tographs/. Its figures are still saved undergraphs/<subject>-<type>/, and the report links them from its new folder.dp-gfup, deprecated in favor ofdc-gfupin 1.3.0, is removed.add_job("dc.gfup", subject, type)draws the same panels beside the follow-up tables. Asking fordp.gfupnow stops with an error namingdc.gfup: an unknown template’s error lists any template with the same qualifier under another prefix.New template
lp-mirror, the mirrored histogram of the propensity score:add_job("lp.mirror", subject, type)with the subject and type of thelm-propensity_binaryjob it plots. It reads that job’s saved bundle and never refits, drawing treated patients above the axis and controls below: all patients, then under the matching weights, and, whenMATCHnames a pair-match column, the matched patients. The y-axis is derived from the counts, the same above and below. The groups are labeled by the declared treatment levels, or byGROUP_LABELS. A bundle without lineage, or whose rebuilt read has different rows, patients or patient hash, stops with the job to render again.MATCHis joined onto the scored patients by a study-keyed digest ofID, so the job keeps and prints no identifier; it counts the scored patients with no flag and the flags with no score, and stops when fewer than half the scored patients are in the dataset, whenMATCHis not a column coded 0 and 1 or flags no scored patient, and when a score, weight or group is missing.lpbecomes a qualified prefix.lm-checkpred’sMODEL_FILEandMODEL_SETchecks are now shared withlp-mirror. AnNAin either now stops with the setting’s own message rather than an R error.The template catalog names the
lgprefix “Logit calibration”, not “Logit trends”, matchinghvti_taxonomy()from hvtiRutilities. The description is unchanged:lgjobs calibrate a covariate against an event to choose its scale.New template
lg, logit calibration:add_job("lg", subject, type), usually with the outcome as the subject, as inlg.dead.calibration. It decides which scale a covariate should enter a model on, before it goes intoPREDICTORSin anlmjob. Each covariate inVARIABLES, continuous or ordinal, is ranked into groups asPROC RANK GROUPS=does, or takes each level as a group, as%logitgrdid. The job takes each group’s event rate on the logit, or, for time-related death, the log of the constant hazard, and plots it against the covariate on six scales (raw, log, inverse, inverse squared, squared and exponential), each panel with the R-squared of a straight line weighted by group size.INTERVAL = c(start, end)takes one phase of follow-up as the SAS early, constant and late jobs did: patients who ended beforestartare left out, and the rest are censored atend, so an event afterendno longer counts. Under the hazard link the time at risk is the time inside the window,pmin(TIME, end) - start; the SAS template summed it from 0, which is the one deliberate difference. Under the logit link the same patients are kept and an event counts only inside the window. The job reports how many patients it left out, how many it censored and how many events fell afterend. The scale chosen for each covariate is saved aslg-scales.rdswith the term to write for it, such aslog(age)orI(50 / weight), and the job prints thePREDICTORSline to copy into anlmjob. A blankchosenwarns in a draft and stops a final render, as an openEDIT:marker does, and so does a chosen scale that is not a number for some patient, such as the log of a covariate that is 0 for a few, though every group mean is fine; the message counts those patients and shows no value. A group whose link value cannot be computed keeps its row, blank, and is named under its table; a scale that cannot be computed for a covariate is drawn as an empty panel. A patient with no event value is ranked but counts in no group’s patients, events or follow-up. A covariate whose median rounds to 0, such as a 0/1 indicator, has no defaultconstant: the inverse, inverse squared, squared and exponential scales are not computable for it untilconstantis set, and raw and log remain. Two checks keep a figure from resting on groups too small to read: an ordinal covariate with more than 20 levels stops the job, and so does any group with fewer than 5 patients, naming the covariate and never a value.rfs-fit,rfc-fitandrfr-fitnow draw the out-of-bag error by number of trees as a curve. With importance off, randomForestSRC records the error only at the last tree, so the figure was one point and could not show whetherNTREEwas large enough. The fit now passesblock.size = 1, which records the error after every tree and leaves importance off (#165).rfs-explain,rfc-explainandrfr-explaingive VarPro the forest’s imputed data when the fit usedna.impute, so VarPro and the forest describe the same patients. Before, varPro dropped every patient with a missing value and its importance and partials described fewer patients than the forest, with a notice in the rendered report. The report now says that VarPro was given the imputed data. A saved-varproresult from an earlier render describes the old, smaller cohort, socache_fit()stops on it and names the change: setREFIT <- TRUEonce to recompute it (#166).The marginal plot of
rfs-explain,rfc-explainandrfr-explaindraws no loess smooth through a predictor with fewer than six distinct values, which stops the dozens of loess warnings a four-valued predictor raised in the rendered report.rfc-explainalso draws its points for a 0/1 outcome: ggRandomForests read the outcome’s levels “0” and “1” as missing, so every point was removed and only the smooths were left (#167).dc-edaprints the abbreviation key under a section from the abbreviationslabel_map()records for that section’s variables, instead of searching the shortened labels for each abbreviation. A label that says “SP” in its own words and is only cut no longer producesSP = Surgical procedureunder its section (#194).No template leaves placeholder prose in the rendered report. Sentences such as “Describe the question here.” in the
rf*-explaintemplates and “Replacesanalyses/<job>: describe its outcome and candidate predictors here.” in therf*-fit,ac,hz,hm,hp,hs,bh,bl,br,bc,nb,bd,dc-stddiff,dc-tables,dc-trendsandlgtemplates now sit inside theEDIT:marker that asks for them, so resolving the marker removes them (#182).The
bhfrequency figure sorts the variables within each phase. Before, one ordering across both phases left a variable offered to two phases out of order in each facet (#189).The
bhdefaultCLUSTERSis an empty list, with a commented example, and an empty list reports no clusters instead of stopping. The old default named terms such asearly.agethat a small run may never carry, andboot_clusters()refuses a name no replicate has (#188).The
bctemplate says what its runner’sBASEsetting does and costs. Neitherboot_select()norfit_cox()can force a term into a Cox model, soBASEonly names a screened candidate whose frequency the tables leave out and whose spread the check watches; the old advice to “force at least one term” could not be followed (#186).hs-concordancetakes its time and event columns from thehmmodels it reads, ashm,hpandhs-setuptake theirs from upstream, instead of declaringTIMEandEVENTitself. Both settings default toNULL; a value that differs from a model’s recorded one stops the job, and so do models that disagree. Anhmmodel saved before it recorded its data selection stops the job, asking for thehmjob to be rerun. Thecohortchunk now follows themodelschunk, because it needs the columns the models record (#176).Every template opens with one
setupchunk, where it opened with four (setup,guard-edits,guard-partialandset). The chunk declaresSUBJECTandTYPE, attaches the template’s packages and calls one internal function,.job_setup(), which finds the study root, sources the study’sR/(with.root,.inand.provenance_dataalready set, as the old chunk had them), runs theEDIT:marker and partial-render guards, checks the file name againstSUBJECTandTYPE, and returns the root andset_path(). A report now opens with one “Code” fold instead of four, and a change to the marker guard is one edit instead of 34. The guard’s behavior and messages are unchanged, with three exceptions: the draft callout’s first sentence read “not” and “a result” run together, thelmandlptemplates now print the same marker list as the rest (bdstill prints only the count), andbd’s file-name check, which stopped with “bd: set (): the file’s name and its SUBJECT and TYPE disagree”, now stops with the message every other template gives, “This file is named … but declares SUBJECT = … Fix the declaration or the filename”. The bootstrap templates bc,bh,blandbrnow source the study’sR/as the others do. Jobs already in studies keep their four chunks and render as before; they gain neither the box nor the end table below. (#303)Every template carries
date: nowanddate-format: "YYYY-MM-DD HH:mm"in its own front matter, so the title block shows when the report was rendered. (#303)A report opens with a one-line box under the title: whether it is final or a draft (with the count of open markers and skipped chunks), the data it reads, who rendered it and on which host, and the hvtiRtemplates version. The box is printed before the data are read, so it names data only when the job’s file settles which: a literal
ANALYSIS_SET <- "name"(with the time the set was written, when its sidecar says), or a literalDATASET <- "name", in single or double quotes, with the dated version file and registration date when the manifest has them. WhenDATASETisNULL, inherited from an upstream handoff, computed or not declared, the box says “data: see About this run” and does not guess the study dataset;bd, which builds a dataset and reads none, names no data. (#303)Every report ends with an unnumbered “About this run” table: the run (start and end with time zone, duration, status, user, host), the job (file, template, subject, type, hvtiRtemplates version), each dataset read (name, version file, registration date, source file, rows and patients kept,
WHERE, the first 12 characters of the sha256 and whether a newer source file is waiting, each for its own source, a joined dataset’s separately), each upstream handoff, the seed when the job setsSEED, and whether renv is active with the first 12 characters ofrenv.lock’s sha256. The same facts are in the embedded provenance underrun. The table holds names, dates, counts and hashes, never a patient identifier or a data value. The user and host are the login and machine names;HVTI_RUN_USERandHVTI_RUN_HOSTreplace them, and the template gallery sets both so its published reports name no one. (#303)The package no longer ships two stray, blank
Rplots.pdffiles (at the top level and intests/testthat/) that a test run had left in the source;R CMD checkflagged the top-level one as a non-standard file.The handoff bundles that jobs save now record
rendered_at, the start of the render that saved them, as ISO 8601 with its time zone. The “About this run” table shows it for each upstream handoff, and “not recorded” for a bundle saved before this change; the file’s modification time is never used in its place. Saving the same results again therefore writes a different file. (#306)A report that turns out to be a draft after its box was printed no longer reads as final. A
stop_here()that the source scan could not see (inside anif ()) and that runs, anlgjob with a covariate whose scale is not chosen, an incomplete bootstrap pool inbc,bh,blorbr, and adc-gfupfollow-up figure that was not drawn each mark the run a draft: the end table’s status and the embeddedrunfacts saydraft(withstopped_earlyfor a stop and the reasons underdraft_reasons), and a callout above the table, “This run ended as a draft”, names each reason. The box under the title is printed first and cannot change; the callout is what corrects it. (#306)In the “About this run” table, two reads of one dataset whose
WHEREshows the same text because its values are masked (age >= <value>) are listed separately; a joined dataset’s rows no longer read “on NULL patients” and carry noWHERErow of its own; and aWHEREthat names a column with a space or backtick is shown whole. (#306)ANALYSIS_SETnow needs hvtiRdatabuild 0.3.0 (was 0.2.1), and 1.3.1 is tested against it. 0.2.4 stopped with “$ operator is invalid for atomic vectors” inread_analysis_set()andwrite_analysis_set()on any study registered with hvtiRutilities 1.5.1, which this package already requires. A stale analysis set no longer stops a draft in 0.3.0: it is read, and the “About this run” table carries a note naming thewrite_analysis_set()call that updates it. A final render (render_job(final = TRUE)) still stops.
hvtiRtemplates 1.3.0
A test now covers a new dataset version registered while a job is reading: the read is rejected as changed, as it already was in practice.
The six forest templates (
rfc,rfsandrfr, fit and explain) now handrfsrc(),vimp()andvarpro()a negativeseed = -abs(SEED)as well as seeding R’s generator throughcache_fit(seed = SEED). A study found that either half alone does not reproduce:rfsrc()withoutseed =draws one from R’s generator, andvarpro()with a fixedseed =still gave different importances after a different chunk. Both together reproduced to the last digit across a 192-thread server and a Mac. The templates also setoptions(rf.cores = 1L), since a varPro cutoff changed with the thread count. Jobs already scaffolded are not changed; to adopt this, make the same edits and render once withREFIT = TRUE.A new test parses every R chunk of every shipped template and fails on a call that draws from R’s generator (
rfsrc,varpro,mice,sample,runifand others) unless it is wrapped incache_fit(seed = )orwith_seed(), or has aset.seed()at most three lines above it in the same chunk. A randomForestSRC call that takes aseedmust also be given a negative one.A job reading a dataset whose source file has been rebuilt but not yet registered runs on the registered version and says so in its “The data this job read” table, with a Note row naming the
update_manifest()call that registers the new file. Before, hvtiRutilities’ message printed bare wherever the data chunk happened to show it. The table’s Source row names the dated version the job read, such asbuilt_20261007.parquet, rather than the source file.dp-postage, which reads outsideread_job_data(), prints the same note under its “Data read” line. Requires the hvtiRutilities release that added dated versions.Running a job’s chunks in the console no longer ends with advice to run
add_job(). The final provenance chunk, the one step that needs a render, now stops with an error of classhvtiRtemplates_not_renderedsaying that every chunk above it ran and that the job should be rendered, withhvtiRtemplates::render_job()or the Render button. A render outside the study’s Quarto project keeps the existing message about installing the hooks.The test suite runs in about two-thirds of the time: expensive fits and renders are shared within a test file, and fixtures are smaller. No assertion was dropped. About 40 tests, the end-to-end Quarto renders, model fits and saved-file identifier scans, are skipped on CRAN (
skip_on_cran()), so a CRAN-mode check fits the 10-minute budget; CI setsNOT_CRAN=trueand still runs every one.add_job()andopen_job()default todir = NULL, which finds the study root by walking up from the working directory, and stop outside a study rather than writing a job into whatever directory R is in. A call from a study’s subfolder now scaffolds into the study instead of under the subfolder. An explicitdirmeans what it did.Tests use at most two cores (
rf.cores,mc.coresandOMP_THREAD_LIMIT) and leave the global random seed alone. The package description names the Cardiovascular Outcomes, Registries and Research (CORR) group in full.Articles on the pkgdown site put the table of contents on the left and use the full width of the window, through
pkgdown/extra.css. The installed vignettes are unchanged: the Quarto vignette engine renders them in its own minimal format, which has no sidebar layout.hm’s first stage now holds every shape of each phase, as its two-stage fit intends: covariates are screened againsthz’s shapes before the shapes are freed. It read the shapes from a field a TemporalHazard phase does not have, so stage 1 held nothing beyondhz’s own fixed set and fitted the same model as stage 2. A shape a g3constraintderives is left free, since it may not be fixed.A part-built job’s report no longer lists a
stop_here()that cannot run: one in a skipped chunk, or in a chunk whoseevalis false as knitr reads it (false,no,offornin any case, or!expr FALSE).Rendering a part-built job leaves the R session’s options as they were. The record of skipped chunks is kept inside the package, and the
skipchunk option acts only on the job that registered it, so another document knit later in the same session is not affected.hzno longer passescondition = 14tohazard(). It mirrored SAS’sCONDITION=option, whichhazard()does not have, so TemporalHazard ignored it and printed a warning into the report for every fit. The fit still reports the conditioning of its Hessian, asrcondin the convergence table (#172).hz’s start probes move only the free parameters. They used to shift every position oftheta0, the fixed late-phase shapes included, so each probe fitted a different model and could beat the reported fit, falsely suggesting it was in the wrong basin. The held positions are read fromphases: each phase’sfixedset and any shape a g3constraintderives. A probe edited to move one stops the job, naming the parameter (#169).acandhzstop at the cohort whenTIMEholds a time of exactly zero or a negative time, giving the count of each. A same-day death used to stophzlater with an optimizer error that named neither. The message says to correct the times in the dataset build, since a job reshapes data and never corrects it: move only the zeros to a small positive value, such as 0.00025 years, and trace a negative time to its source (#175).hmandhs-setupno longer carryEXPECTEDcounts. The counts reconciled with the SAS reference are typed once, inacandhz, whose hand-off already records them;hm,hs-setupand nowhpcount their rebuilt rows and stop when they differ from the upstream job’s, naming the upstream file and both sets of counts. An upstream file that records no counts stops too, asking for it to be rerun (#177).hs-concordancekeeps its ownEXPECTED: it predicts for a cohort it chooses, spanning severalhmmodels, so no single upstream job has its counts.The Work a job and Adopt an existing study articles say to always render a job, never to run its chunks by hand: Run All works in the current R session, where a setting left by another job can stand in for this job’s and read the wrong patients. To look at the data, render a draft; to see only the top of a long job, use
stop_here().An
ANALYSIS_SETthe study has not built is named, with the fix: setANALYSIS_SET <- NULLto read the registered study dataset. This covers every template reading throughread_job_data()and the deprecateddp-postage, which reads its analysis set itself. They used to stop with a missing-file path that named neither, or, without hvtiRdatabuild installed, ask for it (#173).New template,
dc-stddiff: the balance table. It reports the standardized difference of each baseline variable between two groups, fromhvtiRpropensity::ps_stddiff(), the port of the 2019%stddiffmacro. The comparison is unadjusted, plus the matched set whenMATCHnames its column and matching weights whenWEIGHTdoes. It counts the variables above a threshold (0.10 by default) and draws the differences withhvtiPlotR::hv_balance().N_PERMadds the%stddiffcipermutation reference throughps_stddiff_perm(). A study with three or more arms runs a job per pair, keeping the pair withWHEREand naming its group 1 withGROUP_1. NeedshvtiRpropensity0.1.5 or later, which the existingSuggestsbound of 0.1.7 already covers (#216).hmstops when the reported fit has no variance matrix, or when any of its free parameters has no finite standard error, instead of savinghm.rdswith only a TemporalHazard warning in the log. The message names the phases and covariates involved. A degenerate fit, a phase whoselog_muhas run off, used to fail only later in a downstream job (#228). The table that warned of covariates without a standard error is gone, since the stop now covers it.hs-concordancestops in its predictions when a group model gives no confidence limits, naming the model and saying itshmfit has no usable variance matrix. It used to stop later in the decision with “missing value where TRUE/FALSE needed”. A job that has deleted the decision now stops too, where it used to save predictions without limits (#227). Limits that are undefined only because predicted survival atHORIZONis exactly 0 or 1 stop with their own message, not as a missing variance matrix.The
add_job()help page and the template gallery now say plainly whatsubjectandtypeare: names you choose, with no list to pick from, that name the job file, itsSUBJECTandTYPElines and the folder its results are saved in. Every job of one analysis should share them, because the next job in a chain finds the last one’s results by that pair:hmreadshz.rdsonly when it carries the same subject and type ashz.template_list()prints onlyname,prefix,qualifierandfolder, so the listing fits the console.callandfileare still in the data frame: read them with$calland$file, or print a selection such astl[, c("name", "call")], which prints as selected.The descriptive templates report the 15th and 85th percentiles where they reported the quartiles:
dc-general’s quantile table, the follow-up tables indc-gfupanddp-eda, and the variable summaries indp-edaanddp-postage. The follow-up tables take them fromhvtiRutilities::followup_check(), whose default changed in hvtiRutilities 1.4.5, now the minimum.add_job()andopen_job()called withoutsubjectortypenow say which is missing, explain that both are names you choose (any name matching^[A-Za-z0-9_]+$, not a list in the catalog), and show the template’s own call fromtemplate_list()$callas an example, in place of R’s bare “argument is missing, with no default” (#224).Now requires R 4.4.0 or newer, up from 4.1.0, to match the rest of the HVTI family.
hvtiR::install()installs the members together, and several already required 4.4.0, so on an older R the install failed whatever this package declared.New tutorial, Work a job, from template to final report, for a study author new to the package: find a template in the gallery or
template_list(), add it with itscall, find theEDIT:markers three ways, draft and final renders, rendering part of a job withskipandstop_here(), and what the common refusals mean.The template gallery is on the package site, at https://ehrlinger.github.io/hvtiRtemplates/gallery/, linked from the site’s navbar and the README. Each template shows what it is for, the
add_job()call that adds it, and the report it produces on a synthetic study. The table is generated from the catalog and the rendered reports, so it does not drift.Help-page examples for the job workflow.
migrate_job()andrender_job(), which had none, migrate a small legacy SAS job and render a draft and a final. The examples fortemplate_list(),template_path(),template_catalog(),add_job()andbuild_cohort()now find a template by qualifier and copy itscall, select a qualified template, show the refusal to guess an ambiguous prefix and to overwrite a job, show a deprecation warning, and read a cohort’s attrition table.dc-gfupprints one follow-up table, a row per interval and patient group (all patients, event, censored), in place of five one-row tables. It shows missing, negative and zero counts within each group, and one set of quartiles, SASQNTLDEF=5, where it printed two that could disagree. The suspicious-row table is shown only when there are suspicious rows; otherwise a sentence says there are none.dp-eda’s follow-up section shows the same table for each death panel.dc-generalprints one frequency table and one quantile table per variable group, in place of a frequency table, a one-row count table and a quantile table for every variable. Each variable’s extremes stay in their own table.A conditional table no longer leaves a gap in the table numbering when it is not shown.
A part-built job renders. Give a chunk the option
skipwith the reason in quotes,#| skip: "waiting on the corrected coding", to leave it out, or call the newstop_here()in a chunk to leave out everything below it. This replaces commenting unfinished sections out, which the SAS jobs did with a skip macro. A draft lists every skipped chunk and stop, by line, in a PARTIAL callout at the top of the report, and records them in its provenance. A final render,render_job(final = TRUE), refuses them as it refuses an unresolvedEDIT:marker. Every template carries a newguard-partialchunk, just after itsEDIT:guard, that does this.template_list()gains acallcolumn: theadd_job()call that scaffolds each template, with only the arguments it requires and runnable as printed, e.g.add_job("dc-gfup", subject = "cohort", type = "eda"). The full name selects the template on its own, so noqualifier =is needed;subjectandtypeare the template’s own defaults, shown by name because they are yours to change.dc-gfupdraws the goodness-of-follow-up figure beside its tables, so one job covers follow-up. It takes the figure’s choices fromdp-gfup(OPYRS,ORIGIN_YEAR,CLOSE_DATE,PANELS,EVENTS,ALPHAandCOLORS), adds the operation-year and close-date table, and savesdc-gfup-*.pngtographs/. It now needs hvtiPlotR >= 2.8.0.dp-gfupis deprecated in favor ofdc-gfupand will be removed in a later release. It still scaffolds;template_path(),add_job()andopen_job()warn, andmigrate_job()writes adc-gfupjob.DESCRIPTIONnow declares the Quarto command line tool inSystemRequirements. The vignettes have always needed it to build; the field makes that visible to installers and toR CMD check.Every template figure is saved as a PNG and a PDF. Figures go to the job’s
graphs/<subject>-<type>/folder as a 300 dpi PNG, for a Word draft, and a PDF of the same name with fonts embedded, for the publisher. Eight templates saved a PNG before, at 150 dpi; eleven (bc, bh, bl, br, nb-boostmtree and the six random-forest templates) only printed their figures. Each job’s study choices gainSAVE_FIGURESandFIGURESto turn saving off or keep only some figures by name;inst/templates/README.mdlists the names. A PNG the report shows keeps its name and is always written. The PDF falls back to the defaultpdf()device wherecairo_pdf()cannot open, as on a Mac without XQuartz, whose R reports cairo as available anyway.rfc-explain,rfs-explainandrfr-explainkey their partial-dependence cache on randomForestSRC, which computes it. The cache was keyed on ggRandomForests alone, so after a randomForestSRC upgrade it stayed valid andREFIT = TRUEkept the old result while the variable importance beside it was recomputed. Thegg_partial_rfsrc()call now passespackages = "randomForestSRC"tocache_fit(); the VarPro partial is unchanged, since its varpro fit already carries varPro’s version. Each existingrf?-partialcache goes stale exactly once, reportingpackages$randomForestSRC (absent) -> x.y.z, and needs one render withREFIT = TRUE. Requires hvtiRutilities 1.5.0 or newer, up from 1.4.5.Templates name the study dataset
"built", the team’s word for it. Every job’s study choices now sayDATASET <- "built", and so do thebc,bh,blandbrrunners and the jobsmigrate_job()writes."study"still works, so a job scaffolded before this change keeps running unchanged, and a downstream job agrees with an upstream one whichever name each used. Records still say"study": the selection a job hands on and its provenance sidecar compare equal under either name. Requires hvtiRutilities 1.5.1, which made"built"a second name for the study dataset.Jobs are named template first, with periods.
add_job()writes<prefix>[.<qualifier>].<subject>.<type>.qmd, such asac.death.hz.qmdordp.trends.cohort.eda.qmd, and a bootstrap job’s runner beside it as<prefix>.<subject>.<type>.runner.R, so a study’s jobs sort by template, then subject, then type. Jobs scaffolded before this release keep their<subject>-<type>-<prefix>[-<qualifier>]names and keep rendering: every template’s name check reads both spellings.add_job()andmigrate_job()refuse to write a second copy of a job that exists under its old name, andopen_job()opens it.template_list()shows qualified templates asdp.trends, and itscallcolumn uses that name;"dp-trends"is still accepted. Results folders (estimates/<subject>-<type>/,graphs/<subject>-<type>/) are unchanged.add_job()also refuses when only a job’s runner remains under its old-runner.Rname, so an edited runner is never left behind beside a new, empty one.Jobs can join an ancillary dataset to their cohort.
read_job_data()gainsjoin,join_vars,reduceandjoin_key, and the study choices of every template that reads its own data gainJOIN,JOIN_VARS,REDUCEandJOIN_KEY, allNULLand without anEDIT:marker, so a finished job is unchanged.JOINnames one dataset registered withkind = "ancillary", such as echoes or labs. The cohort decides the patients and must be one row per patient; the join keeps one row per joined record, keyed on that dataset’s key, or one row per patient withREDUCE = list(rule = "first" | "last" | "nearest", by = ...). A tie onbystops rather than pick one record silently, and so does a join that matches no cohort patient at all, which is a mismatch of identifiers.WHEREapplies to the joined rows. The job’s data table names the joined dataset and its rows, counts its records outside the cohort and the cohort patients with none, and names the reduction; its provenance records the joined dataset’s version. A downstream job rebuilds the same join from its upstream job’s selection. A template that models one row per patient (ac,hz, everylm-*,rfc-fit,rfr-fit,rfs-fit,dc-stddiff,dc-gfup,dp-gfupandhs-concordance) passesread_job_data(one_row_per_patient = TRUE)and stops on aJOINwithoutREDUCEbefore reading any data, since the long form would count every joined record as a patient; the other descriptive templates andnb-boostmtreekeep the long form.read_job_data()’skeynow defaults to the key registered for the dataset (hvtiRutilities 1.5.1), and toidwhen none is registered, as before. AKEYorJOIN_KEYthat differs from the registered key is used, and noted in the data table. Templates setKEYthemselves, so they read the same rows as before, with that note when the study registered another key.A stale analysis set that hvtiRdatabuild reads in a draft render is now a note in the job’s data table, as an out-of-date dataset already was, rather than a message wherever the chunk prints it.
With
REDUCE,WHEREon a joined column now filters the records before one is chosen per patient (maintainer’s decision).REDUCE <- list(rule = "last", by = "echo_date")withWHERE <- quote(echo_type == "TTE")keeps each patient’s last TTE, where it used to keep the last echo and then drop the patient if that echo was not a TTE. A condition on cohort columns still filters the reduced rows. The data table shows the record filter between the join and the reduction, and counts the patients left with no record after it; the selection records the conditions in the order written.REDUCE’sbymay name several columns, such asc("echo_date", "echo_seq"): each later one breaks a tie on those before it, in the rule’s direction, and a missing tie-break value loses the tie rather than dropping the record. A tie that remains still stops (maintainer’s decision), and the message now says what tied (for"nearest", records equally far from the target, not “the same date”), how many patients, and how to break it.byandtomatch their columns ignoring case, as every other column setting does.nb-boostmtreetakes a long join whose visit time is only in the joined dataset: aKEYthat names a joined column is checked on the joined rows. AKEYof cohort columns is checked on the cohort, as before.A join stops when some joined identifiers match the cohort’s only once surrounding spaces, leading zeros or letter case are set aside, instead of counting those records as outside the cohort. The message gives counts only. A cohort row with a missing identifier stops a join as missing, rather than as a repeated patient. A long join keeps the cohort’s identifier type.
key_hashcovers the joined dataset’s key, so a downstream job sees thatREDUCEchose a different record for a patient.hm,hpandhs-setuprecord the joined dataset’s provenance when they rebuild a joined selection. A selection without a join carries no empty join fields, and a join’sREDUCEandJOIN_VARSare recorded as resolved, so the same join written two ways records the same selection andhpaccepts matchingacandhzhand-offs.hp’s mismatch message names the join settings. A downstreamJOINorREDUCEagainst an upstream selection that read no join stops and names the setting.read_job_data(one_row_per_patient = TRUE)also stops when the rows kept repeat a patient, as a dataset of repeated records read whole would. Its refusal names the templates that take repeated records (dc-general,dc-tables,dp-eda,dp-trends,nb-boostmtree), read from the templates themselves. A patient whose records all lack thebyvalue is no longer counted again as a patient with no record; the “no reduction value” row says how many patients it leaves with no record chosen.KEY <- IDno longer draws a “differs from the registered key” note when the ID falls back to MRN on a dataset registered on its MRN.stop_here()refuses a final render itself, so one the source scan cannot see (insideif (), or called with an argument) no longer truncates a final report silently; in a draft it is recorded as a partial-render stop.A job’s name is read through knitr’s
.knit.mdand.utf8.mdintermediates.dc-stddiffasks for hvtiRpropensity 0.1.7, as DESCRIPTION does. The cairo probe runs once a session.dp-gfup’s deprecation note namesadd_job("dc.gfup", ...). DESCRIPTION spells out the Heart, Vascular and Thoracic Institute.
hvtiRtemplates 1.2.5
- Every figure and table a template shows is numbered. Quarto numbers a figure only with a
fig-label andfig-cap, and a table only with atbl-label andtbl-cap; aknitr::kable(caption = )caption prints without a number. All 33 templates now carry numbered labels and captions, table captions moved intotbl-cap, chunks that showed several outputs are split, and loops that show one table or figure per item emit each as its ownknitr::knit_child()chunk, so every item is numbered and citable. Chunks that holdEDIT:choices no longer also show output. A job built from a template starts numbered. - New
test-template-numbering.Renforces the rule for every template, and rejects the retired ColorBrewer palettes ("Set1","Set2","Dark2","RdYlGn"and others); categorical groups takehvtiPlotR::hv_palette(). A chunk that calls a table or figure function without showing one says so with# unnumbered: <reason>. -
inst/templates/README.mdgains “Figures, tables and color”, pointing to the recipes book chapter Numbering figures and tables. - The label
datais nowtbl-datain every template (dp-postage keepsdata), with the other renames listed in the tests that extract chunks by label.
hvtiRtemplates 1.2.4
ggBoostedTreesis required at 0.9.1 or later.boostmtreenow arrives only throughggBoostedTrees’sRemotes:, and 0.9.1 is the first release that pins thev2.0.3-ccftag this package’sboostmtree (>= 2.0.3)floor needs.New template
00_datasets/bd.qmd, the first at the00_datasetslevel: a build job that cuts a study’s dataset from a master snapshot, applies a cohort list and ordered exclusion rules (condition ~ "Reason", where a missing condition is not excluded and is counted), derives the study’s own variables, writes a draft, and withPUBLISH <- TRUEpublishes a dated release withhvtiRdatabuild::publish_dataset()and registers it, or adopts it when an older release is registered. Its report shows counts and no code, so a rule naming a patient stays out of the HTML. Newbuild_cohort()carries the cohort and attrition step (#223). The catalog listsbdas in flight: it is released so the stat programmers can review it in a real study, and it is marked shipped once they have and a timed render against the real master is recorded.boostmtreenow comes from the CCF fork throughggBoostedTrees’s ownRemotes:rather than a tag pinned here, with a floor of 2.0.3. The two had pinned different tags of the fork, so dependency resolution failed before any check ran; with one pin they cannot drift apart again. Thenb-boostmtreeinstall hint names thev2.0.3-ccftag thatggBoostedTrees0.9.1 installs from.Every template’s HTML now reads full width with its contents on the left (
page-layout: full,toc-location: left, a 2000px body ceiling and a 250px sidebar), alongside the existingembed-resources: true. A new test checks all of it in every template’s own header, so a copied template stays self-contained and laid out the same.
hvtiRtemplates 1.2.3
read_job_data()stops on aWHEREcondition that mentions the patient identifier or a column named MRN or eMRN, directly or through.data$xor.data[["x"]], before any row is filtered. A column reached any other way, as by.data[[paste0(...)]],get()or another name bound toget(), stops too, since it could be the identifier. Values are checked as well as names, after every name check: the check covers literal values (including an outside vector of IDs) of five or more characters that equal a value of the identifier, MRN or eMRN in the data, and folds only arithmetic,c(),paste/paste0and numeric or character coercion of constants (such as4730000000 + 1) to check their result. So a wrapper function or a copy of the ID under another name is caught when compared with such a value. The check never calls any other function: a value produced by any other call, or computed from a data column, as inid2 / 2 != 2365000000.5, is not checked, and a refused condition runs no code. A threshold that happens to equal a patient’s identifier is refused too, and the message says so. Thresholds shorter than five characters, such as 1, 18 or 2015, are not checked, so a study keyed on shorter identifiers relies on the name checks. Each condition is saved, values included, in the job’s output, so a filter on identifier values would be saved with it. Exclude those patients in the dataset build, or with an hvtiRdatabuild analysis set. A downstream job rebuilding an upstream file saved with such a condition names the file and says to rerun the upstream job.read_job_data()stops on aWHEREthat takes a data frame, list, environment or S4 object from outside the data, aswith(lookup, age) > 20does. The condition is saved as text, so the whole object, ID column included, was saved with it; the message names the variable and says to filter on a column of the data instead. An outsideNULLis now fixed in rather than dropped from the call. Closes #218.The bootstrap reports print the data selection their bag carries, and their runners read data through
read_job_data(). Each runner is its own job:add_job()forbl,br,bcandbhalso writes<subject>-<type>-<prefix>-runner.Rbeside the report, with its study choices markedEDIT:, and refuses to overwrite one. A runner stops until itsFINISHEDis set toTRUE, and checks its package floor (hvtiRbootstrap 0.9.3, TemporalHazard 1.2.8) before it screens, so a long run cannot finish only to be refused. The runner saves the selection in the bag’s lineage and resamples only the model’s columns.WHERE,IDandKEYin the report confirm the runner’s values, and the collinearity table reads the rows the runner screened rather than the whole dataset. A bag saved before this change carries no selection, and its report stops: rerun its runner. So does abhpool whose chunks were run on different rows.A bootstrap report stops on a bag that carries a patient identifier. The check is by name: it finds a field, column or matrix column named for the job’s
ID,MRNoreMRN, anywhere in the bag or its carried lineage, including inside the environment a saved formula or function carries. An active binding there is reported, never evaluated. It does not find an ID stored under another name or used as row names, nor data in an environment it does not read (a package or the global environment, or an unforced promise).BOOTSTRAP_DATAis removed from the bootstrap reports. A bag must come from its runner, which attaches the lineage and selection the report reads.nb-boostmtreeis a new template: boosted multivariate trees for a response measured repeatedly over follow-up, fitted withboostmtree2.0.2 or later from the CCF fork (ehrlinger/boostmtree_src), whose fix the CRAN release lacks, and reported with ggBoostedTrees 0.9.0 or later.KEYdefaults to the ID and the visit time. Patients are grouped by the study-keyed digest, so the fit, its cache and the saved file hold no patient ID.PREDICTORS <- NULLtakes every column but the ID, theKEYcolumns, the time, the response and any ccfid, MRN or eMRN column, and names the identifier columns it left out; namedPREDICTORSmay hold none of those, nor a name twice. The partial effects of every response component come in one figure, faceted by component as ggBoostedTrees 0.9.0 draws them, with continuous and factor covariates in separate figures because one figure cannot hold both. BoostMLR (nb-boostmlr) is still to come; the ggBoostedTrees support it waited on shipped in 0.9.0.New template
hs-concordance: every patient predicted through every treatment group’shmmodel at one horizon, the job the corpus calls concordance and discordance. It reads each group’s model from that group’s own set, saves one long artifact,hs-concordance.rds, and has an optional section that picks a best treatment per patient only where the confidence limits separate it. Design:dev/specs/2026-09-30-hs-concordance-design.md.The
hstemplate is nowhs-setup, becausehscarries two job types and a prefix is either wholly qualified or wholly unqualified. The file’s contents are unchanged and it still saveshs.rds.add_job("hs", ...),template_path("hs")andopen_job("hs", ...)without a qualifier stop and listsetupandconcordance. Migrating a SAShsjob needsqualifier =too: its filename’s second field is the endpoint, not the job type. Jobs already scaffolded keep their names.open_job("hs", ..., qualifier = "setup")looks for<subject>-<type>-hs-setup.qmd, so in a study that already has<subject>-<type>-hs.qmdit scaffolds a fresh job beside it. Open the existing job by its file.hs-setupnow requires TemporalHazard >= 1.2.12 and says what itsse.fitcolumn is. On the survival path it is se(S), the standard error of the survival estimate itself. A TemporalHazard build without #281 wrote se(H) into the same column, and #281 shipped inside version 1.2.11, so which one anhs.rdsholds depends on the build that produced it, not on when it was written. The render stops on an older build instead of writing an ambiguous file, and theSuggestsfloor for TemporalHazard rises from 1.2.8 to 1.2.12 to match.hzrenders again with TemporalHazard 1.2.11. Its convergence table read an iteration count that a multiphase fit does not carry, so every render stopped there. The table now reports the optimizer’s function and gradient evaluations, and shows “not reported” for a field a fit leaves out, so a missing field cannot stop the render (#168).read_job_data()is the shared data step for templates: it reads a registered dataset or analysis set, resolves the patient ID (ccfid, thenMRN, theneMRN), dropsMRNandeMRN, keeps the rowsWHEREselects withdplyr::filter()rules, checks rows are unique onKEY, and records what it did.IDandKEYmatch their columns ignoring case, the values of aWHEREcondition onIDorKEYare never printed, and an analysis set’s attrition is returned with the data. A valueWHEREtakes from outside the data, such as.env$min_age, is fixed into the recorded condition when the data are read. A downstream job rebuilds its upstream job’s rows from the selection the upstream hand-off carries, and stops when that hand-off predates it. Templates adopt it family by family.The descriptive templates and
dp-trendsread their data throughread_job_data():ANALYSIS_SETdefaults toNULL, so they run on a newly registered study;WHERE,IDandKEYare new settings;dc-general’sKEY_COLSisID; anddp-edaleaves the job’sIDandKEYcolumns out, never draws theID, and draws aKEYcolumn such as a visit time only whenVARIABLESnames it. The deprecateddp-postagekeeps its own data step. Every template’s setup chunk says to runstudy_setup()when it is not inside a study.The logistic templates read their data through
read_job_data();IDdefaults to"ccfid"(it was"id", which no built dataset carries), andlm-checkpredstops when its validation patients were in the training data: its validation cohort isDATASETorWHERElike any job’s.The
lmtemplates save a study-keyed digest of each patient ID in their model files, not the ID, so a model keyed onMRNno longer stores MRNs. The key is made on first use in the study’s.hvti/folder.lm-checkpreddigests its validation IDs with the same key and compares digests; a model saved before this change still validates.The hazard chain reads its data through
read_job_data():STATUSisEVENT, theiu_dead/ideaddefaults areiv_dead/dead, the filter typed into every job is oneWHEREinacandhz, andhm,hpandhstakeWHERE,ID,KEY,TIMEandEVENTfromhz’s saved fit, stopping if their own differ.hpalso stops whenacandhzread different rows.hmfits on the model’s columns only, sohm.rdsno longer keeps the patient ID or MRN in its saved fits.The random-forest templates read their data through
read_job_data(), gainingWHERE,IDandKEY; text predictors are converted to factors with a note;explainjobs take the fit’s selection.rfs-fit’sSTATUSisEVENT. A fit stops whenPREDICTORSnames the patient ID or aKEYcolumn, or when an outcome (TIME,EVENTorRESPONSE) is the patient ID. AKEYcolumn that is not the ID, such as a visit time, may still be an outcome, and is then saved with the forest. The forest’s formula no longer carries the job’s environment, so the saved forest and theexplaincaches hold no patient ID or MRN wherever the job is run. Anexplainjob stops on a forest saved before this change; rerun its fit job first.dp-postageis deprecated in favor ofdp-eda, and will be removed in the release after 1.2.3. For the same pages, scaffoldadd_job("dp", subject, type, qualifier = "eda")and setSECTIONS <- c("continuous", "percent", "count"): the sections callhv_eda_pages()with the same arguments, and a test checks that the pages matchdp-postage’s byte for byte. They are saved asdp-eda-*.png.dp-postagestill scaffolds and renders;add_job(),open_job()andtemplate_path()warn once when it is used. The template catalog marks it with two new fields,deprecated_byanddeprecation_note, whichtemplate_catalog()returns and the warning reads.migrate_job()now writes adp-edajob for a legacy EDA report (thetp.dp.DescriptiveSummary.qmdandtp.dp.EDA_barplots_scatterplots*.Rlineage), carrying over the same settings as before,VARIABLES,X_VAR,EXCLUDE, the grid size andUNIQUE_LIMIT, and leaving the color and stratification choices for review. The job drawsSECTIONS <- c("continuous", "percent", "count"), because a legacy EDA report had no follow-up panels. Namingqualifier = "postage"warns and writes thedp-edajob.dp-edaanddp-postagetake the medical record number as an identifier only under the exact namesMRNandeMRN(ignoring case). They matchedmrnanywhere in a name, so an mRNA variable such asbnp_mrnawas left out as an identifier. The rule matches hvtiPlotR’shv_eda_pages()(hvtiPlotR#172).The data-route tests pass when run on their own. They evaluated template chunks that call
study_config()without supplying it, so they depended on an earlier test file having attached hvtiRutilities, and a filtered run errored twice. No change to the templates.Every template chunk holding an
EDIT:marker is now labelededit-, so the editor’s chunk outline lists the work a job still needs:study-choicesisedit-study-choices, and nine templates carry furtheredit-chunks such asedit-cohortandedit-derive. The marker guard,edit-guard, is renamedguard-editsso it does not read as an edit site. A test holds the rule in both directions. Existing jobs keep their old labels and still render.add_job(),open_job(),template_path()andmigrate_job()accept a template’s full name, thenamecolumn oftemplate_list(), in place of the prefix and qualifier pair:add_job("dp-trends", "cohort", "eda"). An error asking which template now lists the choices by full name. Giving a full name and aqualifiertogether is an error rather than a silent preference for either. A bare qualifier is not accepted, becausefit,explainandgfupeach name more than one template.The EDA abbreviation tests hold once hvtiRutilities carries its group abbreviation list (1.4.3). Four tests assumed the group list was empty and failed on every platform after it merged: they now turn the group list off when testing the job, study and initials levels,
dp-eda’s provenance check finds its entry by position rather than by a name every row shares, and a new test checks that the group list does shorten labels when it is present. No change to the templates.dp-postagenames its source correctly. Its header said it replaced the oldertp.dp.EDA_barplots_scatterplots*.Rsweep; it was built from the per-variable grid of the legacy EDA report,tp.dp.DescriptiveSummary.qmd, as the catalog records. TheEDIT:note now names both, since a study’s job may be a copy of either, and the descriptive migration vignette maps both.New template
dp-eda(10_descriptive/), the whole EDA report in one render: an overview of every column (proc_contents()), goodness of follow-up withdc-gfup’s tables, then continuous variables, categorical variables as percentages and categorical variables as counts, each with its table.SECTIONSpicks any subset; the report keeps that order. Each section calls the function its standalone job calls (hv_followup_panels(),followup_check(),hv_eda_pages()) with the same arguments, so a section is that job’s figure;test-dp-eda.Rchecks the pages byte for byte againstdp-postage. Needs hvtiPlotR 2.7.18 and hvtiRutilities 1.4.2, the current floors. It draws in the house colors and shortens labels in the house style (both below).dp-postageanddp-edaleave out identifiers written without a separator. UnderVARIABLES <- NULLa column namedccfidorpatientidpassed the identifier rule, which wantedidas its own_-delimited token, and was drawn as one bar per patient and listed in the frequency table. The rule now also takesccfid,patid,patientid,studyid,subjectid,recordid,caseid(each also with_,numorno), the exact namesMRNandeMRN, and any text or factor column of ten or more values in which every value differs. A bare trailingidwas declined: it would takecarotidandsteroid. The report lists what it left out, and naming a column inVARIABLESstill draws it.dp-eda’s overview table leaves identifiers out too, whateverVARIABLESsays, and names them beneath the table; date columns stay in it, because their share missing is worth seeing.dc-generalexplains each study choice where it is made. The notes onCATEGORICAL,CONTINUOUS,CORR_VARS,KEY_COLSandID_COLsat in aspecchunk ninety lines below the settings they describe; they now sit beside them inedit-study-choices, and the empty chunk is gone. No change to what the job does. From Lauren’s 2026-09-24 review.The EDA templates draw in the house colors, which are colorblind safe.
dp-postageanddp-edacolor their categorical pages withhvtiPlotR::scale_fill_hv(): each panel runs blue, vermillion, green and on from its own levels, and missing is light gray.dp-gfupanddp-eda’s follow-up section usescale_color_hv(). The follow-up figures change color: dead moves from Set1 red#E41A1Cto vermillion#D55E00, alive from#377EB8to#0072B2, and a non-fatal event from#4DAF4Ato#009E73. The pattern of red, blue and green is kept.COLORSis nowNULLby default; namealive,deadandeventto choose your own, andc(alive = "#377EB8", dead = "#E41A1C", event = "#4DAF4A")restores the earlier colors. Needs hvtiPlotR 2.7.18, now the floor.dp-gfup’sCOLOURSsetting is renamedCOLORS, anddp-edauses the same name. Astudy-choiceschunk copied from an older job that still setsCOLOURSstops with “COLOURS is now COLORS” rather than drawing the default colors without a word. Rename the setting instudy-choices(edit-study-choicesin a new job); its values are unchanged.The templates, their comments, the reference pages and these notes use US spelling. The templates call
scale_color_manual()andlabs(color = ), which ggplot2 treats as identical to their British spellings, andhvtiPlotR::scale_color_hv(), which needs hvtiPlotR 2.8.0, now the floor.tools/check-us-spelling.shnow fails the lint workflow on a British spelling.dp-postageanddp-edakeep shortened labels distinct, throughhvtiRutilities::label_map(). Two new edit points:LABEL_MAX(default 40), the longest label a figure or table shows, andABBREVIATIONS(defaultNULL), the job’s own abbreviations asc("Phrase" = "Abbrev"). The job merges them over the study’sabbreviations:in_study.ymland the group default list withhvtiRutilities::study_abbreviations(), so every job in a study shortens labels the same way. Labels that share a heading over the cap show it abbreviated (Surgical procedure: ...becomesSP: ...), and each section prints a key of the abbreviations its shortened labels show. A wrongLABEL_MAXorABBREVIATIONSstops the render with a message naming it, as the other edit points do. The cap and the merged list, with each entry’s level, are recorded in the report’s provenance. The settings apply to the figure pages and their captions; tables, includingdp-eda’s Overview, keep full labels, so a reader can look up what a shortened label stands for. Needs hvtiRutilities 1.4.2, now the floor; a migrateddp-postagejob keeps both edit points at their defaults.
hvtiRtemplates 1.2.2
DESCRIPTION: a plain&in the Description (\&is not a valid escape there and printed a literal backslash), and a copyright holder in Authors@R. Both from the 1.2.2 release gate’s CRAN Cookbook audit.dc-gfupanddp-gfupcompute through shared functions, so the EDA report can call the same ones:hvtiRutilities::followup_check()for the follow-up tables andhvtiPlotR::hv_followup_panels()for the panels and their study window. The numbers and figures are unchanged. The validation errors now come from those functions and name their arguments (`event`,`origin_year`and so on) rather than the template’sEDIT:constants, anddc-gfup’s unknown-column error reads “Unknown column(s)”. Needs hvtiRutilities 1.4.1 and hvtiPlotR 2.7.17.Two vignette tests, on the SAS guide and on the tutorials’ study root, now read the installed
doc/copy when the source checkout is absent. UnderR CMD checkthey had skipped on every platform since 2026-09-21.The legacy-article vignette test uses the same checkout-then-
doc/lookup, and therfr-explaintest silences varPro 3.3.0’s notice that it drops rows with missing values, soR CMD checkreports no warnings.dp-postagedraws in sections.SECTIONS <- c("continuous", "percent", "count")replacesSHOW_PERCENT, so one render shows the categorical variables both as percentages and as counts, with the same year bins. Each section opens with its table:proc_means()for continuous variables, and oneproc_freq()table, missing counted, for the categorical ones.VARIABLES <- NULLis the new default and draws every column exceptX_VAR,EXCLUDEand any column that looks like an identifier or a date, which the report lists.ALPHA(0.5) sets point transparency. The pages come fromhvtiPlotR::hv_eda_pages(), so the job needs hvtiPlotR 2.7.16.migrate_job()no longer carries a legacyshow_percentinto adp-postagejob; it records it as replaced bySECTIONS, whose default draws both views. A legacy point alpha now carries over intoALPHA, and a job with no variable list starts fromVARIABLES <- NULL.New vignette, The template catalog: every template with a one-line description, the
add_job()call that scaffolds it, and a delivery light (shipped, in progress, not yet on the way), grouped by study folder, followed by the queued job types ordered by how many studies carry them. It is built from the catalog when rendered, so it cannot fall out of date.template_catalog()gains adescriptioncolumn. Every template on disk has one, which a test andcheck-roadmap-counts.pyboth enforce.New
dp-gfuptemplate (40_graphs/): the goodness-of-follow-up figure overhvtiPlotR::hv_followup(). One panel per death indicator and an optional panel per non-fatal event; blue is alive, red is dead, and points are drawn at alpha 0.5. The close date is an edit point, estimated from the data when leftNULL, and the report says which it drew. Every column named in the study choices is checked at once, so one render lists every missing name.dc-gfupnow renders its cohort, interval,proc_means(), suspicious-row and cross-tab results as tables rather than printed console output, and points todp-gfupfor the figure.Breaking:
add_job(),open_job(), andmigrate_job()rename their publicendpointargument tosubject, and templates rename theirENDPOINTmarker toSUBJECT. Job sets are now named by subject, the leading grouping topic in<subject>-<type>-<prefix>[-<qualifier>].qmd. A subject can be a statistical endpoint such asdeath, or an endpoint-free topic such ascohort. Jobs now own their outcome and cohort definitions; dataset registration does not choose them.Requires hvtiRutilities 1.4.0 or later, whose endpoint-neutral study registration the subject-based jobs depend on. The floor is now 1.4.1, for
followup_check()(see thedc-gfupentry above).Every scaffolded job now captures provenance while it executes, safely embeds that payload in its completed HTML, and publishes a same-stem
.provenance.jsonsidecar through study-level Quarto hooks.add_job()installs the hooks idempotently without replacing existing project settings or user hooks. Publication follows Quarto’s actual output paths, invalidates only the inputs being rendered, preserves prior sidecars when a failed render leaves its outputs untouched, and durably tracks multi-output publication. Failed publication removes partial new sidecars and restores a prior sidecar only while its recorded output hash still matches, retaining mismatched backups for explicit recovery. Records always carry the runtime subject and type and add only the analysis and observed-cohort facts the job actually used.Saved model and report handoffs now carry immutable
hvti_provenancelineage. Random-forest explainers, logistic validation, bootstrap reports, and the actuarial-to-hazard graph chain retain their producers’ data records and hash every artifact they read. A package handoff without lineage stops with rebuild guidance; external bootstrap artifacts instead require explicit original data records. Theactemplate now saves its overall life table as the real upstream artifact consumed byhp.All eight logistic-family templates now record their runtime model and cohort metadata. Records distinguish outcomes, treatments and count exposures; retain accepted and observed levels and applicable coding; and report the fitted formula, family, method, predictors, imputation details, and analyzed-row accounting. Stacked-imputation totals are labeled as stacked rows and accompanied by per-imputation counts.
lm-checkpredkeeps the source model’s training metadata separate from the validation cohort it observes.New vignette, “Start a new study from a delivered dataset”, takes a study from its Study Tracker record and the
study-setupcommand through registration, a first descriptive job and its provenance sidecar, to a firstac,hzandhpchain. It complements “Adopt an existing study”.The provenance test that asserts an embedded payload carries exactly one
<scripttag now counts real matches.gregexpr()reports no match as a length-one-1, so the old assertion also passed when the tag was missing.
hvtiRtemplates 1.2.1
Eight qualified
lmtemplates now cover binary, ordinal and nominal outcome models; the corresponding three propensity shapes; saved binary-model validation without refitting; and count balancing scores. They usehvtiRpropensity 0.1.7model bundles, retain per-imputation fits, display Rubin-pooled estimates and covariance, and save their reviewed bundles below the analysis set’sestimates/directory.The study setup tutorial now follows one adoption path through the complete pre-analysis workflow. It starts from an existing dataset opened as an RStudio Project, preserves an existing R-version pin or selects R 4.6 when none exists, and adopts the study without changing its working-directory layout or deleting
.git,tp.*or copied template directories. Cleanup is deferred to a separate operational command. The tutorial registers data, declares and writes theedaanalysis set, and explains how the descriptive and plot job names work before an endpoint or analysis type has been chosen. It also warns thatupdate_manifest()alone cannot safely refresh a registered dataset while_study.ymlstill carries cohort metadata.fsandyamljoinSuggestsbecause the executable article writes the synthetic analysis-set declarations it teaches without replacing other sets.Six random forest templates:
rfs,rfcandrfr, for survival, classification and continuous outcomes, each as afitjob and anexplainjob. The fit job grows the forest throughhvtiRutilities::cache_fit()and saves it for the explain job, which reads it back for VIMP, VarPro and dependence plots and never refits. Oldrfsrc.*andrf.*jobs map onto the three by outcome;inst/templates/README.mdhas the table. Fit jobs refuse outcomes or duplicate names inPREDICTORS, andrfc-fitrequires the study to name the class whose one-versus-rest ROC curve it reports.randomForestSRC (>= 3.7.0)andvarPro (>= 3.2.0)join Suggests, andggRandomForestsrises to>= 4.0.0.All 28 shipped job templates put editable study choices in one chunk near the top. Reading data and building output happen below those choices; SAS migration continues to fill the same settings.
The 65-row template catalog now ships as
inst/extdata/templates.jsonand is available throughtemplate_catalog(). Roadmap checks read this local catalog, the two CI catalog pins are removed, and thehvtiRutilitiesminimum is 1.3.0 for its machine-readable umbrella marker. The separate strict CI step is retired: with the catalog in the tarball, the first CI run showed every check leg running the catalog tests (SKIP 0on macOS and Ubuntu;SKIP 1on Windows, a deliberate POSIX-permissions skip).The intake-row guard no longer goes quiet when intake is empty. It asserted inside a
forover the intake rows, so with none left it made no expectation at all; testthat reports that as an empty test, which reports as a SKIP. The strict CI step expectsSKIP 0, butHVTI_ROADMAP_STRICTonly promotes the helper-driven skips to hard stops andstop_on_failuredoes not fire on a skip, so the gate would have gone quiet under a green check. Its logic now lives in a helper that returns a value, so the check is an assertion at any row count, and a new test drives the empty case from a temporary catalog. That matters because CI reads the real catalog from a pinnedhvtiRtag, which still has intake rows, so the empty path would otherwise never run there.with_temp_catalog()moves tohelper-ledger.Rso both test files can use it. Found whenrfr,sidandvtleft intake.Requires
hvtiRutilities1.2.1 or newer, up from 1.1.12 in the 1.2.0 release. (mainbriefly carried a 1.2.0 floor that never shipped; this entry describes the change between releases.) 1.2.0 ports SASPROC FREQandPROC UNIVARIATEasproc_freq()andproc_univariate(), and correctsproc_means()weightednobs, weightedstderrand the mode of a single observation. 1.2.1 addscache_fit()and putsrfr,sidandvtinhvti_taxonomy(), which the job catalog this package now reads (hvtiR1.1.15) depends on: with 1.2.0 the catalog names three prefixes the taxonomy lacks. No template calls the new functions yet, so nothing renders differently. The floor binds whenhvtiRtemplatesis installed or loaded; a copied job rendered with a barequarto renderis not covered, and a job that comes to need a newer function carries its ownpackageVersion()guard, asdc-tablesdoes for its dependencies.
hvtiRtemplates 1.2.0
The
hztemplate renders again. Itsphaseschunk read the order ofthetathrough TemporalHazard’s internal.hzr_phase_theta_names(), which changed shape oncehzr_theta_names()was exported, so everyhzjob stopped there. It now callshzr_theta_names(phases)and stops with a clear message on TemporalHazard older than 1.2.8. A job already scaffolded from the old template needs the same edit to itsphaseschunk.TemporalHazard (>= 1.2.8)joinsSuggestsso the test that runs that chunk runs in CI.A migration test no longer warns on Windows while removing its temporary folder link (#124).
Templates find the study root through
_study.yml. Each template callshvtiRutilities::study_root()in place of looking for_quarto.ymlin.or.., so a study needs no_quarto.yml, and a job renders the same from the Render button,quarto renderorrender_job(), at any depth. A study must have been adopted withhvtiRutilities::study_setup().open_job()finds the study root and opens or creates a job. Called from anywhere inside a study, it resolves the root through the nearest_study.ymlat or abovedir, creates the job withadd_job()when it does not exist, and opens an existing job as it stands, never overwriting it.render_job()renders a job from its own directory: a draft by default, and withfinal = TRUEa render that stops on an unfinished job.The
dc-generaljob template ships, replacingdescriptive/dc.general: overall statistics throughhvtiRutilities::proc_contents()andproc_means(), then base-R contingency tables, cumulative distributions (SASQNTLDEF=5quantiles and the five lowest and highest values) and a Pearson pairwise-correlation sweep.ID_COLis off by default, so no patient identifier reaches the report unless a study author sets it.KEY_COLSkeeps identifier columns,ccfidby default, out of the overall statistics too. It reads data the same three ways as the other descriptive templates,DATASETorANALYSIS_SET.The descriptive templates can read a named dataset.
dc-tables,dc-gfupanddp-postagegainDATASET, passed toread_built(dataset = )whenANALYSIS_SETisNULL, so a job can read an extract declared underadditional_datasets:in_study.yml, such as a column subset written for R. Naming such a dataset inANALYSIS_SETfailed with “No analysis set”, because analysis sets and additional datasets are separate registries. Pairing an analysis set with a dataset other than"study"now stops, since a set is always derived from the study dataset. Each job now explains the three data routes above itsdatachunk and prints the dataset, file and dimensions it read, so a report says which data it describes.vignette("sas-to-r-descriptive")gains a “Which data a job reads” section covering the same ground for the biostatistics team.Unworked
EDIT:markers no longer stop a render; the job renders as a draft. Every template’s edit guard used to stop a render while any marker remained, unlessHVTI_TEMPLATE_DRAFTwas set. The default is now the draft: the render warns and the report opens with a DRAFT banner listing the open markers, so an author iterates towards a finished report and the banner goes when the last marker does. The guard only stops blocking: a section still holding a template placeholder stops with its own error, so a fresh job renders as far as the markers already worked.HVTI_TEMPLATE_DRAFTis gone from the templates. SetHVTI_TEMPLATE_STRICT=1to make an unfinished job stop, as a final render should; unset,0,falseandnodraft, and any other value stops. This applies to jobs scaffolded from now on. A job file is a copy of its template, so a job created before this release keeps the old guard: it still stops by default, still needsHVTI_TEMPLATE_DRAFT=1to draft, and ignoresHVTI_TEMPLATE_STRICT. To move one over, replace itsedit-guardchunk with the current template’s.vignette("sas-to-r-descriptive")gains a section on the draft banner and the markers.migrate_job()reads a legacy SAS job and writes the matching template job plus an evidence report, preserving the source and refusing to overwrite either output. The template and its qualifier are read from the SAS filename (<prefix>.<qualifier>.sas), andprefix/qualifieroverride that reading when a source does not follow it; aqualifiergiven alone is used with the prefix from the filename. The study root is found from_study.ymlabove the source, anddiroverrides that when the source sits outside the study. Relativesource,lst,logandreferencepaths resolve against the working directory, anddironly locates the root. The listing and log default to the same-named files beside the source,lst/logoverride them, and the report says whether each was supplied, found or not found. A template with no migration adapter yet is still scaffolded, everyEDIT:marker kept, with a report that says plainly that the migration is manual. Only deterministic extraction can remove anEDIT:marker; uncertain choices remain for review. The three descriptive templates listed below and the existing trends template have migration adapters. Log messages are withheld; reports keep their locations, severity, SAS error codes and recognized aggregate counts for review. Source text the report quotes is masked for every template: string literal contents, R raw strings included, comment bodies,%letvalues, whole even when a macro-quoted value such as%str(a;b)holds a semicolon,%puttext, unquotedtitleandfootnotetext, and digit runs of five or more become placeholders such as"[string]"ortitle2 [text];, and Quarto prose and YAML are withheld.dc-tablesgroup headings, which come from SAS comments, appear in the report as[heading]and in the job asGROUPSkeys. The job itself is not masked. Outputs are placed by hard link. Where the filesystem refuses links, the prepared file is renamed into place after a check that the target is still absent, so a concurrent writer can race that check. A failed placement removes only the outputs that call placed. WhenDATASETis unresolved, thedc-tables,dc-gfup,dp-postageanddp-trendsjobs stop before reading data and point to the migration report.dc-tableswrites editable CORR DOCX output in place of the SAS RTF throughhv_tbl_summary(),hv_man_table(),hv_man_table_save(), andhv_check_docx(). Structural findings stop the job.dc-gfupchecks registered follow-up intervals with identifiers disabled;dp-postagewrites numbered PNG pages throughhv_eda()andpatchwork.vignette("legacy-study-migration")teaches adoption of an existing study, separate registration of study and named-subset data, all four migrations, marker review, and output checks with synthetic data.Three descriptive job templates ship:
dc-tables,dc-gfupanddp-postage, withvignette("sas-to-r-descriptive")walking a SAS user fromdescriptive/dc.tables*,dc.gfup,dp.trendsand the EDA postage stamps to the R job that replaces each.dc-tablescarries the correlation variant onhvtiRtables::hv_correlation_table()andhvtiPlotR::hv_correlation_matrix();dp-postageis thin overhvtiPlotR::hv_eda(). The job-catalog pin advances tohvtiRv1.1.11in both workflows, and the roadmap is re-rendered: 44 templates are in scope and 13 are on disk.add_job()replacesnew_job()and follows the study’s directory layout. New studies receive jobs in numbered folders such as20_distributions/; adopted legacy studies retain bare folders such asdistributions/. Mixed layouts are rejected. This requireshvtiRutilities1.1.12.The job-catalog pin advances to
hvtiRv1.1.10, in bothR-CMD-check.yamlandspec-counts.yaml, and the roadmap is re-rendered from it. 1.1.10 carries hvtiR#73 and hvtiR#74: TemporalHazard functions named on the hazard rows, the relabeled names, andpm’s row dropped now that it folds intolm. It moves together with hvtiRutilities#113, which dropspmfromhvti_taxonomy(), becausetest-roadmap.Rchecks catalog and taxonomy against each other both ways. The propensity-matching workflow reads 2 of 9, withpmno longer a member. 44 templates in scope, 10 on disk, unchanged.The job-catalog pin advances to
hvtiRv1.1.9, in bothR-CMD-check.yamlandspec-counts.yaml, and the roadmap is re-rendered from it. 1.1.9 carries hvtiR#61, hvtiR#63 and hvtiR#64. The roadmap now schedules 44 templates, not 43:dc-trendsanddp-boxplotjoin from the 2026-09-10 triage, andpmleaves, because it folds intolmand isretireoverhvtiRpropensity::bs_count().lmcounts 470 studies and isthinoverhvtiRpropensity,siandmicount jobs (1 and 18), and the measureddcanddprows show R counts for the first time. The propensity-matching workflow reads 3 of 10, becausepmis now owed elsewhere. Ten templates are on disk, unchanged.deadeanddeadlare not landmark jobs, but the early/late landmark split is a real construct. The per-folder parse design (section 8) now records the check. Both names are hazard-phase bagging output from thebhtemplate: everydead[elc]*file on the share is a dataset, and no program is nameddeadeordeadl. Nine programs carry the string inside a longer name, eightdeadexpljobs (death before explant) anddc.deadlate. Separately, job pairs such aslg.dead.earlyandlg.dead.late(10 and 8 studies) ordc.dead.earlyanddc.dead.late(4 and 5) split follow-up at a cutoff: the early job censors at it, and the late job keeps only those who reach it. No taxonomy row names the construct; the job catalog records it as a sharedlandmarkoption from hvtiR#61, shipped inv1.1.9.-
40_graphs/dp-trends.qmdships, the first of the EDA templates and the first QUALIFIED template: a job is scaffolded withadd_job("dp", "cohort", "eda", qualifier = "trends")and lands at40_graphs/cohort-eda-dp-trends.qmdin a new study. It is thin overhvtiPlotR::hv_trends(), which takes one row per patient and computes the per-year points itself, so the job never aggregates by hand. Of 50 trends job files across 24 studies, none used it. One job draws a list of figures, optionally once per subgroup on shared axes.The set key for an EDA job is
(subject, eda):ENDPOINTnames what is described,cohortby default, andTYPEis alwayseda.It stops, rather than drawing, on an operation year outside 1900 to next year (the wrong-origin mistake), on a fractional year (which would draw one “annual” point per patient), on a year missing for every patient, on a percent column holding anything but 0/1 or logical, on a subgroup filter that is not one TRUE/FALSE per patient, and on a subgroup that selects nobody, and it prints each figure’s dropped rows. Axis limits zoom through
coord_cartesian()rather than dropping patients before the smooth is fitted.hvtiPlotR (>= 2.7.7)joinsSuggestsandRemotes; 2.7.7 rather than 2.7.6 because the dropped-row count arrived part-way through 2.7.6 without a version bump. The Suggests-bound test now covers hvtiPlotR.Design:
dev/specs/2026-09-10-dp-trends-template-design.md. The job-catalog pin advances to
hvtiRv1.1.8, in bothR-CMD-check.yamlandspec-counts.yaml. 1.1.8 carries one catalog change, hvtiR#60:dp-trendsisshipped. The pin moves in the pull request that adds the file, as the design’s section 5 requires, because atv1.1.7the row was stillqueuedandspec-countsfailed on a template no ledger row claimed.The job-catalog pin advances to
hvtiRv1.1.7, in bothR-CMD-check.yamlandspec-counts.yaml, whichtools/check_pin_currency.pyrequires to agree. 1.1.7 carries the EDA batch’s catalog changes (hvtiR#57, hvtiR#58):dp-postagejoins the catalog, anddc-general,dc-tablesanddc-gfupmove into batch 3. The roadmap is re-rendered to match: 43 templates in scope, adp-postagerow with no blocker, anddescriptivespanning batches 3 to 4.-
hvtiRlifetables (>= 0.1.2)is now declared, inDESCRIPTIONand in40_graphs/hs.qmd. The template has calledus_cohort_curve()since 1.0.16, but nothing recorded which version provides it.DESCRIPTIONdid not name the package, and the template had no floor check, so a study on 0.1.1 failed with “could not find function” part-way through a render, after the model had been read and every prediction made. It is now a versionedSuggestswith aRemotesentry, ashvtiRbootstrapis, and the setup chunk refuses below 0.1.2 before anything is read.The floor is 0.1.2 because that is where the function was introduced. The 1.0.16 entry below says it “landed in 0.1.3”, which is wrong: 0.1.3 changed only the package’s own tests.
The test comparing
DESCRIPTION‘s bounds with the templates’ floors now covers both packages, and FAILS rather than skips when a template enforces a floorDESCRIPTIONdoes not declare. That is the case it could not see before, and the one this entry fixes. -
A template’s expected folder now comes from the job catalog, not from
hvti_taxonomy()alone. The taxonomy maps a prefix to ONE folder, but a prefix may span several:dpisgraphsfortrends,gfup,spaghettiandprocs, anddistributionsforvariable. The old check assertedtl$folder == tx$folder[match(tl$prefix, tx$prefix)], so it would have rejecteddp-variablethe moment anyone wrote it, anddp-variableis already scheduled in batch 3. The catalog recordsfolderper row, keyed on(prefix, qualifier), and is consulted first; the taxonomy answers for rows the catalog does not have and whenever the catalog is absent. See #97.⚠️ The fallback does not skip. A missing catalog leaves the taxonomy answering for every template rather than the check quietly passing, because the taxonomy still catches a template filed under a folder no study uses, which is most of this guard’s value. A silently skipped guard is worse than no guard.
⚠️ The change had no CI coverage when it was first pushed, and ten green checks said otherwise.
R-CMD-check.yaml’s strict step ranfilter = "roadmap", sotest-taxonomy.Rexecuted only insideR CMD check, where the catalog is always absent and the new lookup always took the taxonomy fallback. The filter is nowroadmap|taxonomy, and five tests drive the lookup against a temporary catalog so the catalog-first branch is asserted rather than merely executed. A test filter naming one file silently decides which code paths CI exercises; widen it whenever a test starts reading the catalog.The ledger helpers moved from
test-roadmap.Rto a newhelper-ledger.R, because testthat gives each test file its own environment andtest-taxonomy.Rneeds them too. A second copy of the path resolution was the alternative, and a copied authority free to drift is what retired theFOLDER_ORDINALguard. spec-countsnow fails when the two hvtiR pins disagree with each other (tools/check_pin_currency.py).R-CMD-check.yamlandspec-counts.yamlcheck the job catalog out independently, and advancing one while forgetting the other is the realistic mistake – it leaves this repository validating against two different catalogs with both sides green.⚠️ The path filters were widened in the same commit, and that is the load-bearing half. They matched
spec-counts.yamlbut notR-CMD-check.yaml, so a pull request advancing only the latter’sref:started no job at all and went green unverified – the exact silent-drift failure this workflow’s own header warns about, aimed at the guard being added.🔴 It also rejects refs that are not immutable tag pins, added after review found the first version passed states it forbids:
ref: mainin both workflows agrees with itself, and so does an emptyref:, and both resolve to mutable default-branch behavior. Equality is not pinning. An emptyref:was the sharp edge — a missingref:line yieldsNoneand was always rejected, butref:with nothing after it yields"", which is notNoneand slipped through while the docstring claimed otherwise. A commit SHA is immutable and still rejected: hvtiR’sjobs-pin-driftcompares these against its newest tag name, so a SHA reads as permanently stale.Lagging hvtiR’s newest tag is deliberately NOT a failure here. That alarm belongs to hvtiR’s
jobs-pin-drift, on a schedule with a grace period; failing every pull request the moment hvtiR cuts a tag would redden reviews for a reason unrelated to the change under review. This check answers the narrower question that only this repository can answer, in the pull request that caused it.The job-catalog pin advances to
hvtiRv1.1.6, in bothR-CMD-check.yamlandspec-counts.yaml. They are pinned independently and both must move; advancing one leaves the other validating against the old catalog with its guards still green.v1.1.5predates thesiandmiimputation rows (hvtiR#54), so until nowtest-roadmap.Rwas checked against a 53-row catalog whilehvtiRshipped 55.The roadmap document is regenerated against the new catalog, which is the half of a pin bump that is easy to miss.
dev/specs/2026-08-29-template-conversion-roadmap.mdis rendered from the catalog, so advancing the pin without re-rendering leaves a checked-in document contradicting the very catalog it was just pointed at. 40 templates in scope becomes 42, with queued rows formiandsi. ⚠️ Their “blocked on” cells differ and both are correct:miis blocked onhvtiRimputationbecauseimpute_multiple()is unbuilt, whilesiis blocked on nothing becauseimpute_mean()has shipped.Reading the catalog by tag is still the right call – a dependency would invert the family, since
hvtiRinstalls it, and trackingmainwould let an edit there fail every pull request here. What was missing is that nothing reported when the pin aged.hvtiR1.1.6 adds that detector, and it now reads these two files directly: it will not consider the family current until both refs name its newest tag.bl,brandbcnow requirehvtiRbootstrap >= 0.9.3, and the reason is not a missing function. Below 0.9.3,boot_select()recordedsleandslson the screen and then selected on AIC regardless, so theboot_provenance()table these reports print named an entry and a stay criterion the screen had never applied – directly above the frequencies a reader attributes to them. A blank would have been obvious; a plausible number that describes nothing is not. 0.9.3’s renamed bag columns do not affect these templates, which read$boot$replicatesand rebuild the matrix themselves rather than reading$boot$summary.The reports also refuse a bag produced below 0.9.3. The version guard at the top of each one checks the
hvtiRbootstrapinstalled where the report renders, which is not the version that matters: the report reads a bag some runner wrote earlier. A 0.9.2 bag survives 0.9.3’s own documented migration, passesboot_validate(), and would render an AIC-selected screen beneath the entry and stay criteria it never used.bag$engineis now checked as well, and an absent,NAor unparseable one is refused rather than crashed on.bhdeliberately stays at>= 0.9.0. Its screen comes fromTemporalHazard, whose own stepwise has always honoredslentryandslstay;hvtiRbootstrapis only the reporting layer there. Raising it would assert a dependency the template does not have.The Phase 3 render gate moves to 0.9.3 as well, because it runs a screen: below that, the bags it produces are not the bags these templates now declare they need.
hvtiRtemplates 1.1.0
Breaking
-
The ordinal is gone from every template and job filename. A template is now
<prefix>[-<qualifier>].qmdand a job is<endpoint>-<type>-<prefix>[-<qualifier>].qmd.03.01-ac.qmdbecomesac.qmd;dead_pa-hz-03.01-ac.qmdbecomesdead_pa-hz-ac.qmd.NNwas the taxonomy folder’s position and duplicated the directory the file already sat in.MMwas a per-folder key asserting an order among a folder’s templates that does not exist. Both are removed. Seedev/specs/2026-09-03-template-identity-design.md. -
inst/templates/directories carry the ordering digits:00_datasets,10_descriptive,20_distributions,30_analyses,40_graphs,50_documents,90_estimates.NNmoved here, where it is the thing rather than a copy of the directory beside it, and the decade gaps are room to insert a folder without renumbering, which is what thebhrename cost last time.⚠️ The digits are ASSIGNED, not derived.
estimatesis 90 though it is fifth in the taxonomy, because it holds saved output rather than jobs.⚠️ A STUDY does not use the numbered names.
new_job()writes into the bare taxonomy folder,distributions/andanalyses/, because that is what studies already have (63,278 and 119,582 corpus files). Writing into20_distributions/would split a study’s estate across two spellings.template_list()reportsfolderwith the digits stripped, for the same reason. template_list()drops itsordinalcolumn, and the roadmap ledger drops bothordinaland theretired_ordinalsregister.
Removed machinery
check_ordinals(),check_retired(),FOLDER_ORDINALandRETIRED_FIELDSare gone fromcheck-roadmap-counts.py, and the roadmap table drops itsordinalcolumn.check_disk()now derives the folder-to-directory map by readinginst/templates/, so there is no hardcoded copy of the numbering to drift.The R test that parsed
FOLDER_ORDINALout of the guard and compared it tohvti_taxonomy()is retired with its reasoning: the right answer to “this constant can go stale” turned out to be deleting the constant.
Note for old filenames
⚠️ 04.06 will never be explained by anything else. The retirement register recorded that 04.06-bh shipped in 1.0.13 and 1.0.14 before bh was renumbered to 04.05; it went with the ordinal. So: an NN.MM in a filename is a pre-1.1.0 job, 04.06-bh and 04.05-bh are the same template, and no ordinal will ever be issued again.
Copilot review credits are reported exhausted until October 2026, and that is recorded in
AGENTS.mdtogether with the evidence against it. Reviews were still arriving the afternoon it was reported, including one on the PR recording it. The note says what the consequence would be if the cutoff lands (the rule does not block a merge, so a PR would go green and get no review, and with the approval rule nobody can satisfy on their own PR it would reachmainunread) without asserting a state the evidence contradicts.-
⚠️
distributions/dpis ONE ledger row, not the three that shipped in 1.0.21. Those three were keyed on measurement scale, recommended from a sample of two template filenames and released before anyone measured them.Measured over all 1,271
distributions/dpjob rows: 666 of them, 52%, carry only the variable and no second field at all, and a scale word appears in 110 stems, 8.7% (binary51,ordinal49,continuous6). As a second qualifierbinary(8 studies) andordinal(10) rank fifth and sixth, behindtrend(21),tavr,gastroparesisandmatched.So it is one job parameterized by variable, which is what the design record said before it talked itself out of it. The row is
dp/variable, 237 job studies.qualifierisvariablerather than null because a prefix must be wholly qualified or wholly unqualified andgraphs/dpis decomposed.Recommending three templates from a sample of two, inside the work that exists because a convention was inferred from a small sample and applied to a whole corpus, is that error committed once more. It reached
mainand shipped.trend(100 rows, 21 studies) is the one second qualifier that might still earn a template, and is noted rather than acted on.
hvtiRtemplates 1.0.21
-
dpanddcare decomposed in the roadmap ledger: two rows become twelve. Enacts section 9 ofdev/specs/2026-09-02-dp-dc-decomposition-design.md.dcbecomesgeneral(759 studies),tables(551),gfup(389),dead(171) andstddiff(59);graphs/dpbecomestrends(80),gfup(48),spaghetti(40) andprocs(35);distributions/dpbecomesbinary,ordinalandcontinuous.No ordinals are assigned. An ordinal is issued when a template is created and a retired one is never reissued, so assigning twelve now to templates that may never be written would burn them. Every row is
queuedwithordinal: null, asdpanddcwere. No template has been written.The
distributions/dpthree are keyed on measurement scale, not variable, and are marked unmeasured: the census extracts the first qualifier, which there is the variable. The evidence is two of three local templates plus the same vocabulary recurring ingraphs, and the rows say so. std_difversusstddiffis resolved: ship the 2019 successor.stddiff.sasis 649 lines handling continuous, ordinal and categorical;std_dif.sasis 108 lines from 2009. Its wider corpus use, 72 studies against 59, is a macro outliving its replacement rather than a preference. The template therefore depends onhvtiRutilities, nothvtiRtables.The roadmap table gains a
jobscolumn.sas_breadth_jobshas been in the ledger since the re-parse and rendered nowhere, so a decomposed row showed a dash for every count it actually has. Two measures, two columns, rather than one column falling back to the other.-
A template name can now say which job it is:
<NN.MM>-<prefix>-<qualifier>.qmd.graphs/dpistrends,spaghetti,procsand more under one prefix, and the split is already present in the template estate rather than being proposed. A name that cannot say which job it is, is one a study author cannot search. Decided 2026-09-02; seedev/specs/2026-09-02-dp-dc-decomposition-design.md.template_list()gains aqualifiercolumn.NAmarks an unqualified template row, which is every template shipped today.template_path()andnew_job()gain aqualifierargument. Nothing existing changes: all nine shipped templates are unqualified and every existing call still resolves.Both refuse to guess. Naming no qualifier where a prefix carries several is an error listing the choices. Selection was
match(), which returns the first row and says nothing about the rest; that is safe only while every prefix has one template, and is the same shape as the bug that madedplook like one job type.The parser’s prefix capture was
.+, which is greedy and read06.03-dp-trends.qmdas the single prefixdp-trends. It parsed, it validated, and it was wrong. The prefix is now[A-Za-z0-9]+, so a prefix may never contain-. The roadmap ledger is keyed on
(prefix, qualifier), not prefix alone. A prefix may hold several rows, one per job type, and must be wholly qualified or wholly unqualified: a half-decomposed prefix would offer an unqualified row in the ambiguity error that no caller can ask for. The renderer labels a qualified rowdp-trendsso two rows for one prefix do not render identically. An empty-string qualifier is rejected outright, rather than being collapsed into an absent one by a truthiness test.-
The allocation note’s cross-package dependency count was wrong for nineteen days, and is now anchored. It read “19 file-level dependencies span 3 package pairs”. That was correct when written on 2026-08-14 and went stale the same day, at
77734a9, which createdhvtiRbootstrapand moved 24 dependencies into two new package pairs without touching the sentence. The real figures are 43 across 5 pairs.The scan now emits
cross_package_dependenciesinto the map, andcheck-spec-counts.pyfails the PR when the prose, the edge list or the acyclic claim disagrees with it. Every other count in that document was already anchored and had been re-synced repeatedly over the same period; this one was prose, and prose is what drifted.The acyclic half of the claim was true throughout, and is now verified by a depth-first coloring rather than asserted. A stale number sitting beside a correct claim is harder to see than a wrong one on its own.
-
The corpus was re-parsed, and the batch order moved. The census that ordered the conversion work was produced by a parser that captured a legacy job name’s first dot-field and discarded the rest, so
dp.trends,dp.gfupanddp.spaghetti.echoall counted as one bucket calleddp. A bucket cannot be templated, which is why Batch 3 was stuck. The parser fix is upstream inhvtiRutilities; here, the roadmap ledger gainssas_breadth_jobsbeside the existingsas_breadth, anddev/specs/2026-09-02-per-folder-naming-parse-design.mdrecords what changed.The two counts differ because the old one counted every extension, so a
.lst, a.log, a.pdfand a.sas7bdateach counted as evidence that a study runs a job of that type. Ten prefixes move three ranks or more.lpdrops nine, from 636 studies to 310, because more than half its breadth was output rather than programs. The top four are unmoved. The
hz +396/hm +172hand corrections are withdrawn.hzdeadandhmdeadare result datasets inestimates,.sas7bdatand.ssd01, not hazard jobs. Adding them added model output to a count of programs. The measured job breadth ishz574 andhm373.deadeanddeadlare identified. Hazard-phase estimate datasets, early and late, withdeadcbeside them at 60 studies. They sit inestimatesand were never jobs.The macro-allocation scan globbed one level deep and missed 13 of the 244 template
.sasfiles. Three files change destination as a result, andstddiff.sasis the one that matters: it was allocated to a single package only because the glob hid the twodctemplates that make it shared. Three retired destination slugs are also corrected, tohvtiRdatabuild,TemporalHazardandhvtiRpropensity.
hvtiRtemplates 1.0.20
Three bootstrap screen templates ship:
analyses/04.02-bl.qmd(logistic),analyses/04.03-br.qmd(linear) andanalyses/04.04-bc.qmd(Cox). All three report throughhvtiRbootstrap’s reporting layer withPHASE_OF <- NULL, which is the single-phase path the same functions serve forbhwith a term-splitting rule. One code path, four templates.They read one screen rather than pooling chunks.
bhpools because a hazard screen is days of compute; these are not, and a chunked run is pooled in the runner, which is the only place that knows how many chunks it launched.Two of
bh’s warnings do not transfer, and were rewritten rather than copied. A formula held in a variable is fine here:boot_select()’s fitters evaluate through an environment that bindsformulaanddatawherestep()looks for them, and a variable-held formula gives byte-identical results.bhwarns the opposite becausehzr_bootstrap()rewrites the stored formula per replicate. The empty-screen and flat-bootstrap refusals stay, with diagnoses that fit aboot_select()screen.bccarries a warning the other two do not need. A Cox model has no intercept, sobase_paramscannot be the"(Intercept)"a logistic runner would pass. With nothing forced there is no free base parameter, the standard-deviation check reportsNArather than passing, and the guard that catches a bootstrap which refit nothing is silently inactive.They require
hvtiRbootstrap (>= 0.9.2), andDESCRIPTIONnow says so. 0.9.2 addsboot_bag(), without which aboot_select()screen cannot be read by the reporting layer at all: the screen returns a wide coefficients matrix and the layer reads a long-form bag, and nothing converted between them. The declared bound had also drifted – it read>= 0.1.1from 1.0.13 while thebhtemplate refused below 0.1.2 and later 0.9.0, for nine releases. A test now asserts the bound is at least the highest floor any template enforces.All three were rendered against a screen run for the gate, not against a screen a study ran. No R job found on the share calls
boot_select(), so nobl,brorbcbag existed to read. The gate screens a real built dataset instead, which exercises real variable names, a real correlation structure andread_built(); what it does not exercise is a candidate pool a study author chose.bcis the weakest of the three: of the 16 studies with abcjob, none has an R exemplar.Ordinals 04.02, 04.03 and 04.04 are free minors in
analyses.04.06remains retired and unissuable.
hvtiRtemplates 1.0.19
-
graphs/06.02-hs.qmdstates its confidence coverage in reader-visible prose, not only in a code comment.CLEVELis0.68268948, plus or minus one standard error and about 68% rather than 95%. That is the SASHAZPREDconvention and the published papers state it, so the limits are correct and only the reading of them can be wrong. The templates shipcode-fold: true, so the comment that said this was invisible to anyone reading the rendered report.03.01-ac.qmdand06.01-hp.qmdalready said it in prose; this was the one job that did not, and it is the job whoseloweranduppertravel into the saved artifact for the plotting jobs to draw, arriving at a figure with nothing beside them recording the coverage.
hvtiRtemplates 1.0.18
analyses/04.05-bh.qmdnow reports throughhvtiRbootstrap’s reporting layer instead of building its own tables by hand. The template requireshvtiRbootstrap (>= 0.9.0)and refuses to render below it. The reporting layer itself exists from 0.1.2; the floor sits higher because 0.1.x strips a phase label whether or not a separator follows it, so a term namedearlyagereports its variable asage, matching no concept and quietly absent from every grouped table. A wrong frequency is worse than a missing function, because nothing downstream can see it. Thecontractchunk now callsboot_validate(), which checks field shapes, not only presence.provenanceandseedscallboot_provenance()andboot_seeds(). Bothdropped-*chunks collapse into oneboot_dropped()call. Thehealthtable comes fromboot_health(); the twostop()refusals stay in the template, becauseboot_health()reports and never refuses on its own.frequenciesandconcept-unioncallboot_frequencies()andboot_concepts(), andretainednow filters the package’s ownretainedcolumn rather than recomputing the cutoff locally.The
healthtable gained two columns. It reports four now (check,value,ok,note) where it reported two before;boot_health()suppliesokandnote, and the original two columns are unchanged.freq, as saved tobh-report.rds, now carries eight columns where it carried seven, in a different order.boot_frequencies()returnsphase, variable, term, n, pct, mc_error, near_threshold, retained; the new column isretained. A study reading the saved object by name is unaffected, but one reading it positionally is not.The template carries an eleventh
EDIT:marker,PHASE_OF, in thecriteriachunk. It names the rule that splits a term into its phase and variable.boot_frequencies()andboot_concepts()also acceptPHASE_OF = NULL, for a single-phase screen, and that is what lets one reporting code path serve the forthcomingbl,brandbctemplates. This template assumes a phase dimension throughout, soPHASE_OFmust name a real rule here.Rendered against a real 25-chunk bag and diffed against a reference captured from the pre-refactor template: the provenance, seeds, frequency and retained tables are byte-identical, the concept table has zero value mismatches across 184 rows, and the frequency figure has the same sha256.
hvtiRtemplates 1.0.17
-
analyses/04.05-bh.qmdcould not render on any multiphase bag — which is every hazard bag, andbhis the bootstrap hazard screen. Itsprovenancechunk passedbag$requestedandbag$usabletodata.frame()as if they were scalars. They are per phase: a real bag carriesc(early = 230, late = 230), so a length-2 value met a length-13 item column anddata.frame()errored. Now collapsed to one labeled string per row —early 230, late 230— with fallbacks for a single-phase run and for a runner that drops the names.Not summed: the pool is offered to each phase, so 230 and 230 is one pool seen twice, not 460 candidates.
⚠️ Found by the first real render, against the very study the template was written from —
EXPECT_CHUNKS <- 25LandEXPECT_BOOT <- 500Lare that study’s own values. The template shipped in 1.0.13 and was renumbered in 1.0.15 without anyone rendering it. Nothing inR CMD check, the test suite,lintror three code reviews could see it: the defect is an assumption about the shape of a field in an artifact produced by another package.
hvtiRtemplates 1.0.16
Guards
Retired the two tests asserting that within-folder ordinal minors follow
hvti_taxonomy()row order. They enforced the derivation #56 removed: an ordinal is a key, assigned once and recorded in the ledger, never recomputed from a position. #56 established that rule inAGENTS.md, the ledger andcheck-roadmap-counts.py, but left the test suite asserting the opposite — two guards on one repository disagreeing, which is the drift the guards exist to prevent. Measured before removal: reordering two templated prefixes within a folder turned CI red, while inserting or reordering an untemplated one was always safe.Replaced them with a key check rather than another position check:
check-roadmap-counts.pyvalidates every ordinal’s major against a hardcodedFOLDER_ORDINALmap, whose authority ishvti_taxonomy()’s folder order — a table that lives inhvtiRutilities, a different repository.test-roadmap.Rnow asserts the map still matches. That closes the same class of failure one level up from the one #56 fixed. The Python guard cannot check its own map: reading the taxonomy needs R, which is why the guards are split by language.
graphs/06.02-hs.qmd
-
Now calls
hvtiRlifetables::us_cohort_curve()instead of deriving the cohort expected-survival curve inline. The template shipped that derivation deliberately, as the specification for hvtiRlifetables#16, with a comment promising that when the function landed “this chunk becomes one call and the result must not change”. It landed inhvtiRlifetables0.1.3; the numbers were checked to 1e-12 against the derivation before the swap.The function is not merely a convenience. Its averaging follows
%usmatchd(lines 338-350) — the unweighted cohort mean, over survival rather than cumulative hazard, no patient weighted by follow-up — and it deliberately does not dropNA, where the inline version usedna.rm = TRUEand would have hidden one. Added an
EDIT:marker forus_cohort_curve()’sby =, covering hvtiRlifetables#17. Grouping is not stratification:table =chooses which strata the life table is built from,bychooses how the resulting curves are grouped for reporting.HORIZONSmust now be sorted and distinct, checked where it is declared.us_matched()enforces sortedness too, but three chunks later and about its owntimesargument rather than the value the author edited — and the expected-survival join pairsus_cohort_curve()’s output, ordered by first appearance, withsplit()’s, ordered numerically. They agree only whileHORIZONSis sorted.
hvtiRtemplates 1.0.15
-
The bootstrap screen template is now
analyses/04.05-bh.qmd, renamed from04.06-bh.qmd. The ordinal had been derived from the prefix’s row position inhvti_taxonomy(), and that derivation had already failed:bhwas 6th inanalyseswhen the number was assigned,hvtiRutilitiesthen movedhsout tographs, and every prefix below it shifted up one while the filename stayed put. The identity was positional across a repository boundary, so a correctness fix in the upstream vocabulary silently invalidated a downstream filename, and nothing caught it: the ordinal checks verify format, folder-major and uniqueness, never position.⚠️ A job already scaffolded from 1.0.13 or 1.0.14 is unaffected.
new_job()bakes the ordinal into the job filename at copy time, so existing jobs keep theirs and do not need renaming. Only newly scaffoldedbhjobs take04.05. An ordinal is now a key, assigned once, and never recomputed from row order.
NNis the taxonomy folder’s position, which is stable;MMis assigned from the next free minor in that folder and recorded in the roadmap ledger. Row order may drift; ordinals may not. Stated inAGENTS.md, so the next template is not numbered by counting rows.04.06is retired, not freed. It shipped in two releases, so a study that scaffoldedbhfrom either carries it in a filename. It is recorded underretired_ordinalsin the ledger, andcheck-roadmap-counts.pynow fails any row that claims a retired ordinal, naming when and from what it was retired.
hvtiRtemplates 1.0.14
-
analyses/04.06-bh.qmdgains the data-driven half of the SAS%clustercontribution.boot_clusters()counts replicates retaining any member of a list you declare and computes no correlation at all, so the section’s claim to group “by DATA” described the SAS macro rather than the R helper. Apool_collinear_pairs()chunk now makes it true. The two are kept apart deliberately: a declaration says a clinician treats several variables as one thing, a correlation says the pool cannot tell them apart, and only the second is a property of the data.This is why the template now reads the built dataset. The one failure that introduces is guarded: a build that moved since the screen ran would leave the correlations describing one dataset and every frequency above describing another.
n_rowscatches it, as a warning rather than a stop, because only this table goes stale. -
Concept-level union frequencies. A per-form row cannot answer “how often was this concept selected”, which is the figure a paper quotes. Nothing is collapsed — every form keeps its own row — but each concept now also carries
union_pct,best_form_pctand thespreadbetween them, which is how much the per-form table understates it.Computed from the replicate table, never from the summary: a union across a concept’s forms cannot be recovered from marginal percentages. Two forms at 30% each are anywhere between 30% and 60% of replicates depending on how often the same replicate took both, and the summary does not record that.
hvtiRtemplates 1.0.13
Superseded 2026-08-31: this template is analyses/04.05-bh.qmd from 1.0.15 onward. The sentences below are left as they shipped. A job scaffolded from this release keeps 04.06-bh in its filename and needs no rename.
-
Added
analyses/04.06-bh.qmd, the bootstrap variable-selection screen that replaces a SAS%hazboot/%sumboot/%clusterchain (#8). Scaffold it withnew_job("bh", <endpoint>, <type>). RequireshvtiRbootstrap>= 0.1.1 andhvtiRutilities>= 1.1.6. Design indev/specs/2026-08-28-bh-template-design.md.The template is the report, not the runner.
hzr_bootstrap()writes nothing until its final replicate and a full screen is days of compute, so the run is chunked from a companion script and this file pools what that script wrote. That file boundary is a durability boundary, not tidiness, and the runner stays untemplated untilnew_job()grows multi-file templates.It carries four traps that are silent rather than loud when they go wrong: competing transformations must be screened and grouped only at read time, never pruned beforehand; a screen that selected nothing is a failed screen rather than a null result, and now stops the render;
EXPECT_CHUNKSandEXPECT_BOOTstate what was launched, so a partial pool is flagged as provisional instead of quietly reporting over the wrong denominator; and the summed elapsed time is CPU cost, not wall clock.bhandhmare parallel analyses, not a pipeline, so the template deliberately writes no handoff file. Added
tests/testthat/test-bh-helpers.R, coveringboot_clusters()andboot_shortfall()against synthetic input. Both are first called from this template, and a render cannot stand behind them: it stops before reaching either whenever no bootstrap output exists, which is every CI run.Declared
hvtiRbootstrapinSuggestsandRemotes. The templates’ study dependencies stay out ofDESCRIPTIONdeliberately, but the test above calls the package directly, andR CMD checkresolves what tests reference — left undeclared it reports an unstated dependency.
hvtiRtemplates 1.0.12
- Added
graphs/06.02-hs.qmd, the patient-level prediction template. It reads the model anhmjob fitted, predicts survival per patient at the horizons a study reports, and sets those against age/sex/race-matched population survival fromhvtiRlifetables. Itsvintagemarker has no default on purpose: the SAS macro it replaces inherited a default that moved twice with no signal, and a job that does not state its vintage cannot be reproduced. Design indev/specs/2026-08-29-hs-template-design.md.
hvtiRtemplates 1.0.11
- Added the template conversion roadmap: a ledger of all 42 taxonomy prefixes plus three proposed ones, the roadmap document generated from it, and two CI guards that fail when the two disagree or when a row claims a template that is not on disk. Design and reasoning in
dev/specs/2026-08-29-template-conversion-roadmap-design.md.
hvtiRtemplates 1.0.10
Documentation
-
The institutional SAS license runs into 2027, not out in 2026. The wrong date
2026-09-29was stated in eleven places: the packageREADME.md, the 1.0.0 provenance note below, and nine statements across the design and artifact records indev/specs/.README.mdis corrected outright, because it states a present fact to a reader. Everything else is a historical record, so the original sentence is left standing with a dated correction beside it, following the convention this repository already uses for a superseded claim. -
2027-09-29is a working date, not a confirmed one. Every note states it the same way and defers to one canonical note, so confirming the real date is a single edit rather than another eleven. What is settled is the year, not the day. Writing an unconfirmed date as flatly as the wrong one was written is the mistake this entry exists to correct. - Two of those statements used the date as a deadline, and they came out differently. The “forcing function” in the AVR/LV-function parity design keeps its argument: fresh SAS execution genuinely needs a live license, so the pressure is real and simply further out than stated. The macro-allocation design’s claim that the license “bounds the window” for re-running the corpus scan is withdrawn, not re-dated. That scan reads
.sasfiles as text and never invokes SAS. What threatens it is loss of the sources, which are unversioned or unreplicated, and the mitigation is a remote rather than a deadline. - What the license date does not bound, in either case, is reading: compiled SAS data stays readable well beyond expiry, so what ends is the ability to run SAS, not to read what it produced.
hvtiRtemplates 1.0.9
Documentation
-
Development records moved to
dev/specs/, adopting the portfolio convention settled inehrlinger/house-style. The stray top-levelplans/is gone with it: this repo carriedplans/andspecs/and aspecs/plans/convention that said plans belonged in neither, which is the drift the new rule exists to stop. All three plans now sit beside the designs they implement, under a shared slug. -
spec-counts.yamlmoved in the same commit. Its two path filters and tworun:paths point atdev/specs/, and a comment at the top of the file says why they cannot be left behind — a stale path filter does not fail the check, it stops the job from starting, and the PR goes green having verified nothing. -
specs/artifacts/becamedev/specs/artifacts/, staying nested inside the prose directory becausecheck-spec-counts.pyresolves its target throughos.pardir. Both checks were run at the new location and pass unchanged. -
inst/templates/README.mdis the one shipped file affected, which is what makes this a patch bump rather than a governance-only change.
hvtiRtemplates 1.0.8
New features
-
analyses/04.01-hm.qmd— the multivariable hazard model, replacing a SASPROC HAZARDjob with aselectionstatement (#8). Scaffold withnew_job("hm", <endpoint>, <type>). Design:dev/specs/2026-08-27-hm-template-design.md. Requires hvtiRutilities >= 1.1.4, which ships the candidate-pool helpers it calls.Unlike
hzandhp,hmhad only one R exemplar, so its second was a SAS job (.../dm_nodm/analyses/hm.dead.sas). That is enough for the gate’s purpose — it exists to stop one study’s choices being encoded as general, and a SAS job states those choices as plainly as an R one does. It earned its place immediately: it carries a%decilescalibration step the R exemplar has no equivalent for, andTemporalHazardalready exportshzr_deciles()for it. Without the second exemplar the template would have shipped a model-selection job that never asks whether the model calibrates, and nothing in its output would have looked missing.⚠️ Two traps specific to this job, both silent:
-
hzr_stepwise()’s defaults are not SAS’s —slentry = 0.3/slstay = 0.2against a typicalsle = 0.1/sls = 0.07. Accepting the R defaults runs a different screen from the job being reproduced, admits more variables, and does not error. The template sets them from the.sas. - The default
criterion = "score"has a known defect (temporal_hazard#130): it declines the strongest candidates, because the observed information goes indefinite atbeta = 0.
Covariates are read out of the SAS job with
sas_variable_block()rather than transcribed, taking only names — aname=valueblock carries the other study’s converged answers.covariate_audit()then stops the render rather than fitting a model whose covariates are not what the job specifies, andpool_collinear_pairs()reports exact complements likemale/female.The fit is two stages, mirroring what the SAS job does: shapes held at the
hzfit while covariates are screened, then freed. Fitting both together from a neutral start lands in the wrong basin and reportsconverged = TRUEwhile doing it. Which stage is reported is anEDIT:decision with the evidence table beside it.The calibration chunk uses
print(dec), notknitr::kable(dec): the print method carries the overall chi-square, its degrees of freedom and its p value — the one number that answers “does this model calibrate” — andkable()renders only the per-group frame, dropping it silently. -
Bug fixes
dev/specs/2026-08-21-template-set-layout-design.md§5 corrected. It said the ordinal minor is “the next free position within it”; the rule that actually reproduces every shipped ordinal is the prefix’s position among its folder’s taxonomy rows. The two agree for everything templated so far and diverge on the thirdanalysestemplate, where next-free would putbhbeforehsand fail the within-folder ordering test intest-taxonomy.R— a failure whose only fix is renumbering a shipped template, which §5 promises never happens.DESCRIPTIONnow requireshvtiRutilities (>= 1.1.4). The old>= 1.1.1was correct until this release –hzandhpcall nothing newer – so the bound went stale as a side effect of adding a template rather than as an error in its own right. An install satisfying the old bound would have shipped a template whose helpers do not exist, failing at render time in a study rather than at install time.The
selection-monotonechunk handles a zero-step screen. A screen that admitted nothing is a real outcome – no candidate cleared the entry level – butwhich.max()on an empty vector isinteger(0)and the report frame then failed with “arguments imply differing number of rows”: an error about the report, in a job whose actual finding is that nothing entered.Convergence failure names the stage and what to check, instead of
stopifnot()’s “isTRUE(stage1converged) is not TRUE” – a message that tells a reader of the rendered report nothing they can act on.
Internal
.lintrgains a file entry for the new template, per the note in that file that a directory key would silently disable every linter on the path.New test: the
hvtiRutilitieshelpers the templates call must appear in a declared list. It cannot check the version bound directly (CI installs the latest, not the declared minimum), but it makes the trigger loud – a template that starts calling a new helper fails the test, and whoever adds it is standing in the right place to ask whetherDESCRIPTIONneeds bumping. The scan reads code chunks only: a test that fires on prose is a test that gets deleted.The
03.01-actemplate’s localimputed_levels()is removed in favor of thehvtiRutilitiesexport. The two were identical, and the duplicate arrived only because hvtiRutilities#47 lifted the same function out of the same study. # hvtiRtemplates 1.0.7
New features
-
graphs/06.01-hp.qmd— the nomogram and hazard figures, replacing a SASHAZPREDjob and the figure jobs over it (#8). Scaffold withnew_job("hp", <endpoint>, <type>). Design:dev/specs/2026-08-27-hp-template-design.md.hpreads its inputs rather than recomputing them — theaclife table and thehzfit, both by set. A job that recomputes its upstream can silently disagree with it.⚠️ Two settings are silently wrong if omitted, both decoded from the
HAZPREDsource during parity work:-
CLEVELdefaults to0.68268948— one standard deviation, not 95%. It is exactlypnorm(1) - pnorm(-1).%kaplanuses the same convention (T_ALPHA = 1), so theaclife table’s limits are also a ~68% band. That collides inhpspecifically, becausehpis where both bands are drawn on one figure: internally consistent, and misread by anyone taking either for 95%. On the same fit and grid the 95% band measures 1.97× the width of the SAS default. The template labels the coverage in the caption. -
Survival limits are formed on the
logitscale.predict.hazard()defaults to"log-log", soconf.type = "logit"must be passed explicitly. Omitting it does not error — it shifts every limit.
Two grids, not one, and two independent horizons: a sparse irregular reporting grid for the nomogram table, and a dense log-spaced plotting grid for the curves. Tabulating to 10 years while plotting to 3 is a normal thing to want, and SAS sets its plotting maximum as its own literal — so
t_maxis its own value rather than derived from the reporting grid.followup-gatechecks both. A linear grid to the last event time under-resolves the early phase, which is where the action is. A wrong grid does not error; it produces a plausible nomogram.followup-gatewarns when either horizon runs past the end of follow-up — a parametric model returns a number at any horizon asked of it, and nothing else marks that as extrapolation. The bound is computed over the rows the fit used, becausehzdrops rows missing either time or event and a max() over everything would overstate follow-up, making the guard under-warn.fig-phasesasserts the components sum tototal, since otherwise it is not a decomposition, and derives the component list from whatpredict()returns rather than naming early/late — so a three-phase model needs no edit. -
hvtiRtemplates 1.0.6
New features
-
distributions/03.02-hz.qmd— the multiphase parametric hazard fit, replacing a SASPROC HAZARDjob (#8). Scaffold it withnew_job("hz", <endpoint>, <type>). Design:dev/specs/2026-08-27-hz-template-design.md.Extracted from three R exemplars —
preserve_root,maze/atricure/genderandlv_function/survival— chosen because they disagree. Two seedthetafrom SAS’s converged estimates so that any disagreement is about the likelihood rather than about where the search began; the third seeds neutral shapes with the time scale taken from the data, because it has no SAS predecessor to seed from. Both are correct for their own case, so thestartchunk carries both as Shape A / Shape B, the way theactemplate’scohortchunk already does. From either exemplar alone the template would have made the other case silently wrong.Three checks are carried from a single exemplar each, because in every case their absence is silent rather than loud:
numderiv-gate(withoutnumDerivan interval-censored fit produces no standard errors and does not say so —convergedisTRUEandvcov()is simply absent);response-check(mis-building the response still converges, on a number that is merely wrong); andconservation-binding(reportingΣ Λ(tᵢ)under bothconservesettings is the only way to tell “the control did nothing” from “the control had almost nothing to do” — identical in the likelihood column, opposite in meaning).Parity is deliberately not in the template: a parity job borrows the ordinal of the job it checks and lives in
parity/.The response formula is built once, in the
responsechunk, and reused by all threehazard()calls, so the Shape A / Shape B choice is made in one place rather than in three literal formulas an author could edit inconsistently.
hvtiRtemplates 1.0.5
Bug fixes
-
An unedited job no longer renders green (#27). Templates now carry an
edit-guardchunk that scans the rendering file for unresolvedEDIT:markers and stops, naming each one.inst/templates/README.mdhas always said that a job still containing a marker has not been finished; nothing checked it, and the03.01-actemplate made the gap concrete — itsderivechunk indexed a placeholder column, and an absent column makes!is.na(d$<col>)logical(0), so every assignment is a silent no-op. Every patient stayed in one category, the downstream life tables ran over a single dummy stratum, and the render completed with 12 markers still in the file. -
derive_cats()and theDERIVEDmap now assert that the columns they name are in the data. This is the half a marker scan cannot reach: a placeholder replaced with a mistyped or renamed column leaves no marker behind.
New features
-
HVTI_TEMPLATE_DRAFT=1renders a partly-worked job anyway. The guard warns instead of stopping and the report carries a DRAFT banner listing the unresolved markers. It is an environment variable rather than a YAML parameter on purpose: a parameter lives in the file, so it would be committed and forgotten, re-opening the hole this closes.1,trueandyesenable it; any other value,0included, leaves the guard strict, so a variable set to0meaning “off” cannot switch it off by being non-empty.
Internal
- Two tests over the shipped template sources: every template must carry an
edit-guardchunk, and no template may write the marker token as a string literal. The second guards a trap worth naming — Quarto knits through an intermediate andknitr::current_input()returns that file, so the scan reads the guard’s own chunk. A literalgrep("<token>", src)matches its own source line and fires on every render, finished or not; a probe found three markers in a file containing two. The token is built withpaste0()instead.
hvtiRtemplates 1.0.4
Internal
-
hvti_taxonomy()andhvti_non_prefixes()now live inhvtiRutilitiesand are re-exported from here. Callers are unaffected — both are still available unqualified fromhvtiRtemplates. The table is shared vocabulary rather than template machinery, andhvtiRutilitiesis the lower layer, so the dependency now points that way. - Added
tests/testthat/test-reexports.R, which checksgetNamespaceExports()directly, so a dropped@exporton a re-export is caught by the suite instead of passing silently.
hvtiRtemplates 1.0.3
Breaking changes
-
new_job()now takesendpointandtypein place ofbasename, anddirdefaults to the study root rather than"qmd". It writes<folder>/<endpoint>-<type>-<NN.MM>-<prefix>.qmd— into the taxonomy folder the template belongs to, not a flatqmd/. Thetypeis required because a set is keyed on(endpoint, analysis type): one endpoint is analyzed by several methods and those chains share their upstream, so keyed on the endpoint alone two sets would write to one filename. -
template_path()’s argument is renamed fromnametoprefix, which is what it always matched on. -
template_list()gains anordinalcolumn, and reportsfolderfrom the directory a template sits in rather than by looking its prefix up inhvti_taxonomy().
Improvements
- Templates are named
<NN.MM>-<prefix>.qmdand live in the taxonomy folder they scaffold into. The name is parsed by pattern rather than by splitting on dots, which the old.prefix_of()heuristic could not do once the ordinal contained one. - The
actemplate declares its set withENDPOINTandTYPEmarkers and resolves artifact paths from them, so a scaffolded job needs no output path edited by hand.new_job()now substitutes the caller’sendpoint/typeinto those declarations after copying the template, so a scaffolded job’s body agrees with its own filename instead of still naming the template’s placeholder set. The template itself checks this at render time — comparing itsENDPOINT/TYPEdeclarations againstknitr::current_input()— and errors if they disagree, because a mismatch resolvesset_path()into another set’s artifact directory silently. The check is a no-op outside a knitr render, so editing the file interactively in RStudio is unaffected. -
new_job()validatesendpointandtype: each must be a single non-NAstring matching^[A-Za-z0-9_]+$, since-is the filename’s field separator and.is reserved to the ordinal. This also rejects a leading../that would otherwise escape the taxonomy folder. - New tests cross-check every template’s ordinal against
hvti_taxonomy()— the major against the folder it sits in, and the minors against row order within that folder — and assert that no two templates share a prefix, sincetemplate_path()andnew_job()both resolve withmatch(), which takes the first hit silently. -
hsis refiled fromdistributionstoanalyses, immediately afterhm, the job it actually consumes.hvti_taxonomy()gains anestimatesrow for the artifact directory that design already relies on; the row carries no prefix (NA_character_), sinceestimatesis an artifact kind rather than an analysis type. Its position shifts the folder majors:graphsis now06anddocuments07(distributionsis unaffected, soacstays03.01).
Bug fixes
-
new_job()no longer leaves a copied-but-unsubstituted job file behind when writing theENDPOINT/TYPEmarkers fails. Previously a template whose markers had moved or been removed still left its copy on disk after erroring — a job named for one set but declaring the template’s placeholder set, and a retry then hit the refuse-to-overwrite guard and reported “already exists” instead of the actual template problem. - The
actemplate’s render-time filename guard strips whatever extensionknitr::current_input()reports instead of a hard-coded.qmd. Quarto knits through an intermediate.rmarkdownfile, so the old pattern never matched under a real render; it happened to be harmless because the stray extension landed on a field the guard does not read, but the fix removes the reliance on that coincidence.
hvtiRtemplates 1.0.2
Bug fixes
-
.Rbuildignorenow excludes.claudeand.remember, which hold session tooling;.claudecan also contain a git worktree. Their contents were raising two NOTEs — hidden files, and non-portable paths over the tarball length limit — neither of which is about the package..rememberwas invisible until.claudewas excluded, becauseR CMD checkreports every hidden path in one NOTE.
hvtiRtemplates 1.0.1
New features
new_job()scaffolds an analysis job from a supported template, naming the file<prefix>.<basename>.qmdand refusing to overwrite an existing job.-
inst/templates/ac.qmd— the first supported job template, for actuarial life tables. It carries its ownformat:block so that a copied job renders standalone, resolves the project root itself so no path needs editing, and marks every study-specific lineEDIT:.The cohort section offers two shapes, because the choice is not cosmetic: a job analyzing the whole study uses
assert_cohort(), while a job analyzing a filtered subset must supply its own gate —_study.ymlrecords the study cohort, soassert_cohort()would pass while the job ran on a cohort nobody checked.hzandhpare deliberately absent: each exists in only one study, and a template extracted from a single example encodes that study’s choices as though they were general.
hvtiRtemplates 1.0.0
Initial release.
hvti_taxonomy()encodes the group’s analysis-prefix system as data rather than as a README, so the test suite checks it against the templates actually present instead of letting it drift.template_list()andtemplate_path()resolve a template name to an installed file, so a study binds to a versioned template rather than to a copy. Both return empty until stage 3 of the templates-and-provenance design adds the templates.hvti_non_prefixes()records the leading name fields that are utilities rather than analysis prefixes, which is what lets the test suite tell “not a prefix” apart from “a prefix nobody documented”.
Provenance
During development this repository also carried the legacy SAS template corpus (240 files) and the SAS macro library (495 files, with history imported from 2014) as a reference specification. Both were removed before release, and every path was purged from every commit with git filter-repo. They are not recoverable from this repository’s history.
Parity checks against the SAS originals therefore need a source outside this repository. The institutional SAS license expires 2026-09-29.
Corrected 2026-08-29: the license runs into 2027, not out in 2026 (working date 2027-09-29, not yet confirmed). The date above was wrong when 1.0.0 shipped. It is left as written and corrected here; see the 1.0.10 entry.
A result filed before the migration still cannot say which macro version produced it. That was already true — %inc bound late to a mutable directory with no version — and removing the corpus neither creates nor worsens it.