Changelog
Source:NEWS.md
saros.base 1.2.1.9001
Documentation
- Added four conceptual vignettes, porting the orientation material that previously existed only as Norwegian prose on the Saros website (#185).
vig_00_about_saroscovers what Saros is, its goals and success criteria, the macro/mesos/micro levels, the three-phase production process and the PISVEEP pass, the technology stack, and the analysis of the traditional report format that motivates generating it.vig_08_adopting_sarosis written for project leads: what adoption costs, what the package can generate, and the ethics of offering institution-specific reports as an incentive to participate.vig_09_projects_using_saroslists the projects the system is built for.vig_10_objections_and_limitationscollects the standard objections with their answers, plus the limitations expected to persist. All four are text-only — no figures, no executed code — and are picked up automatically by pkgdown, which has noarticles:key. - The “possible element types” table is no longer accurate and has been rewritten rather than translated. It documented an
element_namesargument enumerating compound names such asuni_cat_prop_plotandbi_catcat_freq_plot2, in which the number of variables involved, the variable type, the output form and the statistic were all encoded in one string. No such argument exists in saros.base —git log -S"element_names" -- R/returns nothing, so it never did. The successor isrefine_chapter_overview(chunk_templates = ), whose rows carry.template_nameplus.template_variable_type_dep/.template_variable_type_indep; univariate is nowNAin the indep column rather than auni_prefix, and proportions versus frequencies moved into asaros::makeme()argument.vig_08_adopting_sarosdocuments the five default variants as they actually are, and includes a mapping table for anyone arriving from the old vocabulary. Worth noting for future readers:.template_nameis not a validated, fixed set of permitted values —chunk_templatesaccepts arbitrary names — so the only genuinely closed vocabulary issaros::get_makeme_types(). -
inst/WORDLISTgained the Norwegian project and process names the new vignettes introduce (Kompetansebarometeret,Spørringene,Plukk,Sammenfatt, …), the acronyms (PISVEEP,AGU,KYU,SSN), and the tool names (Typst,Pandoc,renv,nifutypst, …). Note thattests/spelling.Rruns witherror = FALSEand there is no spelling job in CI, so the check reports without failing; the pre-existing British spellings inNEWS.md(behaviour,serialised,normalised) are left as they are.
Bug fixes
- Two default chunk templates now open the div they close (#246). Variant 1’s univariate
int_table_htmland variant 5’s univariateint_plot_htmlboth ended with a bare:::and never emitted an opening fence. Both read as a bivariate sibling copied with the opener dropped rather than as a spurious close: the caption line survived intact, and the shape is otherwise identical to the sibling. The unmatched close reached generated.qmd, where Pandoc renders it as literal text or silently absorbs it depending on context — the benign direction, which is why it went unnoticed. An unmatched open is the dangerous one: it swallows subsequent content into the div and drops it from the table of contents, warning only in the render log. The templates now open::: {#tbl-{.chunk_name}}and::: {#fig-{.chunk_name}}respectively, each matching what the body actually emits — the first is a table, the second a plot. Neither template is reachable without a numericdep, and no snapshot fixture had one, which is why nothing caught this;tests/testthat/test-qmd_snapshots.Rnow carries a numeric fixture and pins both bodies. - Variant 1’s univariate
int_table_htmlnow returns its summary table instead of callinggirafe()on it (#246). This is the same template as the missing#tbl-fence above and is fixed with it, because a table cross-reference anchor on a body emitting a ggiraph widget would be a knowingly-broken cross-reference. The template was in fact not renderable at all:makeme(type = 'int_table_html')returns a plain tibble, so{.obj_name}$datawasNULLand the download link silently resolved to nothing, and the followingmake_link(..., save_fn = ggsaver)aborted the whole chapter withno applicable method for 'grid.draw' applied to an object of class "tbl_df"— reported as a misleadingDo you have write access to '.'?. The render never reached thegirafe()call. The body now takes its download link from the table itself and wraps the result ingt(), following thecat_table_htmlsiblings (make_link(data = {.obj_name}),gt({.obj_name})).gt()matters here rather than returning the bare tibble: a data frame printed by knitr becomes a verbatim console dump inside the table float, and tibble’s print method drops trailing columns at the default width — theMaxcolumn vanished from the rendered report. Withgt()the chapter renders a real HTML table carrying all eleven columns, and@tbl-resolves to “Table 1”. The[PNG]download link is gone from this template, deliberately: there is no plot to save. It is the only default template that calledgirafe()on a table, and there is no bivariateint_table_htmlanywhere — it exists once, in variant 1, univariate. - A mesos group whose name sanitizes to an empty string no longer overwrites
<mesos_var>/_metadata.yml(#244).filename_sanitizer()returned""for a group name in which every character was illegal —"***"became separators only, andavoid_ending_with_specials()then removed those — andfs::path()drops an empty segment silently, so the group’s_metadata.ymlwas written to the mesos variable’s metadata file instead of to a folder of its own. That destroyedparams$mesos_varandparams$mesos_var_prettyfor every sibling group, and the group’s own folder was never created; its stub also landed at<mesos_var>/<file>.qmd.setup_mesos_structure()reportedMesos structure created successfullythroughout. Note which group was hit:make.unique()disambiguated the second and later collisions into_1,_2, so the first group with an unsanitizable name got the empty name and the ones after it were merely renamed oddly. Same failure shape as #212 — a path computation that silently escapes its intended directory, with a success message. Two independent guards now:filename_sanitizer()substitutes"unnamed"for any element that would come back empty, andextract_mesos_metadata()aborts if any abbreviation is empty or duplicated. The substitute is one fixed word rather than anything derived from the element’s position, because the function must map equal inputs to equal outputs:add_chapter_foldername_to_chapter_structure()sanitizes the wholechaptercolumn — one element per row, so a chapter repeats — withmake_unique = FALSE, and a positional substitute would give a single chapter a different folder name in each of its rows. Two different names that both sanitize away therefore collapse onto each other, which is ordinary behaviour for this function ("a b"and"a-b"already both give"a_b") and is whatmake_uniqueexists to resolve."unnamed"is purely alphanumeric, so it survivesvalid_obj,to_lower, the trailing-separator trim and anysep; it is deliberately not truncated tomax_chars, since a name a few characters too long is harmless where an empty one is not.NAis still passed through asNA, which callers distinguish from a name that sanitized away. -
setup_mesos()andsetup_mesos_structure()now abort when two mesos groups share an abbreviation (#244).make.unique()is applied to generated abbreviations only, never to a user-supplied abbreviation column, so two groups given the same explicit abbreviation collapsed into one folder and the last one written won — again silently, and again with a success message. The check is inextract_mesos_metadata(), so it covers both entry points and any future route to a bad abbreviation, and metadata for every mesos variable is now extracted before the writing loop begins, so a fault in the second mesos variable no longer leaves the first one half-written. This turns two previously silent cases into errors, which is the intended change: both destroy files that already exist, so continuing is worse than stopping. An all-NAabbreviation column is unaffected — such a column is filtered to length zero upstream, has nothing empty or duplicated in it, and its existing pinned behaviour intests/testthat/test-setup_mesos.Ris unchanged. One caveat on the wording of the error: reached throughsetup_mesos_structure()’s legacy two-column path, an empty explicit abbreviation is reported as a duplicate one, becausehandle_legacy_format()drops the empty string and[[<-.data.framerecycles the shortened column, fabricating a copy of the neighbouring group’s abbreviation before the check ever sees it (#248). The abort still happens and nothing is written, which is what matters here; the fabrication itself is left for #248. -
refine_chapter_overview()now warns when a.templaterepeats the sameinsert_text()call (#210). Projects inject auxiliary text by wrapping every template with abefore=TRUE/before=FALSEpair, and that wrap is written out longhand in two places — the generation script andapply_template_mutations(). Neither knows about the other, so applying both wraps each template twice and every inserted passage is emitted twice in the generated qmd. Neitherinsert_text()norapply_template_mutations()lives in saros.base, so nothing here can prevent the doubling; the package only ever sees the already-doubled string arriving inchunk_templates$.template. This is therefore a lint on the incoming data: it names the affected templates and the repeated call, and returns the templates unchanged. This adds a warning to existing calls that pass doubly-wrapped templates — which are already producing doubled output. The check keys on an identical repeated call rather than a count ofinsert_text()calls, because a template may legitimately address several insertion points; those calls differ in their arguments, whereas re-wrapping reproduces one verbatim. Whitespace is ignored when comparing, since the two copies of the wrap are separately authored and drift in spacing. - A section that matches no rows no longer emits the previous sibling’s chunk under its own heading (#239).
new_outwas threaded through the sibling loop ingen_qmd_node()and reassigned only when a section was non-empty, so an empty section kept whatever the sibling before it produced — a figure or table appearing under a heading it does not belong to, with no error and no warning, and looking entirely plausible in the rendered report. This was latent rather than live:grouped_dataisdistinct()over the grouping columns ofchapter_structure, so every traversal path corresponds to at least one real row and no empty section arises today (instrumenting 66 deepest-level calls across five report shapes found none). But that is an invariant held elsewhere, not a local guarantee — a change to howgrouped_datais derived, toNAhandling inprepare_chapter_structure_section()’s filter, or a new grouping column whose values do not round-trip throughas.character()would have made it live.new_outis now local to each node, so an empty section contributes nothing, not even its heading. Bothqmd_enginevalues were affected identically and both are fixed by the one change; the now-deadnew_outstate has been dropped from the recursion’s loop variable and the loop engine’s stack frame.tests/testthat/test-qmd_empty_section.Rconstructs the empty section directly, since the integration path cannot produce one. -
draft_report(require_common_categories = TRUE)now performs the check it documents (#232). The argument was validated but never read, and thecheck_category_pairs()helper implementing it had no caller. Dependent variables within a section — the set that ends up in one figure — are now checked for at least one shared response category, before any files are written. Only factor columns are compared, since a “common category” is not meaningful for numeric or free-text variables. Setrequire_common_categories = FALSEto skip. -
refine_chapter_overview(keep_dep_indep_if_no_overlap = FALSE)now removes bivariate entries whose dependent and independent variables never co-occur (#232). The call site was short-circuited withif (FALSE && ...), so the argument had no effect. Enabling it exposed a latent crash inremove_from_chapter_structure_if_no_overlap():.variable_name_dep/_indepare factors that may carryNAas an explicit level, for whichis.na()on the factor isFALSE, so such rows reacheddata[[NA]]and aborted. The comparison now runs on the character form. -
create_r_files(r_add_file_scope = FALSE)now actually omits thefile_scopecolumn from the generated placeholder files (#232). The flag was accepted and ignored, so the scope was written either way. The placeholder file is still created in both cases, and the default (TRUE) is unchanged. -
create_email_credentials(ignore_missing_emails = FALSE)now warns about usernames that exist in the password file but have no email address (#232). This is the direction the argument documents; the function previously only warned about the opposite case, and never read the flag. Such accounts silently received no credentials. This adds a warning to existing calls where the password file contains accounts absent fromemail_data_frame; passignore_missing_emails = TRUEto silence it. -
setup_mesos()no longer writes.na.characteras the title of<mesos_var>/index.qmd(#188).extract_mesos_metadata()guarded its fallback withis.null(), butget_raw_labels()returnsNA_character_for an unlabelled column, so the display name stayedNAand was serialised into the site.setup_mesos_structure()was unaffected because it always attaches a label internally — which is what made the two entry points produce different output. They now agree on every generated file except the_metadata.ymlsubtitle, which legitimately includes themain_directoryfolder name only when one is supplied. - Generated mesos stub and
index.qmdfiles now end with a newline. Their absence madereadLines()and other text tools warn about an incomplete final line. -
draft_report()is now reproducible (#213). Heading anchors carried two RNG-drawn digits, so identical inputs produced different.qmdfiles on every run. Quarto’sfreezecache keys on file content, so it missed on every chapter after every regeneration — a one-line change in data preparation forced a full re-render of the entire site. The suffix is now a short hash of the heading’s position in the grouping tree, which is stable across runs and a stronger disambiguator than two digits (which collided for 1% of colliding pairs).draft_report()no longer draws from the session RNG at all. - Chapter files no longer contain two first-level headings (#207).
.chapter_numberhas been added to theignore_heading_for_groupdefault. The default listed"chapter", but the column grouped on is.chapter_number, so the guard never fired and the chapter title was emitted both directly and by the grouping machinery. Remove.chapter_numberfrom the argument to restore the previous grouping-generated heading, which carries a{#sec-}anchor. -
setup_mesos()andsetup_mesos_structure()no longer overwrite the authored_*.qmdchapter sources inmain_directory(#212). A stub was emitted at the top level, replacing each source file with an include pointing outsidemain_directory. The failure was silent and repeated on every run, so restoring the files from version control was not sufficient. - Mesos
{{< include >}}paths now resolve (#212). The relative path scaled with the directory level (rep("../", path_lvl)), but consecutive levels always differ by exactly one component, so every level above the innermost skipped a directory and eventually escapedmain_directory. Quarto does not error on an unresolvable include, so affected group pages rendered as empty documents with correct titles and_metadata.yml. - A multi-component
mesos_var_subfoldersuch as"Rapport/Del1"now nests instead of erroring (#212).write_subfolder_metadata()vectorised over the components rather than nesting them, addressing a non-existent sibling directory and failing withcannot open the connectionafter stub files had already been written. This affected the documented example in?setup_mesos_structure, which usedmesos_var_subfolder = "reports/Q1". - Removed the stray
'#\newpage'element from the tabset chunk templates (#214). The single backslash was re-parsed by R as a newline escape when the generated qmd was rendered, so the textewpageappeared above every tabset on every page. Affected 6 of 7 templates inget_chunk_template_defaults(2)and 3 of 7 in variant 4. A page break was meaningless in these HTML templates in any case. -
delete_freeze()is now actually exported (#219). It was documented with@exportand had a generatedman/delete_freeze.Rd, butNAMESPACEhad not been regenerated, sosaros.base::delete_freeze()failed with “not an exported object”. -
draft_report(title = )is no longer a silent no-op (#208, #184).process_yaml()only assigned the title when an explicityaml_filewas supplied, soindex.qmdandreport.qmdwere written without atitlefield in the default case. - Chapter qmd-files now receive their
chaptername as the YAMLtitle(#208, #184). Previouslygen_qmd_chapters()passedtitle = NULL, leaving Quarto to infer the page title from the first body heading — which is why titles varied across Quarto versions, and why projects post-processed the heading into the header with regexes that truncated at hyphens. - Mesos group
_metadata.ymlfiles now get thetitlefield that?setup_mesosdocuments forsubtitle_separator(#184). The assignment was commented out, and referred to an out-of-scope variable. -
.variable_label_suffixis now whitespace-normalised like the prefix (#216).refine_chapter_overview()passed.variable_label_prefixtotrim_columns()twice and never passed the suffix, so label suffixes kept leading/trailing spaces and internal runs of spaces. These suffixes become section headings, where leading whitespace is significant in Markdown. Only visible with alabel_separatorthat does not itself include surrounding spaces, e.g.":". -
delete_freeze()no longer warnsno non-missing arguments to maxwhen a_freezeentry contains no files (#220). Such an entry is stale and is still deleted; only the spurious warning is gone. Staleness now also ignores directory mtimes, and_freezeitself is excluded when discovering.qmdfiles. - Suggested packages are now used conditionally, per R-exts (#215).
srvyr(inungroup_data()) andwritexl/readr/haven(intabular_write()) are guarded withrlang::check_installed(), which reports an actionable install prompt instead of “there is no package called …”. The singlepurrr::compact()call was replaced with base R.
New features
- Added
default_chunk_templates_5: a new simplified template set for single crowd reports without mesos structure. Uses cleaner helper functions likeget_fig_title_suffix_from_ggplot()for more streamlined code generation. -
draft_report(qmd_engine = )selects how the grouping tree is traversed when assembling each chapter (#19)."recursion"(the default, and the original implementation) makes one R call per node, so a deeporganize_byis bounded byoptions("expressions")and the C stack."loop"walks the same tree with an explicit stack, bounded by heap instead. The two produce byte-identical output;tests/testthat/test-qmd_engines.Rasserts that across five report shapes, including the bundled example, andtests/testthat/test-qmd_engines_ordering.Radditionally pins ordering, grouping and sorting across fiveorganize_byshapes, threearrange_section_bydirections, bothna_first_in_sectionsettings, reversed chapter declaration and a degenerate single-value tree. Measured on the bundled example the two are within noise of each other (5.3s vs 4.8s at the default depth, 18.1s vs 18.2s with one extra grouping level), so this is about depth headroom and having a fallback, not speed.
Testing
- Added snapshot tests of the
.qmdtextdraft_report()writes (tests/testthat/test-qmd_snapshots.R). Nothing previously asserted anything about the generated content — the existing test checks file counts and file sizes — which is why #207, #208/#184 and #216 all shipped. Each of those was re-introduced and confirmed to fail the new tests. Only possible now that #213 made the output deterministic;test-anchor_determinism.Rpins that property separately.
Code quality improvements
- Moved
tibblefromSuggeststoImports(#215)..onLoad()builds the default chunk templates withtibble::add_row(), and R-exts requires a package to declare what its own code uses directly. This corrects the declaration; it does not change observable behaviour.tibbleis a hard dependency ofdplyr,tidyrandforcats— all already inImports— so it has always been installed alongside saros.base, and no installation could have lacked it. - CI now fails when
man/orNAMESPACEdiffer from whatroxygen2::roxygenise()produces from the roxygen comments inR/(#219). This is the drift that hiddelete_freeze():R CMD checkaccepts a package whoseNAMESPACEis missing an export — it is simply a package without that function — and pkgdown indexes.Rdtopics rather than exports, so neither caught it. -
.saros.envis now an actual environment (#218). A package-level.saros.env <- NULLmadeexists(".saros.env")inside.onLoad()always true, so thenew.env()branch never ran; the first$<-coercedNULLto a list, and each of the ~50 subsequent assignments copied the whole accumulating list — including the large chunk-template tables — instead of mutating in place. The superassignments (<<-) are no longer needed and have been replaced with ordinary$<-. - Removed the empty file
R/utils_qmd.R(#220), a leftover of the refactor that moved the QMD helpers intoR/qmd_utils.R. - Removed the unused and broken
create_text_collapse()(#217). It readformals(draft_report)$translations, butdraft_report()has notranslationsargument, so the last separator resolved toNULLandc("a", "b", "c")collapsed to"a, bc"rather than erroring. A new test asserts that everyformals(fn)$namereference inR/names a real argument. - Improved code formatting and readability in
.onLoad()function for better maintainability. - Updated template references in
default_chunk_templates_4for better consistency (usingdatainstead ofdata_{.chapter_foldername}, addedsave = parameters$saveparameter). - Better structured code blocks with consistent indentation and spacing.
saros.base 1.2.1
Bug fixes
- Fixed bug in
setup_mesos()where an incorrect assignment tofiles_to_processwas causing the search and replace functionality to fail.
saros.base 1.2.0
CRAN release: 2025-11-12
New features
- Added file logging for excluded/ignored variables via
log_fileparameter inrefine_chapter_overview(). All removal functions now log which variables/entries are excluded and why (all NA, low n, non-significant, no overlap, type mismatch). - Added
detect_malformed_quarto_project(): exported function to diagnose malformed Quarto website projects (missing index.qmd, missing title in .qmd files, extensible for future checks). - New function
check_variable_labels()to validate variable labels for saros compatibility. - New function
sanitize_chr_vecfor ensuring that character vectors are clean:- Normalized Unicode strings to NFC form.
- Removed non-printable characters.
- Replaced common encoding artifacts (e.g.,
’to').
- Added chunk template variant 4 (
get_chunk_template_defaults(4)) for mesos reports using the new saros package functionscrowd_plots_as_tabset()andtxt_from_cat_mesos_plots(). This provides a more streamlined approach for generating mesos-specific plots and tables.
Performance improvements
- Vectorized password lookup in
refer_main_password_file()for better performance.
Bug fixes
- Fixed critical sorting bug in
refine_chapter_overview()where output was incorrectly sorted by variable labels instead of variable positions when using default arguments. The fix includes:- Corrected
arrange_expr_producer()to properly name arrange expressions with column names instead of logical values. - Added ungrouping before sorting in
arrange_arrangers_and_groups()to prevent grouped data from interfering with global sort order. - Made chapter reordering stable to preserve within-chapter sorting.
- Corrected
- Fixed regex bugs in
check_variable_labels(). - Fixed tidyselect warnings in
look_for_extended(). - Improved robustness of
setup_mesos(). - Added validation checks for email and username columns in
create_email_credentials().
Code quality improvements
- Refactored long functions by extracting helper functions:
validate_refine_chapter_overview_args()validate_draft_report_args()create_mesos_stubs_from_main_files()gen_qmd_file()create_includes_content_path_df()validate_chapter_structure()look_for_extended()process_yaml()
- Removed broken and unused
create_heading()function. - Removed commented-out and unused code.
- Refactored
convert_mesos_groups_to_dfand its helper functions to ensure consistent handling ofmesos_groups. - Added a
clean_group_datainternal helper function to:- Drop unused levels for factors.
- Remove
NAand blank strings.
Testing
- Added 169 comprehensive tests across multiple modules (from 331 to 500+ tests).
- Added 16 tests for
refine_chapter_overview(). - Added comprehensive sorting tests in
test-arrange2.Rto verify position-based sorting with intentionally mismatched variable names, labels, and positions. - Added 27 tests for logging functionality.
- Added tests for setup_mesos helper functions, utility functions, access restriction setup, and directory structure helpers.
Documentation
- Added
check_variable_labels()to pkgdown reference. - Added Copilot instructions for testing and git workflows.
saros.base 1.1.0
CRAN release: 2025-06-01
-
create_directory_structure()example does not create files and folders on disk to save time. - Templates for mesos output now include newlines between target and others. Thanks to Jon Furuholt for the suggestion.
-
draft_report()now has argumentwrite_qmdto toggle the creation of qmd-files. - Attempted fix of internal arrange2 sorting function. Very hard to get right.
saros.base 1.0.0
CRAN release: 2025-01-10
Major changes
- Total revision of the entire architecture for maximum flexibility, stability and performance.
- Uses glue templates for creating chunks, see
refine_chapter_structure(). draft_report()- Breaking changes for mesos setup, now uses
setup_mesos()as well for creating stub files referring to a smaller set of main files created bydraft_report(). - Countless bugfixes.
Minor changes
- Helper function
remove_entry_from_sidebar()for post-processing HTML-files - Many more validations of arguments and better error messages.